UDP协议
UDP协议全称为User Datagram Protocol,用户数据报协议。UDP协议报文格式如下:
- 16UDP长度。表示整个数据报的最大长度,即UDP首部+UDP数据。这个字段帮助我们确保在网络字节流中获取完整的UDP报文信息。
- 校验和:用于检测数据报在传输过程中是否发生错误。
UDP特点
- 无连接:UDP不需要建立连接就可以发送数据,这意味着在发送数据之前,发送方和接收方并没握手过程。
- 不可靠传输:UDP不保证数据包的到达顺序,也不保证数据包一定到达(没有应答)。这意味着数据包可能丢失了都不知道或者乱序达到。
- 低开销:因为不用维护连接,也不用保证传输的可靠性,UDP的报头只有8字节,开销比较小。
- 面向数据报:面向数据报又叫面向消息。面向数据报的意思就是应用层给UDP多少数据,UDP的数据部分就有多少,既不会拆分也不会合并(不灵活)。
- 传输的数据量少:16位长度包括首部也就64k,相当有限。如超过这个范围就需要在应用层手动分包,多次发送,接收后也要采取手段拼装。
UDP的缓冲区
- UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
- UDP具有接收缓冲区。但是这个接收缓冲区不能保证收到的UDP报文和发送报文的顺序。如果缓冲区满了,再到达的UDP报文就会直接被丢弃。
- UDP 的 socket 既能读, 也能写, 所以是 全双工
基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
TCP协议
TCP协议的全称为传输控制层协议(Transmission Control Protocol)。下面是TCP报文的结构:
显然,TCP报文首部要包含的信息比UDP多很多,下面来分析各个字段的作用。
-
源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去
-
32位序号:表示发送数据的字节序号,用于数据重组,该字节序号与发送缓冲区的字节序相关联。
-
32位确认序号:仅在ACK标志置位时有效,表示期望接收的下一个字节序号。
-
4 位 TCP 报头长度:表示TCP头部的长度(以4字节为单位),最小值为5。也就是说如果这4位报头长度的为5,那么实际表示报头的长度就是5*4=20字节。
-
6位标志位:
URG
:紧急指针有效。ACK
:确认号有效。PSH
:提醒接收方尽快读取接收缓冲区的数据RST
:重置连接,携带该标志的报文也称为复位报文段SYN
:同步序号,用户建立连接,携带该标志位的报文也称为同步报文段FIN
:表示发送方已经发送完毕,需要关闭连接,携带该标志位的报文也称为结束报文段
-
16位窗口大小:表示接收方的窗口大小,用于流量控制。如果该值比较大,那就可以一次多发点报文,否则就一次少发一下。
-
16 位校验和: 发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分
-
16 位紧急指针: 一个偏移量,标识哪部分数据是紧急数据。这个指针只在URG标记位为1时生效。
-
40 字节头部选项: 可变长度,用于定义附加选项,比如最大报文段长度(MSS)、时间戳等。假如窗口大小大于16位,就可以附加增大窗口大小的选项。
确认应答机制
TCP为了保证数据传输的可靠性,在每次发送端发送数据之后**,接收端要发送一个携带ACK标记位的报文(后面简称为ACK),来告诉发送端消息已经收到**。这也就意味着,只要发送端收到了ACK,就可以继续发后面的数据了。
TCP 将每个字节的数据都进行了编号. 即为序列号,如下图:
每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.假如发送方发送了字节序号为1-1000的数据,也就是说16位发送序号为1000。当接收方收到数据后,根据发送序号,再发送一个确认序号为1001的ACK报文,提醒发送方1001之前的字节数据已经收到,下次应该发送1001号字节开始往后的数据。
超时重传机制
如果发送端发送报文之后长时间没有收到接收端发过来的ACK,那么说明可能是由于网络拥堵丢包了根本没到达接收端,也有可能接收端已经收到且发送了ACK,只不过ACK由于某种原因丢失了。这个时候发送端就会重新发送一个一模一样的报文。这种机制就称为超时重传机制。
关于超时重传,解释以下几点:
-
如果接收端收到了重复的数据该怎么处理呢?
根据报文中的序列号排序,如果该序列号的数据已经有了,那接收方就直接丢弃这个多余的报文。 -
超时的时间该如何确定?
最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.但是这个时间的长短, 随着网络环境的不同, 是有差异的。
为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间: Linux中,超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍. 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制(握手和挥手)
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接。具体过程如下图:
服务端状态转换过程
[CLOSED -> LISTEN]
服务器端调用 listen 后进入LISTEN
状态, 等待客户端连接;[LISTEN -> SYN_RCVD]
一旦监听到连接请求(同步报文段),收到第一次握手, 就将该连接放入内核等待队列中, 并向客户端发送 SYN 确认报文,这是发起第二次握手[SYN_RCVD -> ESTABLISHED]
服务端一旦收到客户端的确认报文, 就进入ESTABLISHED 状态, 可以进行读写数据了.(收到第三次握手)[ESTABLISHED -> CLOSE_WAIT]
当客户端主动关闭连接(调用 close),即收到第一次挥手 , 服务器会收到结束报文段, 服务器返回确认报文段(发起第二次挥手)并进入 CLOSE_WAIT;[CLOSE_WAIT -> LAST_ACK]
进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时(发起第三次挥手), 会向客户端发送FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN)[LAST_ACK -> CLOSED]
服务器收到了对 FIN 的 ACK, 彻底关闭连接.(收到第四次挥手)
客户端状态转换过程
[CLOSED -> SYN_SENT]
客户端调用 connect, 发送同步报文段(发起第一次握手)[SYN_SENT -> ESTABLISHED]
connect 调用成功(收到第二次握手), 则进入 ESTABLISHED 状态。发送ACK给服务端(发起第三次握手),开始读写数据[ESTABLISHED -> FIN_WAIT_1]
客户端主动调用 close 时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1(发起第一次挥手)[FIN_WAIT_1 -> FIN_WAIT_2]
客户端收到服务器对结束报文段的确认(收到第二次挥手), 则进入 FIN_WAIT_2, 开始等待服务器的结束报文段[FIN_WAIT_2 -> TIME_WAIT]
客户端收到服务器发来的结束报文段(收到第三次挥手), 进入TIME_WAIT, 并发出 LAST_ACK(发送第四次挥手)
TIME_WAIT状态
TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态.此时已经发起了第四次挥手,为了保证这个第四次挥手能被接收方接收到,所以需要等待一段时间。当然,这个ACK也有可能丢失,那么服务器会再重发一个 FIN。
MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7 上默认配置的值是 60s;
这也是为什么有时候主动关闭服务端,马上重启不能绑定同样的端口。这是因为上一个关闭的服务端可能还处于TIME_WAIT状态,还没有彻底的断开连接释放套接字。
同时,等待一定的时间再关闭套接字,也是为了两个传输方向上还没有被接收的数据的报文段都已经消失,保证下次重启同一个端口的服务器不会收到旧数据。
CLOSE_WAIT状态
如果服务端第一次收到客户端发来的FIN报文,发送ACK报确认后服务端就会进入CLOSE_WAIT状态。此时服务端已经知道对方要关闭连接了,自己也要准备关闭了。
在这个状态下的服务端依旧可以向客户端发送数据报,虽然客户端关闭了连接,但依旧可以收报文(接收缓冲区没有关闭)。直到服务端调用close函数,才会结束这个状态。如果应用程序没有及时调用close函数,连接会长时间停留在CLOSE_WAIT状态。这样就会导致资源泄露(如文件描述符耗尽),影响服务器性能。
为什么是三次握手和四次挥手
换个问题,为什么三次握手就能保证建立可靠的连接呢?
- 确认对方主机状态及收发能力的最小次数。三次握手保证了双方网络的连通性,也保证了双方连接的意愿,还保证了双方拥有接收和发送数据的能力。
- 如果只有一次握手,意味着只要发送方发送连接请求,连接就会被建立。那这样服务端容易收到SYN洪流攻击,即服务端建立大量的连接对象消耗太多资源从而导致奔溃。两次握手也是类似,服务端收到一次连接,只要服务端发出一次ACK,就说明连接就建立起来了(发送方可以不要这个ACK,而是只发SYN)。如果有不法分子恶意发送大量的SYN,服务器依旧会创建对应的连接对象,依旧非常消耗资源。
- 如果是三次握手,客户端建立连接和服务端建立连接的代价是一样的,同样需要发送ACK,否则连接就会失败。也就是说,双方是对等的,变相的客户端连接的成本提高了,也就不容易搞垮服务器。
- 四次握手也可以,只不过再多的握手达到的效果和三次是一样的,那为什么还要浪费时间去多握手呢?
为什么是四次挥手而不是像握手一样进行三次挥手?
因为当客户端发起第一次挥手之后,只是意味着不再向服务端端发送数据包,不代表不能接收数据包。有可能服务端处理的数据包还没被客户端接收,此时彻底关闭连接就丢失数据了。所以客户端需要有一个时间间隔来读取服务端发送的数据包,什么时候接收方发送完了呢?就是等服务端也发起挥手的时候,此时就代表服务端发送完了,客户端也不需要再收取数据了。
同样的,四次挥手保证了通信双方都有关闭连接的行为,且都能得到应答,也就是说对方都能知道你要退出。这样就保证了数据传输的完整性。
五次挥手?和不需要四次握手是一样的道理。
滑动窗口
滑动窗口是TCP保证传输效率的一个重要手段。具体来说,滑动窗口维护了一个报文区间,处于这个区间的报文都可以暂时不考虑应答直接全部发送。如下图。
更详细的:
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是 4000 个字节(四个段).
- 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据;窗口向右移动。
- 窗口越大, 则网络的吞吐率就越高
对于滑动窗口,解释以下几点:
如果发生丢包如何进行重传?
由于一次发送多个报文,丢包的概率就会上升。那么发生丢包之后如何进行重传呢?下面分情况讨论。
- 情况一:数据包到达,但是ACK确认 丢了
回顾之前TCP报文中的确认序号的作用,即告诉发送端下一次发送数据应该从哪个字节序开始,换句话来说,假设ACK的确认序号为2001,则表示1-2000的数据全部都收到了。这样一来,即使中间有ACK丢失,只要后面有ACK没有丢那就不影响。比如确认序号为2001、3001的ACK都丢失了,但是4001没有丢失,那么接收端就会认为4000以前的数据都被收到了,即使没有收到2001和3001的ACK。
- 情况二:数据包丢失
这种情况接收方并没有收到部分数据包,也就不会发送ACK应答。假设前面字节序的数据包丢失,接收端在收到靠后的数据包后,通过对数据包排序会检测最前面还没有收到的字节序,并以此发送ACK。例如:
上图中一次发送了7个数据包,发送序号依次为1000、2000到7000。现在第2个数据包丢失,即发送序号为2000,接收方只收到了字节序1000和3000-7000的数据,这个时候,接收方发现最前面的2000个字节的数据还没有收到,发送的ACK的确认序号就是1001,表示1001以前的数据全部收到。为什么不发送3001或者7001呢?这是因为如果发送ACK的确认序号为3001的话,发送方就会认为1-3000的数据都被成功接收了,可实际上不是。也就是说,7个报文中,最后接收方成功收到了6个报文,且这6个报文的ACK全部都是1001。
当发送方连续收到的3个相同的1001ACK时,就会重新发送1001-2000的数据包。这种机制就称为高速重发控制,也称为快重传。为什么说是高速呢?因为这种机制减少了发送端的等待重传的时间。当然,如果没有收到3个,即最后只有两个报文丢失了前面的一个,这个时候发送方就会超时重传。利用快重传和超时重传,即保证了数据传输的可靠性,也一定保证了传输的速度。
流量控制
接收端处理数据的速度是有限的,如果发送端发送的太快就会导致接收端的接收缓冲区打满。这个时候如果发送方继续发送数据包就会造成丢包。
TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制。
具体是怎么做到的呢?
上面我们已经说过,TCP报文中用16位比特位表示窗口大小,由于窗口是根据接收方的实际接收缓冲区变化而变化的。所以,如果收到的报文中窗口大小字段比较大,那么说明接收方的接收缓冲区还有很多剩余空间,这个时候就可以一次多发一点数据包,否则就可以少发一点。这样就达到了控制流量的效果。
此外,如果接收端的缓冲区满了,就会把窗口值设为0。这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.也可以设置PSH标记位来提醒接收端快点从缓冲区里读数据。
本质上,所谓的窗口探测报文就是一个极少数据量的TCP数据包,和普通报文没区别,只是为了获得ACK的窗口大小。
拥塞控制
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是还可能会受到网络情况的影响。比如网络现在比较拥堵,即使接收方的窗口大小还很大,发送方发来的数据被堵在网络中,依旧达不到高效传输的目的,而且还很有可能加重拥堵。为了解决这个问题,或者说避免加重有拥堵,引入拥塞窗口的概念。
既然接收缓冲区的窗口大小直接表示了缓冲区接受数据的能力,那么拥塞窗口的大小也能直接表示当前网络的拥塞情况,拥塞窗口越大表示网络越不拥堵,相反则越拥堵。
那发送方如何根据拥塞窗口来调整自己的发送数据包的量呢?
- 如果发送方总是收不到ACK,那么说明丢包率比较高,此时就可以判断网络已经阻塞了。
- 此时就不能发送太多的数据包,否则就会加重阻塞。开始发送一个数据包,并定义拥塞窗口的大小为1,每次收到一个 ACK 应答, 拥塞窗口加 1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取 较小的值 作为实际发送的窗口;
慢启动
慢启动是TCP拥塞控制的一部分,用于在连接建立或重传超时后逐步增加拥塞窗口的大小,以避免网络拥塞。与其名称相反,慢启动的实际增长速度非常快。(只是指开始启动慢)
发送方每次发送的报文数量是上一次的的两倍,即2的n次方的速度增长。具体过程如下图:
- 但是指数增长的太快了,为了限制增长速度,需要设置一个慢启动阈值。慢启动阈值是一个预设值,用于限制慢启动阶段的指数增长,当拥塞窗口的大小达到了阈值,就不再按照指数方式增长, 而是按照线性方式增长。
- 当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
- 如果又遇到了网络阻塞,就会重新慢启动,且本次慢启动的阈值是上一次最大拥塞窗口的一半。
延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.此时可以采用延迟应答,即不立刻返回ACK,而是等一下,等待的时间里可能接收端会从从缓冲区中处理一些数据,到时候发送的ACK的窗口大小就会大一点,发送方收到后也就可以多发一点数据包。
当然,延迟的时间肯定不能太长(有最大延迟时间),不然发送端就超时重发了。
捎带应答
如果接收方收到了发送方的数据包,发送的ACK报文可以携带其它的数据,从而减少单独发送确认报文的次数。这就叫做捎带应答。比如第二次握手的过程,就是ACK标记位和SYN标记位一起生效发给接收方。
为什么第二次挥手的时候不能使用捎带应答的方式来减少挥手次数呢?
这是因为,服务器收到FIN报文后,发送一个ACK报文确认接收到了客户端的FIN报文,同时服务器可能还在发送一些未完成的数据。也就是说,即使收到发送方的关闭请求,服务器还是需要向接收方发送消息,中间需要时间。
粘包问题
粘包是指在接收端读取数据时,可能会将多个发送端的数据包拼接在一起(粘在一起),或将一个数据包拆成多次读取。这通常发生在基于流的传输协议(如TCP)中。
每个UDP数据包都是一个独立的报文,具有明确的边界。因此,UDP协议本身不会出现粘包问题。每次调用recvfrom或recv函数时,都会接收到一个完整的UDP数据包。
但是TCP接收方按照固定长度读取数据,就可能读取不完整或者是一次读取多个数据包。因此需要使用特殊字符(如换行符、空格等)作为数据包之间的分隔符。接收方根据特殊字符来拆分数据包。
总结TCP
为什么 TCP 这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
- 可靠性:校验和、序列号(按序到达)、确认应答、超时重发、连接管理、流量控制、拥塞控制
- 传输效率:滑动窗口、快速重传、延迟应答、捎带应答
基于TCP的应用层协议:
- HTTP
- HTTPS
- SSH
- Telent
- FTP
- SMTP
对比TCP和UDP
TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的优点和缺点, 不能简单, 绝对的进行比较.我们需要根据实际的场景来选择合适的协议。
- TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景
- UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传输等. 另外 UDP 可以用于广播;
- 如果不知道使用什么传输层协议,那就优先考虑TCP
如何用UDP实现可靠传输(面试题目)
参考 TCP 的可靠性机制, 在应用层实现类似的逻辑;
比如:
- 引入序列号,保证数据顺序
- 引入确认应答,确保收到了数据
- 引入超时重传, 如果隔一段时间没有应答, 就重发数据;
- 引入窗口大小,控制流量
- 等等