欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > 【Linux网络与网络编程】09.传输层协议TCP

【Linux网络与网络编程】09.传输层协议TCP

2025/4/18 21:16:08 来源:https://blog.csdn.net/2301_80258336/article/details/147172494  浏览:    关键词:【Linux网络与网络编程】09.传输层协议TCP

前言

TCP 即 传输控制协议 (Transmission Control Protocol),该协议要对数据的传输进行一个详细的控制(数据传输时什么时候传输,一次发多少,怎么发,出错了怎么办……)

本篇博客将从下面这张TCP协议格式图展开:

可以看到整体格式为:固定大小 20 字节的报头 + 扩展选项 +数据。

各部分的功能(整体认识,下文依次展开):

源/目的端口号:表示数据是从哪个进程来,到哪个进程去

32 位序号/32 位确认序号:下文详细讲

4 位 TCP 报头长度:表示该 TCP 头部有多少个4 字节

标志位:

         URG:紧急指针是否有效

        ○ ACK:确认号是否有效

        ○ PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走

        ○ RST:对方要求重新建立连接。我们把携带 RST 标识的称为复位报文段

        ○ SYN:请求建立连接。我们把携带 SYN 标识的称为同步报文段

        ○ FIN : 通知对方本端要关闭了。我们称携带 FIN 标识的为结束报文段

16 位窗口大小: 下文详细讲

16 位校验和:发送端填充,CRC 校验,接收端校验不通过则认为数据有问题。此处的检验和不光包含 TCP 首部,也包含 TCP 数据部分

16 位紧急指针:标识哪部分数据是紧急数据

选项: 暂时忽略

TCP是如何完成解包,如何完成分用的?

首先读取报头的20字节,拿到其中的4位首部长度(其基本单位为4字节)就能分离出 报头+选项 与  数据。而后通过目的端口号完成分用。

下面是Linux中的TCP报头部分源码:

struct tcphdr {__u16	source;__u16	dest;__u32	seq;__u32	ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)__u16	res1:4,doff:4,fin:1,syn:1,rst:1,psh:1,ack:1,urg:1,ece:1,cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)__u16	doff:4,res1:4,cwr:1,ece:1,urg:1,ack:1,psh:1,rst:1,syn:1,fin:1;
#else
#error	"Adjust your <asm/byteorder.h> defines"
#endif	__u16	window;__u16	check;__u16	urg_ptr;
};

1. 确认应答机制——可靠性问题

主机A通过网络给主机B发了条消息,这条消息在网络中的传输也是要花费时间的并且主机A不能确定主机B是否收到了消息,因此TCP要求对接收到的消息做一次应答。只要主机A接收到了应答就能确保对端已经接收到了消息,这一机制就是确认应答

但是与此同时我们也产生了新的问题:主机B给主机A发送应答,我怎么知道主机A是否接收到了呢?这就需要主机A对主机B做出的应答再次应答。不过这样的话岂不是无休止了吗?

所以我们可以有这样的结论:长距离通信时没有100%的可靠性,因为总有最新的一条消息是没有应答的。但是对于已经应答过的消息能做到100%可靠的。可靠性指的是历史消息的可靠性。

在上图中我们可以看到,收发消息的动作是串行的,但是这样的效率未免也太低了吧,只有当我接收应答之后我才能发送下一条消息?TCP协议在设计时就考虑到了这一点,因此真实情况下是并行的。并行的话客户端会收到很多的应答,可是客户端怎么能知道这些应答是对那条消息的应答呢?所以要给每个报文都带上编号,这也就是32位序号;同理,确认应答也对应一个编号,即32位确认序号。

确认序号 = 序号 + 1 ,表示在确认序号之前的内容已经全部收到了。

如何理解序号与确认序号?

TCP报头中不存在描述数据长度的字段,这是因为TCP时面向字节流的,它不对报文做任何完整性检测。即在接收到报文之后,它会将报头去掉,剩下的数据放到接收缓冲区中,随着缓冲区数据的不断增多就形成了流式结构,TCP的发送和接收缓冲区大小一般是确定的(16位窗口大小),于是TCP的缓冲区就可以理解为char类型的数组,因此就产生了序号。

收发消息就是典型的生产者消费者模型。

为什么要有两个序号呢?

请求的报文一般是tcphdr+有效载荷,而确认的报文一般是裸的tcphdr。但是其实在确认报头中也可以添加数据,这种机制称为捎带应带机制。这也很大概率上说明了tcp报文就是应答又是数据,故而存在的序号和确认序号更符合实际情况。

2. 超时重传机制——丢包问题

在TCP中丢包可以分为两种情况:1. 数据丢了 2. 应答丢了 

但是主机A怎么能确认是数据丢了还是应答丢了吗?不能,因为从主机A来看都是没有收到应答。那么主机A真的能确定自己的报文丢了吗?也不能,因此TCP只能规定超时重传

这又产生了一个新的问题:如果主机A发送的数据因为网络问题导致超时了,而主机A又重新发了一遍,这不会导致数据重复吗?

报文 = 报头 + 有效载荷 ,在报头中包含的序号字段可以用来去重。

那么超时的时间如何确定呢?

最理想的情况下,找到一个最小的时间来保证 "确认应答一定能在这个时间内返回"。但是这个时间的长短随着网络环境的不同是有差异的,如果超时时间设的太长则会影响整体的重传效率;如果超时时间设的太短则有可能会频繁发送重复的包。TCP为了保证无论在任何环境下都能比较高性能的通信会动态计算这个最大超时时间

Linux 中(BSD Unix 和 Windows 也是如此)超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。如果重发一次之后仍然得不到应答,将会等待 2*500ms 后再进行重传,如果仍然得不到应答,就会等待 4*500ms 进行重传。依次类推,以指数形式递增,累计到一定的重传次数之后,TCP 认为网络或者对端主机出现异常就会强制关闭连接。


当某一段报文段丢失之后,发送端会一直收到 1001 这样的 ACK,这就像是在提醒发送端 "我想要的是 1001" 一样。如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送。这个时候接收端收到了 1001 之后,再次返回的 ACK 就是7001了,(因为 2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。这种机制被称为 "高速重发控制" (也叫 "快重传")

3. 连接管理机制

在正常情况下,TCP 要经过三次握手建立连接,四次挥手断开连接。

三次握手是双方的操作系统自动完成的。三次握手也是存在赌得成分的,赌得是最后一个ACK被对方收到了,如果失败就会发送一个将报头中RST置1的报头,重新建立联系。

四次挥手是由应用层的close触发的。四次挥手断开连接可以看出是需要双方同意的,因为TCP是全双工的,需要关闭两个朝向上的连接。

为什么是三次握手?

三次握手实质上来讲就是四次握手,只不过服务端的ACK和SYN合并形成了捎带应答。

三次握手建立了双方主机的通信意愿,同时验证了全双工通信的流畅性。

三次握手之后我们就建立起了连接,什么是连接?

一个连接一定和一个文件相对应,因为一个连接对应一个sockfd(fd)。连接在操作系统内部肯定会存在很多个,于是操作系统就要对这些连接进行管理——先描述后组织。所以连接就是结构。

状态转化示意: 

 

看到这里各位或许有一个问题:客户端已经关闭连接了,但是服务端还想发送数据怎么办呢?

这里就引出了另一个关闭文件描述符的函数:

 int shutdown(int sockfd, int how);

这里的how就是关闭方式,有SHUT_WR,SHUT_RDWR,SHUT_RD。

3.1 TIME_WAIT状态

首先我们来看这样一个现象:

caryon@VM-24-10-ubuntu:~/linux/TCP_Socket/EchoSever$ ./sever_tcp 
[2025-3-13 12:9:15] [INFO] [2444134] [TCPSever.hpp] [114] - socket succeed
[2025-3-13 12:9:15] [INFO] [2444134] [TCPSever.hpp] [122] - bind succeed
[2025-3-13 12:9:15] [INFO] [2444134] [TCPSever.hpp] [130] - listen succeed
[2025-3-13 12:10:14] [INFO] [2444134] [TCPSever.hpp] [221] - accept succeed connected is 111.61.198.165:29564 sockfd is 4...
^C
caryon@VM-24-10-ubuntu:~/linux/TCP_Socket/EchoSever$ ./sever_tcp 
[2025-3-13 12:10:28] [INFO] [2444504] [TCPSever.hpp] [114] - socket succeed
[2025-3-13 12:10:28] [FATAL] [2444504] [TCPSever.hpp] [119] - bind failed
caryon@VM-24-10-ubuntu:~/linux/TCP_Socket/EchoSever$ netstat -tap
(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 localhost:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:mysql         0.0.0.0:*               LISTEN      -                   
tcp        0      0 _localdnsstub:domain    0.0.0.0:*               LISTEN      -                   
tcp        0      0 _localdnsproxy:domain   0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:6010          0.0.0.0:*               LISTEN      -                   
tcp        0      0 VM-24-10-ubuntu:8888    111.61.198.165:29564    TIME_WAIT   -                   
tcp        0      0 VM-24-10-ubuntu:51530   169.254.0.55:5574       ESTABLISHED -                   
tcp        1      0 VM-24-10-ubuntu:60092   169.254.0.3:http        CLOSE_WAIT  -                   
tcp        0      0 VM-24-10-ubuntu:51538   169.254.0.55:5574       ESTABLISHED -                   
tcp        0      0 VM-24-10-ubuntu:54608   169.254.0.138:8186      ESTABLISHED -                   
tcp        0      0 VM-24-10-ubuntu:52032   169.254.0.4:http        TIME_WAIT   -                   
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN      -                   
tcp6       0      0 ip6-localhost:6010      [::]:*                  LISTEN      -                   
tcp6       0    804 VM-24-10-ubuntu:ssh     111.61.198.165:28845    ESTABLISHED -                   

经过上述操作之后8888号端口变成了TIME_WAIT状态。

这是因为TCP 协议规定:主动关闭连接的一方要处于 TIME_ WAIT 状态等待两个 MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态。我们使用 Ctrl-C 终止了 server,所以 server 是主动关闭连接的一方,在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口。

至于MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同,在 Centos7 上 默认配置的值是 60s。可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值。

那么为什么 TIME_WAIT 的时间是 2MSL?

MSL 是 TCP 报文的最大生存时间,因此 TIME_WAIT 持续存在 2MSL 的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)。同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失,那么服务器会再重发一个 FIN。这时虽然客户端的进程不在了,但是 TCP 连接还在,仍然可以重发 LAST_ACK)。

在上面的现象展示中我们还可以看到 bind 失败了,这是因为在 server 的 TCP 连接没有完全断开之前不允许重新监听。服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求)。这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉) 就会产生大量 TIME_WAIT 连接。由于我们的请求量很大,这就可能导致 TIME_WAIT 的连接数很多,并且每个连接都会占用一个通信五元组(源 ip、源端口、目的 ip、目的端口、协议),其中服务器的 ip 、端口和协议是固定的,如果新来的客户端连接的 ip 和端口号和 TIME_WAIT 占用的连接重复了就会出现问题。

使用 setsockopt()设置 socket 描述符的选项 SO_REUSEADDR 为 1 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符。

3.2 CLOSE_WAIT状态

以我们之前写过的TCP服务器为例,当我们不close掉sockfd时而客户端退出时,可以看到8888号端口对应的服务端状态为CLOSE_WAIT:

caryon@VM-24-10-ubuntu:~$ netstat -tap
(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      2480011/./sever_tcp 
tcp        0      0 localhost:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:mysql         0.0.0.0:*               LISTEN      -                   
tcp        0      0 _localdnsstub:domain    0.0.0.0:*               LISTEN      -                   
tcp        0      0 _localdnsproxy:domain   0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:6010          0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:6011          0.0.0.0:*               LISTEN      -                   
tcp        0      0 VM-24-10-ubuntu:51530   169.254.0.55:5574       ESTABLISHED -                   
tcp        1      0 VM-24-10-ubuntu:60092   169.254.0.3:http        CLOSE_WAIT  -                   
tcp        0      0 VM-24-10-ubuntu:51538   169.254.0.55:5574       ESTABLISHED -                   
tcp        0      0 VM-24-10-ubuntu:8888    111.61.198.169:31802    CLOSE_WAIT  2480011/./sever_tcp 
tcp        0      0 VM-24-10-ubuntu:54608   169.254.0.138:8186      ESTABLISHED -                   
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN      -                   
tcp6       0      0 ip6-localhost:6010      [::]:*                  LISTEN      -                   
tcp6       0      0 ip6-localhost:6011      [::]:*                  LISTEN      -                   
tcp6       0      0 VM-24-10-ubuntu:ssh     111.61.198.169:31668    ESTABLISHED -                   
tcp6       0    280 VM-24-10-ubuntu:ssh     111.61.198.169:31786    ESTABLISHED -    

而当我们关闭之后则不会有这样的显示:

caryon@VM-24-10-ubuntu:~$ netstat -tap
(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      2482667/./sever_tcp 
tcp        0      0 localhost:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:mysql         0.0.0.0:*               LISTEN      -                   
tcp        0      0 _localdnsstub:domain    0.0.0.0:*               LISTEN      -                   
tcp        0      0 _localdnsproxy:domain   0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:33689         0.0.0.0:*               LISTEN      2481073/code-494970 
tcp        0      0 localhost:6010          0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:6011          0.0.0.0:*               LISTEN      -                   
tcp        0      0 localhost:60080         localhost:33689         ESTABLISHED -                   
tcp        0      0 VM-24-10-ubuntu:51530   169.254.0.55:5574       ESTABLISHED -                   
tcp        1      0 VM-24-10-ubuntu:60092   169.254.0.3:http        CLOSE_WAIT  -                   
tcp        0      0 VM-24-10-ubuntu:51538   169.254.0.55:5574       ESTABLISHED -                   
tcp        0      0 localhost:33689         localhost:60080         ESTABLISHED 2481073/code-494970 
tcp        0      0 VM-24-10-ubuntu:54608   169.254.0.138:8186      ESTABLISHED -                   
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN      -                   
tcp6       0      0 ip6-localhost:6010      [::]:*                  LISTEN      -                   
tcp6       0      0 ip6-localhost:6011      [::]:*                  LISTEN      -                   
tcp6       0      0 VM-24-10-ubuntu:ssh     111.61.198.169:31668    ESTABLISHED -                   
tcp6       0      0 VM-24-10-ubuntu:ssh     111.61.198.169:31905    ESTABLISHED -                   
tcp6       0    376 VM-24-10-ubuntu:ssh     111.61.198.169:31786    ESTABLISHED -    

这也是在警示我们服务端使用完毕的sockfd一定要及时关闭

4. 流量控制——滑动窗口问题

这里我们抛出一个问题:客户端可能会给服务端发送大量的数据,但是服务端来不及接收怎么办?

正常来讲就只能丢弃掉了,但是这个也没什么的,因为客户端会重发,可以重发可是会浪费时间和空间的啊,操作系统可不会做这样的事情,所以操作系统的角度就会想,既然来不及接收了,那我们就应该减少客户端发送的数据量,这样的操作我们称为流量控制。但是客户端凭什么进行流量控制呢?客户端是怎么知道服务端来不及接收了呢?这就需要明确服务端的接收能力有什么决定,很显然是由接收方的接收缓冲区的剩余大小决定的。因此在服务端应答时,报头中有一个16位窗口大小的属性,它会将自己的接收缓冲区的大小填入其中,服务端接收到报头也就会明确了。

刚才我们在讨论确认应答策略时对每一个发送的数据段,都要给一个 ACK 确认应答。在收到 ACK 后再发送下一个数据段,这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。既然这样一发一收的方式性能较低,那么我们一次发送多条数据就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。

窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是 4000 个字节(四个段)。在发送前四个段时不需要等待任何 ACK即可直接发送。在收到第一个 ACK 后,滑动窗口向后移动,继续发送第五个段的数据…… 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据才能从缓冲区删掉,窗口越大,则网络的吞吐率就越高。

如何理解滑动窗口?

发送缓冲区可以可以理解为一个 char buff [N]; 的环形结构(基于环形队列的生产者消费者模型),所谓的滑动窗口就是有开始序号到结束序号之间的一段空间区域。滑动窗口的滑动是通过ACK返回的确认序列号和窗口大小决定,因此它是动态变化的。滑动窗口只能向右滑动,因为序列号是连续的。

滑动窗口的大小?

滑动窗口的大小 = min( 对方缓冲区剩余空间的大小,拥塞窗口的大小)。那么问题来了。16位数字最大表示 65535,那么 TCP 窗口最大就是 65535 字节么?实际上,TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M,实际窗口大小是窗口字段的值左移 M 位。

滑动窗口里面的数据为什么是一段一段的?直接干成一个大报文不行吗?

不行,因为数据链路层不允许发送大报文。

5. 拥塞控制——网络问题

虽然 TCP 有了滑动窗口这个大杀器能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据仍然可能引发问题。因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据是很有可能引起雪上加霜的。于是TCP 引入慢启动机制,先发少量的数据探探路,摸清当前的网络拥堵状态后再决定按照多大的速度传输数据。

实际情况如图所示,发送的数据是慢增长的:

拥塞窗口:

在发送开始的时候,定义拥塞窗口大小为 1,每次收到一个 ACK 应答之后拥塞窗口加 1。每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口。像下面这样的拥塞窗口增长速度是指数级别的。"慢启动" 只是指初始时慢,但是增长速度非常快。为了不增长的那么快,因此不能使拥塞窗口单纯的加倍,当拥塞窗口超过慢启动的阈值时,不再按照指数方式增长, 而是按照线性方式增长。

当 TCP 开始启动的时候,慢启动阈值等于窗口最大值,在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回 1。

面对少量的丢包,我们仅仅是触发超时重传,而面对大量的丢包,我们就认为网络拥塞。当 TCP 通信开始后网络吞吐量会逐渐上升,随着网络发生拥堵,吞吐量会立刻下降。拥塞控制归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

6. 延迟应答机制

如果接收数据的主机立刻返回 ACK 应答,这时候返回的窗口可能比较小。假设接收端缓冲区为 1M,一次收到了 500K 的数据,如果立刻应答,返回的窗口就是 500K。但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了。在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些也能处理过来。如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是 1M。窗口越大,网络吞吐量就越大,传输效率就越高。

我们的目标是在保证网络不拥塞的情况下尽量提高传输效率,那么所有的包都可以延迟应答吗? 肯定也不是

数量限制:每隔 N 个包就应答一次

时间限制:超过最大延迟时间就应答一次

具体的数量和超时时间,由于操作系统的不同而不同。一般 N 取 2,超时时间取 200ms。

7. 面向字节流——粘包问题

创建一个 TCP 的 socket 的同时会在内核中创建一个发送缓冲区和一个接收缓冲区。在调用 write 时,数据会先写入发送缓冲区中,如果发送的字节数太长的话会被拆分成多个 TCP 的数据包发出;如果发送的字节数太短就会先在缓冲区里等待,等到缓冲区长度差不多了或者其他合适的时机发送出去。在接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用 read 从接收缓冲区拿数据。

TCP 的一个连接,既有发送缓冲区也有接收缓冲区,那么对于这一个连接的话既可以读数据,也可以写数据,这个概念叫做全双工。

由于缓冲区的存在使得TCP 程序的读和写不需要一一匹配,例如: 写 100 个字节数据时可以调用一次 write 写 100 个字节,也可以调用 100 次 write,每次写一个字节;读 100 个字节数据时也完全不需要考虑写的时候是怎么写的,既可以一次 read 100 个字节,也可以一次 read 一个字节重复100 次。

正是由于TCP的面向字节流的特性,使得存在着粘包问题。

首先要明确,粘包问题中的"包"指的是应用层的数据包。在 TCP 的协议头中没有如同 UDP 一样的 "报文长度" 这样的字段,但是有一个序号这样的字段。这可以使得站在传输层的角度来看,TCP 是一个一个报文过来,并按照序号排好序放在缓冲区中;而站在应用层的角度来看,看到的只是一串连续的字节数据。

应用程序看到了这么一连串的字节数据压根就不知道从哪个部分开始到哪个部分结束,一个完整的应用层数据包如何避免粘包问题呢? 归根结底就是一句话,明确两个包之间的边界。

对于定长的包,保证每次都按固定大小读取即可;对于变长的包,可以在包头的位置约定一个包总长度的字段,从而就知道了包的结束位置,还可以在包和包之间使用明确的分隔符(应用层协议是程序猿自己来定的,只要保证分隔符不和正文冲突即可)。

思考:对于 UDP 协议来说,是否也存在 "粘包问题" 呢?

对于 UDP,如果还没有上层交付数据,那么UDP 的报文长度仍然在。同时,UDP 是一个一个把数据交付给应用层的,这就有很明确的数据边界。站在应用层的角度,在使用 UDP 时要么收到完整的 UDP 报文,要么不收,不会出现"半个"的情况。

8. TCP异常情况

• 进程终止:进程终止会释放文件描述符,仍然可以发送 FIN ,这个和正常关闭没有什么区别。

机器重启:和进程终止的情况相同

机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作就会发现连接已经不在了,就会进行 reset。即使没有写入操作,TCP 自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在的话会把连接释放。

9. 基于 TCP 应用层协议

• HTTP

• HTTPS

• SSH

• Telnet

• FTP

• SMTP

• 自定义的基于TCP的应用层协议

10. 小结

TCP 这么复杂是因为要保证可靠性的同时又尽可能的提高性能。

可靠性:

        • 校验和

        • 序列号

        • 确认应答

        • 超时重发

        • 连接管理

        • 流量控制

        • 拥塞控制

提高性能:

        • 滑动窗口

        • 快速重传

        • 延迟应答

        • 捎带应答

其他:

        • 定时器(超时重传定时器, 保活定时器, TIME_WAIT 定时器等)

11. TCP/UDP 对比

我们说了 TCP 是可靠连接,那么是 TCP 是不是一定就优于 UDP 呢?

TCP 和 UDP 之间的优点和缺点不能简单的、绝对的进行比较。

• TCP 用于可靠传输的情况,应用于文件传输、重要状态更新等场景

• UDP 用于对高速传输和实时性要求较高的通信领域。例如:早期的 QQ、视频传输等场景,另外 UDP 可以用于广播

归根结底,TCP 和 UDP 都是程序员的工具。什么时机用,具体怎么用,还是要根据具体的需求场景去判定。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词