UDP 协议
UDP 协议端格式
- 16 位 UDP 长度, 表示整个数据报(UDP 首部+UDP 数据)的最大长度
- 如果校验和出错, 就会直接丢弃
UDP 的特点
UDP
传输的过程类似于寄信
.
- 无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息
- 面向数据报: 不能够灵活的控制读写数据的次数和数量,即应用层交给 UDP 多长的报文,UDP原样发送,既不会拆分,也不会合并
UDP 的缓冲区
UDP 没有真正意义上的
发送缓冲区,
调用
sendto
会直接交给内核,
由内核将数据传给网络层协议进行后续的传输动作。UDP 具有接收缓冲区.
但是这个接收缓冲区不能保证收到的
UDP
报的顺序和发送 UDP
报的顺序一致;
如果缓冲区满了
,
再到达的
UDP
数据就会被丢弃。
UDP
的
socket
既能读
,
也能写
,
这个概念叫做
全双工。
UDP 使用注意事项
我们注意到,
UDP
协议首部中有一个
16
位的最大长度,
也就是说一个
UDP
能传输的数据最大长度是 64K(
包含
UDP
首部
)。然而 64K
在当今的互联网环境下
,
是一个非常小的数字,
如果我们需要传输的数据超过 64K,
就需要在应用层手动的分包,
多次发送,
并在接收端手动拼装。
TCP 协议
TCP 协议段格式
- 源端口(16位):标识发送方的端口号,范围是0到65535。
- 目的端口(16位):标识接收方的端口号,范围是0到65535。
- 序列号(32位):用于标识每个报文段,使目的主机可确认已收到指定报文段中的数据。在SYN标志未置位时,该字段指示了用户数据区中第一个字节的序号;在SYN标志置位时,该字段指示的是初始发送的序列号。
- 确认号(32位):目的主机返回确认号,使源主机知道某个或几个报文段已被接收。如果ACK控制位被设置为1,则该字段有效。确认号等于顺序接收到的最后一个报文段的序号加1,这也是目的主机希望下次接收的报文段的序号值。
- 数据偏移(首部长度,4位):指出TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。由于TCP报头的长度随TCP选项字段内容的不同而变化,因此报头中包含一个指定报头字段的字段。该字段以32比特为单位,所以报头长度一定是32比特的整数倍,有时需要在报头末尾补0。如果报头没有TCP选项字段,则报头长度值为5,表示报头一个有160比特,即20字节。
- 保留位(6位):由跟在数据偏移字段后的6位构成,全部为0。
- 控制位(6位):
- URG(紧急比特,1位):当URG=1时,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送。
- ACK(确认比特,1位):仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有传达的报文段都必须把ACK置1。
- PSH(推送比特,1位):当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作,这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去,接收方收到PSH=1的报文段,就尽快地(即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付。
- RST(复位比特,1位):用于复位相应的TCP连接。
- SYN(同步比特,1位):仅在三次握手建立TCP连接时有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1。因此,SYN置1就表示这是一个连接请求或连接接受报文。
- FIN(终止比特,1位):用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接。
- 窗口(16位):此字段用来进行流量控制,这个值是本机期望一次接收的字节数,即发送数据的窗口大小。告诉对方在不等待确认的情况下,可以发来多大的数据。这里表示的最大长度是2^16-1=65535,如需要使用更大的窗口大小,需要使用选项中的窗口扩大因子选项。指发送本报文段的一方的接收窗口(而不是自己的发送窗口)。
- 校验和(16位):源主机和目的主机根据TCP报文段以及伪报头的内容计算校验和。在伪报头中存放着来自IP报头以及TCP报文段长度信息。与UDP一样,伪报头并不在网络中传输,并且在校验和中包含伪报头的目的是为了防止目的主机错误地接收存在路由的错误数据报。
- 紧急指针(16位):仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据。如果URG为1,则紧急指针标志着紧急数据的结束。其值是紧急数据最后1字节的序号,表示报文段序号的偏移量。
首部可变部分
- 选项字段:长度可变,最长可达40字节,当没有使用选项时,TCP首部长度是20字节。可能包括“窗口扩大因子”、“时间戳”等选项。
- 填充字段:用于保证任选项为32bit的整数倍。
数据部分
- 数据(长度可变):TCP首部结束之后的部分,其长度由IP包的长度减去IP头部长度和TCP头部长度得出。
确认应答(ACK)机制
- 数据发送:发送方将数据分割成较小的单元,称为TCP段,并为每个段分配一个唯一的序列号,实际上一个字节的数据对应一个序列号。然后将这些TCP段发送给接收方,并启动一个定时器来跟踪每个已发送段的确认。
- 数据接收与确认:接收方收到TCP段后,将按序将它们重新组装成完整的数据流。一旦接收方成功接收并处理了数据,它会发送一个确认(ACK)给发送方。确认中包含接收到的最高序列号,表示该序列号之前的所有数据都已正确接收。
- 发送方处理确认:发送方在接收到确认后,会停止相应定时器,并继续发送下一个序列号的TCP段。如果发送方在定时器超时之前未收到确认,它将重新发送未确认的TCP段。
超时重传机制
主机
A
发送数据给
B
之后
,
可能因为网络拥堵等原因
,
数据无法到达主机
B,如果主机 A 在一个特定时间间隔内没有收到
B
发来的确认应答
,
就会进行重发。
但是
,
主机
A
未收到
B
发来的确认应答
,
也可能是因为
ACK
丢失了
因此主机
B
会收到很多重复数据
.
那么
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 状态, 开始读写数据
- [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 -> CLOSED] 客户端要等待一个 2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入 CLOSED 状态.
下图是
TCP
状态转换的一个汇总
:
- 较粗的虚线表示服务端的状态变化情况
- 较粗的实线表示客户端的状态变化情况
- CLOSED 是一个假想的起始点, 不是真实状态
三次握手建立连接
- 第一次握手:客户端向服务器发送一个带有SYN(Synchronize Sequence Numbers,同步序列号)标志位的TCP数据包,请求建立连接。这个数据包中包含了客户端的初始序列号(ISN,Initial Sequence Number)。
- 第二次握手:服务器收到客户端的SYN请求后,会回复一个带有SYN和ACK(Acknowledgment,确认)标志位的数据包,称为SYN-ACK响应。这个响应中,服务器确认了客户端的SYN请求,并指定了服务器的初始序列号(ISN)。同时,服务器还会对客户端的初始序列号进行确认,即发送一个确认号(ACK号),表示已经收到客户端发送的序列号加1的数据。
- 第三次握手:客户端收到服务器的SYN-ACK响应后,会发送一个带有ACK标志位的数据包,表示确认了服务器的响应。这个ACK数据包中,客户端会确认收到了服务器的SYN响应,并指定了下一个要发送的序列号(即服务器的初始序列号加1)。至此,三次握手完成,TCP连接建立成功,双方可以开始进行数据传输。
四次挥手断开连接
- 第一次挥手:客户端发送一个FIN(Finish,结束)标志位的数据包,表示自己的数据已经发送完毕,请求关闭连接。此时客户端进入FIN_WAIT_1状态。
- 第二次挥手:服务器收到客户端的FIN包后,会发送一个ACK包进行确认,表示已经收到客户端的关闭请求。此时服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
- 第三次挥手:服务器在确认自己的数据也发送完毕后,会发送一个FIN包给客户端,表示自己也准备关闭连接。此时服务器进入LAST_ACK状态。
- 第四次挥手:客户端收到服务器的FIN包后,会发送一个ACK包进行确认。此时客户端进入TIME_WAIT状态,经过一段时间(通常为2MSL,Maximum Segment Lifetime,最大报文段生存时间)后,客户端进入CLOSED状态,服务器在收到客户端的ACK包后也进入CLOSED状态,连接彻底关闭。
理解 TIME_WAIT 状态
现在做一个测试
,
首先启动
server,
然后启动
client,
然后用
Ctrl-C
使
server
终止
,
这时马上再运行 server,
结果是
:
这是因为
虽然
server
的应用程序终止了,
但
TCP
协议层的连接并没有完全断开,
因此不能再次监听同样的 server
端口。
我们用
netstat
命令查看一下
:
TCP 协议规定
,
主动关闭连接的一方要处于
TIME_ WAIT
状态
,
等待两个MSL(maximum segment lifetime)的时间后才能回到
CLOSED
状态。我们使用 Ctrl-C 终止了
server,
所以
server
是主动关闭连接的一方
,
在 TIME_WAIT 期间仍然不能再次监听同样的
server
端口。MSL 在 RFC1122
中规定为两分钟
,
但是各操作系统的实现不同,
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看
msl 的值,
在
Centos7/Ubuntu
上默认配置的值是 60s。
对于为什么处于
TIME_ WAIT
状态的一方
,需要等待两个MSL的时间后才能回到 CLOSED
状态。这是因为MSL 是
TCP
报文的最大生存时间
,
因此
TIME_WAIT
持续存在
2MSL
的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,
可能会收到来自上一个进程的迟到的数据
,
但是这种数据很可能是错误的),同时也是在理论上保证最后一个报文可靠到达(假设最后一个
ACK
丢失
,
那么服务器会再重发一个 FIN.
这时虽然客户端的进程不在了
,
但是
TCP
连接还在
,
仍然可以重发 LAST_ACK)
setsockopt()
要解决 TIME_WAIT 状态引起的 bind 失败的方法,我们可以使用 setsockopt 函数
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
参数说明:
- sock:将要被设置选项的套接字描述符。
- level:选项所在的协议层,常见的取值有SOL_SOCKET(通用套接字选项)、IPPROTO_IP(IP选项)、IPPROTO_TCP(TCP选项)等,使用 SOL_SOCKET 即可。
- optname:需要设置的选项名,如SO_BROADCAST、SO_REUSEADDR、TCP_NODELAY等,设置为 SO_REUSEADDR , 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符。
- optval:指向包含新选项值的缓冲区,根据选项名称的数据类型进行转换。
- optlen:现选项的长度。
返回值:
- 成功执行时,返回0。
- 失败返回-1,errno被设为以下的某个值:
- EBADF:sock不是有效的文件描述词。
- EFAULT:optval指向的内存并非有效的进程空间。
- EINVAL:在调用setsockopt()时,optlen无效。
- ENOPROTOOPT:指定的协议层不能识别选项。
- ENOTSOCK:sock描述的不是套接字。
使用如下:
int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
滑动窗口
刚才我们讨论了确认应答策略,
对每一个发送的数据段,
都要给一个
ACK
确认应答。
收到 ACK
后再发送下一个数据段。
这样做有一个比较大的缺点,
就是性能较差,
尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低,
那么我们一次发送多条数据,
就可以大大的提高性能(
其实是将多个段的等待时间重叠在一起了
)。
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,
上图的窗口大小就是 4000
个字节
(
四个段
)。
发送前四个段的时候,
不需要等待任何
ACK,
直接发送。收到第一个 ACK 后
,
滑动窗口向后移动
,
继续发送第五个段的数据,
依次类推。操作系统内核为了维护这个滑动窗口, 需要开辟
发送缓冲区
来记录当前还有哪些数据没有应答,
只有确认应答过的数据
,
才能从缓冲区删掉。窗口越大, 则网络的吞吐率就越高。
那么如果出现了丢包
,
如何进行重传
?
这里分两种情况讨论
情况一
:
数据包已经抵达
, ACK
被丢了
这种情况下
,
部分
ACK
丢了并不要紧
,
因为可以通过后续的
ACK
进行确认
情况二
:
数据包就直接丢了
当某一段报文段丢失之后
,
发送端会一直收到
1001
这样的
ACK,
就像是在提醒发送端 "
我想要的是
1001"
一样。如果发送端主机连续三次收到了同样一个 "1001" 这样的应答
,
就会将对应的数据 1001 - 2000
重新发送。这个时候接收端收到了 1001
之后
,
再次返回的
ACK
就是
7001
了
(
因为
2001 - 7000)接收端其实之前就已经收到了
,
被放到了接收端操作系统内核的
接收缓冲区
中。
这种机制被称为 "
高速重发控制
"(
也叫
"
快重传
")。
流量控制
接收端处理数据的速度是有限的。
如果发送端发的太快,
导致接收端的缓冲区被打满,
这个时候如果发送端继续发送,
就会造成丢包,
继而引起丢包重传等等一系列连锁反应。因此 TCP
支持根据接收端的处理能力,
来决定发送端的发送速度,
这个机制就叫做
流量
控制。
接收端将自己可以接收的缓冲区大小放入
TCP
首部中的
"
窗口大小
"
字段
,
通过 ACK
端通知发送端。接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口之后, 就会减慢自己的发送速度。如果接收端缓冲区满了, 就会将窗口置为
0,
这时发送方不再发送数据
,
但是需要定期发送一个窗口探测数据段,
使接收端把窗口大小告诉发送端。
拥塞控制
虽然
TCP
有了滑动窗口这个大杀器
,
能够高效可靠的发送大量的数据,
但是如果在刚开始阶段就发送大量的数据,
仍然可能引发问题。因为网络上有很多的计算机,
可能当前的网络状态就已经比较拥堵。
在不清楚当前网络状态下,
贸然发送大量的数据,
是很有可能引起雪上加霜的。TCP 引入
慢启动
机制,
先发少量的数据,
摸清当前的网络拥堵状态,
再决定按照多大的速度传输数据。
此处引入一个概念称为
拥塞窗口,发送开始的时候, 定义拥塞窗口大小为
1,每次收到一个 ACK 应答,
拥塞窗口加
1。每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,
取较小的值作为实际发送的窗口。
像上面的拥塞窗口增长速度,
是指数级别的,
"
慢启动
"
只是指初使时慢,
但是增长速度非常快。
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍,
引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长
,
而是按照线性方式增长。
当
TCP
开始启动的时候
,
慢启动阈值等于窗口最大值,在每次超时重发的时候, 慢启动阈值会变成原来的一半
,
同时拥塞窗口置回
1。少量的丢包,
我们仅仅是触发超时重传;
大量的丢包
,
我们就认为网络拥塞。当 TCP
通信开始后
,
网络吞吐量会逐渐上升
;
随着网络发生拥堵
,
吞吐量会立刻下降
;
拥塞控制
,
归根结底是
TCP
协议想尽可能快的把数据传输给对方
,
但是又要避免给网络造成太大压力的折中方案。
延迟应答
如果接收数据的主机立刻返回
ACK
应答
,
这时候返回的窗口可能比较小。
假设接收端缓冲区为 1M. 一次收到了
500K
的数据
;
如果立刻应答
,
返回的窗口就是 500K。但实际上可能处理端处理的速度很快, 10ms 之内就把
500K
数据从缓冲区消费掉了。
在这种情况下,
接收端处理还远没有达到自己的极限
,
即使窗口再放大一些
,
也能处理过来,
如果接收端稍微等一会再应答,
比如等待
200ms
再应答
,
那么这个时候返回的窗口大小就是 1M。
- 数量限制: 每隔 N 个包就应答一次
- 时间限制: 超过最大延迟时间就应答一次
具体的数量和超时时间,
依操作系统不同也有差异。
一般
N
取
2,
超时时间取
200ms。
TCP/UDP 对比
TCP与UDP的对比
对比维度 | TCP | UDP |
---|---|---|
连接性 | 面向连接,需要建立连接(3次握手)和断开连接(4次挥手) | 无连接,不需要建立连接 |
可靠性 | 可靠交付,通过编号与确认、流量控制、计时器等机制保证数据无差错、不丢失、不重复且按序到达 | 不可靠交付,尽最大努力交付,但不保证可靠 |
报文首部 | 20字节,开销大 | 8字节,开销小 |
吞吐量控制 | 有拥塞控制、流量控制、重传机制、滑动窗口等机制 | 无 |
双工性 | 点对点全双工通信 | 支持一对一、一对多、多对一和多对多的交互通信 |
编程步骤 | 复杂,需要监听、接收、连接等步骤 | 简单,不需要监听和接收连接 |
使用场景 | 对传输效率要求低,但准确率要求高的场景,如HTTP、FTP、SMTP等 | 对传输效率要求高,但准确率要求低的场景,如DNS、NFS等 |
结论
- TCP:适用于需要可靠传输的场景,如文件传输、电子邮件等。
- UDP:适用于对实时性要求高,对可靠性要求不高的场景,如视频会议、直播等。