原文链接:图解网络介绍 | 小林coding
一、TCP基本认识
1、TCP头部格式(首部+数据)
什么是TCP?
- 面向连接:一对一连接后才能进行通信
- 可靠的:保证报文一定能到达接收端
- 基于字节流:通过TCP通信时,当消息被操作系统分成多个TCP报文,接收方程序若不知道消息的边界则无法读出有效消息,并且TCP报文是有序的,中间缺失会导致后面的报文都不能处理
TCP与UDP区别?
- TCP面向连接,一对一,可靠交付数据,拥塞控制,流量控制,首部开销,面向字节流传输;TCP数据大于MSS时,则在传输层进行分片,接收端也在传输层进行组装
- UDP不需要连接,可多对多,不保证可靠交付数据,面向报文传输;UDP数据大于MTU时,则在IP层进行分片,接收端也在IP层进行组装再发给传输层
IP层分片,TCP为什么需要在传输层分片?
MTU:一个网络包的最大长度
MSS:除去IP头部和TCP头部以外一个网络包能容纳的最大TCP数据长度
IP层本身没有超时重传机制,由传输层的TCP来负责超时和重传,当IP的数据超过MTU长度时,IP层会对其进行分片,保证每一片都小于MTU,接收端的IP层对数据进行组装发送给传输层,当某个IP分片丢失时,接收端数据无法传送到传输层,发送端无法收到ACK响应,于是触发超时重传机制重传IP报文的所有分片。使用MSS时在传输层进行分片,IP层就不会触发MTU,某个TCP分片丢失时只会重发丢失的TCP分片
二、TCP建立连接-三次握手
1、三次握手过程
- 首先两端都处于CLOSE状态。服务端主动监听某个端口,处于LISTEN状态
- 第一次握手客户端发送SYN报文,TCP头部序列号为客户端随机初始化的序号client_isn,SYN标志位置值为1。处于SYN_SENT状态
- 第二次握手服务端发送SYN+ACK报文,TCP头部序列号为服务端随机初始化的序号server_isn,确认应答号为client_isn+1,SYN标志位置为1,ACK标志位置为1。处于SYN_RCVD状态
- 第三次握手客户端发送ACK报文,TCP头部确认应答号为server_isn+1,ACK标志位置为1。变为ESTABLISHED状态
- 服务端收到ACK报文后,变为ESTABLISHED状态,连接建立,双方可相互发送数据
第三次握手可以携带数据,前两次握手不可以携带数据
为什么握手需要三次?
- 避免历史连接:网络拥堵时,客户端先发的SYN报文比后发的SYN先到,服务端回应对应的SYN+ACK报文,客户端发现确认应答号不是最新的client_isn+1,则会发送RST报文让服务端释放连接,等到最新SYN报文抵达后则可正确连接
- 同步双方初始序列号:各发送一次SYN与ACK,确保双方的初始序列号能被可靠的同步
- 序列号作用:接收方可去除重复的数据;接收方可根据序列号进行按序接收;可标识发送的数据哪些已被对方成功接收
- 避免资源浪费:当网络拥堵时,客户端连续发送多个SYN报文,服务端每收到一个SYN报文即建立一个连接,建立多个冗余的连接造成资源浪费
为什么每次建立连接时,初始化的序列号都要求不一样?
- 防止历史报文被下一个相同四元组的连接接收
- 防止黑客伪造相同序列号的TCP报文被对方接收
初始序列号如何随机生成?
ISN = M + F(localhost, localport, remotehost, remoteport)
- M是一个计时器,每4微秒+1
- F为一个Hash算法,例如MD5,不被外部轻易推算出,根据源IP、目的IP、源端口、目的端口生成一个随机数值
已建立连接的TCP,收到SYN会发生什么?
TCP由 [四元组] 唯一确定:源IP、源端口、目标IP、目标端口
- SYN报文的端口号与历史连接的端口号不一致:通过三次握手建立新的连接。历史连接如果未断开端有数据传输,则断开的接收端内核会回复RST报文来断开连接;如果没有数据传输,TCP保活机制启动检测到对方未存活则断开连接
- TCP Keepalive保活机制:TCP连接成功后一直无数据交互,则会触发TCP保活机制,内核中的TCP协议栈则会发送探测报文,若对端正常工作则重置保活时间,若对端无响应,连续发送几次达到保活探测次数后TCP连接死亡
- SYN报文的端口号与历史连接的端口号一致:处于ESTABLISHED状态的接收端接收到了SYN报文(序列号随机生成,SYN报文乱序),此时接收端会返回一个正确的序列号和确认号的ACK报文,发送端收到了ACK报文发现与自己期望的确认号不一致则会断开连接
2、TCP半连接队列(SYN队列) 和全连接队列(accept队列)
- 半连接队列:客户端向服务端发送SYN请求,服务端收到SYN请求后内核会把该连接存储到半连接队列
- 全连接队列:服务端收到第三握手的ACK报文后,内核会把该连接从半连接队列中取出,然后创建新的完全的连接,并将其添加到全连接队列中
半连接队列本质上是一个哈希表(方便查找),全连接队列本质上是一个链表队列
全连接队列最大值sk_max_ack_backlog=min(somaxconn,backlog)
全连接队列满时,Linux指定怎样回应客户端?
- tcp_abort_on_overflow
- 等于0时,server扔掉client发送的ACK报文,开启定时器,重传第二次握手报文,超过限制则删除连接
- 等于1时,server发送RST报文给client,连接失败(端口未监听)
SYN包因长度被半连接队列丢失的情况
- 半连接队列满,未开启tcp_syncookies
- 全连接队列满,未重传SYN+ACK包的请求多于1个
- 未开启tcp_syncookies,且max_syn_backlog - 当前半队列长度<(max_syn_backlog>>2
半连接队列的最大值max_qlen_log
- 当max_syn_backlog>min(somaxconn,backlog),max_qlen_log=min(somaxconn,backlong*2)
- 当max_syn_backlog<min(somaxconn,backlog),max_qlen_log=max_syn_backlog*2
max_qlen_log表示半连接队列理论最大值,不一定表示服务端处于SYN_RECV状态的最大个数
开启tcp_syncookies=1功能可在不使用SYN半连接队列情况下成功建立连接
- 第二次握手时,服务端不把连接放入半连接队列,而是生成一个cookie值,存在SYN+ACK报文的序列号传给客户端,客户端返回ACK报文时,服务端取出该值验证是否合法
如何避免SYN攻击?
- 调大netdev_max_backlog:当网卡接收数据包的速度大于内核处理的速度时,有一个队列保存这些数据包,netdev_max_backlog为该队列长度的最大值
- 增大TCP半连接队列
- 增大 net.ipv4.tcp_max_syn_backlog
- 增大 listen() 函数中的 backlog
- 增大 net.core.somaxconn
- 开启tcp_syncookies
- 减少SYN+ACK重传次数:当服务端受到SYN攻击时,会有大量的TCP连接处于SYN_RECV状态,处于这个状态的TCP会重传SYN+ACK,重传达到上限会断开连接,tcp_synack_retries设置SYN_ACK最大重传次数 (默认为5次)
3、三次握手细节
- 服务端:
- 首先执行socket()方法创建socket
- 执行bind()方法,将socket绑定在服务端指定的IP和端口
- 执行listen()方法服务端开启监听,进入监听状态(LISTEN),内核为其分配半连接队列和全连接队列
- 阻塞在accept()方法,等待客户端建立连接,全连接队列存入连接后,会等待服务端调用accept()方法取出socket连接使用
- 服务端调用read()方法读取数据
- 客户端:
- 首先执行socket()方法创建socket
- 阻塞在connect()方法,向服务端发起连接请求,收到第二次握手SYN+ACK报文后应用程序从connect()调用返回,表示客户端到服务端的单向连接成功
- 客户端调用write()方法写入数据
- 断开连接时,客户端调用close,此时TCP协议栈会为FIN包插入一个文件结束符EOF到接收缓冲区,EOF会被放在已排队等候接收的数据之后,服务端执行read()读数据处理完后会读取到EOF,然后调用close方法断开连接
没有lisent,能建立TCP连接吗?
服务端未执行listen,客户端向其发送SYN报文,服务端会回复RST报文
不用listen,建立TCP连接:
- 客户端可形成自连接:客户端在执行connect方法时,最后会将自己的连接信息放入到这个全局hash表中,然后将信息发出,消息在经过回环地址重新回到 TCP 传输层的时候,就会根据 IP + 端口信息,再一次从这个全局 hash 中取出信息。于是握手包一来一回,最后成功建立连接
- 两个客户端同时向对方发起TCP连接请求
三、TCP连接断开-四次挥手
1、四次挥手过程
- 客户端准备关闭连接,发送一个TCP首部FIN标志位置为1的FIN报文,客户端进入FIN_WAIT_1状态
- 服务端收到FIN报文后,回复客户端ACK报文,服务端进入CLOSE_WAIT状态,此时客户端不能发送数据,服务端会将未发送完成的数据继续发送完成
- 客户端收到ACK报文后,进入FIN_WAIT_2状态
- 服务端发送完成数据后,向客户端发送FIN报文,服务端进入LAST_ACK报文
- 客户端收到服务端的FIN报文后,向服务端回应ACK报文,客户端进入TIME_WAIT状态,等待2MSL时间后,自动进入CLOSE状态关闭连接,服务端收到ACK报文后进入CLOSE状态
为什么TIME_WAIT是2MSL?
MSL:报文最大生存时间,超过这个时间报文将被丢弃。
TCP基于IP协议,IP协议首部中有一个TTL字段,表示报文能经过的最大路由数。
MSL基于时间单位,TTL基于路由器数量单位,所以MSL应大于等于TTL消耗为0的时间,以确保报文已被自然消亡。TTL一般默认值为64,Linux将MSL默认为30秒。2MSL时长是允许报文丢失一次,若ACK包丢失则重发的FIN包会在2MSL内到达,2MSL将重新计时
TIME_WAIT的作用是什么?
- 防止历史连接被后面相同四元组的连接错误接收:网络拥堵时报文在四次握手后延迟到达接收端,序列号恰好在接收端的接收窗口内,此时TIME_WAIT若过短或没有,则会导致错误的连接。2MSL足以保证两边的报文都自然消亡
- 保证被动关闭方能正确关闭:防止第四次握手的ACK报文丢失而导致FIN报文重传后主动关闭方无法正确回应ACK报文
四、TCP重传、滑动窗口、流量控制、拥塞控制
1、TCP重传
- 超时重传:发送报文时设定一个计时器,若在指定时间内未收到对方的ACK应答报文,则重传该报文。超时重传时间RTO应略大于往返时延RTT,过长会导致效率低,网络的空隙时间大;过短会导致网络负荷大
- 快速重传:连续发送多个报文,当接收端连续收到服务端返回三个相同的ACK报文时,会在定时器过期前重传丢失报文
- SACK (选择性确认) 方法:在TCP头部选项字段中加SACK,可将已收到的数据的信息发送给发送方。在触发了快速重传的情况下,可以知道哪些数据丢失了然后重传
- Duplicate SACK:使用SACK告诉发送方哪些数据被重传
- ACK丢包:触发重传机制后,接收端收到了相同的数据于是返回ACK+SACK告诉发送端已确认号以及重复收到的序列范围
- 网络延时:触发重传机制后,接收端重新发送的数据被接收端正确接收并且返回了ACK报文,后续到达的阻塞报文,接收端则会回复ACK+SACK告诉发送端已确认号以及重复收到的序列范围
2、滑动窗口
窗口大小:无需等待确认应答,而可以发送数据的最大值。窗口实际上是操作系统开辟的一段缓存空间,发送方在等到接收方的确认应答之前必须将发送的数据保存在缓存空间中,若按期收到了确认应答则将发送的数据从缓存中清除。窗口大小由接收方的窗口大小决定,通过TCP头部window字段告诉发送端
累计应答/累计确认:最后ACK可确认前面所有数据是否接收成功
- 发送窗口=可用窗口+已发送但未收到ACK确认的数据
- 接收窗口
3、流量控制
- 发送方:根据接收方的窗口大小进行发送数据,接受方窗口减小则发送方窗口相应减小
- 接收方:接收数据后若未被应用进程完全读取,剩余的数据则会占用缓冲区导致接收窗口减小,接收方窗口减小后会在ACK报文确认数据时通知发送方窗口减小,当接收方窗口减小为0时,发送方窗口也减小为0,发送方启动持续计时器,当计时器超时就发送窗口探测报文,若接收窗口还是为0则重新启动持续计时器,一般窗口探测为3次,3次之后部分TCP发送RST释放连接
先收缩窗口大小,再减小操作系统缓冲区大小:
- 操作系统缓冲区减小后,接收端窗口大小也相应减小,当接收方窗口大小小于当前要接收的数据则会导致数据丢失,同时之前通知发送窗口减小时,减小后的窗口小于发送方已发送但未收到确认的消息长度则会导致窗口出现负值
糊涂窗口综合症
当窗口变得很小时,发送方继续发送小数据,导致数据比TCP头部更小,会造成资源浪费等问题
- 接收方不通知小窗口:当窗口大小小于min(MSS,缓存空间/2),就会向发送方发送窗口为0得通知
- 避免发送发发送小数据:Nagle算法(满足其一)
- 窗口大小大于等于MSS,且数据大小大于等于MSS
- 收到之前发送数据的ACK报文
4、拥塞控制
避免发送发的数据过多挤满网络
什么是拥塞窗口?
拥塞窗口cwnd表达了网络的拥堵情况,根据网络的拥塞程度动态变化
发送窗口swnd=min(拥塞窗口cwnd,接收窗口rwnd)
发送了超时重传,则认为网络出现了拥塞
- 慢启动算法:当发送方每收到一个ACK报文,拥塞窗口大小则加1
- 初始化cwnd为1,当收到了n个ACK报文时,cwnd+n,慢启动算法发包的个数呈指数增长。当cwnd大于等于慢启动门限ssthresh(默认65535字节) 时,就会使用拥塞避免算法
- 拥塞避免算法:当发送方每收到一个ACK报文时,拥塞窗口大小则加1/cwnd
- 一个ACK报文增加1/cwnd,cwnd个ACK报文增加1,则拥塞避免算法发包的个数呈线性增长。当触发了重传机制,则启动拥塞发生算法
- 拥塞发生算法
- 超时重传:ssthresh=cwnd/2,cwnd恢复为初始值,重新开始慢启动算法
- 快速重传:cwnd=cwnd/2,ssthresh=cwnd,进入快速恢复算法
- cwnd=ssthresh+3,重传丢失的数据包
- 收到重复的ACK包,cwnd+1
- 收到新数据的ACK包,cwnd=ssthresh,重新进入拥塞避免算法
- cwnd=ssthresh+3,重传丢失的数据包
五、优化TCP
1、三次握手优化
TCP Fast Open功能:tcp_fastopen=3
- 首次建立连接时,客户端发送SYN报文,包含Fast Open且Cookie为空,表示请求Fast Open Cookie
- 支持TCP Fast Open的服务端生成Cookie,并放在返回的SYN+ACK报文的Fast Open选项一起返回
- 客户端收到SYN+ACK报文后,缓存Fast Open选项中的Cookie
第一次建立连接正常三次握手,之后建立连接时:
- 客户端发送SYN报文,报文包含 [数据] 和之前缓存的Cookie
- 支持TCP Fast Open的服务端会检查Cookie是否有效,若有效服务端将发送SYN+ACK报文对发来的SYN和数据进行确认;反之服务端将丢弃发来的SYN报文中的数据,并发出SYN+ACK对发来的SYN进行确认
- 客户端收到SYN+ACK报文后,若服务端确认了数据则发送ACK确认发回的SYN和数据,若服务端未确认数据,则客户端重新发送数据
2、四次挥手优化
当主动关闭方接收到ACK报文时,关闭方式有以下:
- close函数关闭:孤儿连接,若在tcp_fin_timeout秒内没有收到对方的FIN报文,则连接直接关闭,同时tcp_max_orphans定义了最大孤儿连接的数量,超过直接释放
- shutdown函数关闭
当TIME_WAIT状态过多时,可通过tcp_tw_reuse和tcp_timestamps为1,将TIME_WAIT状态的端口复用于作为客户端的新连接(只适用于客户端)