概述
TCP个人理解应该是最常用的传输层协议了
TCP报文结构
- 报头32位
高16位表示源端口号
低16位表示目标端口号 - 序号 32位
- 确认序号 32位
-
报头标识(16位)
4位首部长度(报头长度)
6位保留长度
6位报文标志位()
URG 标志紧急指针是否有效
ACK 标志确认序号是否有效
PSH 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
RST 要求重新建立连接. 我们把含有RST标识的报文称为复位报文段
SYN 请求建立连接. 我们把含有SYN标识的报文称为同步报文段
FIN 通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段 - 报头长度(4位首部长度+6位保留)
- 窗口大小 16位
- 校验和 16位
- 数据…
连接过程
三次握手过程(握手指的是发送一个数据包,即连接需要三个数据包)
前置准备:服务端创建传输控制块(TCB),时刻准备接收客户端请求(Listen状态)
- 客户端向服务端发送请求连接报文
客户端创建自己的传输控制块(TCB),然后向服务端发送报文
此时客户端进入SYN_SENT(同步已发送)状态
请求连接报文标志位固定SYN=1,序号为X(从请求开始就会消耗序号),且不携带数据 - 服务端收到请求连接报文,发送客户端请求连接确认报文,服务器为SYN_RCVD
请求连接确认报文固定SYN=1,ACK=1,确认序号固定为X+1,同时给一个序号Y - 客户端收到请求连接确认报文后,再向服务端发送[请求连接确认]的确认收到报文
报文固定ACK=1,确认序号为Y+1,报文序号为X+1
从这一步开始,客户端进入ESTABLISHED(连接已建立),服务端收到该报文也进入连接已建立
此时,连接视为真正连通
三次握手的根本目的是为了保证TCP可靠
如果是两次握手,即去掉客户端发生确认报文的确认操作
正常情况没有问题,双方都也可以正常连通
但如果发现异常,即如果第二次握手包丢失,此时对服务端来说这是一个有效连接,但对客户端来说不知道连接是否已建立,客户端的选择要么放弃要么重连,无论哪种情况该连接都是已经失效的,但服务端依旧会视为有效从而继续往该连接发送
没有必要四次握手是因为
在第三次握手中,就已经完成了互相ACK的过程,继续重复ACK没有意义
释放过程
四次挥手过程(挥手也是一个数据包,即释放需要四个数据包才能完整释放)
- 客户端发起连接释放报文,同时停止发送数据
此时客户端进入FIN_WAIT-1(终止等待状态-1),仅仅是停止发送,但照常接受数据 报文标志位FIN,序号为前报文+1 - 服务端收到关闭请求报文后,发回ACK确认收到报文,此时服务端进入CLOSE_WAIT(关闭等待)
此时服务端可以继续处理缓存区内剩下的数据 - 服务端在合适的(处理完毕)时候,向客户端发送连接释放报文,此时服务端进入LAST_ACK
- 客户端接受服务端的连接释放后,发回ACK
服务端在收到最后ACK后,会立即关闭进入CLOSE
但客户端作为发起方会进入TIME_WAIT,即必须等待2*MSL(最长报文生命)才会进入CLOSE
四次挥手的目的也是为了TCP可靠性
四次挥手的本质是两个两次挥手(两个[请求-ACK])过程,而之所以有两个两次挥手是因为
第一次挥手仅仅是客户端提议释放,真正的是关闭是由接收方也就是服务端发起的,因为必须保证
- 服务端确认已经接收处理完毕
- 服务端之前对客户端的数据,客户端已经全部收到
TIME_WAIT
TIME_WAIT是关闭的发起方概念(不一定是连接的客户端,服务端也可以发起关闭)
一个简答理解是,任何关闭发起方都必须等待2*MSL后才能真正关闭
TIME_WAIT的意义在于防止旧连接的影响干扰
四元组中服务端的IP和端口是固定的,需要排除同一个客户端的不同连接干扰
TIME_WAIT只存于发起方是因为,第四次挥手(最后的ACK)由发起方发出,之后再没有包了
因此无法确定第四次挥手的ACK对方是否成功接收
假设第四次ACK接收失败,客户端可能再次发出FIN.而此时如果没有TIMEOUT服务端将连接用于其它,则此时再次收到FIN即视为关闭,而事实上这个FIN是上一次的连接发出
CLOSE_WAIT
CLOSE_WAIT是关闭的接收方的概念
出现CLOSE_WAIT原因是服务端没有发出自己的FIN
网上有句话出现CLOSE_WAIT,最需要检查服务端代码
因为理论上讲,CLOSE_WAIT只会持续很短的时间,即接收方处理完毕(自己的处理完毕和消息已全部发送)后会向关闭提交方发出连接释放,收到ACK后即可以结束CLOSE_WAIT进入LAST_ACK
服务端发不出自己FIN的很大可能是没有发出关闭连接
- 服务端忘记关闭连接了
- 服务端线程被阻塞,迟迟结束不了所以无法关闭连接
RST
RST表示复位,简单来说是指连接的异常型关闭
RST包关闭连接时
- 对于发送方而言立即丢弃缓冲区直接发送RST包后即关闭
- 对于接受方而言接收RST包无需发回ACK
RST可能出现的情况有
- 端口未打开
比如Telnet一个未打开的端口,服务器可能会直接返回RST(可能不理会,比如Win7) - 请求超时
- 提前关闭
传输过程
滑动窗口
为了提升发送效率,TCP将以批量的形式发送缓冲区内数据包,批量范围就是滑动窗口
注意
这个窗口仅仅是逻辑范围或者说ACK范围
严格来说,缓存区发送的单位是段而不是包(这个段就是字节长度,与业务无关)
窗口逻辑范围是指,比如窗口5中,前四段不必等待ACK而等待最后一段,如果有问题则5段重发(窗口重发)
滑动窗口范围是由服务端决定的,这体现的是发送限速,或者叫流量控制
窗口范围由ACK包告知客户端,客户端根据ACK包决定下次的窗口范围
- 如果服务端发现自己的缓冲区快满了,就会减少窗口范围
- 如果服务端发现已经满了就会将窗口置0,此时客户端就不会发送了
此时客户端将定期发送窗口探寻(因为已经ACK,但窗口被置0了)
慢启动
在滑动窗口中,已经实现由服务端控制发送速度了
但完全由服务端控制是不科学的,因为还有网路问题.即服务端可能非常空闲,但网路传输非常拥堵
所以加入慢启动机制(减少初始发送速度,逐步提速)
- 初始拥堵窗口范围1
- 每次ACK后拥堵窗口翻一倍(到某个阈值后改为线性递增)
- 每次超时重发后拥堵窗口降一倍
- 实际发送窗口大小等于 拥堵窗口和服务ACK窗口中的最小值
粘包拆包
TCP中数据的发生与接收都是数据流基于固定字节数的段概念,与业务无关
所以必然出现一个逻辑数据被分成两段提交
粘包拆包的核心在于可以有某个机制在服务端能够标识逻辑分位
- 特殊字符区分
比如FTP,就以换行作为一个逻辑数据 - 长度声明
比如固定前四位为长度位,其标识后续多少个字节为一个逻辑数据
传输总结
- 传输是可靠的
- 批量发送,依据窗口范围来控制批量发送速度
这是因为必须ACK后才能继续发送,如果没有窗口批量发送,就会每发一段就必须等待ACK
而事实上服务端处理可能非常快,一次ACK传输时间都可能大于服务端处理时间,这样服务端将处于饥饿状态 - 窗口范围(发送速率)由两个指标来共同控制
服务端ACK返回的窗口(服务端自身声明的处理能力)
客户端慢启动的拥堵窗口速率
可靠性体现
校验和
校验和的主要为了防半包达到
即在发送时会将该包计算一个校验和,接收端收到后会再次计算以确认数据是否完整
如果计算校验和不匹配,则该包可能出现丢失.会直接丢弃该包让客户度重传
ACK机制
正常情况下的TCP所有包都是需要ACK的(RST不用)
- 连接请求的两次ACK
- 释放请求的两次ACK
- 传输过程的所有包ACK
ACK机制是指
所有的TCP包都有唯一递增序列号
每一个ACK都包含成功接收到的序列号,客户端接受到ACK后才会继续发生,否则超时重传
ACK更像是服务端说我需要哪里
比如1000-4000中.只有2000-3000丢失了,服务端会持续返回ACK2001
客户端三次收到ACK2001后,会将2001-3000重新发送,此时服务端收到后会继续返回ACK4001
(2001-3000已经补全,3001-4000已经收到了,所以ACK4001)
这就是快速重传机制
超时重传
如果发送后一定时间内没有收到ACK结果(因为无法知道数据是否已到达),就会重传
超时重传的依赖是TCP包都有唯一递增序列号,服务端据此处理幂等
超时时间是动态计算的(指数递增)
即发送500MS未收到ACK后即重传第一次,之后等到1000MS,再之后2000MS
累计一定次数后就会认为连接异常而强制关闭连接
Linux-TCP 调优
查看参数
# 查看端口占用
netstat -pan | grep 端口号
->tcp 0 0 192.168.198.151:50070 0.0.0.0:* LISTEN 2390/java
# 根据进程号查看进程详细
ps -ef | grep 2390(进程号)
# 查看TCP连接数以及状态命令
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
用 netstat 检查情况看看是哪个进程的锅
根据 netstat 的检查,使用 tcpdump 抓包分析一下为什么连接会被动断开(TCP知识非常重要)
如果熟悉代码应该直接去检查业务代码,如果不熟悉则可以使用 perf 把代码的调用链路打印出来;
TCP最大连接数
理论最大连接数
- 客户端
客户端需要随机使用一个端口发出数据
抛开0端口的特殊用途,客户端最大连接数理论为65536-1=65535个 - 服务端
服务端一般固定使用一个端口监听数据(这一个端口是独占的)
服务端通过客户端包的IP+端口区分,所以其最大连接数理论为2的32次方(IP)*2的16次方(端口)
实际最大连接数
在Linux中,TCP-Socket视为一种文件描述符,所以最大连接数实际受应用最大文件打开数量限制
最大文件打开数量受用户和系统两级管制(这里的文件打开数量还包括标准输入输出等等)
**系统级 **
vi etc/sysctl.conf
fs.file-max=65535(默认)
用户级
ulimit -n # 查看系统内同一时间运行打开文件描述符的最大值(默认1024)
ulimit -n x # 会话级设置,检测设置数字是否允许
vi /etc/security/limits.conf
root soft nofile 102400
root hard nofile 102400
[*用户] ....
TIME_WAIT
vi /etc/sysctl.conf
# 开启SYN Cookies 防范少量的SYN攻击
# SYN的服务端ACK时不创建缓冲区
# 等到客户端的ACK收到后确认连接有效,再创建缓存区
net.ipv4.tcp_syncookies=1
# 客户端通过时间戳来讲来决定使用继续使用tw连接
# 如果包的时间戳比以前的最大时间戳大(新包),会将tw重新分配使用(服务与客户端同时打开时间戳)
# (客户端才有意义,因为客户端有65535上限.tw过多会影响处理能力)
net.ipv4.tcp_tw_reuse=1
# 服务端通过时间戳来来决定是否接受(旧包忽略,从而不用等待tw)
# 服务端线上不建议打开(NAT网络下,时间戳递增性无可保证,会造成大量连接失败)
net.ipv4.tcp_tw_recycle = 1
# fin-wait_2维持多长时间(30秒)
net.ipv4.tcp_fin_timeout = 30
# tw上限,超过
net.ipv4.tcp_max_tw_buckets = 30
# 修改立即应用
sysctl -p