UDP协议
UDP协议端格式

- 16位UDP长度,表示整个数据包(UDP首部+UDP数据)的最大长度
- 如果校验和出错,直接丢弃
UDP的特点
- 无连接:知道端的IP和端口号就直接传输,不需要建立连接
- 不可靠传输:因为没有确认应答机制,所以对方是否成功接收到了数据
- 面向数据报:不能灵活的控制读写数据的次数和数量
UDP的缓冲区
- UDP没有真正意义上的鹅发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
- UDP有接收缓冲区,但是接收缓冲区不能保证收到的UDP数据报的顺序是否和发送的一致。如果缓冲区满了,之后到来的数据报会被丢弃。
UDP的Socket既能读,也能写,这就是 全双工
基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
TCP协议
TCP为传输控制协议,它对数据的传输进行了一个详细的控制
TCP协议段格式

- 源/目的端口号:表示数据从哪个进程来,到哪个进程中去
- 32位序号:发送数据的字节序号,用来重组数据。该字节序号与发送缓冲区的字节序相关联。
- 32位确认序号:仅在ACK为1的情况下有效,表示下一次期望接收的字节序
- 4位首部长度:表示该TCP头部有多少个(4字节),比如 若当前4位首部长度为5,则TCP头部有5*4=20字节
- 6位标志位
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端尽快把缓冲区中的数据取出
- RST:要求重新建立连接;携带RST标识的成为复位报文段
- SYN:请求建立连接;携带SYN的叫做同步报文段
- FIN:请求结束连接;携带FIN的叫做结束报文段
- 16位窗口大小:表示接收方的窗口大小。如果这个值比较大,可以多发一点数据;否则,少发一点
- 16位检验和:发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分
- 16位紧急指针:一个偏移量,标识哪部分数据是紧急数据。这个指针只在URG标记位为1时生效。
- 40 字节头部选项: 可变长度,用于定义附加选项,比如最大报文段长度(MSS)、时间戳等。假如窗口大小大于16位,就可以附加增大窗口大小的选项。
确认应答机制
TCP为了保证发送数据的可靠性,要求接收端在接收到数据之后,向发送端发送一个携带ACK的报文段,来告诉发送端上一个报文段已经成功送达。

TCP为每个字节的数据都进行了编号,也就是序列号

每个ACK都带有对应的确认序号,告诉发送者,我已经收到了那些数据,下次发送你应该从哪里开始
比如上图中,接收端向发送端发送了"确认应答(下一个是1001)",这表明前1000个字节的数据已经发送并接受成功了,发送端下一次发送应该从1001开始发送
超时重传机制

如果发送端发送报文之后长时间没有收到来自接收端的ACK报文,那么就说明可能是由于网络拥塞丢包没有到达接收端,或者接收端发送的ACK由于某种原因丢失了 。这个时候发送端会再次发送一个一模一样的报文段。这就叫做超时重传机制
- 如果接收端收到了重复的数据该怎么处理?
- 根据TCP报文段中的32位序号,如果这个序号的数据已经有了,那么接收方直接丢弃这个多余的报文,然后向发送端发送ACK确定报文段即可
- 超时的时间如何确定?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".但是这个时间的长短, 随着网络环境的不同, 是有差异的。
为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间: Linux中,超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍. 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制(握手和挥手)
正常情况下,TCP建立连接需要三次握手,断开连接需要四次挥手

服务端状态转变:
-
CLOSED -\> LISTEN\] 服务端调用listen进入LISTEN状态,等待客户端连接
-
SYN_RCVD-\>ESTABLISHED\] 服务端一旦接收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据
-
CLOSE_WAIT-\>LAST_ACK\] 进入CLOSE_WAIT状态表示服务器准备关闭连接;当服务器真正调用close关闭连接,会向客户端发送一个FIN,此时服务端进入LAST_ACK状态,等待最后一个ACK的到来
客户端状态转变:
-
CLOSED-\>SYN_SENT\] 客户端调用connect,向服务端发送SYN同步报文
-
ESTABLISHED-\>FIN_WAIT_1\] 客户端调用close,并向服务端发送FIN结束报文,进入FIN_WAIT_1状态
-
FIN_WAIT_2-\>TIME_WAIT\] 客户端收到来自服务端的FIN结束报文段,进入TIME_WAIT状态,发出LAST_ACK确认报文
- 为什么等待 2MSL 就可以了?
- 假如说客户端发送的ACK没有到达服务端的话,在等待第一个MSL之后,服务端就会因为超时重传机制,重新发送一个一模一样的报文(第二个MSL);这时候客户端再次接受到这个报文,就会重新发送一个LAST_ACK确认报文
- 为什么等待 2MSL 就可以了?
TIME_WAIT状态
TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL之后才能回到CLOSED状态。此时已经发起了第四次挥手,为了保证这个第四次挥手能被接收方接收到,所以需要等待一段时间。当然,这个ACK也有可能丢失,那么服务器会再重发一个 FIN。
同时,等待一定的时间再关闭套接字,也是为了两个传输方向上还没有被接收的数据的报文段都已经消失,保证下次重启同一个端口的服务器不会收到旧数据。
CLOSE_WAIT
如果服务端收到客户端的FIN结束报文,服务端发送ACK确认报文之后会进入CLOSE_WAIT,表示服务端自己也准备要关闭了。
这个状态下,服务端依旧可以先向客户端发送数据,直到服务端调用close函数,结束这个状态。
如果应用程序没有及时调用close函数,连接会长时间停留在CLOSE_WAIT状态。这样就会导致资源泄露(如文件描述符耗尽),影响服务器性能。
为什么是三次握手和四次挥手
为什么三次握手就能保证建立可靠的连接呢?
- 确认对方主机的状态和收发能力的最小次数。三次握手保证了双方网络的连通性,也保证了双方连接的意愿,还保证了双方拥有接收和发送数据的能力
- 如果只有一次握手,意外着只要发送方发送连接请求,连接就直接被建立了。那如果服务端收到了SYN洪流攻击,服务端建立大量的连接对象消耗太多资源从而导致崩溃。两次握手也是如此,服务端收到一次连接,只要服务端发出一次ACK,就说明连接就建立起来了(发送方可以不要这个ACK,而是只发SYN)。如果有不法分子恶意发送大量的SYN,服务器依旧会创建对应的连接对象,依旧非常消耗资源。
- 第一次(客户端 → 服务器,SYN):客户端告诉服务器 "我能发,我要连接,我的 ISN 是 X";
- 第二次(服务器 → 客户端,SYN+ACK):服务器告诉客户端 "我能收(收到你的 SYN),我也能发(我的 ISN 是 Y),确认你的 X";
- 第三次(客户端 → 服务器,ACK):客户端告诉服务器 "我能收(收到你的 SYN+ACK),确认你的 Y,我已准备好通信"。
为什么是四次挥手而不是像握手一样进行三次挥手?
当客户端发起第一个挥手的时候,只是意味着客户端不会向服务端发送数据了,但是不以为这个服务端不会像客户端发送数据。有可能服务端还没有将数据完全发送给客户端,这时候如果关闭了连接,这些数据就会丢失。所以客户端需要等待一些时间来接受这些数据,直到服务端主动关闭连接,此时就代表服务端发送完了,客户端也不需要再收取数据
滑动窗口

收到ACK 后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数
据往返的时间较长的时候.
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性
能(其实是将多个段的等待时间重叠在一起了).

- 窗口大小指的是无需等待确认应答可以继续发送数据的最大值。比如上图可以窗口大小就是4000字节(4个段)
- 发送前4个段的时候,不需要等待任何ACK,直接发送
- 收到第一个ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
- 窗口越大,网络的吞吐量越高

那么如果出现丢包了,如果进行重传?
情况一,数据报已经抵达,ACK被丢了

ACK丢失不要紧,因为后序的ACK可以进行确认。
情况二,数据包直接丢失了

- 当某一段报文段丢失之后,发送端会一直受到1001这样的ACK
- 如果发送端主机连续收到三次同样的应答(1001),就会将对应的数据1001~2000的数据重新发送
- 这个时候接收端收到了1001 之后, 再次返回的ACK 就是7001 了(因为2001 -
7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为**"高速重发控制"(快重传)**
流量控制
接收端处理数据的速度是有限的,如果发送端发送的太快就会导致接收端的接收缓冲区打满。这个时候如果发送方继续发送数据包就会造成丢包。
TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制。
上面提到TCP的协议格式中有一个32位窗口大小。这个32位窗口大小就是接收端缓冲区的大小,如果这个窗口大小比较大,表示接收端缓冲区还有较多的空间,可以一次发送多一点数据,否则一次少发一点数据。
16 位数字最大表示65535, 那么TCP 窗口最大就是65535 字节么?
TCP 首部40 字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移M(<<M) 位;
此外,如果接收端的缓冲区满了,就会把窗口值设为0。这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.也可以设置PSH标记位来提醒接收端快点从缓冲区里读数据。

拥塞控制
虽然TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开
始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络
状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
为了解决这个问题,或者说避免加重有拥堵,引入拥塞窗口的概念。
那发送方如何根据拥塞窗口来调整自己的发送数据包的量呢?
- 如果发送方总是收不到ACK,说明丢包率高,可以判断网络已经拥堵了
- 此时不能发送太多的数据,否则加重网络的拥堵。开始发送一个数据报,定义拥塞窗口的大小为1,每收到一个ACK,窗口大小+1
- 每次发送的时候都将拥塞窗口和接收端提供的窗口取最小值,作为实际窗口大小。
TCP 引入慢启动机制 , 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按
照多大的速度传输数据;

慢启动
"慢启动" 只是指初使时慢, 但是增长速度非常快.
- 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍
- 此处引入一个慢启动的阈值
- 当慢启动超过这个阈值,不再按照指数方式增长,而是按照线性方式增长

- 当TCP 开始启动的时候, 慢启动阈值等于窗口最大值
- 每次超时重传之后,慢启动阈值会变成原来的一半,同时拥塞窗口为1
延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。此时可以采用延迟应答,不立刻返回ACK,而是等一下,等待的过程中可能接收端从缓冲区中处理一些数据,到时候发送的ACK的窗口会大一点,发送方收到后也就能多发一点数据报
当然,延迟的时间肯定不能太长(有最大延迟时间),不然发送端就超时重发了。

捎带应答
在延迟应答的基础上,发现,很多情况下,客户端服务器子啊应用层上也是一发一收的,意味着客户端给服务端说"你好",服务端也给客户端说"世界",那么这个时候ACK就可以搭顺风车,和服务端回应的"世界"一起发给客户端。

比如第二次握手的时候,就是ACK和SYN标记为一起生效发送给客户端

为什么第二次挥手的时候不能使用捎带应答的方法类减少挥手次数呢?
第二次挥手是,服务端向客户端发送ACK确认报文的过程,这只是表明客户端不需要向服务端发送数据,但不意味着服务端不需要向客户端发送数据,如果捎带应答上了FIN标记,那么服务端的一些数据可能就会被丢弃,无法发送给客户端。
粘包问题
粘包是指接收端在读取数据的时候,可能会将多个数据报拼接在一起,获奖一个数据报分成多次读取。这通常发生了流的传输协议(TCP)中
每个UDP数据包都是一个独立的报文,具有明确的边界。因此,UDP协议本身不会出现粘包问题。每次调用recvfrom或recv函数时,都会接收到一个完整的UDP数据包。
但是TCP接收方按照固定长度读取数据,就可能读取不完整或者是一次读取多个数据包。因此需要使用特殊字符(如换行符、空格等)作为数据包之间的分隔符。接收方根据特殊字符来拆分数据包
对比TCP和UDP
- TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景
- UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传输等. 另外 UDP 可以用于广播;
- 如果不知道使用什么传输层协议,那就优先考虑TCP
如何使用UDP实现可靠传输
主要参考一下TCP的可靠性机制,在应用层实现类似的逻辑
- 引入序列号,保证数据顺序
- 引入确认应答机制,确保收到了数据
- 引入超时重传,如果一段时间没有应答就要重发数据
- 引入窗口大小,控制流量
- 拥塞控制、捎带应答、延迟应答
- 等等