Linux socket 关闭场景

测试环境 :

[[email protected] ~]# cat /etc/system-release
CentOS release 6.9 (Final)

工具

服务器 192.168.1.12 ipython Python 2.7.5

客户端 192.168.1.119 Jupyter QtConsole python3.6.1

测试经过:

为了测试效果,将服务器的发送缓冲区和客户端的接收缓冲区都设置为较小的 128 字节。整个过程需要服务器和客户端两边协同配合。

服务器执行代码和解释:

import socket
import struct
import sys

host = socket.inet_ntoa(struct.pack("i", socket.INADDR_ANY))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, 6667))
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128)
s.listen(5)

场景 1 发送缓冲区为空时关闭套接字

#查看当前的 TCP 状态。

In [14]: os.system('ss -apn | grep 6667')
LISTEN     0      5                         *:6667                     *:*      users:(("ipython",643,8),("sh",21695,8),("ss",21696,8),("grep",21697,8))

sockfd,addr = s.accept()

#至此,服务器将阻塞在 accept 上,直到客户端连接上来。

#一旦 accept 返回,表示客户端已经连接,再次查看连接状态。

In [16]: os.system('ss -apn | grep 6667')
LISTEN     0      5                         *:6667                     *:*      users:(("ipython",643,8),("sh",23052,8),("ss",23053,8),("grep",23054,8))
ESTAB      0      0              192.168.1.12:6667         192.168.1.119:2516   users:(("ipython",643,9),("sh",23052,9),("ss",23053,9),("grep",23054,9))

扫描二维码关注公众号,回复: 13378578 查看本文章

#服务器开始发送数据。由于发送的长度大于发送缓冲区和接收缓冲区的长度之和。如果接收缓冲区不进行接收,服务器将一直阻塞在 send 上。在客户端进行多次读取,直到服务器刚好从 send 上返回。

In [19]: sockfd.send(b'b'*1600)
Out[19]: 1600

#服务器从 send 上返回,再次查看 TCP 状态。

In [20]: os.system('ss -apn | grep 6667')  (s.recv 返回b'')
LISTEN     0      5                         *:6667                     *:*      users:(("ipython",643,8),("sh",26820,8),("ss",26821,8),("grep",26822,8))
ESTAB      0      64             192.168.1.12:6667         192.168.1.119:2516   users:(("ipython",643,9),("sh",26820,9),("ss",26821,9),("grep",26822,9))
Out[20]: 0

# 查看此时发送缓冲区还有 64 字节,客户端再次读取,直到客户端读取所有的内容(s.recv 返回b'').

#此时查看 TCP 状态,服务器的发送缓冲区已经为空。

In [21]: os.system('ss -apn | grep 6667')
LISTEN     0      5                         *:6667                     *:*      users:(("ipython",643,8),("sh",27273,8),("ss",27274,8),("grep",27275,8))
ESTAB      0      0              192.168.1.12:6667         192.168.1.119:2516   users:(("ipython",643,9),("sh",27273,9),("ss",27274,9),("grep",27275,9))

#关闭服务器套接字,接着再次查看 TCP 状态。

sockfd.close()

In [24]: os.system('ss -apn | grep 6667') 
LISTEN     0      5                         *:6667                     *:*      users:(("ipython",643,8),("sh",28003,8),("ss",28004,8),("grep",28005,8))
FIN-WAIT-2 0      0              192.168.1.12:6667         192.168.1.119:2516 

#关闭之后 服务器的连接状态变为 FIN-WAIT-2

#客户端关闭套接字,再次查看 TCP 状态。

In [25]: os.system('ss -apn | grep 6667')
LISTEN     0      5                         *:6667                     *:*      users:(("ipython",643,8),("sh",29854,8),("ss",29855,8),("grep",29856,8))

# 关闭客户端套接字,查看连接的状态已经被删除。

客户端代码:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128)
s.connect(("127.0.0.1", 6667))

#客户端中间的多次读取

s.recv(2300)
Out[28]: b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'

#读取完毕关闭套接字。

s.recv(2300)
Out[21]: b''

s.close()

服务器上 tcpdump 抓包结果拿 Wireshark 分析:

几点说明:

1、TCP Window Full 表示发送方发满了接收方的通告窗口。第 3 个包客户端通告的窗口大小为 128 字节;第 4 个包服务器发送了128字节,刚好填满通告窗口。

2、TCP ZeroWindow 即通告窗口大小为 0,表示接收方暂时不能接收了。此时发送方将进入窗口探测模式,其实就是不断的给接收方发送 len 为 0 的包(图中标记为 TCP Keep-Alive),希望接收方响应最新的可以接收的窗口。

3、一旦客户端进行读取,接收缓冲区空出来部分,客户端便像服务器通告最新的非 0 窗口,这个特殊的包即 TCP Window Update.

 4、可以看到服务器关闭套接字,图中的时间为 FIN 包时,客户端几乎立马返回了 ACK.

5、SYN 和 FIN 都是要占用一个字节的,因为 SYN 和 FIN 可能会被重传。所以三次握手的第二阶段,服务器发送 Ack = 1 = 0 + 1;四次挥手的第二阶段,客户端 Ack 包的 Ack = 1602 = 1061 + 1

场景 2 服务的发送缓冲区中还包含数据时,关闭服务器的套接字

服务器的缓冲区中还有数据未发送,此时关闭服务器套接字。可以看到发送缓冲区多了一个字节,这个字节就是因为 FIN 会占用一个序号,但是此时 FIN 是还未发送,而服务器的 TCP 状态是 FIN-WAIT-1.

一旦客户端进行读取,服务器内核继续发送缓冲区的数据,服务器的TCP 状态进入 FIN-WAIT-2.  服务器闭的 FIN 是随缓冲区的数据一起发送的,并没有立即发送。

场景 3 设置服务器的 so_linger 参数,服务的发送缓冲区中还包含数据时,关闭服务器的套接字

sockfd.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 5))

设置服务器的 so_linger 参数,linger.l_onoff = 1,linger.l_linger = 5. 开启 Linger 效果并设置超时为 0. 此时当服务器的发送缓冲区中还有数据时,sockfd.close() 调用将阻塞(即使 sockfd 被设置为非阻塞套接字,这里依然会阻塞)。 通常的说法,在超时之后,sockfd.close() 才返回(实验确实如此)。发送缓冲区中的数据将被丢弃,实验结果表明超时之后,现象和场景 2 中的相同:发送缓冲区的数据仍然会被内核发往客户端。

猜你喜欢

转载自blog.csdn.net/zhouguoqionghai/article/details/88694967