目录
[紧急指针 | URG标记位](#紧急指针 | URG标记位)
端口号
在TCP/IP协议中, 用 ++"源IP", "源端口号", "目的IP", "目的端口号", "协议号"++ 这样一个五元组来标识一个通信(可以通过netstat -n查看);
这样即使目的IP和端口相同,源IP和源端口的不同也能区分请求来源。

这其中,IP协议(网络层)负责++源和目标IP以及上层(传输层)的协议++ ,TCP协议(传输层)负责++源和目标端口++
由于这两个协议的重要性,所以才把目前最常用的网络分层叫做TCP/IP四层模型
对于端口号,只需要保证 进程 --> 端口号 的唯一关系,而不需要保证 端口号 --> 进程 的唯一关系。因此一个端口号只能绑定一个进程,但一个进程可以绑定多个端口号
端口本质上是进程的"门",开多个门是为了让不同来源、不同目的的流量各走各的通道,实现关注点分离
例如,一个进程同时提供多种服务:
bash
进程:某游戏服务器
├── 端口 8080 → HTTP API(管理后台)
├── 端口 9090 → WebSocket(实时通信)
├── 端口 5000 → TCP(游戏逻辑)
└── 端口 5000/UDP → 语音聊天
UDP
在传输层中,若使用的是UDP协议,报文格式如下

- 源和目的端口的作用与TCP中一样,是为了区分请求资源与响应资源 。由于UDP报文内规定端口号是16位,所以我们在编码阶段时端口号的类型是uint16_t
- UDP长度即为整个UDP报文(首部+数据部分)的长度,由于它只有16位,所以一个UDP报文最大约为64KB 。
若想传输的报文超出了这个范围,就需要拆分为多个UDP报文发送 - UDP校验和检测数据报在网络传输过程中是否发生了比特错误(数据损坏), 核心思想与HTTPS中证书内的哈希摘要类似
在编码阶段(应用层)的sendto系统调用,并不是将数据直接发送给网络,而是交给了下层(传输层),就在UDP报文的数据内
UDP的封装/解包与复用/分用
封装/复用:当数据从应用层向下交付给UDP(传输层)时,只需给数据添加8字节的UDP报头后继续向下交付即可
解包/分用:当数据从下往上交付到传输层时,网络层成功交付给了UDP协议,此时只需将固定的前8字节报头剥开,通过看报头内的目的端口号,将剩下数据继续往上交付即可
TCP协议有自己的发送缓冲区和接受缓冲区,以此达到全双工

这是因为TCP是面向字节流的,假设客户端发送了10段报文,但服务端并不知道客户端发送了几段报文,只是全接收过来 (可能并不需要接收10次,而是接收1次2次)。相对的,要发送10段报文时,客户端也不一定是分10次发放,可能1次就发完 了。这就是发送缓冲区和接收缓冲区存在的原因。因此使用TCP有时需要在应用层确保一次接收的是一段完整的报文
而UDP是面向数据报的,假设客户端发送了10段报文,服务端也必须花10次接收完 ,不能一次接收2段、1.5段、0.5段等等,因此使用UDP协议不需要确保一次接收的是一段完整的报文。
因此UDP不需要发送缓冲区,当有报文要发送时,UDP会直接将该报文交给操作系统,不会停留攒着一起发
但UDP有接收缓冲区 ,当上层还在处理之前接收过来的报文 时,没有空来接收新数据,因此需要接收缓冲区(UDP的接收缓冲区不能保证顺序不变,这也是不可靠的特点之一)
UDP通过这一特点也是全双工通信
TCP
TCP(Transmission Control Protocol)全称传输控制协议,在上面讲过,TCP有发送/接收缓冲区,当应用层将数据拷贝给TCP的发送缓冲区后,什么时候发都是由TCP决定
TCP的报文结构如下:

源和目的端口的作用与上面UDP时一样
4位首部长度表示的是整个TCP的报头长度 。虽然只有4位,最多可以表示到15,但该字段的单位是字节,即最多 可以表示到15*4 = 60字节的报头长度
标准报头或者说最小报头是20字节 ,还有一个选项字段是可有可无的,为了知道选项到底有多少字节,就可以用4位首部长度所表示的字节数减去20字节 (即TCP报头的总长度在20,60)
若要发送的数据大于64KB,就会被拆成多个报文发送
例如,在send接口中,参数中的len 表示想发多少字节 ,返回值表示实际发了多少字节 ,可以通过判断返回值来循环发送

为什么TCP报文没有可以表示数据长度的字段?
TCP是字节流传输 ,无需告知发送了几段报文,完全取决于对方接收时的策略
Linux是用C语言编写的, 表示报文这种结构化的数据就会用struct结构体
在UDP时,由于都是16位字段,可以直接用uint16_t类型表示
cpp
struct udp_hdr
{
uint16_t src_port; // 源端口号
uint16_t dsc_port; // 目的端口号
uint16_t length; // UDP报文长度
uint16_t check; // 校验和
};
但在TCP中,有例如4位首部长度的字段,此时就要用位段表示
cpp
struct tcp_hdr
{
uint16_t src_port; // 源端口号
uint16_t dsc_port; // 目的端口号
uint32_t seq; // 序号
uint32_t ack_seq; // 确认序号
uint16_t header_length:4; // 4位首部长度 (位段表示)
// ......
};
在应用层将数据拷贝给传输层时,传输层直接在该数据前面加上该报头即可
解包与分用
在这之前,我们都说传输层协议通过报文中的目的端口号来分用给特定进程,具体是怎么找到进程的呢?
在内核中OS会维护哈希表 ,里面是port与进程PCB的映射关系 ,在编码阶段的**bind()**其实就是将特定进程与特定port绑定了起来(大体可以这么理解)
当找到进程PCB后,由于在PCB中维护这一张files_struct 里的文件描述符表 ,而编码阶段我们的网络通信socket也是文件描述符 ,于是找到对应文件描述符的struct file结构体(描述一个文件的结构),把数据拷贝到该struct_file 的缓冲区中,即完成了数据的分用
用户再通过socket文件描述符从缓冲区去中读取数据

可靠性
网络传输中有不可靠性的本质原因就是距离远,短距离传输没有可靠性一说(例如主机内的各个独立硬件设备之间的通信)
不可靠性的场景有++丢包、乱序、校验错误、重复发送++ 等等。TCP为了保证可靠性,引入了很多机制,对可靠性影响力最大的就是确认应答机制
当双方远距离通信时,若只是给对方发消息,并不能保证对方100%收到,除非对方根据这条消息返回一条"我收到了"的消息

但如果向上图这样B给A返还了一条消息,确实可以保证之前A发的那条消息B肯定收到了,但不能保证B返还的这条消息A肯定收到了,除非A再返还一条消息

但这样的话,又不能保证A新发的这条消息被B收到
因此,双方通信,只有历史消息可以100%确认对方收到,一定存在最新的数据无法保证可靠性(没有绝对的可靠性!)
对于上面例子,"我吃了"这条消息,既包含了对上面报文的确认,也包含了自己要发送的消息,即【应答确认与传递消息的合并】;而"哦哦"这条消息就只有对上面报文的确认,没有自己要传递的消息;
这两种情况是TCP通信中真实的两种情况:若某种服务器不发消息,只对传来的请求做确认,就是第二种情况,反之则第一种

TCP通信机制
序号&确认序号
要理解TCP通信,就不能将TCP看作发送数据的一方与接收数据的一方,无论上层应用层角色如何,TCP层都将双方视为平等的通信实体。所有数据包的地位均等,不区分请求与响应。
在最常见的TCP通信场景中,双方往往都会一次性发送多个TCP报文 (数据段),对方在应答时也一次性发送多个应答报文(对应着发来的报文数量)

但这样数据段到达服务器的顺序可能与发送顺序不同 ,若中间有一个数据段丢失了,客户端怎么才能知道丢失的是哪一个报文呢?这时候就要用到TCP报文结构中的**"序号"和"确认序号"**了
假设客户端发的的数据段序号是10~13,服务端在发送确认数据段时,报文的确认序号就是对应报文序号+1 ,这是告诉服务端在这个确认序号之前的序号报文全部接收到(例如确认序号为11就代表序号11之前的报文全部接收到)

那么如果12号报文在中途丢失,即使传来了13号报文,服务端发送的报文中的确认序号最高也是12

只看上面的例子,貌似只有一个序号就够了,当发送正常数据段时,序号里填本身报文的序号,当发送确认数据段时,序号里填确认序号的数字。
为什么一个TCP报文中要同时包含序号和确认序号?
因为TCP是全双工的,意味着一个报文可以同时完成"发送数据"与"确认数据"两件事 ,那么这个报文就要同时拥有序号和确认序号

窗口大小
由于TCP有自己的发送/接收缓冲区,当双方互相发数据时,发快了会导致对方接收缓冲区满 ,再发报文时对方只能丢弃;发慢了又会导致效率过低。
要想知道现在发送数据是快是慢,就需要知道对方的接收缓冲区还有多大 ,因此窗口大小字段 就是用于表示自己的接收缓冲区的剩余空间的 ,当对方收到报文并查看窗口大小时,就可以根据剩余空间灵活调整发送频率,这正是TCP流量控制的机制
双方在通信时,就可以通过该字段交换接受能力 由于窗口大小只有16位,也就代表TCP中接收缓冲区的默认大小为2^16=64KB(可以修改)
在TCP报头的选项中,包含一个M扩大因子 ,通过左移窗口字段的值来实现窗口大小的扩大,可以与缓冲区大小的更改配合
发送方怎么在第一次就知道对方的接收能力?
在三次握手时,就已经交换了窗口大小
报文类型标志位
TCP的报文是有类型 的,对于一个服务端来说,它可能会同时接收到A客户端的建立连接报文,B客户端的正常通信的报文,还有C客户端的断开连接报文。服务端就需要根据报文类型对A客户端发起三次握手,对B客户端发送/确认数据,对C客户端发起四次挥手

每种报文类型都对应着一种标志位

- SYN:当SYN位 置1时,代表该报文是握手请求(建立连接)
- FIN:当FIN位 置1时,代表该报文是挥手请求(断开连接)
- ACK:当ACK位 置1时,代表该报文也表示了对历史报文的确认 (确认报文或正常报文有着确认的能力),此时就会查看它的确认序号。
一般在建立连接成功后的每个报文的ACK标记位都为1,因为都需要对历史报文做确认(第一个报文不会) - PSH:当PSH位(全称push)置1时,代表催促对方 (接收方),让应用层尽快读取数据 (促使应用层赶快调用read/recv等等接口读取数据)。
但这个用处放在现代OS来说已经没有用了,TCP现在的算法策略很智能,并且很有可能即使看到该标记位也不会理会
紧急指针 | URG标记位
当发送方连续发了很多报文后,接收方在接收时需要靠序号字段来保证有序性 (无序也是不可靠的体现),以确保应用层在读取时是按顺序读取的,即可以看作一个队列
当有想尽快让接收方读取的数据时,就可以将URG标记位置1 ,并在紧急指针字段中设置好该紧急数据(1字节 )在有效载荷的哪个地方(偏移量 )。
当该报文进到对方的接收缓冲区后,应用层就可以调用recv或其他接口先将紧急数据读上来,再继续按顺序读
具体读法:
类似于recv,send的系统调用,都有一个flags参数,

若将参数设置为MSG_OOB,就代表要制造/接收一个紧急数据
这个1字节的紧急数据称为带外数据
RST标记位
RST全称reset,用于重置双方之间的连接
当双方三次握手建立连接成功后,后续也有可能单方面连接出问题。例如服务器重启 了,服务器就会认为该连接不存在 。但客户端视角,由于没有进行四次挥手,客户端还会认为连接存在,就会继续向服务器发报文
而服务器认为我们之间还没有建立连接就直接发报文,已经越界了,因此服务端会发送一个RST标记位置1的报头,代表让客户端重新和它建立连接

ps:实际上即使客户端不发消息,也会因为TCP定时发送的测试报文而被重置
确认应答
发送缓冲区中的数据,每个字节都有对应的编号,该编号就是序号字段中填写的值

若发送的字节是1,1000,该报文内的序号就是1(第一个字节的序号),其余字节位置通过序号+偏移量的方式推算
对方发的应答报文内的确认序号,就是1001,表示1001序号之前的都收到了


超时重传
当丢包时,就会触发TCP的重传机制,将数据再发一份
丢包的情况有两种:
第一种:数据真的在传输到对方时丢失了,发送方就会在一定的时间间隔后重新发送

第二种:数据成功传给对面了,但对面的应答没有传过来,在发送方的视角来看是和第一种一样的,因此在一定时间间隔后依然会重传

这样的话主机B就会收到多次重复数据,这也是不可靠的一种体现,所以可以用序号字段去重 (重复数据的序号是一致的)
主机A也会发送重复的数据,所以数据被发送后不会立即清除,会维持一段时间(滑动窗口)
多少时间算超时?
超时时间不是固定的,因为网络状况也有好与坏两种情况,固定超时时间可能导致效率低下(网络状况好时)或频繁触发重传(网络状况坏时)
一般来说超时以500ms为单位 ,重发一次后若仍没有得到应答,就等待2*500ms后再重传
若仍没有得到应答,就等待4*500ms后再重传,以此类推,指数倍递增(RTO为超时重传时间)
当累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
三次握手
在三次握手中,一般为客户端的一方主动发起连接请求,即将报文的SYN位置1(SYN_SENT状态表示发送同步请求)
服务端返回的报文同时兼顾了建立连接请求与确认上一次报文,即将SYN,ACK位置1(SYN_RCVD状态表示收到同步请求)
当客户端收到服务端的确认后,客户端就认为已经建立连接 了(ESTABLISHED状态表示建立连接),此时再针对服务端发来的建立连接请求发送ACK确认报文,当服务端收到后,服务端就认为已经建立连接 了(因此,客户端和服务器认为连接建立的时间存在差异)
每次握手/挥手时的状态在OS中都用宏表示

在三次握手的过程中,前两次握手中若丢包,由于第一次和第二次握手后都会返回ACK,所以一定时间没有收到后就会重传
但若第三次握手丢包,由于不会再给客户端返回,此时客户端认为建立连接 ,但服务端认为没有建立连接,当客户端发送正常数据段时,服务端会发送将RST标记位置1的报文让客户端重连
为什么是三次握手?(而不是一、二、四次)
对于一个服务器可能会接收到大量的连接请求,在这种情况下,服务器需要区分正在建立、已经建立、正在通信、重传或需要被重置的连接。因此连接也需要被管理,先描述再组织 ,那么维护连接就有时间与空间的成本,服务端和客户端双方都有维护连接的成本
如果只需要一次握手,客户端只需要向服务端发送连接请求,服务端就认为建立连接。若客户端通过死循环一直向服务端发送连接请求(SYN洪水 ),服务端每收到一个连接请求就认为建立了一个连接。客户端的成本只有一次握手,而服务端需要一直维护该连接 ,这样就会导致服务器资源耗尽
如果只需要两次握手,当服务端向客户端发送,可能报文根本没有发送到客户端 ,而服务端认为连接建立好了,依然会让服务端付出与客户端不匹配的成本

而三次握手,可以有效防止两次握手会出现的情况,它可以让双方的报文都**"一进一出"**,并且双方的"出"都有应答来确认,这样双方都可以确认连接,实现全双工

并且三次握手在服务端建立连接时,可以确保客户端一定比服务端先一步建立连接 ,这样即使客户端真的想实施SYN洪水攻击,也要先让自己先承受连接的成本 ,即三次握手可以避免单主机对服务器进行DDoS攻击(但如果是多主机也没办法)
若是四次握手,就代表Client是最后收到确认并建立连接的,无法满足让Client先建立连接 的去请求,并且若最后一个报文丢包,Client端认为没建立连接,但Server端认为建立连接了,会让Server端累积大量未成功的连接请求
若是五次握手,确实可以满足三次握手的规则,但没有必要,单纯的增加握手次数只会浪费资源
四次挥手
在四次挥手中,客户端与服务端都有可能是主动发起断开连接的一方,这里以客户端为例。
因为TCP连接是全双工,要断开连接就要让双方都认同,若客户端先发起断开连接请求,服务端收到后给客户端一个应答。服务端再发送断开连接的请求,客户端收到后再给个应答

在四次挥手中,被动方(这里是服务端)的ACK与FIN不一起发,这是因为主动方(这里是客户端)想要关闭连接了,被动方只需要给个回应代表收到即可,但此时被动方可能还有没发完的是数据 ,不能直接断开连接。当数据发完,应用层调用了close()接口 后,被动方的TCP层再发送断开连接请求
当服务端的 TCP 收到客户端的 FIN 时:
- TCP 层可以立刻回 ACK ------ 因为确认收到是对方的事,TCP 协议栈自己就能处理,必须尽快回复。
- 但 TCP 层不能立刻发 FIN ------ 因为是否关闭发送方向,取决于应用层是否还有数据要发。应用层可能还在处理请求、还有响应数据没发完。
但如果主动方发来断开连接请求时,被动方此时没有数据要发了,也会将ACK与FIN一并发送,实际抓包中这种情况确实会出现。
为什么主动方收到断开连接的确认后还能向被动方发送ACK确认报文?
TCP报文有的携带用户数据,有的纯粹是控制信息。主动方断开连接后,只是代表不发用户数据(应用层),而像ACK这样的控制报文依旧可以通过底层(传输层)发送。
在四次挥手完成之前,连接并没有真正关闭,TCP 协议栈始终在正常工作,纯控制报文(ACK/FIN/RST)的发送从来都不依赖应用层,它们是传输层自己的职责。
为什么建立了连接就可以确保可靠性?
TCP在三次握手后,双方都会维护一个TCP控制块(TCB),TCB的维护使得TCP的可靠性机制(例如超时重传,流量控制等)得以实施,因此建立的连接间接保证了TCP的可靠性
双方状态变化
虽然在四次挥手中双方的状态变化这么多,但只需要记住两个:主动方的TIME_WAIT 和被动方的CLOSE_WAIT

当主动方关闭连接后,被动方会处于CLOSE_WAIT,预关闭状态,但此时还没有关闭,
当主动方确认对方也关闭后,就会进入TIME_WAIT状态,等待一定时间后自动关闭
若被动方始终不调用close(),就始终不会发起断开连接请求 ,就可以长期让连接在CLOSE_WAIT状态
在实际通信中,若服务端长期处于CLOSE_WAIT状态,只有两种可能:服务器端代码未正确关闭文件描述符、服务器高压力持续推送消息,没空close()
TIME_WAIT状态的持续时间为2*MSL ,MSL为最大报文段生存时间,它的目的是为了罩住最大的 RTO(超时重传时间) ,确保报文在重传前不会"老死"在网络里。
MSL的时间可以通过下面命令查看
bash
cat /proc/sys/net/ipv4/tcp_fin_timeout

主动断开连接的一方最后都会进入TIME_WAIT状态,要等待一定时间才能真正关闭 ,这是为了确保最后一个ACK到达,若最后一个ACK丢包,被动方就会触发超时重传重新发送FIN报文,若此时主动方已经彻底关闭,就无法收到该报文,就会连接异常(当然TCP也有解决异常的机制)。

而TIME_WAIT持续2倍MSL时间,就正好对应了一次ACK和一次重传的FIN的最大时间
TIME_WAIT的存在还有一个原因:让网络中滞留的报文消失,避免新旧数据包混淆。若对方还有没有发送完的数据,也需要接收完再关闭,不然之后在建立连接时会收到这些旧数据段(虽然TCP有机制会丢掉这种段)
服务器重启失败问题
由于主动断开连接的一方最后会进入TIME_WAIT状态,若服务端主动断开连接,就会在TIME_WAIT的时间内不能再绑定同一端口号
若在没有连接的情况下服务器重启,不需要四次握手,也就不会存在TIME_WAIT状态,因此不影响

若有连接的情况下客户端先断开,服务端再重启也不影响(这里是用Windows的telnet连接的服务端)

若是有连接的情况下服务端先断开,就会因为原端口号还在TIME_WAIT而启动失败

要想解决这个问题,需要在创建好listen套接字后,执行下面代码:
cpp
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
意思是对listenfd套接字设置了【允许地址重用】,即使端口在TIME_WAIT状态,也可以立即重新绑定该端口
滑动窗口
发送方通过接收方报文的窗口大小控制发送速度,当接收方的窗口大小为0时,发送方会暂时停止发送。同时会有两种策略一并开始执行,优先处理先到达的数据:
- 发送方每隔一段时间会发送一个窗口探测报文,用于知晓当前对方的窗口大小
- 接收方当接收缓冲区有空间了会,会主动向发送方发一个窗口更新通知报文,用于告知对方当前自己的窗口大小
并且在实际的传输中,是批量发送数据而非串行一发一应答以提升效率,如图:

在上面说过,发送的数据在没有收到应答前需要暂时保存起来,为了支持超时重传,而它就存在发送缓冲区
在发送缓冲区中,有数据的区域被分成了三部分,已经发送且未应答的数据会被两个指针圈起来,这个区域就是滑动窗口
ps:正确来讲是四部分,滑动窗口内又被分为了已发送&&未应答和未发送,这是因为MSS的限制,导致滑动窗口内数据不一定会被一次性发完,但这里先暂时不考虑

应用层要发送的数据会先拷贝到紫色区域,等待滑动窗口右移发送
滑动窗口的初始大小是根据对方的窗口大小(以及后面讲的拥塞窗口)来设定 ,win_start默认为0,win_end = win_start + tcp_win(对方窗口大小),这样可以保证发送的数据一定可以被对方接收
win_start的移动取决于对方发过来报文的ACK确认序号,拿上面图片举例,若收到的ACK确认序号为1501(1501之前的所有字节我都收到),win_start就会移动到1501位置
win_end的移动取决于对方报文中的窗口大小(以及后面讲的拥塞窗口大小):若对方窗口大小变大了,win_end就会往右移动(滑动窗口变大),相反则往左移动(滑动窗口变小)
如果收到的ACK确认序号不是滑动窗口最左侧,而是中间的怎么办?
若只是对方收到数据的应答丢失了,由于确认序号的机制是这之前的字节都收到,那么只要后续应答的确认序号包括了最左侧的字节,就会认为左侧也收到了

若是数据丢包,后续应答的确认序号也只能是丢包开始的序号

当收到3个重复确认序号的ACK时,就会触发超时重传。(滑动窗口不会向左滑动)
为什么是3个?
当收到第一个时,很可能只是网络抖动导致乱序,再等等看
当收到第二个时,还是可能是乱序,继续等
当收到第三个时,大概率是真的丢了(折中)
并且,当收到第三个时,就代表丢包之后,至少有3个后续包成功到达 ,这说明网络通畅。反过来,如果只是乱序,一般来说不太可能连续3个包都跑到了前面
滑动窗口也不一定一直滑动,若对方一直未从接收缓冲区拿数据,滑动窗口就会变为0
实际上,发送缓冲区也不是线性结构,而是环形结构,当滑动窗口走到最后时,会通过模除重新回来最左侧
为什么TCP在发送数据时要将数据拆成好几个报文,不能一个报文发完吗?
下层网络层IP协议会将报文分片,并且数据链路层对报文大小有限制
而且从设计哲学角度看。TCP拆分数据更是为了实现精准重传、网络公平、精细控流、低延迟
拥塞控制
滑动窗口,是TCP提高效率的机制,而滑动窗口考虑的是端到端 的问题,但在报文发送到对方之前,需要先经过网络,如果网络出现了问题,TCP也有自己的策略,即拥塞控制

若丢包严重,不应该超时重传,这会让本来就有负担的网络雪上加霜!
此时应该让客户端暂停传数据,等网络缓过来再继续传
就像滑动窗口体现了对方的接受能力一样,还有一个拥塞窗口体现网络的接受能力
而决定发多少数据的是发送缓冲区内的滑动窗口,因此滑动窗口的大小还与拥塞窗口有关,滑动窗口大小最终是 Min(对方窗口大小,拥塞窗口大小)
TCP采用慢启动策略,当发送刚开始时,设置拥塞窗口为1 ,每收到一个ACK确认,拥塞窗口+1,这样指数级增长到ssthresh(慢启动阈值) 时,变为线性增长。
当TCP识别到网络拥塞时 ,会将慢启动阈值变为原来拥塞窗口大小的一半,并将拥塞窗口置1重新开始

慢启动算法的精髓在于只有开始慢,但增长快,是想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
延迟应答
当发送方发送给接收方报文后,若接收方紧接着就给应答,通告的窗口大小就只能是没有处理数据之前的

但如果先等一会,可能应用层马上就要将数据取走了,取走后再应答,就可以通告更大的窗口大小,这就是延迟应答。
当然,应用层拿走数据也是概率问题,若等了一段时间后应用层依然没拿数据,那也没招

并不是所有报文都会延迟应答,若在延迟应答时有第二个数据包,第二个就不会进行延迟应答(即同时最多只能有 1 个数据包处于"被延迟应答"的状态 ),还有其他情况例如有数据需要发送(捎带确认)、接收窗口发生变化、收到乱序报文时也不会延迟应答
捎带应答
在 发送方发送数据后,接收方若也要发数据,通常会将ACK应答也一起夹在该数据包中一并发出,即捎带应答
粘包问题
由于TCP是面向字节流,本身不关心报文与报文的边界 ,它的眼里只有字节,确定一个完整的报文是由应用层自主完成的
如果应用层没有做到分离报文 ,确认一个完整报文的工作,就有可能导致读取第一个报文时出现漏读或误读,从而影响后续报文的读取,这就是粘包问题
要想避免该问题,可以通过定长、特殊字符、自描述的方式明确报文边界
粘包问题只会出现在TCP,因为UDP面向数据报,在UDP层就可以确保一次读取一个完整的报文,应用层无需关心
TCP异常情况
当建立连接后,若客户端或服务端其中一方关闭了进程,TCP会正常触发四次挥手
当电脑正常关机时,会在关机前关闭所有用户进程,这其中也会正常触发四次挥手
但当电脑不正常关机(断网),例如拔网线,拔电源时,TCP都来不及反应,没有时间告诉对面,这就导致自己认为连接已经断开,而对方还会认为连接存在
不过当对方发送报文过来时(或保活机制的检测报文),因为没有回应,就可以判断出该连接已断开
全连接队列(listen的第二个参数)
TCP建立的连接分为全连接和半连接 ,分别由全连接队列和半连接队列管理
例如在服务端,开始握手时,收到 SYN 并回复 SYN-ACK 后 ,会将该连接信息放入半连接队列 。三次握手成功 的连接被放在全连接队列,当全连接队列满后,后面来的连接请求大概率被服务器拒绝在外
半连接:

但如果长时间处于半连接队列,客户端超时重传时间到了后会放弃连接
下面我将代码中的listen 接口的第二个参数改为2 ,并在开启监听状态后一直循环sleep(即不作任何操作,例如accpet),再累加对服务器发起连接
cpp
listen(_ListenSock, 2)
这里用telnet模拟4个连接

当用netstat 命令查看连接时,会发现前三个连接都建立成功(双向连接),只有第四个连接的客户端处于SYN_SENT,并且服务端没有该连接

因此可以得知,listen()系统调用的第二个参数就会影响全连接队列的长度 ,是listen的backlog参数值+1
若backlog设置的太大,也会无效,全连接队列长度 = Min(应用程序传入的 backlog,内核参数somaxconn) +1