在进行网络编程时,我们经常用到socket、bind、listen、connect、accept等套接字函数进行连接处理,使用read、write、send、recv等函数进行信息的发送、接收。然而,对于传输层的TCP协议的连接、释放等相关细节对于上层应用来说是透明的,而且我们也不用关心,完全交给底层驱动去完成。但是从高性能、高稳定性等程序优化方面来考虑,熟悉TCP连接和释放的过程对于应用程序的编写有很大的帮助。比如在程序退出时,忘记close,从程序角度来说没有什么影响也没有异常,而且数量级较小的情况下,服务器也不会出现问题;但是你是否考虑当大规模并发是会有什么问题,一个小小的close函数就可能造成服务器宕机或者性能大幅下降。所以了解一下TCP的一些细节,对于不管是维护人员还是开发人员都是有很大好处的。
一、三次握手
这是TCP协议开始的部分也是我们接触TCP协议最多的部分,这一部分接触网络的人已经耳熟能详了,主要过程如下:
(1)服务器打开监听端口,在linux下用socket、bind和listen三个函数完成。
(2)客户端打开连接端口,使用connect函数完成。这是客户端会发送一个不携带任何数据的TCP分解,里面包含SYN的初始序列号值,经过网络层IP协议的封装,这个IP数据报只含有IP协议头+TCP协议头+TCP选项。
(3)服务端收到SYN请求后,需要对此进行确认。它发送一个自己的SYN分节,并且包含客户单SYN的确认ACK=SYN+1。当然这里数据部分也是空的。
(4)客户端接收到确认报文后再确认服务端的SYN。
至此握手结束,连接建立。下面是连接的流程图:
三次握手的过程很简单,很多时候再握手期间客户端和服务端都会在TCP选项中加入双方的一些约定如:MSS、时间戳等等,我们可以通过setsockopt函数进行设置,这里不做过多解释。
在三次握手中还有一个”bug“,它是造成DDOS的根源,一般称之为”SYN flooding“,洪水攻击。其原理就是使用原始套接字,应用程序自己完成三次握手的过程并把握手的第一个报文的源ip地址改成一个非本机ip,这样服务器在响应第二个报文时就会发送到一个未连接的地址,而进入长时间的阻塞;当大量的这种错误的握手报文发送到服务器时就会是服务器内核未完成连接对列始终处于满负荷状态尽而不能响应正常的连接请求,从而造成洪水攻击。不过随着硬件设施及人们专业水平的提高,这种攻击在当前环境下很难造成影响,而且SCTP协议也解决了这种攻击漏洞。
二、四次挥手
对于四次挥手很多人都简单的理解为close就搞定的事,事实上也就是这么简单,但是里面的通信细节又知道多少呢?为什么有时会出现三个挥手包?为什么程序退出了端口状态还处于TIME_WAIT状态?下面将会一一解开这些问题。
四次挥手的过程:
(1)通信双方的一端(一般是客户端)调用close,主动关闭。此时发送一个FIN分节给另一端,表示数据发送完成。
(2)另一端接收到FIN分节时,就会给上层应用程序传递一个文件结束符(end-of-file)(read函数返回0),表示对端数据发送完毕。并返回给主动端一个FIN的ACK=FIN+1的确认报文。
(3)当应用程序判断read函数返回0时,调用close关闭套接字,并给主动端也发送一个FIN分节的报文。
(4)主动端接收最后的FIN分节也返回个被动端一个ACK=FIN+1的确认报文。
至此挥手结束,连接中断。下面试流程图:
根据UNP里面的介绍,四次挥手的”四次“是可选择的,有些情况下(1)的FIN报文随最后的数据发送;(2)和(3)的报文也可能合成一个分节发送。到时具体情况具体分析。
执行主动关闭的一方可以是客户单也可以是服务端,通常情况下是客户端,当然也有例外如HTTP协议。
需要注意的是在(2)和(3)之间被动端也可以发送数据,称为半关闭,在某些情况下很有用处。
三、TCP从建立到结束的端口状态变化
下图展示了TCP通信的分组交换过程,包括连接建立、数据通信、连接断开的过程:
其中,客户端的MSS(最大分节大小)是536,服务端的MSS是1460。MSS的作用是通知对端,能够发送的TCP数据的最大大小,避免在层进行分片造成应用程序的性能问题。这个值一般是MSS=MTU(最大传输单元)-TCP头部长度-IP头部长度。MTU是由数据链路层告知的,如果应用程序的数据长度超过了两端MSS的最小值,则TCP协议会对数据进行分组,从而避免分片的问题。IP首部的DF为也可以设置部分片,但是如果IP报文长度超出了通信路径中的某个路由器的外出链路的MTU时就会返回一个”需要分片,但是DF为已设置的错误)。
在这里数据应答与请求确认是一起发送的,这种方式叫做捎带(piggybacking),一般在服务器请求处理并产生应答时间小于200ms时发生。通常情况下请求确认与数据应答是先后到达的。
下面我在另一台机器搭建了一个简易服务端,本机telnet连接,并用tcpdump查看tcp通信的各个阶段的数据包:
-
float@ubuntu:~$ telnet 192.168.20.70 1123
-
Trying 192.168.20.70...
-
Connected to 192.168.20.70.
-
Escape character is '^]'.
-
aaaaaa
-
^]
-
-
telnet> quit
-
Connection closed.
-
float@ubuntu:~$
-
float@ubuntu:~/codeworkstation/UNPCode$ sudo tcpdump -Ax -i eth1 host 192.168.20.70
-
[sudo] password for float:
-
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
-
listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes
-
20:05:55.629414 IP localhost.39899 > 192.168.20.70.1123: Flags [S], seq 126222502, win 5840, options [mss 1460,sackOK,TS val 14178459 ecr 0,nop,wscale 6], length 0 -- 第一次握手(客户端MSS=1460)还有一些其他选项 syn=126222502
-
0x0000: 4510 003c 583d 4000 4006 1d57 c0a8 2f81
-
0x0010: c0a8 1446 9bdb 0463 0786 00a6 0000 0000
-
0x0020: a002 16d0 6a3b 0000 0204 05b4 0402 080a
-
0x0030: 00d8 589b 0000 0000 0103 0306
-
20:05:55.630657 IP 192.168.20.70.1123 > localhost.39899: Flags [S.], seq 866006351, ack 126222503, win 64240, options [mss 1460], length 0
-
-- 第二次握手(服务器MSS=1460) ack=126222503(客户端syn+1) syn=866006351
-
0x0000: 4500 002c ff0c 0000 8006 76a7 c0a8 1446
-
0x0010: c0a8 2f81 0463 9bdb 339e 354f 0786 00a7
-
0x0020: 6012 faf0 c6b4 0000 0204 05b4 0000
-
20:05:55.630687 IP localhost.39899 > 192.168.20.70.1123: Flags [.], ack 1, win 5840, length 0
-
-- 第三次握手 ack = 866006352
-
0x0000: 4510 0028 583e 4000 4006 1d6a c0a8 2f81
-
0x0010: c0a8 1446 9bdb 0463 0786 00a7 339e 3550
-
0x0020: 5010 16d0 c292 0000
-
20:07:39.147023 IP localhost.39899 > 192.168.20.70.1123: Flags [P.], seq 1:9, ack 1, win 5840, length 8
-
-- 发送数据“aaaaaa”并且包含“\n\r”结束符 syn=9
-
0x0000: 4510 0030 583f 4000 4006 1d61 c0a8 2f81
-
0x0010: c0a8 1446 9bdb 0463 0786 00a7 339e 3550
-
0x0020: 5018 16d0 9154 0000 6161 6161 6161 0d0a
-
20:07:39.147340 IP 192.168.20.70.1123 > localhost.39899: Flags [.], ack 9, win 64240, length 0
-
-- 服务器返回的请求确认 ack=9
-
0x0000: 4500 0028 ff0f 0000 8006 76a8 c0a8 1446
-
0x0010: c0a8 2f81 0463 9bdb 339e 3550 0786 00af
-
0x0020: 5010 faf0 de69 0000 0000 0000 0000
-
20:10:21.827636 IP localhost.39899 > 192.168.20.70.1123: Flags [F.], seq 9, ack 1, win 5840, length 0
-
-- 第一次挥手 syn=9
-
0x0000: 4510 0028 5840 4000 4006 1d68 c0a8 2f81
-
0x0010: c0a8 1446 9bdb 0463 0786 00af 339e 3550
-
0x0020: 5011 16d0 c289 0000
-
20:10:21.827948 IP 192.168.20.70.1123 > localhost.39899: Flags [.], ack 10, win 64239, length 0
-
-- 第二次挥手 ack=10(客户端syn+1)
-
0x0000: 4500 0028 ff10 0000 8006 76a7 c0a8 1446
-
0x0010: c0a8 2f81 0463 9bdb 339e 3550 0786 00b0
-
0x0020: 5010 faef de69 0000 0000 0000 0000
-
20:10:21.844605 IP 192.168.20.70.1123 > localhost.39899: Flags [FP.], seq 1, ack 10, win 64239, length 0
-
-- 第三次挥手 syn=1
-
0x0000: 4500 0028 ff11 0000 8006 76a6 c0a8 1446
-
0x0010: c0a8 2f81 0463 9bdb 339e 3550 0786 00b0
-
0x0020: 5019 faef de60 0000 0000 0000 0000
-
20:10:21.844936 IP localhost.39899 > 192.168.20.70.1123: Flags [.], ack 2, win 5840, length 0
-
-- 第四次挥手 ack=2
-
0x0000: 4500 0028 0000 4000 4006 75b8 c0a8 2f81
-
0x0010: c0a8 1446 9bdb 0463 0786 00b0 339e 3551
-
0x0020: 5010 16d0 c288 0000
-
^C
-
9 packets captured
-
9 packets received by filter
-
0 packets dropped by kernel
-
float@ubuntu:~/codeworkstation/UNPCode$
四、TIME_WAIT状态
在程序执行close关闭套接字后,使用netstat命令经常会看到,程序使用的端口处于TIME_WAIT状态,一般维持2MSL,即2倍的最长分节生命期(MSL)的时间,而后进入close状态。
保持这种状态有两个原因:
1、保持TCP的可靠性终止
2、允许老的分节在网络中消逝
第一个原因的解释是,假设四次挥手的最后一个ACK确认报文丢失,被动关闭端会重新发送FIN的报文,如果此时主动端关闭了端口,那TCP会返回一个错误的RST报文,此时被动端会认为是错误的,所以为了保证TCP全双工的可靠性必须要保证最后一个ACK确认报文正确到达被动端。
第二个原因,由于数据在网络中传输的各种因素不确定,可能由于路由器的问题导致数据包延误或超时,TCP重传数据报文。若重传后端口执行关闭命令,接着另外一个程序重新使用这个端口,而此时出现问题的路由器重新修复,原来延迟的包重新转发,这样新程序就会接受的老程序的数据包,导致错误的接受。所以TCP必须保证新连接不能接受到老连接的数据报文。所以进入TIME_WAIT状态并且持续2MSL的时间。这样就能保证每个方向的重复报文会被丢弃,新连接不会受到老连接的影响。
在服务器维护时,经常会看到TIME_WAIT状态的端口很多,系统的CPU占用率紧张,可以采用一种临时的办法解决:修改系统参数,将TCP中TIME_WAIT的时间设置更小的值。这只是临时的方法而且也存在一定的风险。而后进行报文分析和源码分析,查看问题是由于研发人员疏忽还是由于恶意攻击造成的,然后采取相应的措施。
阅读(3009) | 评论(0) | 转发(0) |