准备写一个 Linux网络调优,忽然想到很多运维对TCP/IP协议不是很了解,网上的文章也基本不是站在运维的角度来讲述,而且很多有关TCP/IP三次握手,四次断开的文章都是错的(你没有看错,很多写的似很厉害的文章,都是错的),所以还是准备自己写一篇,也加深自己的理解。
注: 本文所有的Client和Server都是广义的,狭义的表示应该是一个套接字,也就是 ip:port
一、TCP 三次握手

第一步:
Client会向Server发送一个有SYN标志位的TCP包,表示自己要建立TCP连接。

第二步:
Server就会返回一个SYN+ACK包,ACK是确认之前Client发送过来的SYN包,SYN表示自己也准备好建立连接了。

第三步:
Client会向Server发送一个有ACK标志位的 TCP报文,表示自己确认Server发送过来的带SYN标志位TCP连接请求

在这里要仔细说明一下。Acknowledgment number (确认序列号) 不是 ACK(Acknowledgment),这就是我一开始说的,很多人错的地方。上面我特意把Acknowledgment number 和 Sequence number 没有抹掉的原因。Acknowledgment number 和 Sequence number 就是序列号和确认序列号,用来确认序列的。而所谓的SYN、ACK。其实就是一个标志位。也就是下面图中的 TCP Flags,实际上就是六位二进制表示的。标志位所在位为0就是Not set,标志位所在位为1就是Set,从上面Wireshark抓的包也可以看出来。0x012不就是001010,对应下图不就是ACK + SYN吗。

(注: 上图我是从 images.Google.com 随便找的, 如有侵权请联系 [email protected], 立即更换….)
标志位解释(只解释对我们最有用的,相信太多人字多不看…)
- 同步标志位SYN:在建立连接是用来同步序号。
SYN表示一个连接请求报文段。 - 确认标志位ACK:ACK表示这是一个确认的TCP包
- 终止标志位FIN:表明此报文段的发送端的数据已经发送完毕,并要求释放传输连接。在后面会出现

各状态解释:
CLOSED: 表示端口未被启用。LISTEN:Server先启动Service,Server就会进入LISTEN模式。相信大家已经看到过太多的bind-address,Listen配置了。这个就是监听状态。SYN_RCVD: 和SYN_SEND一个发送,一个接收,还加上了SYN的关键词,相信一下就可以记得住,而且这个状态基本看不到。SYN_SEND: 这个是Clinet独有状态,当然Zabbix_server向Zabbix_Agent取数据的时候也会进入这个状态。到底怎么来的呢?可能有编程经验的童鞋容易理解。当Client打开连接Server的Socket的时候就会发送带SYN标志位的 TCP报文。然后就进入了这个状态,如果不理解就不需要理解了。就是Clinet发送了一个SYNTCP 报文 给Server之后就进入了这个状态。ESTABLISHED: 已连接,这个时候就应该是Client GET POST的时候了。
二、TCP connection 四次断开
四次断开有两种情况,一种是 Client先断开,一种是Server先断开。为什么会出现这两种情况,后面会详细讲述,而且会进行测试,我们先讲述一下在标志位上面的通信

为了表达清楚意思,所以我就不使用 Server 和 Client,而上图所表示的也没有Client和Server,因为谁都可以先断开
第一步:
先断开端向后断开端发送带FIN的TCP报文,表示自己要断开这个 TCP 连接

第二步:
后断开端向先断开端发送带ACK的TCP报文,表示自己已经知道对方想要断开连接了。

第三步:
后断开端向先断开端发送带FIN的TCP报文,表示自己已经准备好断开连接了,可能有童鞋要问,为什么这个FIN为什么不和上面那个ACK一起就发送过去了呢?两次分开发送不是增加开销吗?这是因为后断开端也需要准备啊。不能你说断开就断开吧,首先我得试一试能不能断开,确定能断开了,我就会发送FIN确定。

第四步:
先断开端向后断开端发送带ACK的TCP报文,确认自己已经断开连接,你也可以断开连接了。

我们看一下各状态的状态图:

解释:
FIN_WAIT_1: 这个就是先断开端发送了FIN状态码之后出现的状态。在程序层面来看,就是Close Scoket.因为后断开端会马上回复带ACK标志位的TCP报文确认,所以这个基本看不到。FIN_WAIT_2: 在自己发送带FIN的TCP报文之后,其实就进入了FIN_WAIT,等待对方回复FIN,只不过中间有个ACK,所以就分为了FIN_WAIT_1和FIN_WAIT_2CLOSE_WAIT: 这个表示Close wait。就是字面的意思。LAST_ACK: 这个也是字面的意思。等待最后的ACKTIME_WAIT: 这个是见的最多的。意思就是等待2MLS时长之后,就进入CLOSED状态。当然如果是在服务器上面,那么就是销毁了这个TCP连接,当然可以设置kernel参数让此TCP连接不销毁,然后重新被使用。
PS:
2MSL(Maximum Segment Life 报文最大生存时间):TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的 FIN),MSL过长会导致无用TIME_WAIT过多,大量的Time_wait会带来一些不好的影响,每个TCP连接都有自己的Transmission Control Block,也就是数据结构,在TIME_WAIT状态的时候这个数据结构还没有被释放。
$ sysctl net.ipv4.tcp_fin_timeout /* 查看MSL */
net.ipv4.tcp_fin_timeout = 60
$ cat /proc/sys/net/ipv4/tcp_fin_timeout /* 查看MSL */
60
$ sudo vim /etc/sysctl.conf
net.ipv4.tcp_fin_timeout = 20 /* 后面数字可以根据情况来 */
$ sudo sysctl -p
三、运维角度的延伸
3.1 HTTP持久连接(keepalive)
最后到了解释上面为什么是先断开连接和后断开连接了。
HTTP1.0的时候,HTTP协议是没有HTTP持久连接 (keepalive,在后面会不加区别的使用keepalive和持久连接)这个概念的,基本传输一个Resocurce,就需要建立一次连接。HTTP 1.1默认启用的HTTP持久连接能够在keepalive_timeout前省去每次传输报文都要建立TCP连接(三次握手)的时间和开销.
非持久连接:

持久连接(少了TCP的三次握手和四次断开):

(注: 上图我是从 images.Google.com 随便找的, 如有侵权请联系 [email protected], 立即更换….)
HTTP1.0基本可以忽略了,所以这里就出现keeplive_timeout就是关键,也就是说Server和Client谁keeplive_timeout先到期,谁就发送FIN TCP报文以断开连接。
3.2 keepalive_timeout 测试
HTTP keep-alive connection timeouts
Firefox: 约115秒(定义在about:config中的network.http.keep-alive.timeout)
Chrome: 约320秒
Opera: 约120秒
MSIE: 约60秒(可以在注册表中自定义)
https://support.microsoft.com/en-us/kb/813827
Nginx: 默认值65秒(keepalive_timeout 65s)
Firefox 默认 HTTP connection keep-alive timeout: 115s
Firefox 在 about:config 中的 network.http.keep-alive.timeout 可以进行修改


Nginx 可以在 /etc/nginx/nginx.conf 配置配置项 keepalive_timeout 来调整默认 HTTP connection keep-alive timeout
$ vim /etc/nginx/nginx.conf
keepalive_timeout 65; /* 可以任意修改 */
当Nginx HTTP connection keep-alive timeout 为默认的 65s,使用默认设置的Firefox来访问Nginx,测试是否是Server端先断开TCP连接,能否出现 120S (2MSL) 的 TIME_WAIT



和预料中的一样,出现了TIME_WAIT:

测试将 Nginx 超时时长调整为 120s, 看是否不出现TIME_WAIT


经过测试,120s无用,设置成130s然后出现Client先断开连接。
如果也是运维,在这个地方就应该思考一下,上面的测试到底说明了什么。
3.2 自定义是否启用 keepalive_timeout
上面说HTTP1.1默认启用的是keepalive。HTTP Hearder中的Connection可以控制,当Connection为close的时候,就是短连接,当Connection为keepalive的时候,就是使用长连接。Client可以设置,Server也可以设置。
先测试Client设置Connection: close,这里使用最简单的Telnet:
$ telnet 10.21.56.4 80
HEAD /index.php HTTP/1.1
Host: 10.21.56.4
Connection: close
HTTP/1.1 200 OK
Server: nginx/1.10.1
Connection: close
为了方便大家查看,我把不是很重要的信息全部都删除掉了。上面就可以很清楚的展示,当Connection 为 close的时候,双方都会协商使用短连接。
在Server上面进行观测:
netstat -antl | grep 80 | awk '/^tcp/{sum[$NF]++}END{for (i in sum) {printf "%-20s %d\n",i,sum[i]}}'
TIME_WAIT 1
LISTEN 1
发现有一个连接进入TIME_WAIT,也就是说就算是Client协商使用短连接,主动断开连接的还是Server
使用Firefox访问网站, Nginx HTTP响应报文 的 Hearder
HTTP/1.1 200 OK
Server: nginx/1.10.1
Connection: keep-alive
在Server端将keepailve_timeout设置为0
$ vim /etc/nginx/nginx.conf
keepalive_timeout 0;
使用Firefox访问网站, Nginx HTTP响应报文 的 Hearder
HTTP/1.1 200 OK
Server: nginx/1.10.1
Connection: close
到这里大家肯定对运维需要掌握的TCP/IP部分有了深入的了解。也能看懂netstat -antl中的那些 TCP 状态到底是什么含义了。后面会在此文的基础上讲述一下网络调优。
参考资料:
The TCP/IP Guide - TCP Connection Establishment Process: The “Three-Way Handshake”
The TCP/IP Guide - TCP Connection Termination
Wikipedia - Transmission Control Protocol
《TCP/IP详解 卷一:协议》