目录
1. TCP协议段格式
2. 解包/分用
3. 确认应答(ACK)机制
4. 超时重传机制
5. 连接管理机制
5.1 三次握手
5.2 四次挥手
5.3 TIME_WAIT状态
5.4 CLOSE_WAIT状态
1. TCP协议段格式
- 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
- 32位序号/32位确认号: 后面详细讲;
- 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60
- 6位标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段 - 16位窗口大小: 后面会详细介绍
- 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也 包含 TCP 数据部分 .
- 16位紧急指针: 标识哪部分数据是紧急数据 ;
- 40字节头部选项: 暂时忽略 ;
//TCP头部,总长度20字节
typedef struct _tcp_hdr
{ unsigned short src_port; //源端口号 unsigned short dst_port; //目的端口号 unsigned int seq_no; //序列号 unsigned int ack_no; //确认号 #if LITTLE_ENDIAN unsigned char reserved_1:4; //保留6位中的4位首部长度 unsigned char thl:4; //tcp头部长度 unsigned char flag:6; //6位标志 unsigned char reseverd_2:2; //保留6位中的2位 #else unsigned char thl:4; //tcp头部长度 unsigned char reserved_1:4; //保留6位中的4位首部长度 unsigned char reseverd_2:2; //保留6位中的2位 unsigned char flag:6; //6位标志 #endif unsigned short wnd_size; //16位窗口大小 unsigned short chk_sum; //16位TCP检验和 unsigned short urgt_p; //16为紧急指针
}tcp_hdr;
2. 解包/分用
解包:
- tcp协议是有固定首部的长度的,为20字节,先读取20字节
- 将前20字节转换为上述的结构化数据,立马提取标准报头中的4位首部长度(最大为15)
- 就得到了后续报头(TCP选项)的剩余大小:选项长度 = 4*4位首部长度 - 20
- 只要把tcp报头处理读取完毕,剩下的就是有效载荷的长度。
分用:报头中包含目的Port,就可以找到应用层的进程,数据交付给进程。 Port和PCB实际上被存储在hash中,key:port,value:PCB
3. 确认应答(ACK)机制
在网络传输中,由于传输的距离变长了,就会出现一些不可靠的问题。例如:丢包、乱序、校验错误、重复...... 为了保证tcp传输的相对可靠性,提出了一种确认应答机制。
在双方通信的过程中,除了传输信息的数据段,还包括确认应答的数据段。tcp数据段需要有方式标识数据段,为此包头中包含了序号和确认序号。
确认序号为1001,表明已经收到了1001之前的所有连续的报文。TCP将每个字节的数据都编号:
每个ACK都带有对应的确认序号,意思是告诉发送者,我已经收到了哪些数据,下一次从哪里发送。在TCP中,数据到达的顺序和数据发送的顺序可能是不同的,那么接收方就根据数据段段报头中的序号来确认顺序,将数据排好序。
4. 超时重传机制
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时
- 时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
TCP在发送数据的时候,不能发送的过快(接收方可能来不及接收),也不能太慢(效率低)。那么如何保证传输的速度呢?-- 根据对方的接收缓冲区的大小来确认发送速度的。
16位窗口大小:是用来进行流量控制(后面会详细讲解)的,在接收方的确认ACK中设置,标识接受方的接受缓冲区中还剩余多少空间,也可以说是:发送方在接收到下一个ACK之前可以发送的最大的数据量大小。也就是说,该位置在填充的时候是填充自己接收缓冲区的大小。
6个标志位:
- SYN:链接时设为1
- FIN:断开连接时设为1
- PSH:催促接收方让上层尽快把数据拿走
- ACK:确认数据段设置为1
- URG:指示TCP段中存在紧急数据,当URG被设置为1时,表明该段存在紧急数据,告知接收方应该优先处理该段中的紧急数据。这个紧急数据又被16位紧急指针指出:指出紧急数据的最后一个字节的序列号。紧急数据的开始位置 = (紧急指针 - 当前段的序列号) + 1;
- RST:强制断开一个TCP链接(不是一个正常的关闭链接的机制)。
5. 连接管理机制
在正常情况下,TCP都要经过三次握手链接和四次挥手断开。
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
- [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态
5.1 三次握手
上图所示,第一次握手客户端向服务端发送的SYN请求段中的初始序号为client_isn;
第二次握手时服务端向客户端发送SYN请求和ACK应答,初始序号为server_isn,确认序号为client_isn+1(即服务端下次期待收到客户端发出的序号为client_isn+1的段);
第三次握手客户端向服务端发送ACK应答,序号seq为client_isn+1,确认序号为server_isn+1(即客户端下次期待收到服务端发出的序号为server_isn+1的段),到此握手结束,服务端和客户端建立链接。
为什么需要三次握手呢?
一次握手?-- 不行,一次握手无法同步序列号、无法验证对方的接收能力、受到SYN洪水;
两次握手? -- 不行,无法防止旧的连接请求,造成资源的浪费、SYN洪水;
三次及以上? -- 三次就足够了,多了会浪费时间空间资源。
那三次握手为什么行呢?
- 用最小成本验证全双工通信信道时通畅的
- 可以有效防止单机对服务器进行攻击
三次握手最害怕的时第三次握手的ACK丢失,若其丢失,在经过2*MSL后服务端会再次发送SYN-ACK数据段,客户端不会将其丢弃,而会接收并且再发一次确认ACK,直到客户端服务端建立连接。且在一个OS中,必然存在很多个TCP链接,这些TCP是要被管理起来的(先描述再组织),也就意味着是有成本的。
5.2 四次挥手
如上图所示,在TCP链接中,若一方不想继续通信/没有要发送的数据时,会主动的提出关闭TCP链接;
第一次挥手,客户端发送FIN段到服务端,FIN=1,序列号为m,此时客户端不在发送数据但仍可以接收数据,服务端接收到FIN后,进入半关闭状态其可以接受和发送数据;
第二次挥手服务端向客户端发送一个ACK段,以此来确认收到了客户端FIN请求;在第三次挥手之前服务端仍然可以向客户端发送数据(不会发送新的数据),客户端也会发送ACK确认;在所有发送的有效数据的段都接收到ACK应答后,服务器才会第三次挥手;第三次挥手之前所有的数据传输和ACK应答都应经被确认。
第三次挥手,当服务端么已有数据向客户端发送时,也会向客户端发送FIN段;
第四次三挥手,客户端收到服务端发送的FIN段后,会向服务端发送一个ACK确认报文,服务端收到后,断开连接。
而客户端在发送完最后一个ACK会等待2*MSL的时间在断开连接。-- 目的:1. 保证最后一个ACK尽可能的被对方收到 2. 防止旧的连接请求被当为新的连接请求。这段时间内存在在网络中的数据报是之前由于网络拥堵或者其他情况,导致的服务端/客户端是未应答的报文。
主动断开连接的一方,最终状态未TIME_WAIT状态;被动断开连接的一方,两次挥手后会进入CLOSE_WAIT状态。
5.3 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的值
- MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到 来自上一个进程的迟到的数据, 但是这种数据很可能是错误的); 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这 时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK)。
如何解决这种问题呢?使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符。
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SOREUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
5.4 CLOSE_WAIT状态
在基于tcp编写的服务器中,我们在最后不关闭sock,也就是不执行close(sock),服务端就不会发起第三次挥手,不会发送FIN段,此时的服务器就会一直处于CLOSE_WAIT状态,原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确 完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题。