重点
-
传输层的作用:负责数据能够从发送端传输接收端
-
理解端口号的概念。
-
认识 UDP 协议,了解 UDP 协议的特点。
-
认识 TCP 协议,理解 TCP 协议的可靠性
-
理解 TCP 协议的状态转化
-
掌握 TCP 的连接管理,确认应答,超时重传,滑动窗口,流量控制,拥塞控制,延迟应答,捎带应答特性
-
理解 TCP 面向字节流,理解粘包问题和解决方案
-
能够基于 UDP 实现可靠传输
-
理解 MTU 对 UDP/TCP 的影响
一、TCP协议
协议格式
-
源/目的端口号:表示数据是从哪个进程来,到发送到对端主机上的哪个进程。
-
32位序号/32位确认序号:分别代表TCP报文中数据的编号以及对编号的确认。
-
4位首部长度:表示该TCP报头的长度,读取 tcp 报文。
-
6位保留字段:TCP报头中暂时未使用的6个比特位。
-
16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段。
-
16位检验和:由发送端填充,采用CRC校验。接收端校验不通过,则认为接收到的数据有问题。(检验和包含TCP首部+TCP数据部分)
-
16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用。
-
选项字段:TCP报头当中允许携带额外的选项字段,最多40字节。
32位序号/32位确认序号
从客户端发送的序号是位序号,从服务端发送的序号是确认序号,两组序号保证 tcp 在双向交流中的可靠性,例如A:吃了吗?B:吃了
确认应答
确认信息发出的顺序,全双工的
- 客户端向服务器发送第一个报头,并将序号设置为 1
- 服务端向客户端返回一个报头,将确认序号设置为 2
- 客户端向服务器发送第二个报头,并将序号设置为 3
- 服务端向客户端返回一个报头,将确认序号设置为 4
- 如果服务端接收到的报头是1 2 3 5 6,那么服务器返回的序号是4
4位首部长度
单位是4字节,例如首部显示1001(9),那么tcp报文长度是9*4=36字节,其中报文长度最小是20,所以首都长度最小是20/4=5
需要读取 20 字节,从这 20 字节中取出 4 位首部长度,获得报头的实际长度;再重新读取获得了完整的 tcp 报头,剩下的部分就是有效载荷
六个标记位
区分报文的种类,执行对应动作,六个标志位都只占用一个比特位,为0表示假,为1表示真
- SYN-Start Your Network: 请求与对方建立连接(又称同步报文)
- FIN-Final INteraction: 表示请求关闭连接,又称为结束报文
- ACK-Acknowledge Key:确认序号是否有效
- PSH- Push Send Here:要求对方立马从 tcp 缓冲区中取走数据
- URG-URgent Go:紧急指针是否有效
- RST-ReSeT:要求重置连接(双方重新建立一次新的 tcp 连接)
SYN
报文当中的SYN被设置为1,连接建立阶段,SYN才被设置,正常通信时SYN不会被设置
FIN
报文当中的FIN被设置为1,断开连接阶段,FIN才被设置,正常通信时FIN不会被设置
ACK
报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认,除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK
PSH
报文当中的PSH被设置为1,要求对方立马取走缓冲区中数据
URG
当URG标志位被设置为1时,设置此标记位的报文具有较高优先级(插队),通过TCP报头中的16位紧急指针来找紧急数据。
16 位紧急指针:
指向是紧急数据在 tcp 报文中的偏移量。紧急数据规定只有 1 个字节。
紧急数据字节号(urgSeq) = TCP报文序号(seq) + 紧急指针(urgpoint) −1
例如:seq = 10,urgpoint = 5,那么字节序号 urgSeq = 10 + 5 -1 = 14
RST
报文当中的RST被设置为1,表示需要让对方重新建立连接
窗口大小
TCP的接收缓冲区和发送缓冲区
-
调用write/send接口时,数据从应用层拷贝到发送缓冲区。
-
调用read/recv接口时,数据从接收缓冲区拷贝应用层。
-
类比调用read和write进行文件读写时,不是直接从磁盘读取数据,而是从文件缓冲区进行的读写操作。
缓冲区意义
为防止缓冲区满了,后续到来的数据被拒收或者丢弃,在最开始告知对方收缓大小,通信时告诉对方缓冲区容量。
线程使用的tcp缓冲区是独立的,互相之间不会产生冲突。
校验和-不是重点
在发送端,TCP 将数据段中的所有字节(包括 TCP 头部和数据部分)加起来,然后取其反码作为校验和。接收端也采用同样的方式计算校验和,并与接收到的校验和进行比较,以验证数据的完整性。
连接管理机制
三次握手
- 最开始时客户端和服务器都处于 CLOSED 状态
- 客户端向服务端发送 SYN=1报文,请求建立连接(客户端 进入 SYN-SENT 状态)
- 服务端在收到报文后,回应 ACK=1和SYN=1 的报文,在确认应答的同时,请求建立连接(服务端进入 SYN-RCVD 状态)
- 客户端收到这条报文后,发送应答 ACK=1(客户端认为连接成功建立 ESTABLISHED)
- 服务端收到客户端发送的应答,三次握手完成(服务端认为连接成功建立 ESTABLISHED)
- 通信双方可以开始进行数据交互
为什么是三次握手
一次握手没有回应不安全,两次握手,在服务器返回应答时如果丢包了,连接没有建立,也不安全。
三次握手,第三次握手由服务器确认,说明第二次握手客户端收到了,此时就证明自己和客户端都是能发能收的,如果第三次握手服务器没有收到,服务器就会发送 RST 让客户端重新建立开始握手
套接字和三次握手之间的关系
-
服务器进入LISTEN状态,调用对应listen函数。
-
客户端就向服务器发起三次握手,调用的就是connect函数。
-
完成了三次握手,服务器端建立连接,在内核的等待队列当中,服务器端调用accept函数将建立好的连接获取上来。
-
当服务器端将建立好的连接获取上来后,双方就可以通过调用read/recv函数和write/send函数进行数据交互了。
四次挥手
- 最开始时客户端和服务器都处于 ESTABLISHED状态
- 客户端要断开连接,发送 FIN=1(客户端进入 FIN WAIT 1 状态)
- 服务器收到了报文,发送 ACK=1(服务器 进入 CLOSE-WAIT 半关闭状态)
- 客户端收到了报文(客户端 进入 FIN WAIT 2 状态),此时只是客户端要和服务器单方面分手,客户端->服务器 的路被切断了
- 服务器发完数据了,服务器发送 FIN=1(服务器进入 LAST ACK 状态)
- 客户端收到报文,发送回应 ACK=1(客户端进入 TIME WAIT 状态,将在一段时间后进入 CLOSE 断连状态)
- 服务器收到了报文(服务器 进入 CLOSE 状态)
为什么是四次挥手
断开连接时要断开从客户端到服务器方向的通信信道,也要断开从服务器到客户端的通信信道,其中每两次挥手对应就是关闭一个方向的通信信道,因此断开连接时需要进行四次挥手
第二次和第三次挥手不能合并在一起,因为第三次挥手是服务器端想要与客户端断开连接时发给客户端的请求,而当客户端收到服务器断开连接的请求并响应后,服务器不一定会马上发起第三次挥手,因为服务器可能还有某些数据要发送给客户端,只有当服务器端将这些数据发送完后才会向客户端发起第三次挥手
套接字和四次挥手之间的关系
- 客户端发起断开连接请求调用close函数。
- 服务器发起断开连接请求调用close函数。
- 一个close对应的就是两次挥手,双方都要调用close,因此就是四次挥手。
四次挥手中前三次挥手丢包时的解决方法
- 第一次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
- 第二次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
- 第三次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
- 第四次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
CLOSE_WAIT状态
客户端调用close函数关闭对应的文件描述符,底层操作系统就会向服务器发起FIN请求,服务器收到客户端的FIN请求后向客户端发送应答ACK,并进入CLOSE_WAIT状态。
如果服务器端不调用close函数关闭对应的文件描述符,服务器就不会给客户端发送FIN请求,相当于只完成了四次挥手当中的前两次挥手,客户端和服务器的连接状态分别会维持为FIN_WAIT_2和CLOSE_WAIT
TIME_WAIT状态
客户端完成四次挥手后,不会立即进入CLOSED状态,而是进入短暂的TIME_WAIT状态后,才会进入CLOSED状态,短暂TIME_WAIT状态,可能为了保证了最后一次客户端的 ACK 的正常递达,和客户端服务器两者消息通信
TIME_WAIT状态引起的bind失败
如果立马把 tcp 服务器关了后开,同一个端口无法被 bind ,以为此时端口处于TIME-WAIT 状态
解决办法-setsockopt
端口复用
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-
sockfd:需要设置的套接字对应的文件描述符。
-
level:被设置选项的层次。比如在套接字层设置选项对应就是SOL_SOCKET。
-
optname:需要设置的选项。该选项的可取值与设置的level参数有关。
-
optval:指向存放选项待设置的新值的指针。
-
optlen:待设置的新值的长度。
返回值说明:
- 设置成功返回0,设置失败返回-1,同时错误码会被设置。
只需要在 bind 函数之前添加上如下代码,就能实现端口复用
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
此时当服务器崩溃后我们就可以立马重新启动服务器,而不用等待TIME_WAIT结束
超时重传
个报文长时间未收到对方的 ACK 回应,则需要进行超时重传。
如果同一个报文超时重传了好几次,还没有收到对方的应答,就会认为对方的服务挂掉了,此时本端会强制断连
滑动窗口
发送方不用等待ACK一次所能发送的数据最大量
缓冲区数据分类
- 已经发送并且已经收到ACK的数据。
- 已经发送还但没有收到ACK的数据-滑动窗口
- 还没有发送的数据。
窗口大小
- 窗口大小是由对方的接收能力决定的,tcp 报头中,16 位窗口大小就是滑动窗口的大小
- 当对方接受能力变小时,左边向右移动,滑动窗口右边不变
- 当对方接受能力变大时,滑动窗口左边不变,右边向右移动
实现滑动窗口
窗口左端= 服务器返回确认序号ACK,窗口右端 = 窗口左端+窗口大小
中间丢失
- 数据包丢了,客户端发送1-1000,1001-2000,2001-3000三个数据包,其中1001-2000数据包丢失,则服务器返回序号是1001响应报文,后面接收到2001-3000数据包不算数。而客户端因为重传发送1001-2000数据包。
- 数据包已经抵达,ACK丢包,客户端发送1-1000,1001-2000,2001-3000三个数据包,2001-3000数据包对应的ACK丢失,客户端下一次序号从3001开始发送。虽然中间ACK丢失,但是最后ACK接收到了,说明所有数据包客户端接收到了
快重传
数据包丢了,客户端发送1-1000,1001-2000,2001-3000三个数据包,其中1001-2000数据包丢失,则服务器返回序号是1001响应报文,客户端来纳许三次接收到1001响应报文,就会将1001-2000的数据包重新进行发送,服务器收到1001-2000的数据包后,下次返回的就是3001响应报文
快重传 VS 超时重传
- 快重传是能够快速进行数据的重发,当发送端连续收到三次相同的应答时就会触发快重传
- 超时重传一样需要通过设置重传定时器,在固定的时间后才会进行重传
流量控制
TCP支持根据接收端的接收数据的能力来决定发送端发送数据的速度
-
接收端将接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK通知发送端。
-
接收端发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。
-
接收端缓冲区满了,就会将窗口值设置为0,这时发送方不再发送数据,但需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
拥塞控制
发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞,出现大量的丢包,网络出现拥塞问题,不应该立即将这些报文进行重传,而应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率。
步骤:慢开始、拥塞避免
慢开始
刚开始发送的少,逐渐增多
- 拥塞窗口从1 开始
- 每收到一个ACK应答拥塞窗口的值就加1,拥塞窗口就是以指数级别进行增长
- 拥塞窗口>=阈值,开始拥塞避免
拥塞避免
当发送数据超过阈值,开始拥塞避免
- 从指数增长变为线性增涨,拥塞窗口值每次 + 1
- 当发送超时,即发生网络拥塞时,慢启动阈值设置为拥塞窗口/2,拥塞窗口大小设置 1 ,如此循环
滑动窗口 = min(拥塞窗口,对方接收窗口大小)
延迟应答
服务器收到消息后,等一会再给客户端应答,此时等待的是应用层取走接收缓冲区中的数据,之后回应 ACK 时,ACK 中的接收窗口大小也就更大
- 假设接收端缓冲区为 1MB,一次收到了 500KB 的数据;如果立刻应答,此时返回的窗口大小就是 500KB;
- 但实际上,接收端处理的很快,在 30ms 后,应用层就取走了收到的这 500KB 的数据
- 这时候,缓冲区就已经恢复成 1MB 了,返回窗口大小是 1MB
策略
- 隔 N 个包应答一次,一般N取2
- 隔一定时间应答一次(要在延迟的同时,避免对方进行超时重传)
捎带应答
在回答对方的消息 (ACK) 的同时,携带上自己要发送给对方的信息,双方通信时就可以不用再发送单纯的确认报文
面向字节流
TCP 不关心这些数据的组成,在TCP看来这些只是一个个的字节数据,TCP只需将这些数据发送到对方的接收缓冲区当中
- 调用 write 时,数据先写入发送缓冲区中,如果一次性发送的字节数太长,会被拆分成多个 TCP 的数据包发出,如果发送的字节数太短,可能会在缓冲区中等待,到一定数据量后,再发出
- 接收数据时,数据从网卡被读取后,流入操作系统的接收缓冲区,调用 read 从接收缓冲区中拿到数据
粘包问题
粘包问题中的“包”,是指的应用层的数据包,在应用层角度,看到TCP传输过来的数据是连续的字节数据,读取数据时可能会出现一次读取1.5个报文。
解决办法
明确报文和报文之间的边界
- 定长报文,保证每次都按固定大小读取
- 变长报文,报文开头写明本条报文长度
- 变长报文,包和包之间使用明确的分隔符
UDP是不存在粘包问题,因为有报文长度的字段
用UDP实现可靠传输
- 引入序列号,保证数据按序到达。
- 引入确认应答,确保对端接收到了数据。
- 引入超时重传,如果隔一段时间没有应答,就进行数据重发。
- …