TCP基本认识
头格式
- 序列号:建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接受端主机,每发送一次数据,就累加一次该数据字节数的大小,用来解决网络包乱序问题。
- 确认应答号:指下一次期望收到数据的序列号,发送端收到这个确认应答后可以认为在这个序列号之前的数据都已经被正常接收。用来解决丢包问题。
- 控制位:
- ACK:确认应答的字段变为有效,TCP除最是建立连接的SYN包之外,该位必须设置为1
- RST:表示TCP连接中出现异常必须强制断开连接
- SYN:建立连接,并在其序列号字段进行序列号初始值
- FIN:表示这以后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以交换FIN为1的TCP段

为什么需要TCP,TCP工作在哪层?
TCP是传输层协议,保证接收端接收的网络包是无损、无间隔、非冗余和有序的。
什么是TCP
- 面向连接:一定是一对一的
- 可靠的:无论网络链路怎么变化,TCP都可以保证一个报文一定能到达接收端
- 字节流的:用户消息通过TCP传输时,消息可能会被操作系统分组成多个TCP报文,如果接收方的程序如果不知道消息的边界,是无法读出一个有效用户消息的。并且TCP报文是有序的,当前一个报文没有收到是,即使先收到了后面的报文,那么也不能交给应用层去处理,同时对重复的报文会自动丢弃。
什么是TCP连接
用于保证可靠性和流量控制维护某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
- Socker:ip和端口组成
- 序列号:解决乱序问题
- 窗口大小:流量控制
如何唯一确定一个TCP连接
四元组:源地址、源端口、目标地址、目标端口
源地址和目标地址(32位)在IP头部,通过IP协议发送报文给对方主机。
源端口和目标端口(16位)在TCP头部,告诉TCP协议应该发给那个进程。
服务端最大并发TCP连接数受以下因素影响:
- 文件描述符,每个TCP连接都是一个文件,如果文件描述符被占满了,会发生Too many open files。Linux对可打开的文件描述符分别做了三方面限制:
- 系统级:系统可打开最大数量
- 用户级:指定用户可打开数量
- 进程级:单进程可打开数量
- 内存限制:每个TCP连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发送OOM。
UDP和TCP有什么区别和分别的应用场景
UDP不提供复杂控制机制,利用IP提供面向连接的通信服务。协议简单,头部只有8个字节。
- 目标和源端口:表示报文发给哪个进程
- 包长度:保存了UDP首部和数据长度之和
- 校验和:防止收到在网络传输中受损的UDP包
区别
- 连接。TCP面向连接,需要先建连;UDP不需要。
- 服务对象。TCP是一对一两点服务;UDP支持一对一、一对多、多对多的交互通信。
- 可靠性。TCP可靠交付数据,可以无差错、不丢失、不重复、有序;UDP不保证可靠交付,但是可以基于UDP实现可靠传输协议,如QUIC。
- 首部开销。TCP首部长,在没有使用选项字段是是20字节;UDP首部之后8字节。
- 传输方式。TCP是流式传输,没有边界但保证顺序和可靠;UDP是一个包一个包发送,有边界但是可能丢包和乱序
- 分片不同。TCP数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也会在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片;UDP数据大小如果大于MTU,则会在IP成进行分片,目标主机收到后,在IP成组装完数据,接着在传输给传输层。
应用场景
- TCP:FTP、HTTP
- DNS、SNMP、视频音频等多媒体通信、广播通信
为什么UDP头部没有首部长度字段,而TCP头部有首部字段?
- TCP有可变长的选项字段,而UDP头部长度则不会变化,无需多一个字段去记录UDP首部长度。
为什么UDP头部有包长度字段,而TCP头部没有包长度字段
- TCP数据长度=IP总长度-IP首部长度-TCP首部长度
为什么是三次握手
- 三次才能阻止重复历史连接的初始化(主要)
- 两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费
- 三次才能同步双方的初始化序列号
- 序列号的作用:接收方可以去除重复的数据;接收方可以根据数据包序列号按序接收;可以标识发送出去的数据包总,那些是已经被对方收到的(通过ACK报文总的序列号知道)
- 两次握手只保证一方的序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
- 三次才能避免资源浪费
- 如果客户端发送的SYN报文在网络中阻塞了,重复发送多次SYN报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
为什么每次建立TCP连接时,初始化的序列号都要求不一样?
- 防止历史报文被下一个相同西元组的连接接收(主要)
- 防止黑客伪造相同序列号的TCP报文被对方接收
初始化序列号ISN是如何随机产生的
- ISN = M + F(localhost, localport, remotehost, remoteport)
- M是个计时器,每4微秒加1
- F是个hash算法
既然IP层会分片,为什么TCP还需要MSS
- MTU:一个网络包的最大长度,以太网中一般为1500字节
- MSS:出去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度
当IP层有一个大于MTU大小的数据要发送,那么IP层就要进行分片,把数据分片成若干片,保证每个分片都小于MTU。把一份IP数据进行分片以后,有目标主机的IP层来进行重新组装后,再交给上一层的TCP传输层。
看起来井然有序,但如果一个IP分片丢失,整个IP报文的所有分片都得重传。因为IP层本身没有超时重传机制,它由传输层的TCP来负责超时和重传。为了达到最佳传输效能,TCP协议在建立连接的时候通常要协商双方的MSS值,当TCP层发现数据超过MSS时,就会先进行分片,形成的IP包长度也就不会大于MTU。经过TCP层分片之后,如果一个TCP分片丢失,进行重发也是以MSS为单位,而不用重传所有分片,大大增加了重传的效率。
第一次握手丢失,会发生什么
- 退避方式重传syn包,而后close
第二次握手丢失,会发生什么
- 客户端重发syn包到close
- 服务端重发syn-ack包到close
第三次握手丢失,会发生什么
- 服务端重发syn-ack
- 客户端进入established状态
如何避免SYN攻击
- 调大netdev_max_backlog,当网卡接受数据包的数据大于内核处理速度是,会有一个队列保存这些数据包。控制这个队列的最大值。
- 增大TCP半连接队列
- 开启net.ipv4.tcp_syncookies,开启后可以在不使用SYN半连接的情况下成功建立连接,相当于绕过SYN半连接来建立连接。
- 减少SYN-ACK重传次数,以加快处于SYN-REVC状态TCP连接的断开
TCP断开
四次挥手过程
- 客户端打算关闭连接,发送FIN报文,之后进入FIN_WAIT1状态
- 服务端收到后,想向客户端发送ACK,进入CLOSE_WAIT状态
- 客户端收到ACK后,进入FIN_WAIT2
- 服务端处理完数据后,想客户端发送FIN,进入LAST_ACK
- 客户端收到FIN后,回ACK,进入TIME_WAIT
- 服务端收到ACK,进入CLOSE,至此服务端结束连接
- 客户端经过2MSL后进入CLOSE,客户端结束连接
每个方向都发送了一个FIN一个ACK,主动关闭的连接,才有TIME_WAIT状态。
为什么等待2MSL
MSL,maximum Segment Lifetime,报文最大生存时间,是任何报文在网络上存在的最长时间,超过这个时间会被丢弃。2MSL相当于至少允许报文丢失一次,客户端收到服务端重传的FIN后,2MSL时间将重新计时。
为什么需要TIME_WAIT
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收
- 保证被动关闭连接的一方,能被正确的关闭
TIME_WAIT过多有什么危害
- 占用系统资源,比如文件描述符,内存资源,CPU资源,线程资源等
- 占用端口资源
如何优化TIME_WAIT
- 打开net.ipv4.tcp_reuse和net.ipv4.tcp_timestamps
- 客户端开启reuse后,在调用connect()函数时,内核会随机找一个time_wait状态超过1秒的连接给行的连接复用(net.ipv4.tcp_timestamps=1)。
- net.ipv4.tcp_tw_buckets
- 系统重处于TIME_WAIT的连接一旦超过这个值,系统就会将后面的TIME_WAIT连接状态重置。
- 使用SO_LINGER应用强制使用RST关闭
- 调用close后,会立即发送一个RST标志给对端,改TCP连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。
服务端出现大量TIME_WAIT状态的原因有哪些
说明服务端主动断开了很多TCP连接
- HTTP没有使用长连接
- 客户端和服务端任意一方没有开启HTTP Keep-Alive,都会导致服务端在处理完一个HTTP请求后,就主动关闭连接,此时服务端上就会出现大量TIME_WAIT状态的连接。可以让客户端和服务端都开启HTTP Keep-Alive机制。
- HTTP长连接超时
- 如果客户端在完成一个HTTP请求后,在60秒内都没有发起新请求,定时器时间一到,服务端就会出现TIME_WAIT状态连接。
- HTTP长连接的请求数量达到上限
- 当超过一条HTTP长连接上最大能处理的请求后,服务端会主动关闭连接。如果qps很高,nginx就会很频繁的关闭连接,此时服务端上就会出现大量TIME_WAIT状态。调大nginx的keepalive_requests参数就行
服务端出现大量CLOSE_WAIT状态的原因有哪些
被动方才会有的状态,而且被动方没有调用close函数关闭连接,无法发出FIN报文,从而无法是CLOSE_WAIT转变为LAST_ACK状态。