文章目录
- 1 重点知识
- 2 应用层
- 3 再谈 "协议"
- 4 HTTP协议
- 4.1 认识URL
- 4.2 urlencode和urldecode
- 4.3 HTTP协议格式
- 4.4 HTTP的方法
- 4.5 HTTP的状态码
- 4.6 HTTP常见Header
- 4.7 最简单的HTTP服务器
- 3 传输层
- 4 再谈端口号
- 4.1 端口号范围划分
- 4.2 认识知名端口号(Well-Know Port Number)
- 4.3 两个问题
- 4.4 netstat
- 4.5 pidof
- 5 UDP协议
- 5.1 UDP协议端格式
- 5.2 UDP的特点
- 5.3 面向数据报
- 5.4 UDP的缓冲区
- 5.5 UDP使用注意事项
- 5.6 基于UDP的应用层协议
- 6 TCP协议
- 6.1 TCP协议段格式
- 7 确认应答(ACK)机制
- 8 连接管理机制
- 8.1 理解TIME_WAIT状态
- 8.2 解决TIME_WAIT状态引起的bind失败的原因和方法
- 8.3 深入理解 CLOSE_WAIT 状态
- 8.4 滑动窗口
- 8.5 流量控制
- 8.6 拥塞控制
- 8.7 延迟应答
- 8.8 捎带应答
- 8.9 面向字节流
- 8.10 粘包问题
- 9 TCP异常情况
- 10 TCP小结
- 11 基于TCP应用层协议
- 12 TCP/UDP对比
1 重点知识
- 理解应用层的作用, 初识HTTP协议;
- 理解传输层的作用, 深入理解TCP的各项特性和机制;
- 对整个TCP/IP协议有系统的理解;
- 对TCP/IP协议体系下的其他重要协议和技术有一定的了解;
- 学会使用一些分析网络问题的工具和方法;
注意!! 注意!! 注意!!
- 本博客是网络编程的理论基础;
- 是一个服务器开发程序员的重要基本功;
- 是整个Linux课程中的重点和难点;
- 也是各大公司笔试面试的核心考点;
2 应用层
首谈四层网络协议模型中的应用层:
四层网络协议模型中的应用层是模型的最上层,负责提供应用程序与网络之间的接口
。它包含了各种应用层协议,如HTTP
、FTP、SMTP等,用于实现不同应用的功能和数据交换。应用层协议定义了数据的格式、交换的规则和通信的过程
,使不同设备上的应用程序能够相互通信和交换数据
。应用层协议还负责实现应用程序与网络之间的通信,以及数据传输时的分组和拆分。
通俗地讲,应用层协议就是各种应用程序用来进行通信的协议。下面举一些生活中常见的应用层协议的例子
:
- HTTP协议:这是最常见的应用层协议之一,用于在
Web浏览器和服务器之间进行通信
。当你通过浏览器访问一个网页时,HTTP协议就用于从服务器请求和发送网页内容。(Web浏览器(通常称为浏览器)是一种软件应用程序
) - FTP协议:文件传输协议,用于在网络中传输文件。例如,你可能需要使用FTP上传或下载某个文件,这就是FTP协议在网络通信中的应用。
- SMTP协议:简单邮件传输协议,用于发送电子邮件。当你使用电子邮件客户端发送一封电子邮件时,SMTP协议就用于将邮件从你的计算机发送到邮件服务器。(各种邮箱:QQ邮箱、网易邮箱等的通信)
- DNS协议:域名系统(DNS)是一种应用层协议,
用于将域名转换为IP地址
。例如,当你访问一个网址时,DNS协议会被用来将网址(如www.example.com)转换为相应的IP地址。 - SSH协议:安全外壳协议(SSH)是一种应用层协议,用于加密网络上的数据,以便安全地远程访问服务器或执行其他网络任务。(如:Linux中的SSH+服务器IP的登陆云服务器语句)
- Telnet协议:远程终端协议(Telnet)是一种应用层协议,用于在网络上提供文本交互的远程登录服务。
这些应用层协议在网络通信中发挥着重要作用,它们允许各种应用程序在互联网上进行通信和交互。通过使用这些协议,我们可以发送和接收电子邮件、浏览网页、传输文件、远程登录到服务器等。
3 再谈 “协议”
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 "字符串"
的方式来发送接收的
. 如果我们要传输一些
“结构化的数据” 怎么办呢?
方案一:使用字符串协议
- 客户端发送一个形如"1+1"的字符串:客户端将需要计算的两个数字和运算符封装在一个字符串中,通过Socket API发送给服务器。
- 服务器端解析字符串:服务器端接收到字符串后,需要解析这个字符串,将其拆分为两个数字和一个运算符。可以使用字符串分割、正则表达式等方法实现。
- 执行计算:服务器端对解析出的两个数字进行加法运算。
- 返回结果:服务器将计算结果再次封装为一个字符串,通过Socket API发送给客户端。
方案二:使用结构化数据协议
- 定义结构体:定义一个结构体(在C语言中是struct),用来表示需要交互的信息。例如:
typedef struct {
int num1;
int num2;
char operator;
} CalculationRequest;
- 序列化结构体:在
发送数据时,将结构体转换为一个字符串
。这个过程需要确保数据的完整性和一致性,可以使用一些序列化库或自定义的序列化方法。 - 反序列化结构体:在
接收到数据时,将字符串转换回为结构体
。这个过程与序列化相反,需要按照相同的规则将字符串解析为结构体的各个字段。 - 执行计算:服务器端根据反序列化出的结构体中的数据进行计算。
- 返回结果:服务器将结果再次封装为一个结构体,进行序列化后发送给客户端。客户端收到数据后进行反序列化,提取结果。
注意:在实现结构化数据的传输时,需要注意数据的大小和格式。如果结构体中的数据很大或者格式复杂,序列化和反序列化的过程可能会变得复杂和耗时。此外,还需要考虑数据的完整性和安全性问题,例如数据截断、恶意数据注入等。
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解
析, 就是ok的. 这种约定, 就是 应用层协议
。
4 HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的。
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议
, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。
4.1 认识URL
平时我们俗称的 “网址” 其实就是说的 URL,URL是Uniform Resource Locator的缩写
,中文翻译为统一资源定位符,也被称为网页地址
,是因特网上标准的资源的地址。URL通过一个标准的格式来表示互联网资源的位置,并且可以指向各种类型的资源,如网页、图片、视频、文件等。
URL通常由多个部分组成,包括协议类型(如HTTP、HTTPS)、主机名(域名或IP地址)、路径(资源在主机上的位置)以及可选的查询参数(用于传递额外的信息)
。通过URL,用户可以方便地访问和定位到特定的网络资源。
例如:
https://www.lanqiao.cn/courses/?from_login_page=true,
解析以上URL的信息:
要解析一个URL,我们可以将其分解为不同的部分以了解其结构和工作方式。下面是对给定URL:https://www.lanqiao.cn/courses/?from_login_page=true
的详细解析:
-
协议:
https://
https
表示这是一个使用安全的超文本传输协议(HTTP)的网页链接。使用 HTTPS 可以确保数据在传输过程中的安全。
-
域名:
www.lanqiao.cn
- 这是网站的域名。在这里,“www”通常是web服务器的缩写。
- “lanqiao”很可能是网站的名称或特定部分的名称。
- “cn”表示这个域名属于中国。
-
路径:
/courses/
- 这表示在网站的主目录下有一个名为“courses”的子目录或页面。
-
查询参数:
?from_login_page=true
- 查询参数通常用于传递额外的信息给服务器。
- 在这个例子中,
from_login_page=true
可能表示这个页面是从登录页面跳转过来的。这可能是为了跟踪用户的行为或提供某些特定的功能或内容。
-
片段标识符:URL中没有片段标识符。
总的来说,这个URL指向www.lanqiao.cn
网站下的“courses”页面,并且可能从登录页面跳转过来。查询参数可能用于跟踪或提供特定的功能或内容。
4.2 urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
可以看出:“+” 被转义成了 “%2B”
这其实是运用了urlencode和urldecode的方法。
urlencode和urldecode是两种与URL编码和解码相关的函数
。在互联网上,当我们需要在URL中传递一些特殊字符
时,比如空格、特殊符号等
,这些字符在URL中是不能直接使用的,因为它们可能会影响URL的正确解析
。为了解决这个问题,我们需要将这些特殊字符进行转换,使其能够被正确地传递和处理。这个转换的过程就是URL编码。
同样地,当我们从URL中获取到这些经过编码的特殊字符时,我们需要将其转换回原始的形式,这个过程就是URL解码。
那么,urlencode和urldecode是什么呢?
urlencode是一种函数,它可以将字符串中的某些字符进行转换,使其符合URL的编码规则
。例如,空格会被转换为"+“,而特殊符号会被转换为”%"加上其ASCII码的十六进制表示。这样,这些特殊字符就可以在URL中被正确地传递和处理了。
而urldecode则是与urlencode相反的过程。它可以将经过urlencode编码的字符串还原回其原始的形式
。例如,将"%"加上其ASCII码的十六进制表示转换回相应的字符。
简单来说,urlencode就是将特殊字符转换为可以在URL中传递的形式,而urldecode则是将这些经过转换的字符还原回其原始的形式
。
4.3 HTTP协议格式
HTTP协议格式主要包含以下几部分:
HTTP请求
,格式如下:
- 请求行:包含请求方法(如GET、POST等)、请求的URL和HTTP协议的版本。
- 请求头部:包含一些键值对,提供了关于请求的附加信息,如主机、用户代理、内容类型等。
- 空行:用于分隔请求头部和请求正文。
- 请求正文:可选的,用于传递POST请求的数据。
举例说明:
当我们在浏览器中输入一个网址并按下回车键时,浏览器会向服务器发送一个HTTP请求。以下是一个GET请求的
例子:
GET /webDemo/Hellow HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: text/html
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8
{请求正文数据}
这个请求包含了以下信息:
- 请求行:指定了请求方法为GET,请求的URL为
/webDemo/Hellow
,使用的HTTP协议版本为1.1。 - 请求头部:包含了几个字段,用于提供关于请求的附加信息。
Host
:指定了请求的目标主机和端口号。Connection
:指定了保持连接的选项。Accept
:指定了客户端可以接受的媒体类型,这里是HTML。Accept-Encoding
:指定了客户端可以接受的编码类型。Accept-Language
:指定了客户端的语言偏好。
- 空行:用于分隔请求头部和请求正文。在这个例子中,请求正文是空的,因为GET请求通常不包含请求体。
- 请求正文:在实际的GET请求中,请求正文通常为空,因为所有需要的数据都包含在URL中。
以上就是一个基本的HTTP GET请求的例子。其他类型的HTTP请求(如POST、PUT、DELETE等)可能会有不同的格式和用途,但基本的结构是相似的。
HTTP响应
,格式如下:
- 状态行:包含HTTP协议的版本、状态码和状态消息。
- 响应头部:也包含一些键值对,提供了关于响应的附加信息,如内容类型、日期等。
- 空行:用于分隔响应头部和响应正文。
- 响应正文:实际的网页内容。
例子:
当服务器接收到一个HTTP请求后,它会返回一个HTTP响应。以下是一个HTTP响应的例子:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 23
<html>
<body>
Hello, World!
</body>
</html>
这个响应包含了以下信息:
- 状态行:指定了HTTP协议的版本为1.1,状态码为200,表示请求成功,以及状态消息为"OK"。
- 响应头部:包含了几个字段,用于提供关于响应的附加信息。
Content-Type
:指定了响应正文的媒体类型,这里是HTML。Content-Length
:指定了响应正文的长度。
- 空行:用于分隔响应头部和响应正文。
- 响应正文:实际的网页内容,这里是HTML代码。
以上就是一个基本的HTTP响应的例子。不同的状态码可能对应不同的含义,如404表示未找到资源,500表示服务器内部错误等。具体的状态码和其含义可以在HTTP协议规范中查找到。
以上就是HTTP协议的基本格式,实际使用中可能会有所变化,具体取决于HTTP版本和特定的应用场景。
4.4 HTTP的方法
HTTP协议定义了多种请求方法,每种方法都有不同的用途和语义。以下是HTTP的几种常见方法,并举例说明:
- GET:用于请求指定的资源。
它是最常见的方法之一
,通常用于从服务器检索信息。例如,当我们在浏览器中输入一个URL并按下回车键时,我们实际上是在向服务器发送一个GET请求。 - POST:用于向指定的资源提交数据,常见于表单提交和上传文件。当用户在网页上填写表单并提交时,通常会使用POST方法将数据发送到服务器进行处理。
- PUT:用于替换目标资源的所有当前表述。它通常用于更新资源的内容。例如,当用户在网页上编辑一个文档并保存时,可以使用PUT方法将更新后的文档内容发送到服务器进行保存。
- DELETE:用于请求删除指定的资源。当用户想要删除某个资源时,可以使用DELETE方法向服务器发送请求来删除该资源。
- HEAD:与GET方法类似,但服务器在响应中只返回HTTP头部,不返回实际的数据。它常用于检查资源的元数据信息。
- OPTIONS:用于获取目标资源所支持的通信选项。它可以用于检查服务器的性能和确定客户端与服务器之间可以进行哪些通信选项。
这些方法具有不同的语义和用途,使得HTTP协议更加灵活和强大。根据不同的需求和场景,可以选择合适的方法来与服务器进行通信。
4.5 HTTP的状态码
HTTP状态码就像是我们与人沟通时使用的语言中的词汇,用于告诉对方我们请求的结果是好还是坏,或者是需要对方进行一些额外的操作
。
当我们在浏览器中输入一个网址并按下回车键时,我们的电脑会向这个网址对应的服务器发送一个HTTP请求。服务器在处理这个请求后,会返回一个状态码,就像是我们接收到别人的回复时,会知道这个回复是积极的、消极的,还是需要我们做进一步的操作。
HTTP状态码的第一个数字代表了响应的五种状态之一
,这些数字有着特定的含义。例如,200表示请求成功,404表示未找到资源,500表示服务器内部错误等。这些状态码就像是我们网络世界的“行话”,让浏览器和服务器能够理解彼此的意思。
通过了解HTTP状态码,我们可以更好地理解网络请求的结果,并对可能出现的问题进行调试和解决。
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
4.6 HTTP常见Header
HTTP的Header是HTTP协议中的一部分,用于在客户端和服务器之间传递附加信息
。这些信息可以包含关于请求
、响应
或者其他的发送实体的详细信息。
HTTP常见的Header包括:
- Accept:告诉服务器客户端能够处理的数据类型,例如:text/html、application/json等。
- Content-Type:告诉服务器发送的数据类型,例如:application/json、application/xml等。
- Cookie:存储在客户端上的数据,服务器可以通过这个Header获取数据。
- Authorization:用于身份验证,告诉服务器客户端的身份。
- User-Agent:表示发出请求的用户代理,通常包含发出请求的浏览器的类型、版本等信息。
- Referer:告诉服务器请求的来源页面地址。
- Cache-Control:用于控制缓存行为。
- If-Modified-Since:告诉服务器只有在这个时间之后修改过的资源才会被返回。
- If-None-Match:告诉服务器只有在这个Etag值改变的情况下才会返回新的资源。
- Host:指定请求的域名或IP地址和端口号。
以上是HTTP常见的一些Header,实际上还有许多其他的Header,具体使用哪些取决于实际的需求。
4.7 最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易
能做到;
// 引入必要的头文件,这些文件提供了用于网络编程和系统调用的各种功能。
#include <sys/socket.h> // 提供socket相关的函数
#include <netinet/in.h> // 提供网络地址和端口号的数据结构
#include <arpa/inet.h> // 提供inet_addr和inet_ntoa函数
#include <unistd.h> // 提供低级I/O相关的函数,如read和write
#include <stdio.h> // 提供输入输出相关的函数,如printf
#include <string.h> // 提供字符串操作相关的函数,如strlen
#include <stdlib.h> // 提供一些通用的函数,如atoi和malloc
// 定义一个函数,用于打印用法信息
void Usage() {
printf("usage: ./server [ip] [port]\n"); // 打印正确的命令行参数格式
}
// 主函数开始
int main(int argc, char* argv[])
{ // argc是命令行参数的数量,argv是命令行参数的数组
// 检查命令行参数的数量是否正确
if (argc != 3) {
Usage(); // 如果参数数量不正确,打印用法信息并退出程序
return 1; // 返回错误代码1
}
// 创建一个socket描述符,用于后续的网络通信
int fd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET表示IPv4,SOCK_STREAM表示TCP流式套接字
if (fd < 0) { // 如果socket调用失败,打印错误信息并退出程序
perror("socket"); // perror打印错误信息,参数是错误消息的字符串
return 1; // 返回错误代码1
}
// 定义一个结构体变量,用于存储服务器的网络地址信息
struct sockaddr_in addr; // 使用sockaddr_in结构体来表示IPv4地址和端口号
addr.sin_family = AF_INET; // 设置地址家族为IPv4
addr.sin_addr.s_addr = inet_addr(argv[1]); // 将命令行参数中的IP地址字符串转换为32位无符号整数形式
addr.sin_port = htons(atoi(argv[2])); // 将命令行参数中的端口号字符串转换为网络字节序的无符号整数形式
// 将socket绑定到指定的地址和端口号上,这样服务器就可以开始监听连接请求了
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); // 如果bind调用失败,打印错误信息并退出程序
if (ret < 0) {
perror("bind"); // perror打印错误信息,参数是错误消息的字符串
return 1; // 返回错误代码1
}
// 开始监听连接请求,最多可以同时处理10个连接请求(由第三个参数指定)
ret = listen(fd, 10); // 如果listen调用失败,打印错误信息并退出程序
if (ret < 0) {
perror("listen"); // perror打印错误信息,参数是错误消息的字符串
return 1; // 返回错误代码1
}
// 进入一个无限循环,等待客户端的连接请求并处理它们
for (;;)
{ // 无限循环开始,直到程序被外部中断或出错退出
struct sockaddr_in client_addr; // 定义一个结构体变量,用于存储客户端的网络地址信息
socklen_t len; // 存储客户端地址的长度(需要在使用accept之前设置为正确的值)
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len); // 接受客户端的连接请求,并返回一个新的socket描述符用于与客户端通信。如果出错则返回-1。
if (client_fd < 0) { // 如果accept调用失败,打印错误信息并继续等待下一个连接请求(跳过此次循环)
perror("accept"); // perror打印错误信息,参数是错误消息的字符串
continue; // 继续下一次循环,等待下一个连接请求或处理其他事件(例如超时或信号)
}
char input_buf[1024 * 10] = {0}; // 用一个足够大的缓冲区来读取客户端发送的数据。
// 用一个足够大的缓冲区直接把数据读完.
ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);
if (read_size < 0)
{
return 1;
}
printf("[Request] %s", input_buf); // 打印客户端发送的数据
char buf[1024] = {0}; // 定义一个缓冲区,用于存储HTTP响应的头部和正文
const char* hello = "<h1>hello world</h1>"; // 定义一个常量字符串,作为HTTP响应的正文内容
sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello), hello);
// 使用sprintf函数将HTTP响应的头部和正文格式化到一个字符串中
write(client_fd, buf, strlen(buf)); // 将HTTP响应发送回客户端
}
return 0;
}
以上代码仅仅是一个非常简化的示例,用于演示如何使用C语言创建一个基本的TCP服务器,它并没有真正实现完整的HTTP协议
。
要实现一个真正的HTTP服务器,你需要处理HTTP请求的各个方面,包括请求方法(如GET、POST等)、请求头部、请求正文以及响应头部和正文。这通常涉及到解析HTTP请求和生成HTTP响应,这些请求和响应都遵循特定的语法和格式。
3 传输层
传输层是整个网络体系结构中的关键层次之一,主要负责向两个主机中进程之间的通信提供服务
。 传输层是解决计算机程序到计算机程序之间的通信问题
,即所谓的“端”到 “端”的通信
。 引入传输层的原因是增加复用和分用的功能、消除网络层的不可靠性、提供从源端主机到目的端主机的可靠的、与实际使用的网络无关的信息传输。
传输层在终端用户之间提供透明的数据传输,向上层提供可靠的数据传输服务。 传输层在给定的链路上通过流量控制、分段/重组和差错控制来保证数据传输的可靠性。 传输层的一些协议是面向链接的,这就意味着传输层能保持对分段的跟踪,并且重传那些失败的分段。
4 再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在TCP/IP协议中, 用 “源IP
”, “源端口号
”, “目的IP
”, “目的端口号
”, “协议号
” 这样一个五元组
来标识一个通信(可以通过netstat -n查看);
4.1 端口号范围划分
- 0 - 1023:
知名端口号
, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。 - 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.
4.2 认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
- ssh服务器, 使用22端口。
- ftp服务器, 使用21端口。
- telnet服务器, 使用23端口。
- http服务器, 使用80端口。
- https服务器, 使用443端口
执行下面的命令, 可以看到知名端口号:
cat /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号.
4.3 两个问题
- 一个进程是否可以bind多个端口号?
- 一个端口号是否可以被多个进程bind?
恭喜你,这两个都不能,好记多了。
在TCP/IP协议中,一个进程不能绑定多个端口号
,每个进程只能绑定一个端口号。这是因为每个TCP连接都有一个源端口和目的端口,如果一个进程绑定多个端口号,那么在接收到数据时无法确定应该将数据传递给哪个进程。
同样地,一个端口号也不能被多个进程同时绑定
。在一个系统中,每个端口号只能被一个进程绑定。这是因为每个TCP连接都有一个唯一的源端口标识,如果多个进程绑定同一个端口号,那么系统无法区分不同的连接应该传递给哪个进程。
因此,一个进程只能绑定一个端口号,一个端口号也只能被一个进程绑定
。
4.4 netstat
netstat是一个用来查看网络状态的重要工具.
语法:netstat -[选项]
功能:查看网络状态
常用选项:
- n 拒绝显示别名,能显示数字的全部转化成数字
- l 仅列出有在 Listen (监听) 的服务状态
- p 显示建立相关链接的程序名
- t (tcp)仅显示tcp相关选项
- u (udp)仅显示udp相关选项
- a (all)显示所有选项,默认不显示LISTEN相关
4.5 pidof
在查看服务器的进程id
时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id
5 UDP协议
5.1 UDP协议端格式
- 16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;
- 如果校验出错, 就会直接丢弃;
5.2 UDP的特点
UDP传输的过程类似于寄信
.
- 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
- 面向数据报: 不能够灵活的控制读写数据的次数和数量;
5.3 面向数据报
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
用UDP传输100个字节的数据:
- 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
5.4 UDP的缓冲区
- UDP没有真正意义上的发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
- UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;
UDP的socket既能读, 也能写
, 这个概念叫做 全双工
。
5.5 UDP使用注意事项
UDP(用户数据报协议)的首部确实包含了一个16位的长度字段,这个字段指定了UDP数据报(包括UDP首部和数据部分)的最大长度。由于这个字段是16位的,因此它所能表示的最大数值是2^16 - 1,即65535字节,或者说64KB(千字节)。
然而,需要注意的是,这64KB是包括了UDP首部的。UDP首部本身占用了8个字节,因此实际可用于数据传输的最大长度是65527字节(65535 - 8)。
64K在当今互联网环境下显得很小。随着互联网的发展和各种应用需求的增长,传输的数据量也在不断增加。因此,64KB的限制可能会成为某些应用场景下的瓶颈。
为了解决这个问题,实际应用中通常会采取以下几种策略:
-
分割数据:如果需要传输的数据量超过了UDP的最大长度限制,可以将数据分割成多个较小的数据块,然后分别通过多个UDP数据报进行传输。接收端在收到所有数据报后,再将这些数据块重新组合成完整的数据。
-
使用TCP协议:对于需要可靠传输和大数据量支持的应用,可以选择使用TCP(传输控制协议)而不是UDP。TCP协议没有单个数据报大小的限制,而是通过序列号和确认机制来确保数据的可靠传输和顺序。
-
应用层协议设计:在应用层协议的设计中,可以采取一些策略来绕过UDP大小的限制。例如,可以定义自己的数据分割和重组机制,或者使用流控制协议来管理数据的传输。
-
使用其他传输协议:除了TCP和UDP之外,还有一些其他的传输协议可供选择,如SCTP(流控制传输协议)等。这些协议可能提供了更灵活的数据传输选项和更大的数据报大小支持。
总之,虽然UDP协议本身有数据报大小的限制,但通过合适的设计和选择,可以有效地解决这个限制带来的问题。
5.6 基于UDP的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议
- BOOTP: 启动协议(用于无盘设备启动)
- DNS: 域名解析协议
当然, 也包括你自己写UDP程序时自定义的应用层协议;
6 TCP协议
TCP
全称为 “传输控制协议(Transmission Control Protocol)”. 人如其名, 要对数据的传输进行一个详细的控制;
6.1 TCP协议段格式
- 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
- 32位序号/32位确认号: 后面详细讲;
- 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60。
- 6位标志位:
URG: 紧急指针是否有效。
ACK: 确认号是否有效。
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了
- 16位窗口大小: 后面再说
- 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
- 16位紧急指针: 标识哪部分数据是紧急数据;
- 40字节头部选项: 暂时忽略;
7 确认应答(ACK)机制
TCP将每个字节的数据都进行了编号, 即为序列号。
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出哪些包是重复的包, 并且把重复的丢弃掉, 就可以很容易做到去重的效果. 这时候我们可以利用前面提到的序列号。那么, 如果超时的时间如何确定?
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
.
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
8 连接管理机制
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接
。
TCP(传输控制协议)是一种面向连接的协议,因此它需要一种机制来建立和断开连接。TCP的连接管理机制主要包括连接的建立、数据传输和连接的断开三个阶段。以下是这三个阶段的详细介绍:
-
连接建立(三次握手):
- SYN:客户端向服务器发送一个SYN报文,请求建立连接。该报文包含了客户端的初始序列号。
- SYN-ACK:服务器收到SYN报文后,会发送一个SYN-ACK报文作为回应。这个报文包含了服务器的初始序列号,以及对客户端初始序列号的确认。
- ACK:客户端收到SYN-ACK报文后,会发送一个ACK报文作为回应,确认服务器的初始序列号。此时,TCP连接建立完成。
-
数据传输:一旦连接建立,客户端和服务器就可以通过这个连接进行数据传输。TCP提供了一种可靠的服务,确保数据按照发送的顺序和完整性进行传输。
-
连接断开(四次挥手):
- FIN:当一方(客户端或服务器)想要断开连接时,它会发送一个FIN报文,请求释放连接。
- ACK:另一方收到FIN报文后,会发送一个ACK报文作为回应,确认收到断开请求。
- FIN-ACK:收到ACK报文的一方会发送一个FIN-ACK报文,再次确认断开请求,并请求释放连接。
- ACK:另一方最后发送一个ACK报文作为回应,确认收到断开请求。此时,TCP连接正式关闭。
TCP的这种连接管理机制确保了数据传输的可靠性和顺序性,同时也允许应用程序在完成数据传输后安全地关闭连接。
服务端状态转化:
在TCP连接管理中,服务器的状态转化可以从以下几种状态描述:
- LISTEN状态:服务器在监听来自客户端的连接请求。当一个TCP三次握手建立完成时,服务器端将进入ESTABLISHED状态。
- ESTABLISHED状态:服务器与客户端之间的连接已建立,可以进行正常的数据传输。当服务器接收到FIN报文请求断开连接时,会进入CLOSE-WAIT状态。
- CLOSE-WAIT状态:服务器已收到断开连接请求,等待应用层关闭连接。在此状态下,服务器已经发送了FIN-ACK报文,等待客户端发送FIN报文并确认。
- LAST-ACK状态:服务器等待客户端的最后一个ACK报文,以便完全关闭连接。一旦收到这个ACK报文,服务器将进入TIME-WAIT状态。
- TIME-WAIT状态:服务器等待一段时间以确保客户端收到了服务器的FIN-ACK报文。这是为了处理网络中的延迟和确保连接可靠地关闭。
- CLOSING状态:在某些情况下,例如同时收到两个FIN报文,服务器可能会进入CLOSING状态。这表示服务器和客户端都在尝试关闭连接。当收到一个ACK报文时,服务器将进入TIME-WAIT状态。
- CLOSED状态:连接已完全关闭,服务器不再监听来自客户端的连接请求。
这些状态转化是TCP协议中用于管理连接的重要机制,确保了数据传输的可靠性和资源管理的正确性。在实际的网络应用中,这些状态转换也是TCP实现的一部分,通过不同的标志位和计时器来实现状态转换。
客户端状态转化:
在TCP连接管理中,客户端的状态转化同样经历了多个阶段,以下是一些主要的客户端状态转换:
- CLOSED状态:客户端未建立任何连接,处于关闭状态。当客户端发起连接请求时,会进入SYN-SENT状态。
- SYN-SENT状态:客户端已发送SYN报文请求建立连接,正在等待服务器的回应。如果收到服务器的SYN-ACK报文,客户端会进入ESTABLISHED状态。
- ESTABLISHED状态:连接已成功建立,客户端和服务器可以开始进行数据传输。当客户端想要断开连接时,会进入FIN-WAIT-1状态。
- FIN-WAIT-1状态:客户端发送FIN报文请求断开连接,并等待服务器的回应。收到服务器的ACK报文后,客户端进入FIN-WAIT-2状态。
- FIN-WAIT-2状态:客户端等待服务器发送FIN报文,表示服务器同意断开连接。收到服务器的FIN报文后,客户端进入TIME-WAIT状态。
- TIME-WAIT状态:客户端等待一段时间以确保服务器收到了其FIN报文。这是为了处理网络中的延迟和确保连接可靠地关闭。一旦时间到期,客户端将进入CLOSED状态。
- CLOSING状态:在某些情况下,例如同时收到来自客户端和服务器的FIN报文,客户端可能会进入CLOSING状态。这表示客户端和服务器都在尝试关闭连接。当收到一个ACK报文时,客户端将进入TIME-WAIT状态。
这些状态转换确保了TCP连接的可靠性和有序性,使得数据能够正确地在客户端和服务器之间传输。需要注意的是,实际的状态转换可能因不同的TCP实现和操作系统而有所不同。
下图是TCP状态转换的一个汇总:
- 较粗的虚线表示服务端的状态变化情况;
- 较粗的实线表示客户端的状态变化情况;
- CLOSED是一个假想的起始点, 不是真实状态;
关于 “半关闭” , 男女朋友分手例子
关于CLOSING状态. 同学们可以课后调研一下。
8.1 理解TIME_WAIT状态
TIME_WAIT状态是TCP协议中的一种状态,表示主动关闭的客户端发送完最后一个ACK报文后进入的状态。该状态持续的时间是两倍的MSL(Maximum Segment Lifetime),以确保网络中的所有数据包都已过期并被丢弃。
TIME_WAIT状态的存在是为了实现TCP全双工连接的可靠释放,并防止旧连接的数据在网络中未过期的情况下被新连接误认为是自己的数据。在该状态下,客户端会等待一段时间,以确保服务器收到了最后的ACK报文并关闭了连接。这样可以防止由于网络延迟或丢包导致的不必要的连接冲突和数据混淆。
需要注意的是,TIME_WAIT状态并不一定出现在所有的TCP连接关闭序列中,它的出现与否取决于具体的TCP实现和网络环境。在一些情况下,如使用TCP的某些选项或特定的网络配置,TIME_WAIT状态可能被跳过或缩短。
总之,TIME_WAIT状态是TCP协议中的一种状态,用于确保连接的可靠释放和避免数据混淆。它是TCP连接管理机制中的重要组成部分,有助于维护网络连接的可靠性和稳定性。
8.2 解决TIME_WAIT状态引起的bind失败的原因和方法
解决TIME_WAIT状态引起的bind失败的原因和方法可以分为以下几个方面:
-
TIME_WAIT状态产生的原因:
- TIME_WAIT状态是TCP协议中的一种状态,出现在主动关闭连接的客户端发送完最后一个ACK报文后。该状态持续的时间是两倍的MSL(Maximum Segment Lifetime),以确保网络中的所有数据包都已过期并被丢弃。
- TIME_WAIT状态的存在是为了实现TCP全双工连接的可靠释放,并防止旧连接的数据在网络中未过期的情况下被新连接误认为是自己的数据。
-
TIME_WAIT状态引起的bind失败的原因:
- 当服务器端的TCP连接在TIME_WAIT状态下没有完全断开,尝试在该端口上重新监听,就会发生bind失败。因为TIME_WAIT状态下,地址和端口仍然被占用,不允许在同一端口上创建新的连接。
- 服务器需要处理大量客户端的连接,特别是短生命周期的连接,但每个连接都可能进入TIME_WAIT状态。当新来请求的客户端连接的IP和端口号与处于TIME_WAIT状态的连接相同,就会出现bind失败的问题。
-
解决TIME_WAIT状态引起的bind失败的方法:
- 使用setsockopt()设置socket选项:通过设置socket描述符的SO_REUSEADDR选项为1,允许创建端口号相同但IP地址不同的多个socket描述符。这样,即使上一个连接处于TIME_WAIT状态,新的连接也可以在相同的端口上建立。
- 合理控制和管理连接:尽量减少短生命周期连接的数量,或通过合理的超时设置避免过多的TIME_WAIT状态的连接。对服务器进行适当的优化和调整,以减少需要处理的大量短生命周期连接的数量。
- 选择合适的网络协议栈:根据具体的应用场景和需求,选择适合的网络协议栈。不同的协议栈可能具有不同的性能和行为特点,包括对TIME_WAIT状态的处理方式。
- 监控和诊断工具:使用适当的监控和诊断工具,如netstat、ss或tcpdump等,来检查和分析当前的网络连接状态和问题所在。通过监控和分析网络活动,可以更好地了解和解决bind失败的问题。
总之,解决TIME_WAIT状态引起的bind失败需要深入理解TCP协议的工作机制和网络环境的特点。通过合理配置和管理服务器、使用适当的工具和技术,可以有效地解决这个问题,并确保服务器能够可靠地处理大量的客户端连接请求。
8.3 深入理解 CLOSE_WAIT 状态
CLOSE_WAIT状态是TCP协议中的一种状态,表示在一个TCP连接中,一方已经发送了关闭连接的请求,但是另一方还没有完全关闭连接,仍在等待对方的关闭请求。在TCP连接中,当一方发送了关闭连接的请求(FIN),另一方会发送一个确认(ACK)表示接受关闭请求。然后另一方会发送自己的关闭请求,同样等待对方的确认。在这个过程中,如果一方先发送了关闭请求,那么它就会进入CLOSE_WAIT状态。
CLOSE_WAIT状态的产生主要是因为在TCP中关闭一个连接需要双方都同意。一般情况下,如果关闭连接时客户端最后一次发送数据,需要等待服务器响应;若服务器最后一次发送数据,需要等待客户端响应,而导致CLOSE_WAIT状态的产生。
CLOSE_WAIT状态持续时间应该很短,因为一旦收到对方的关闭请求并发送确认后,连接应该很快进入LAST_ACK状态。然而,在某些特殊情况下,可能会出现连接长时间处于CLOSE_WAIT状态的情况。例如,当程序代码问题导致忘记关闭相应的socket连接时,或者当一方超时并直接关闭连接而另一方仍在处理耗时逻辑时。
解决CLOSE_WAIT状态问题的方法包括检查和修复代码中的问题,例如确保及时释放资源并正确关闭连接。此外,适当调整超时设置或增加资源可用性也有助于减少CLOSE_WAIT状态的出现。
总之,CLOSE_WAIT状态是TCP协议中的一种正常状态,表示一方已经发送了关闭连接的请求但另一方还未完全关闭。了解CLOSE_WAIT状态产生的原因和解决方案有助于确保TCP连接的可靠释放和资源管理。
总结:如果服务器上出现大量的CLOSE_WAIT状态,这通常意味着有一些连接未能正确关闭。这可能是由于服务器代码中的问题,例如忘记关闭socket连接或处理关闭请求时发生错误。
在TCP协议中,关闭连接需要经过四次挥手的过程。当一方发送FIN报文请求关闭连接时,另一方需要发送ACK报文确认收到请求。然后,另一方也可以发送FIN报文,等待对方的确认。这个过程需要双方的协调和正确处理。
如果服务器没有正确关闭socket,可能会导致连接长时间处于CLOSE_WAIT状态。这可能是由于代码中的BUG,例如忘记调用close()函数来释放资源。解决这个问题的方法是检查和修复代码中的问题,确保及时释放资源并正确关闭连接。
在编写网络应用程序时,建议使用适当的错误处理和资源管理机制,以确保连接能够正确地关闭。例如,可以使用异常处理来捕获和处理错误,以及使用智能指针或RAII(Resource Acquisition Is Initialization)技术来自动管理资源生命周期。这些机制可以帮助减少代码中的BUG,并确保TCP连接的可靠释放和资源管理。
8.4 滑动窗口
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时
间重叠在一起了).
您描述的是TCP协议中的流量控制和确认机制。确实,TCP为了保证数据的可靠传输,采用了确认应答(ACK)机制,即每个发送的数据段(通常称为TCP段或数据包)都需要接收方返回一个确认应答,以确保数据已经被成功接收。这种机制确保了数据的可靠性和顺序性。
然而,您指出的性能问题也是存在的。特别是在网络延迟较大或带宽较小时,每个数据段都等待ACK会导致传输效率降低,因为发送方在收到上一个数据段的ACK之前不能发送下一个数据段。这种方式称为“停止-等待”协议。
为了解决这个问题,TCP采用了几种技术来提高性能:
-
窗口机制:TCP引入了窗口的概念,允许发送方在不需要等待每个数据段的ACK的情况下连续发送多个数据段。接收方会确认接收到的最后一个连续的数据段,这样发送方就知道哪些数据段已经被成功接收,哪些还需要重发。这种机制允许数据段的传输和确认过程重叠,从而提高了性能。
-
滑动窗口:滑动窗口是窗口机制的一种实现,它允许窗口在确认接收到的数据段后向前“滑动”,从而包含更多未确认的数据段。这减少了等待时间,并使发送方能够更连续地发送数据。
-
选择确认(SACK):在某些情况下,接收方可能只收到了部分数据段,而不是整个窗口的数据。为了更高效地处理这种情况,TCP可以选择性地确认接收到的数据段,而不是仅仅确认最后一个连续的数据段。这样发送方可以只重发丢失的数据段,而不是整个窗口的数据。
-
Nagle算法:为了减少小数据段的传输,Nagle算法被引入到TCP中。它允许发送方将多个小数据段组合成一个较大的数据段进行发送,从而减少了网络中的数据包数量和确认应答的数量。然而,Nagle算法也可能会导致延迟增加,因为它会等待足够的数据或ACK来触发发送。
-
TCP延迟确认:为了提高效率,接收方可以选择延迟发送ACK,而不是对每个接收到的数据段立即发送ACK。它可以将多个ACK组合成一个ACK进行发送,或者等待一段时间后再发送ACK。这样可以减少网络中ACK的数量,但可能会增加数据重传的风险。
综上所述,TCP通过引入窗口机制、滑动窗口、选择确认、Nagle算法和延迟确认等技术来优化性能,并减少等待时间。这些技术使得TCP能够在保证数据可靠性的同时,实现更高效的数据传输。
8.5 流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)
;
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数
据段, 使接收端把窗口大小告诉发送端.
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
8.6 拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍
然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
- 此处引入一个概念程为拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快.
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
TCP拥塞控制这样的过程, 就好像 热恋的感觉
8.7 延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
- 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
8.8 捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说
了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”;那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端。
8.9 面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出
去; - 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可
以写数据. 这个概念叫做 全双工。
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次
read一个字节, 重复100次;
8.10 粘包问题
HTTP中的粘包问题主要出现在TCP/IP协议中,因为TCP/IP协议是面向字节流的,不具有消息边界。当多个HTTP消息被封装到同一个TCP包中时,接收方需要解析出每个消息的边界,这就是粘包问题的根源。
解决粘包问题主要有以下几种方法:
- 固定长度的消息:每个消息都采用固定长度的格式,这样接收方就可以根据长度来分割消息。但是这种方法不适合可变长度消息的情况,而且如果消息长度太短,会浪费空间。
- 特殊分隔符:每个消息都以特殊的分隔符结束,接收方通过查找分隔符来分割消息。这种方法简单易懂,但是要求分隔符不能出现在正常的消息内容中,否则会导致解析错误。
- 包头+内容:每个消息都有一个包头,用于指明消息的长度或其他标识信息。接收方通过解析包头来获取消息的实际长度或标识信息,从而正确地分割和解析消息。这种方法能够处理可变长度消息,但是增加了协议的复杂性。
- 分段传输:将每个消息分成多个小段进行传输,接收方通过连续接收小段并重组出完整的消息。这种方法能够避免粘包问题,但是需要复杂的逻辑和额外的传输开销。
在实际应用中,可以根据具体场景和需求选择适合的解决方法。例如,HTTP/1.1协议中采用了特殊分隔符(即CRLF)来标识消息的结束,同时支持消息体的分块传输,从而在一定程度上避免了粘包问题。但是,在某些情况下仍然需要采取额外的措施来解决粘包问题,例如使用HTTP/2协议中的多路复用技术等。
9 TCP异常情况
TCP异常情况包括但不限于以下几个方面:
- 服务器崩溃或重启:当服务器主机崩溃或重启时,客户端可能会发送数据到服务器,但由于服务器主机崩溃或重启,客户端可能会超时并重传数据。如果服务器主机在崩溃或重启期间未发送确认应答,客户端可能会关闭TCP连接。
- 网络断开:当服务器主机或客户端与网络断开连接时,客户端可能会尝试向服务器发送数据,但由于目的主机不可达或目的网络不可达,客户端可能会超时并重传数据。如果网络断开时间较长,客户端可能会关闭TCP连接。
- 服务器处理缓慢:如果服务器处理请求的速度较慢,客户端可能会超时并重传数据。如果服务器一直处理缓慢,客户端可能会关闭TCP连接。
- 恶意攻击:恶意攻击可能导致TCP连接异常,例如SYN洪泛攻击和DoS攻击等。这些攻击可能会使服务器资源耗尽,导致TCP连接无法正常建立或传输数据。
- 协议缺陷:TCP协议本身可能存在缺陷,例如TCP的慢启动和拥塞控制机制可能导致网络拥塞或数据传输延迟。虽然这些缺陷可能不会直接导致TCP连接异常关闭,但它们可能会导致数据传输效率低下或不稳定。
为了处理这些异常情况,可以采用一些技术手段,例如使用超时重传机制、流量控制和拥塞控制机制等来保证TCP连接的可靠性和性能。同时,也可以采用一些高级技术,例如使用代理服务器、负载均衡和容错机制等来提高系统的可用性和稳定性。
10 TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
确实,TCP之所以复杂,主要是因为它要同时保证可靠性和性能。下面是对您提到的各个点的简要解释:
- 校验和:这是一种错误检测机制,用于检查数据在传输过程中是否发生错误。通过在发送端计算数据的校验和并将其发送到接收端,然后在接收端重新计算校验和并与发送的校验和进行比较,可以检测出任何传输错误。
- 序列号:TCP通过为每个字节分配一个唯一的序列号来确保数据的按序到达。这样,接收端可以重新组织数据包的顺序,确保数据以正确的顺序到达。
- 确认应答:这是TCP可靠性的另一个关键组成部分。当接收端成功接收到数据时,它会发送一个确认应答给发送端,表示数据已安全接收。如果发送端没有在一定时间内收到确认应答,它会重发数据。
- 超时重发:当发送端未在预定时间内收到确认应答时,它会重发数据。这是一种处理网络延迟或数据丢失情况的机制。
- 连接管理:TCP使用一系列的连接建立(三次握手)和关闭(四次挥手)过程来管理连接。这些过程确保了双方都知道连接的状态,并能够正确地开始和结束数据传输。
- 流量控制:为了防止接收端来不及处理接收到的数据,TCP使用滑动窗口机制进行流量控制。这种机制允许发送端动态地调整发送数据的速度,以便与接收端的处理能力相匹配。
- 拥塞控制:当网络拥塞时,TCP会减慢发送数据的速度,以防止过多的数据在网络中造成拥塞。这种机制通过一系列算法(如慢开始、拥塞避免、快重传和快恢复)来动态地调整发送数据的速度。
- 滑动窗口:这是一种流量控制的机制,允许发送端根据接收端的处理能力动态地调整发送数据的数量。窗口大小表示接收端可以缓冲的数据量。
- 快速重传:这是一种拥塞控制的机制,用于快速恢复丢失的数据包。当检测到丢失的数据包时,发送端会快速重发该数据包,而不是等待超时重发定时器到期。
- 延迟应答:在某些情况下,接收端可能不立即发送确认应答,而是延迟一段时间后再发送。这可以在某些情况下提高性能,例如当接收到的数据包与下一个要发送的数据包有关联时。
- 捎带应答:这是指在数据传输过程中,将确认应答与数据一起发送。这可以减少单独的确认包的数量,从而提高性能。
- 定时器:TCP使用各种定时器来管理连接和数据传输。例如,超时重发定时器用于在未收到确认应答时重发数据,保活定时器用于检测死连接等。
总之,TCP通过这些机制提供了可靠的数据传输服务,同时尽可能地提高了性能。这些机制使得TCP能够在各种网络条件下提供稳定、可靠的数据传输服务。
11 基于TCP应用层协议
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
当然, 也包括你自己写TCP程序时自定义的应用层协议;
12 TCP/UDP对比
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较:
- TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
- UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.