W.Richard Stevens写了很多关于Unix的书籍,不幸于1999离世。他的离去,是计算机界巨大的损失。
著作
- UNIX Network Programming, Volume 2, Second Edition: Interprocess Communications, Prentice Hall, 1999.
- UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI, Prentice Hall, 1998.
- TCP/IP Illustrated, Volume 3: TCP for Transactions, HTTP, NNTP,and the UNIX Domain Protocols, Addison-Wesley, 1996.
- TCP/IP Illustrated, Volume 2: The Implementation, Addison-Wesley, 1995.
- TCP/IP Illustrated, Volume 1: The Protocols, Addison-Wesley, 1994.
- Advanced Programming in the UNIX Environment, Addison-Wesley, 1992.
- UNIX Network Programming, Prentice Hall, 1990.
01 测试方法
PC A服务器:LinuxMint IP:192.168.2.102
PC B客户端:Manjaro IP:192.168.2.108
A主机运行一个Python Socket Server, B主机使用telnet连接。在A主机上使用Wireshark查看TCP报文。
A安装运行Wireshark
apt update
apt install wireshark
sudo wireshark
server.py
import socket
import sys
# create socket
server = socket.socket()
# set port reusse
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
# bind port
server.bind(('0.0.0.0', 8000))
# listing
server.listen(1)
# get client data
while 1:
print("waiting for new client...")
client, addr = server.accept()
print("new client:", addr)
buf = client.recv(1024)
print(buf)
client.close()
在A运行server
python3 server.py
然后在B客户端上telnet连接上 A
telnet 192.168.2.108 8000
然后输入hello,回车给A发送数据。
02 根据Wireshark看懂TCP包首部
![](https://img.haomeiwen.com/i1597444/5b8ad99a68895325.png)
TCP的数据包裹在IP报文中,TCP首部占20字节,一个字节八位。TCP包首部图片的第四行,4位首部+保留6位+6各标志位总共为16位,也就是两个字节。
![](https://img.haomeiwen.com/i1597444/8e18a919687c2579.png)
上图是总过程。
我们先点击第一行,也就是TCP连接握手的第一步。从上到下点击红色实体区域,来查看详细信息。
![](https://img.haomeiwen.com/i1597444/7569d1045a57b3f8.png)
上图我们可以看到,
DF 5E
就是对应的端口号,每一个数字/字符代表4位,两个合起来就是一个字节,DF
就是一个字节。,也就正对应着TCP包首部那张图上的说的16位源端口号,DF 5E
转化为10进制为57182。注:tcp客户端会默认随机绑定一个端口。
- 再往后看两个字节,是8000,也就是目标端口。
- 目标端口后面的是32位序号
2e f2 cc d0
, 它是随机产生的一个序号,为了方便看,可能会显示为0; - 再后面的是32位确认序号,第一次握手,数据都为0
- 后面的
a0
是4位首部长度加上(保留6位的前4位),a0
对应的2进制为10100000
,我们取前面4位,得到4位首部长度。这里我们得到10,首部长40字节(20个已知首部+选项20个字节)。4个bit对应最大是15,得到最多有60个字节, 32bit * 5 = 20字节,32bit*15=60字节。
2020-02-27_23-53.png
- 然后我们看保留位,
a0 02
之间的6位,都为0 - 然后我们看
02
这个数据,02
中的后6位包含了6个标志位,02
对应2进制为0000 0010
,倒数第二个1,也就是TCP包首部中的SYN标志位。也就印证了TCP握手第一步会发送一个SYN为1的报文。 - 然后是
fa f0
窗口大小,窗口大小表明了发送这个报文的主机的接收缓冲区的大小。 -
4e f1
校验和 -
00 00
16位紧急指针 - 选项 我们可以看到后面的20个字节都是属于选项,这20个字节加上前面20个,40个字节,也就是首部的总长度大小。
- 我们可以在选项的数据中得到TCP最大报文长度为
1460
,因为以太网链路层的传输最大长度通常是1500,再减去TCP头部和IP头部得到。如果不设置这个mss,那么就不知道一个报文最大多少才不会让IP协议分片,IP分片会导致传输速率减低。所以有了这个MSS告诉TCP层,你不要给我报文传太多。如果应用层传输的数据大于MSS,那么会TCP会分开这个数据块,分为一个个MSS大小的发出去,也就是常说的分组。UDP则不会,UDP的数据建议也不要太大,需要小于MSS。
第一次握手,客户端B发送了一个SYN为1的报文给服务器A:
- 6个标志位只有SYN设置为了1
- 32位确认序号为0,只有ACK标志位为1才有效
- 32位序号为一个随机值
2e f2 cc d0
其中, 32位序号表示了自己发送了多少,初始的时候会有个不为0值,发送一个会字节+1(发送一个SYN也会+1);
32位确认序号为下一次期望收到的数据序号,同时可以计算得到已经传过来的数据长度。
03 第二次握手
![](https://img.haomeiwen.com/i1597444/2c17cf8930b53ff8.png)
在02节中,我们不仅知道了怎么看对应的数据,我们同时也知道了第一次握手的所有传输的字节数据。
- 查看6位标志位处,发现变为了
12
, 也就是ACK位和SYN位都设置为了1 - 查看32位序号,发现为
79 5b 83 99
,是第一次随机生成的。 - 查看32位确认序号,发现为
2e f2 cc d1
,在第一次握手的32位序号为2e f2 cc d0
,可以看到,发送回去的32位确认序号加了1,同时也代表了A主机收到了A的SYN数据包。
第二次握手,服务器A发送一个SYN和ACK标志都为1的报文给客户端A
- 6个标志位,SYN和ACK都设置为了1
- A服务器产生一个32位序列,以及将从B收到的32位序号+1返回给B客户端
04 第三次握手
![](https://img.haomeiwen.com/i1597444/300a38f30676808f.png)
- 查看32位序号为
2e f2 cc d1
,这个序列代表了自己将要发送的数据的第一个字节的序号 - 32位确认序号为
79 5b 83 9a
,这个值就是B客户端的发送过来的序号+1 - 只有ACK位为1
第三次握手,客户端B给服务器发送了一个ACK标识的报文,用以标识客户端收到了消息。
以下称32位确认序号位Ack,称32位序号位Seq。为了不搞混,理解Seq为自己发送了多少数据,Ack为自己收到了多少别人的数据。
05 B客户端数据传输A服务器
![](https://img.haomeiwen.com/i1597444/d55c257cc79ae0ed.png)
- 查看Seq为
2e f2 cc d1
,和上一次ACK标识报文的Seq一致。说明ACK报文是不消耗Seq - 查看Ack为
79 5b 83 9a
,也是和上一次一致。 - 传输的数据为后面的7位,最后两个是换行符号。
06 A服务器发送数据B客户端确认
- 只有ACK位为1
- Seq=
79 5b 83 9a
,相对于初始Seq,多了1个。一个SYN占一个。 - Ack=
2e f2 cc d8
,相对于初始Seq,多了9个,一个SYN+8字节数据。
07 A服务器告诉B客户端, A服务器不会再向B发送任何数据。A服务器主动关闭发送。
- Seq=
79 5b 83 9a
- ACK和FIN都为1,四次握手的中间两步合并在一起发送。
- Ack为
2e f2 cc d8
服务器代码的close
就会让操作系统去发送FIN
发送这个FIN代表,关闭从A->B方向的数据传输。
08 B收到A的服务器FIN,B也说,我不会再向A服务器发送数据。B客户端主动关闭发送和接收。
- Seq=
2e f2 cc d8
- Ack=‘79 5b 83 9a’, 可以看到FIN也会消耗一个序号。
- FIN和ACK都为1
09 服务器收到B的ACK报文后,执行关闭接收。
- Seq=
79 5b 83 9b
- Ack=
2e f2 cc d9
- ACK=1
整个挥手如下
![](https://img.haomeiwen.com/i1597444/3d6cca5c0003b341.png)
![](https://img.haomeiwen.com/i1597444/0703c5eb46cbaebe.png)
如果在TCP数据挥手握手过程中,任何一方主动关闭了连接,另一方没有手动被动关闭,另一方有可能造成CLOSE_WAIT状态,注意FIN需要自己手动调用close发送。CLOSE_WAIT和LAST_ACK中对应的代码逻辑通常是判断接收到数据是否是EOF,如果是调用close。就会进入LAST_ACK状态。
如果出现,在你的程序里TIME_WAIT连接状态过多的,TCP在设计时候,因为为了防止意外,一个端口在关闭连接后需要等待2MSL时间才能再次使用。
如果你的程序大量的段时间的连接断开socket连接,你就会有可能出现大量的TIME_WAIT。严重的可能会把所有可用端口占满。解决的办法是改变你的代码逻辑和使用SO_REUSEADDR
选项复用端口。
如果在连接握手中,任何一方没有正确的握手,都有可能造成资源的浪费,例如SYN洪水攻击。
窗口大小问题
如果接收端recv处理不过来,一开始会造成自动窗口变大,直到接收缓冲区饱满。可以写一个客户端,一直发送数据,服务端不调用recv就能看到这个问题。
如有不正确的地方,欢迎大家指正!
网友评论