📕 TCP 概述
2022年6月20日
- 计算机网络
📕 TCP 概述
1. 是什么
1) TCP 概念
- 面向连接
- 一对一
- 可靠的
- 面向字节流
- 工作在传输层
- 数据传输服务
- 用 TCP 传输的网络包是 无损坏、无间隔、非冗余、有序的
2) TCP VS UDP
TCP | UDP | |
---|---|---|
连接建立 | 三次握手 | 不需要建立连接,即可传输数据 |
对象数 | 一对一 | 一对一、一对多、多对多 |
可靠性 | 可靠传输 | 尽最大努力交付,不保证一定可靠 |
首部长度 | [20, 60] 字节 | 固定 8 字节 |
传输方式 | 面向字节流 | 面向报文 |
数据分片 | TCP 会在传输层进行分片 | 传输层不负责分片,IP 层可能进行分片操作 |
适用场景 | 文件传输、http/https | 音视频传输、广播通信 |
2. 头格式
1) 源端口号 & 目的端口号
- 源端口号
> 1023
的 16 位数
- 目的端口号
- 接受者所用的端口号
2) 序列号 SEQ
- 初始序列号 [ISN] 的确定
- 建立连接时,由客户端随机生成
- 基于时钟,4 ms 自动 ++,转一圈大约需要 4.55 小时
- ISN = M [计时器生成的值] + hash(源 ip,源端口号,目的 ip,目的端口号)
- 后续改变
- 每发送一个字节数据,SEQ ++
- 达到 max [== 32 位无符号整数] 后序列号回绕,再次从 0 开始
- 序列号有效性问题
- 当在一个速度足够快的网络中传输大量数据时,序列号的回绕时间会变得很短,此时无法有效判断到底是历史序列号还是当前序列号
- 如何解决?
- 添加 TCP 时间戳,
tcp_timestamps
参数是默认开启的,此时就进入了 PAWS 机制 - 开启后,TCP 头部就会有个时间戳选项,当收到数据包中的时间戳不是递增的,就表示该数据包是过期数据包,直接丢弃
- 作用:
- 便于精确计算 RTT
- 防止序列号的回绕
- 时间戳也是 32 位数据,若是也发生回绕,如何避免?
- 增大时间戳的位数,可以设置为 64 位
- 添加 TCP 时间戳,
3) 确认号
- 确认号 == 初始序列号 + TCP 段长度
- 下一次期望收到的对方数据的序列号
- 在这个序列号之前的数据本方已经全部接收完毕,用来解决丢包问题
4) 首部长度 & 保留位 & 控制位 & 窗口大小
- 首部长度 [4 bit]
- 表示 TCP 头的长度
- 2^4 ∈ [0, 15],每个 1 代表 4 字节
- 因此可以得出结论,TCP 头部 max = 15 * 4 = 60 字节
- 首部长度 min == 20 字节
- 保留位 [6 bit]
- 未使用,置零
- 控制位 [6 bit]
- UAG
- 本报文段发送的数据是否含有 紧急数据,只有当 UAG == 1 时,紧急指针才有效
- ACK
- 只有当 ACK == 1 时,前面的 确认号才有效,除了第一次握手没有 ACK 置一外,之后均应该 == 1
- PSH
- PSH == 1 时,表示对方应立即从 TCP 接收缓冲区中读走数据,为接收后续数据让出空间
- RST
- RST ==1 时,说明需要释放连接,并重新建立连接 | 上次发送给主机的数据有问题,主机拒绝响应
- SYN
- 只有当 SYN == 1 时,表示处于第一次握手 | 第二次握手阶段
- FIN
- FIN == 1 时,说明本方数据已经发送完,希望断开连接
- UAG
- 窗口大小
- 现在允许对方发送的数据量
- 达到阈值后,需要本方 ACK 确认后,对方才可继续发送后面的数据
5) 校验和 & 紧急指针
- 校验和
- 提供额外的可靠性
- 紧急指针
- 标记紧急数据在数据字段的多少位,只有当 UAG == 1 时才有效
6) 选项
- 最长可为 40 字节,必须为 4 字节的整数倍,才能保证 TCP 首部长度为 4 字节的整数倍
3. Socket 编程
1) 流程
2) 说明
- 服务端 & 客户端初始化
socket
,得到文件描述符 - 服务端调用
bind()
,绑定客户端与自己建立 TCP 连接的 ip & port - 服务端调用 listen(),监听 port
int listen (int socketfd, int backlog)
socketfd
文件描述符backlog
早期为半连接队列大小,目前通常认为是全连接队列大小
- 服务端调用
accept()
,等待客户端连接 - 客户端调用
connect()
,向服务器 ip & port 请求建立连接,通过 3 次握手建立连接- connect() 成功,表示第二次握手 - 客户端成功接收 ACK 报文 成功
- 服务端调用
accept()
,返回客户端用于传输的 socket 文件描述符- accept() 成功,表示第三次握手成功,双方连接建立完毕
- 客户端调用
write()
,写请求数据,发送给服务端 - 服务端调用
read()
,读取客户端来的数据 - 服务端调用
write()
,返回客户端响应数据 - 客户端调用
read()
,读取服务端传来的响应报文 - 客户端端口连接时,会调用
close()
,发送给服务端- close() 调用后,客户端会向服务端发送
FIN
报文,进入fin_wait_1
状态
- close() 调用后,客户端会向服务端发送
- 服务端接收到客户端
FIN
报文后,TCP 协议栈为 FIN 包插入一个文件结束符EOF
到接收缓冲区- 服务端调用
read()
读取报文时,可以识别到EOF
,知道客户端数据发送完毕,服务端进入close_wait
状态 - 服务端发完要发的数据后,调用
close()
发送FIN
包给客户端,服务端进入lack_ack
状态
- 服务端调用
- 客户端收到服务端的
FIN
包后,发送ACK
包给服务端,进入time_wait
状态 - 服务端收到
ACK
包后,进入close
状态 - 客户端经过
2MSL
时间后,也进入close
状态
4. 粘包问题
1) TCP 面向字节流
- TCP 传输数据时,消息可能会被分成多个 TCP 报文,因此一个 TCP 报文并不能代表一条完整是用户信息
- 数据如何进行拆分,取决于发送窗口、拥塞窗口、当前发送缓冲区的大小等因素
- 因此 TCP 不能称为面向报文,而是称作面向字节流
2) TCP 粘包问题
- 当 2 个消息的某个部分被分到一个 TCP 报文中时,就是 TCP 粘包
- 如何解决?
- 分成固定长度的消息
- 接收方接收固定消息后,就认为这个内容是完整的
- 特殊字符作为分包边界
- 接收方读到某个特殊字符后,认为之前的内容是完整的
- 自定义消息结构
- 双方协商好消息结构,通过消息结构对数据进行区分,例如消息结构中包含了本次包的长度,那么读取到这个长度的数据后,之前的内容就是完整的
- 分成固定长度的消息