TCP 协议
文章来自于:https://github.com/xuanhao44/net-lab-2023
1 TCP 协议概览
TCP 协议的内容和 TCP 报文的结构都比较简单。所以不多赘述。这次的重点是客户端和服务器建立连接和释放连接的过程。
2 结合实验框架的实现
这次实验要实现的 TCP 协议的部分只有 tcp_in()(服务器端 TCP 收包)。并且框架给出的提示的部分足够完成实验。但是具体的建立连接和释放的过程,可以去参考王道考研上总结的部分。
比较重要的是服务器的状态。
建立连接:(CLOSE 省略),LISTEN(初始创建的状态),SYN-REVD,ESTABLISHED
释放连接:(先前状态为 ESTABLISHED),(CLOSE-WAIT 被省略),LAST-ACK,(CLOSE 省略)
2.1 流程
- 大小检查。检查
buf长度是否小于 TCP 头部。如果是,则丢弃。 - 检查
checksum字段。如果checksum出错,则丢弃。 - 从 TCP 头部字段中获取以下参数:
source port,destination port,sequence number,acknowledge number,flags,以及window_size等需要的参数,最后注意大小端转换。 - 调用
map_get()函数,根据destination port查找对应的 handler 函数。 - 调用
new_tcp_key()函数,根据通信五元组中的:源 IP 地址、目标 IP 地址、目标端口号,确定一个 TCP 链接 key。 - 调用
map_get()函数,根据key查找一个tcp_connect_t* connect。如果没有找到,则调用map_set建立新的链接,并设置为CONNECT_LISTEN状态,然后调用mag_get()获取到该链接。 (状态
TCP_LISTEN):如果为TCP_LISTEN状态,则需要完成如下功能:- 如果收到的
flag带有rst,则close_tcp关闭 TCP 链接; - 如果收到的
flag不是syn,则reset_tcp复位通知,因为收到的第一个包必须是syn; - 调用
init_tcp_connect_rcvd()函数,初始化connect,将状态设为TCP_SYN_RCVD; 填充
connect字段,包括:local_port,remote_port,ip,unack_seq(设为随机值);- 由于是对
syn的ack应答包,next_seq与unack_seq一致; ack设为对方的sequence number+1;- 设置
remote_win为对方的窗口大小,注意大小端转换;
- 调用
buf_init()初始化txbuf; - 调用
tcp_send()将txbuf发送出去,也就是回复一个tcp_flags_ack_syn(SYN + ACK)报文; - 处理结束,返回。
- 如果收到的
- (状态
TCP_LISTEN):检查接收到的sequence number,如果与ack序号不一致,则reset_tcp复位通知。 - 检查
flags是否有rst标志,如果有,则close_tcp连接重置。 - 序号相同时的处理,调用
buf_remove_header()去除头部后剩下的都是数据。 - (状态
TCP_SYN_RCVD):在RCVD状态,如果收到的包没有ack flag,则不做任何处理。 (状态
TCP_SYN_RCVD):如果是ack包,需要完成如下功能:- 将
unack_seq+1; - 将状态转成
ESTABLISHED; - 调用回调函数,完成三次握手,进入连接状态
TCP_CONN_CONNECTED。
- 将
- (状态
TCP_ESTABLISHED):如果收到的包没有ack且没有fin这两个标志,则不做任何处理。 (状态
TCP_ESTABLISHED):处理ACK的值。- 如果是
ack包, - 且
unack_seq小于sequence number(说明有部分数据被对端接收确认了,否则可能是之前重发的ack,可以不处理), - 且
next_seq大于 sequence number, - 则调用
buf_remove_header()函数,去掉被对端接收确认的部分数据,并更新unack_seq值。
- 如果是
- (状态
TCP_ESTABLISHED):接收数据,调用tcp_read_from_buf函数,把buf放入rx_buf中。 (状态
TCP_ESTABLISHED):根据当前的标志位进一步处理:- 首先调用
buf_init()初始化txbuf; - 判断是否收到关闭请求(
FIN),如果是,将状态改为TCP_LAST_ACK,ack + 1,再发送一个 ACK + FIN 包,并退出,这样就无需进入CLOSE_WAIT,直接等待对方的 ACK; - 如果不是 FIN,则看看是否有数据,如果有,则发 ACK 相应,并调用
handler回调函数进行处理; - 调用
tcp_write_to_buf()函数,看看是否有数据需要发送,如果有,同时发数据和 ACK; - 没有收到数据,可能对方只发一个 ACK,可以不响应。
- 首先调用
- (状态
TCP_FIN_WAIT_1):如果收到 FIN && ACK,则close_tcp直接关闭 TCP;如果只收到 ACK,则将状态转为TCP_FIN_WAIT_2。 (状态
TCP_FIN_WAIT_1):如果不是 FIN,则不做处理;如果是,则:- 将 ACK + 1;
- 调用
buf_init()初始化txbuf; - 调用
tcp_send()发送一个 ACK 数据包; - 再
close_tcp关闭 TCP。
(状态
TCP_LAST_ACK):如果不是 ACK,则不做处理;如果是,则:- 调用 handler 函数,进入
TCP_CONN_CLOSED状态; - 再
close_tcp关闭 TCP。
- 调用 handler 函数,进入
2.2 需要注意的点
tcp_checksum()函数写的很差,建议改成自己之前写的 UDP 的函数,但是注意把 UDP 替换成 TCP。(我就是没替换完全,不过还好通过 Debug 发现了这个问题)- 还是要把 ip.c 代码中 TCP 的判断加上。(同样通过 Debug 发现了该问题)
- 记得开启 Wireshark 的 TCP checksum 验证。
3 实验结果和分析
命令行和测试工具的输入输出:

Wireshark 捕获到的报文数据:(Wireshark 的 tcp.pcap)

可以看到,通信的双方是:
- 192.168.56.1:64505(测试工具,作为客户端)
- 192.168.56.45:61000(框架,作为服务器端)
过程:
- 第 43 组,测试工具向框架发送了 SYN = 1, seq = x = 0。
- 第 46 组,框架向测试工具发送了 SYN = 1, ACK = 1, seq = y = 0, ack = x + 1 = 1。
- 第 47 组,测试工具向框架发送了 ACK = 1, seq = x + 1 = 1, ack = y + 1 = 1。
- 第 48 - 51 组,正常连接。
- 第 56 组,由于我按下了"断开连接",故测试工具向框架直接发送了 FIN = 1, ACK = 1, seq = 4, ack = 4。
- 第 57 组,框架向测试工具发送了 FIN = 1, ACK = 1, seq = 1, ack = 5。
- 第 58 组,测试工具向框架发送了 ACK = 1, seq = 5, ack = 5。
4 实验中遇到的问题及解决方法
在前面的 2.2 已经提到过了。
5 意见和建议
- 纠正一个小 bug:TCP 头部的
checksum16被拼写成了chunksum16。 tcp_in()的注释有一点小问题。不只是服务器端,也包括客户机端。不过本次实验中,客户端为 TCP 测试工具,而服务器端为本框架。故而下面代码中关于客户端的状态的部分可删可不删。