TCP协议(包括TCP的连接过程,数据分段,TCP有关服务器优化)
Transmission Control Protocol/Internet Protocol 传输控制协议/因特网互联协议
TCP/IP是一个Protocol Stack(协议栈),包括TCP、IP、UDP、ICMP、RIP、TELNET、FTP、SMTP、ARP等许多协议,最早发源于美国国防部(缩写为DoD)的因特网的前身ARPA网项目,1983年1月1日,TCP/IP取代了旧的网络控制协议NCP,成为今天的互联网和局域网的基石和标准,由互联网工程任务组负责维护。
TCP/IP共定义了四层和ISO参考模型的分层有对应关系
TCP/IP所对应的应用层的协议:
(应用层的我们这里不阐述了,主要说传输层的TCP协议)
传输层所对应的协议:
传输层主要有两个重要的协议,TCP和UDP协议。
TCP:可靠地面向连接的服务
TCP的特性:
工作在传输层
面向连接协议
全双工协议
半关闭
错误检查
将数据打包成段,排序
确认机制
数据恢复,重传
流量控制,滑动窗口
拥塞控制,慢启动和拥塞避免算法
TCP的首部:
TCP的首部由最少5个32bit组成,其中每32个bit为一组:
第一行 :前16bit为source port, 后16bit为destination port ,自身发起的端口,和要到达端口(端口范围0-65535)
第二行 :32bit表示数据包的序号 Seq 即Seqence ,其实的Sequnce是随机数并不固定(双方进行通讯所使用的Seq均为自己的,互不相干,请正确理解这句好,再通俗一点讲就是,自己算自己的序列编号。)
第三行 :32bit表示数据包确认的序号 Ack 即 Acknowledgment ,该位是用来确认对方的Seq的编号,如果正确收到了对方信号,这位的值回事,对方发送到己方的Seq的值+1,主要的作用就是告诉对方,“你发的包,我收到了,请发下一个包”,所以Ack的值为对方Seq的值+1。
第四行:4bit数据偏移,用来表示整个TCP首部的长度(最少5行最多15行,每行4个字节);6bit的保留位,这里不阐述;6bit的状态标示位,分别是:URG、ACK、PSH、RST、SYN、FIN,其中每一位都只有0和1来表示,1表示有状态,0表示无状态。 窗口大小:表示现在允许对方发送的数据量,也就是告诉对方,从本报文段的确认号开始允许对方发送的数据包的数量。
URG:紧急指针位,表示本报文段中发送的数据是否包含紧急数据;当该位为1时,TCP首部的第5行,紧急指针位数据表示有效。
ACK:确认位,表示确认位生效,该ACK 和第三行的Ack不同,第三行为序号,这里的ACK是标志位。
PSH:我也不知道该怎么叫,暂且叫推送位吧,功能:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间。如果该位为1,在接收数据后,立即马上传送给应用程序,不会放在内核的buffer中。
RST:重置位,当应用程序出现异常,重新创建新的连接。
SYN:同步位,建立连接时使用,用来同步序号(序号)
FIN:终止位,通知对方要关闭连接
第五行:前16bit校验和后16bit紧急指针位;checksum 是个通用的计算机概念,做完整性校验之用,在很多协议(IP,UDP,ICMP)中都有应用,这个值由包的发送方去计算,之后由包的接收方取出来校验。Urgent pointer 为两个字节的偏移量,加上当前包的 sequence number,用来标记某一个范围内的 bytes 为特殊用途数据。
第六行以及以后:选项
选项部分:其最大长度可根据TCP首部长度进行推算。TCP首部长度用4位表示,选项部分最长为:(2^4-1)*4-20=40字节
常见选项:
最大报文段长度:Maxium Segment Size,MSS
指明自己期望对方发送TCP报文段时那个数据字段的长度。默认是536字节。数据字段的长度加上TCP首部的长度才等于整个TCP报文段的长度。MSS不宜设的太大也不宜设的太小。若选择太小,极端情况下,TCP报文段只含有1字节数据,在IP层传输的数据报的开销至少有40字节(包括TCP报文段的首部和IP数据报的首部)。这样,网络的利用率就不会超过1/41。若TCP报文段非常长,那么在IP层传输时就有可能要分解成多个短数据报片。在终点要把收到的各个短数据报片装配成原来的TCP报文段。当传输出错时还要进行重传,这些也都会使开销增大。因此MSS应尽可能大,只要在IP层传输时不需要再分片就行。在连接建立过程中,双方都把自己能够支持的MSS写入这一字段。 MSS只出现在SYN报文中。
窗口扩大:Windows Scaling
为了扩大窗口,由于TCP首部的窗口大小字段长度是16位,所以其表示的最大数是65535。但是随着时延和带宽比较大的通信产生(如卫星通信),需要更大的窗口来满足性能和吞吐率,所以产生了这个窗口扩大选项
时间戳: Timestamps
可以用来计算RTT(往返时间),发送方发送TCP报文时,把当前的时间值放入时间戳字段,接收方收到后发送确认报文时,把这个时间戳字段的值复制到确认报文中,当发送方收到确认报文后即可计算出RTT。也可以用来防止回绕序号PAWS,也可以说可以用来区分相同序列号的不同报文。因为序列号用32为表示,每2^32个序列号就会产生回绕,那么使用时间戳字段就很容易区分相同序列号的不同报文。
TCP首部图片:
建立TCP连接
先看图:
咱们来描述一下上图:
一般发起TCP连接的都是client端
1、首先client端和server端都是处于关闭状态,之后,服务器端打开端口,并绑定IP和端口,使其端口处于LISTEN状态(阻塞)。
2、client端打开端口,像服务器端发出建立tcp连接请求,TCP报文头SYN标记位为1,第二行序号为一个初始随机数x,client端连接状态从CLOSED改变为SYN-SENT
3、server端收到报文后,回应client端报文 SYN,ACK=1确认收到请求,请client端放松下一条信息,TCP第二行seq为自己生成的初始随机数y,TCP首第三行Ack为client端发送报文中seq的值+1,即x+1,server端修改自身状态为SYN-RCVD
4、client端收到server端回馈的信息,再次发送ACK=1表示收到server端信息,根据历史发送的信息,更改seq为之前+1,而TCP首部第三行ack则为server端的seq信号+1,即y+1,修改自身状态为ESTABLISHED
5、server端收到client端的ACK确认TCP连接成立,并修改自身连接状态为ESTABLISHED
上图就是经典的TCP3次握手建立连接的过程。
TCP四次挥手
先看图:
4次挥手就是断开TCP连接的过程,要发起断开TCP连接,无论client端发起还是server端发起,都是可以的。
1、A要求断开TCP,会向B机器发送FIN信号,并发送序列seq=u(继承之前生成的序号+1),自己进入FIN-WAIT-1状态
2、B收到要断开的请求之后回应A一个ACK,并发送序列seq=v(继承之前生成的序号+1),自身进入CLOSE-WAIT状态(处理未处理完的数据),在A收到这条信息后,A的状态改变为FIN-WAIT-2
3、当B处理完未完成的数据后,再次向A发出信号,FIN=1,ACK=1确认关闭连接,自身状态变为LAST-ACK状态
4、A收到B发来的FIN和ACK后,再次回应ACK,自身进入TIME-WAIT状态并等待2倍MSL时间关闭连接(msl为报文段生存时间)
5、B收到A的ack后连接就关闭了。
客户端从FIN_WAIT_1状态可能直接进入TIME_WAIT状态(不经过FIN_WAIT_2状态),前提是处于FIN_WAIT_1状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)
所有状态的描述:
CLOSED 没有任何连接状态
LISTEN 侦听状态,等待来自远方TCP端口的连接请求
SYN-SENT 在发送连接请求后,等待对方确认
SYN-RECEIVED 在收到和发送一个连接请求后,等待对方确认
ESTABLISHED 代表传输连接建立,双方进入数据传送状态
FIN-WAIT-1 主动关闭,主机已发送关闭连接请求,等待对方确认
FIN-WAIT-2 主动关闭,主机已收到对方关闭传输连接确认,等待对方发送关闭传输连接请求(服务器需要优化的项目)
TIME-WAIT 完成双向传输连接关闭,等待所有分组消失(高并发时,服务器需要优化的项目)
CLOSE-WAIT 被动关闭,收到对方发来的关闭连接请求,并已确认
LAST-ACK 被动关闭,等待最后一个关闭传输连接确认,并等待所有分组消失
CLOSING 双方同时尝试关闭传输连接,等待对方确认(断开连接时不会产生4次握手,直接断开,双方同时发送FIN请求关闭)
TCP连接优化:
处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移至TIME_WAIT状态,否则它将一直停留在这个状态。如果不是为了在半关闭状态下继续接收数据,连接长时间地停留在FIN_WAIT_2状态并无益处。连接停留在FIN_WAIT_2状态的情况可能发生在:客户端执行半关闭后,未等服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称之为孤儿连接(和孤儿进程类似)
Linux为了防止孤儿连接长时间存留在内核中,定义了两个内核参数:
/proc/sys/net/ipv4/tcp_max_orphans 指定内核能接管的孤儿连接数目(centos7 默认4096,centos6默认65536)
/proc/sys/net/ipv4/tcp_fin_timeout 指定孤儿连接在内核中生存的时间(默认为60秒)
Linux中Time-Wait值过高优化参数:
本方法只对拥有大量TIME_WAIT状态的连接导致系统资源消耗有效,不是这个原因的情况下,效果可能不明显。使用netstat命令。查看当前TCP/IP连接的状态和对应的个数:
# netstat -an | awk ‘/^tcp/ {++s[$NF]} END {for(a in s) print a, s[a]}’
假如这个命令会显示出类似下面的结果:
TIME_WAIT 63648
FIN_WAIT1 3
FIN_WAIT2 4
ESTABLISHED 184
LISTEN 17
我们只用关心TIME_WAIT的个数,在这里可以看到,有6w多个TIME_WAIT,这样就占用了6w多个端口。要知道端口的数量只有65535个,占用一个少一个,会严重的影响到后继的新连接。这种情况下,我们就有必要调整下Linux的TCP/IP内核参数,让系统更快的释放 TIME_WAIT连接。
/proc/sys/net/ipv4/tcp_syncookies = 1 #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
/proc/sys/net/ipv4/tcp_tw_reuse = 1 #表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
/proc/sys/net/ipv4/tcp_tw_recycle = 1 #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
/proc/sys/net/ipv4/tcp_fin_timeout = 5 # 指定孤儿连接在内核中生存的时间为5秒
如果你的连接数本身就很多,我们可以再优化一下TCP/IP的可使用端口范围,进一步提升服务器的并发能力。加入下面这些配置:
/proc/sys/net/ipv4/tcp_keepalive_time = 1200 #表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟
/proc/sys/net/ipv4/ip_local_port_range = 10000 65000 #表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为10000到65000
/proc/sys/net/ipv4/tcp_max_syn_backlog = 8192 #表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数
/proc/sys/net/ipv4/tcp_max_tw_buckets = 5000 #表示系统同时保持TIME_WAIT的最大数量,如果超过这个数字,TIME_WAIT将立刻被清除并打印警告信息。默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于 Squid,效果却不大。此项参数可以控制TIME_WAIT的最大数量,避免Squid服务器被大量的TIME_WAIT拖死。
经过这样的配置之后,你的服务器的TCP/IP并发能力又会上一个新台阶。
但是当服务器收到大量的短连接时,Linux的TCP栈一般还是会生成大量的 TIME_WAIT 状态的socket。tcp_fin_timeout参数并不能缩短timewait的时间,在linux内核中真正管用的是一个宏定义,在 $KERNEL/include/net/tcp.h里面,有下面的行:
#define TCP_TIMEWAIT_LEN (60*HZ)
而这个宏是真正控制 TCP TIME_WAIT 状态的超时时间的。如果我们希望减少 TIME_WAIT 状态的数目(从而节省一点点内核操作时间),那么可以把这个数值设置低一些,把上面的修改为:
#define TCP_TIMEWAIT_LEN (10*HZ)
然后重新编译内核,重启系统即可发现短连接造成的TIME_WAIT状态大大减少,一般情况都可以至少减少2/3。也能相应提高系统应对短连接的速度