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 测试工具,而服务器端为本框架。故而下面代码中关于客户端的状态的部分可删可不删。