测试很简单,从客户端用nc -vz连接服务器,并观察tcpdump的输出,-v参数让nc提供额外的输出,-z参数让nc连上之后直接断开。
这里用的是我自制的tcpdump
客户端输出:
> nc -vz 46.51.216.138 80 Connection to 46.51.216.138 80 port [tcp/http] succeeded!
tcpdump的输出如下
> python mytcpdump.py --ipaddr=46.51.216.138 20:57:39.663589 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:42848 > 46.51.216.138:80: seq 2268797791, syn 20:57:40.008436 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 46.51.216.138:80 > 192.168.10.4:42848: seq 3791213581, ack 2268797792, syn 20:57:40.009015 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:42848 > 46.51.216.138:80: seq 2268797792, ack 3791213582 20:57:40.009420 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:42848 > 46.51.216.138:80: seq 2268797792, ack 3791213582, fin 20:57:40.357852 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 46.51.216.138:80 > 192.168.10.4:42848: seq 3791213582, ack 2268797793, fin 20:57:40.358212 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:42848 > 46.51.216.138:80: seq 2268797793, ack 3791213583
其中前2-4行是connect()产生的TCP三路握手,5-7行是关闭时的四路握手。
这里可能会让人觉得奇怪,为什么四路握手只有3个包?这个稍后再议。
先看第2行,92.168.10.4向46.51.216.138发送了syn标志,并提供了自己的ISN(Initial Sequence Number) 2268797791,。
第3行,服务器发送ack标志,值为客户端的ISN + 1, 这是因为syn占用一个seq,表示服务器端接收到了syn标志并等待序号为ISN + 1的数据。
同时服务器端在同一个tcp分节中发送向客户端发送一个syn,并附带了自身的ISN 3791213581。
第4行,客户端向服务器发送ack确认了syn标志,其值也是服务器的ISN + 1, 同时seq相对于ISN也加了1,但是这是个单纯的的ack包,不包含任何数据和别的标志。
至此tcp连接完成,双方完成ISN的传递,可以开始进行全双工的通讯。
从逻辑上来说,tcp连接的建立其实也是4次握手,两对syn和ack,但是由于passive open的一方总是可以把自己的syn跟ack放在同一个数据包里面,没有必要分成两个包来发。
因此,tcp 三路握手实际上通常是3个数据包。所谓通常,是因为还有两端同时connect的罕见情况,这时候能观察到4个包。
接下来第5行,nc在连上之后,直接关闭连接,向服务器发送fin标志表示断开连接,其seq值不变
第6行,服务器回复ack,可以看到它的值相对于第5行又加了1,这表明fin也占用一个seq,同时服务器也发送了fin关闭自己的连接。
第7行,客户端发送ack确认服务器的fin。
可以看到,这里tcp关闭时实际上也只有3个包,那为什么叫4路握手呢?
当然,两端同时调用close()关闭是能产生4个包。
但是更多的原因是tcp支持半关闭(shutdown系统调用)。
用python写个小程序试试看:
import socket. s = socket.socket (socket.AF_INET, socket.SOCK_STREAM, 0) s.connect (('46.51.216.138', 80)) s.shutdown (socket.SHUT_WR) raw_input ('Enter to continue') s.close ()
运行这个程序再次观察,嗯?关闭还是3个包,并3个包在显示Enter to continue前就已经发送完毕,表示连接已经关闭。
把第4行改成s.shutdown (socket.SHUT_RD)再试一次,Enter to continue前没有任何fin包,但是一按下回车后,仍然是3路握手。
为什么呢?合理的推测是shutdown(socket.SHUT_WR)发送了fin包,并因此在服务器端触发EOF,使得服务器端也同时关闭连接,因此对端的fin标志再次跟ack一起发过来。
同时可以看到,shutdown (socket.SHUT_RD)不会发送任何数据。
TODO: 如果向一个shutdown (socket.SHUT_RD)的socket发送数据会发生什么?我猜是一个ICMP错误,有待验证。
那么要怎么样才能看到4路握手呢?很简单,让服务器在收到EOF之后等一会再close(),这里用unp的tcpserv01稍作修改,在EOF之后等待10秒再关闭,如下:
if ((childpid = fork()) == 0) { Close (listenfd); str_echo (connfd); sleep (10); Close (connfd); exit(0); }
观察tcpdump,第5行和第6行是客户端的fin和服务器的ack。
10秒以后服务器才关闭另一端的连接,在第7行发送fin。
> python mytcpdump.py --ipaddr=46.51.216.138 20:58:58.753431 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:47748 > 46.51.216.138:9877: seq 544417141, syn 20:58:59.117789 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 46.51.216.138:9877 > 192.168.10.4:47748: seq 1133470968, ack 544417142, syn 20:58:59.118318 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:47748 > 46.51.216.138:9877: seq 544417142, ack 1133470969 20:58:59.118629 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:47748 > 46.51.216.138:9877: seq 544417142, ack 1133470969, fin 20:58:59.489793 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 46.51.216.138:9877 > 192.168.10.4:47748: seq 1133470969, ack 544417143 20:59:09.481970 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 46.51.216.138:9877 > 192.168.10.4:47748: seq 1133470969, ack 544417143, fin 20:59:09.482332 ETH_P_IP(0x0800) IPPROTO_TCP(0x6) 192.168.10.4:47748 > 46.51.216.138:9877: seq 544417143, ack 1133470970