5、TCP/IP通信原理
1、TCP/IP协议族
TCP/IP 是供已连接因特网的计算机进行通信的通信协议。
TCP/IP 指传输控制协议/网际协议(Transmission Control Protocol / Internet Protocol)。
TCP/IP 定义了电子设备(比如计算机)如何连入因特网,以及数据如何在它们之间传输的标准。
TCP/IP 通信协议是对计算机必须遵守的规则的描述,只有遵守这些规则,计算机之间才能进行通信。
目前使用的网络模型都是 TCP/IP 模型,它是对 OSI 模型进行了简化,只包含了四层,从上到下分别是应用层、传输层、网络层和链路层(网络接口层),每一层都包含了若干协议。
TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,其中 TCP 和 IP 是最常用的两种底层协议,所以把它们统称为“TCP/IP 协议族”。
也就是说,“TCP/IP模型”中所涉及到的协议称为“TCP/IP协议族”,你可以区分这两个概念,也可以认为它们是等价的。
TCP/IP 是一个复杂的协议族,它是由一组专业化协议组成的,其中包含ftp、http、tcp、udp、ip、icmp、dns等。
常用协议:
TCP:用来检测网络传输中差错的传输控制协议
IP:专门负责对不同网络进行互联的互联网协议
UDP:用户数据报协议
ICMP:互联网控制信息协议
SMTP:简单邮件传输协议
SNMP:简单网络管理协议
HTTP:超文本传输协议
FTP:文件传输协议
ARP:地址解析协议
我们所说的 socket 编程是基于 TCP 和 UDP 协议的,是站在传输层的基础上,所以可以使用 TCP/UDP 协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。它们的层级关系如下图所示:
把协议分成多个层次有哪些优点?协议设计更容易?当然这也足以成为优点之一。但是还有更重要的原因,就是为了通过标准化操作设计成开放式系统。
标准本身就是对外公开的,会引导更多的人遵守规范。以多个标准为依据设计的系统称为开放式系统(Open System),我们现在学习的 TCP/IP 协议族也属于其中之一。
开放式系统的优点:
路由器用来完成 IP 层的交互任务。某个网络原来使用 A 公司的路由器,现要将其替换成 B 公司的,是否可行?这并非难事,并不一定要换成同一公司的同一型号路由器,因为所有生产商都会按照 IP 层标准制造。
再举个例子。大家的计算机是否装有网络接口卡,也就是所谓的网卡?尚未安装也无妨,其实很容易买到,因为所有网卡制造商都会遵守链路层的协议标准。这就是开放式系统的优点。
标准的存在意味着高速的技术发展,这也是开放式系统设计最大的原因所在。实际上,软件工程中的“面向对象(Object Oriented)”的诞生背景中也有标准化的影子。也就是说,标准对于技术发展起着举足轻重的作用。
2、TCP和UDP
在TCP/IP协议族中有很多协议,TCP/IP协议群中的核心协议被设计在网络层和传输层,他们为网络中的各主机提供通信服务,也为模型的最高层(应用层)中的协议提供服务。
2.1、TCP
TCP向应用层提供可靠的面向连接的数据流传输服务。他能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达)。
通过源/目的IP可以唯一的区分网络中的两个设备,在通过源/目的端口可以唯一的区分网络中两个通信的应用程序。
2.1.1、TCP数据包结构
(1)源端口:16bit,本地端口号
(2)目标端口:16bit,远端的端口号
(3)顺序号(Seq):32bit,Seq(Sequence Number)用来标识从计算机A发送到计算机B的数据包的顺序,即数据包的序号,注意,这个序号不是按1递增的,而是按tcp包内数据字节长度加上,比如包内数据是21字节,而当前计算机A发到计算机B的包的seq是10的话,那下个计算机1发送到计算机2的包的seq就是10+21=31,
(4)确认号(Ack):32bit,Ack(Acknowledge Number)表示希望收到的下一个数据包的序列号,客户端和服务器端都可以发送,Ack = Seq + 1。
(5)首部长度:4bit,用来表示TCP头中包含多少个32bit
(6)保留:6bit未用
(7)标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:
URG:紧急指针(urgent pointer)有效。
ACK:确认序号有效。ACK位置1表示确认号是合法的;如果ACK为0,那么数据包就不包含确认信息,确认字段被省略。
PSH:表示是带有PUSH标志的数据。因此请求数据包一到接收方便可送往应用程序而不必等到缓冲区装满时才传送。接收方应该尽快将这个报文交给应用层。
RST:重置连接。用于复位由于主机崩溃或其它原因而出现的错误的连接,还可以用于拒绝非法的数据包或拒绝连接请求。
SYN:建立一个新连接。
FIN:断开一个连接。
(8)窗口:16bit',窗口用来表示在确认了字节数之后还可以发送多少字节
(9)校验和:16bit,是为了确保高可靠性而设置的,它校验头部和数据之和
(10)可选项:0或32bit,包括最大TCP载荷、窗口比例、选择重发数据包等选项
Seq 是 Sequence 的缩写,表示序列;Ack(ACK) 是 Acknowledge 的缩写,表示确认;SYN 是 Synchronous 的缩写,愿意是“同步的”,这里表示建立同步连接;FIN 是 Finish 的缩写,表示完成。
2.1.2、TCP套接字中的I/O缓冲
TCP套接字的数据收发无边界性,即使服务器端调用1次write()函数传输100字节的数据,客户端也有可能通过调用10次read()函数,每次读取10个字节来读取这100个字节的数据。那么在第一次读取10个字节的时候,剩下的90字节在什么地方了,这就需要I/O缓冲。
write()函数在调用之后并非立即传输数据,read()函数调用后也并非马上接收数据。在调用write()函数之后,数据将移至输出缓冲;在调用read()函数时,从输入缓冲中读取数据。
如上图所示,调用write()函数时,数据将移至输出缓冲,在适当的时候传向对方的输入缓冲,这时对方将调用read()函数从输入缓冲中读取数据。
这些I/O缓冲特性如下:
I/O缓冲在每个TCP套接字中单独存在;
I/O缓冲在创建套接字时自动生产;
即使关闭套接字也会继续传输缓冲区中遗留的数据;
关闭套接字将丢失输入缓冲区中的数据。
2.1.3、三次握手协议
TCP是一种面向连接的、可靠的、基于字节流的通信协议。所谓面向连接,就是数据在传输前要建立连接,然后进行数据数据传输,传输完毕后还要断开连接。
客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
套接字十一全双工方式工作的,也就是说,它可以双向传递数据。因此收发数据前需要做一些准备。
TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
[Shake 2] 套接字B:“好的,我这边已准备就绪。”
[Shake 3] 套接字A:“谢谢你受理我的请求。”
使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:
客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。
这个时候,客户端开始发起请求:
第1步:当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字Seq=1000(1000表示客户端现在传递的数据包序号为1000,如果服务器接收无误,请服务器通知客户端向服务器传递1001号数据包),填充“序号(Seq)”字段,表示该数据包的序号,Ack为空。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN_SEND状态,等待服务器的确认。这是首次请求连接时使用的消息,又称SYN。SYN是Synchronization的简写,表示收发数据前传输的同步消息。
第2步:服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来回复客户端收到了刚才发送的数据包。此时服务器进入SYN_RECV状态。
服务器生成一个随机数Seq=2000(2000表示服务器端现在传递的数据包序号为2000,如果客户端接收无误,请客户端通知服务器向客户端传递2001号数据包),填充“序号(Seq)”字段。2000 和客户端数据包没有关系。
服务器将客户端数据包序号(1000)加1,得到1001(通知客户端刚才传输的Seq为1000的数据包接收无误,现在请传递Seq为1001的数据包),并用这个数字填充“确认号(Ack)”字段。
服务器将数据包发出,进入SYN-RECV状态。
服务器对客户端首次传输的数据包的确认消息(Ack=1001)和为主机B传输数据做准备的同步消息(Seq 2000)捆绑发送,因此,此种的消息又称为SYN+ACK。
收发数据前向数据包分配序号,并向对方通报此序号,这都是为防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP可以保证可靠地数据传输。
第3步:客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。
接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。将Seq设置为1001,同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。
客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。
第4步:服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。
三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。
2.1.4、传输数据
建立连接后,两台主机就可以相互传输数据了。如下图所示:
上图给出了主机A分2次(分2个数据包)向主机B传递200字节的过程。首先,主机A通过1个数据包发送100个字节的数据,数据包的 Seq 号设置为 1200。主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。
为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。
此时 Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下的公式确认 Ack 号:Ack号 = Seq号 + 传递的字节数 + 1
与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。
下面分析传输过程中数据包丢失的情况,如下图所示:
上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。
为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。
上图演示的是数据包丢失的情况,也会有 ACK 包丢失的情况,一样会重传。
(1)、重传超时时间(RTO, Retransmission Time Out)
这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。
往返时间(RTT,Round-Trip Time)表示从发送端发送数据开始,到发送端收到来自接收端的 ACK 确认包(接收端收到数据后便立即确认),总共经历的时延。
(2)、重传次数
TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互。
最后需要说明的是,发送端只有在收到对方的 ACK 确认包后,才会清空输出缓冲区中的数据。
2.1.5、四次挥手
建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。
建立连接需要三次握手,断开连接需要四次挥手,可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
[Shake 3] 套接字B:“我准备好了,可以断开连接了。”
[Shake 4] 套接字A:“好的,谢谢合作。”
下图演示了客户端主动断开连接的场景:
建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
(1)客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。
(2)服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。
注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。
(3)客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
(4)等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。
(5)客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。
(6) 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。
关于 TIME_WAIT 状态的说明:
客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?
TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。
客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?
超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。
2.2、UDP
UDP即用户数据报协议,是一种面向无连接的不可靠传输协议,具有资源消耗小、处理速度快的特点。
由于UDP通信之前不需要先建立一个连接,因此UDP应用要比TCP应用更加简单。UDP比TCP更为高效,也能更好的解决实时性的问题。目前为止,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP。
如果只考虑可靠性,TCP的确比UDP好。但UDP在结构上比TCP更简洁。UDP不会发送类似ACK的应答消息,也不会像SEQ那样给数据包分配序号。因此,UDP的性能有时比TCP高出很多。编程中实现UDP也比TCP简单。另外,UDP的可靠性虽然比不上TCP,但也不会像想象中那么频繁的发生数据损坏。因此,在更重视性能而非可靠性的情况下,UDP是一种很好的选择。
为了提供可靠的数据传输服务,TCP在不可靠的IP层进行流控制,而UDP就缺少这种流控制机制。流控制是区分TCP和UDP的最重要的标志。但如果从TCP中除去流控制,所剩内容也屈指可数。也就是说,TCP的生命在于流控制。
UDP数据包结构
(1)源端口:16bit,标识出本地的端口号
(2)目标端口:16bit,标识出远端的端口号
(3)数据包的长度是指包括包头和数据部分在内的总的字节数。因为包头的长度是固定的,所以该域主要用来表示数据部分的长度(又称为数据负载)。
2.2.1、UDP内部工作原理
与TCP不同,UDP不会进行流控制。接下来具体讨论UDP的作用,如下图所示:
从上图可以看出,IP的作用就是让离开主机B的UDP数据包准备传递到主机A。但把UDP包最终交给主机A的某一UDP套接字的过程是由UDP完成的,UDP最重要的作用就是根据端口号将传到主机的数据包交付给最终的UDP套接字
2.2.2、UDP的高效使用
虽然大部分网络编程都是基于TCP实现的,但也有一些是用UDP实现的。接下来考虑何时实用UDP更有效,其实UDP也具有一定的可靠性,但是网络传输特性导致信息丢失频发,比如传递压缩文件要发送一万个数据包,只要丢失一个这个压缩文件都无法解压。但通过网络实时传输视频或音频时情况就不同了。对于多媒体数据而言,丢失一部分也没太大问题,只会引起短暂的画面抖动或出现细微的杂音。但因为需要提供实时服务,速度就成为至关重要的因素。因此,TCP的流控制就显得有点多余,此时需要考虑的是UDP。
但UDP并非每次都快于TCP,TCP比UDP慢的原因通常有以下两点:收发数据前后进行的连接设置及清除过程、收发数据过程中为保证可靠性而添加的流控制。
如果收发的数据量小但需要频繁连接时,UDP比TCP更高效。
2.2.3、UDP中的服务器端和客户端没有连接
UDP服务端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必像TCP连接过程中调用listen和accept函数。UDP中只有创建套接字过程和数据交换过程。
2.2.4、UDP服务器端和客户端均只需一个套接字
TCP中,套接字之间应该是一对一的关系。若要向十个客户端提供服务,则除了服务端套接字外,还需要十个用于与客户端连接的套接字,也就是由accept所产生的套接字。但在UDP中,不管是服务器端还是客户端都只需要一个UDP套接字就可以向任何主机传输数据,如下图UDP套接字通信模型:
上图展示了一个UDP套接字与两个不同主机交换数据的过程,也就是说,只需一个UDP套接字就能和多台主机通信
2.2.5、UDP客户端套接字的地址分配
从UDP的编程流程可以看出UDP缺少把IP端口分配给套接字的过程,TCP客户端调用connect函数自动完成此过程,而UDP连能承担相同功能的函数调用语句都没有,究竟是在何时分配IP地址和端口号呢?
UDP程序中,调用sendto函数传输数据前完成对套接字的地址分配工作,因此调用bind函数。当然,bind函数在TCP程序中出现过,但bind函数不区分TCP和UDP。另外调用sendto函数时尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP地址和端口。而且此时分配的地址一直保留到程序结束为止。因此也可用来与其他UDP套接字进行数据交换,当然IP用主机IP,端口号选尚未使用的任意端口号。
综上所述,调用sendto函数时自动分配IP和端口号,因此UDP客户端中通常无需额外的地址分配过程。
2.2.6、基于UDP的数据I/O函数
创建好TCP套接字后,传输数据时无需再添加地址信息,因为TCP套接字将保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态(UDP套接字只有简单的邮筒功能),因此每次传输数据都要添加目标地址信息。这相当于寄信前在信件中填写地址,接下来介绍填写地址并传输数据时调用的UDP相关函数 。
#include <sys/socket.h> ssize_t sendto(int sock, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);//成功时返回传输的字节数,失败时返回-1
- sock:用于传输数据的UDP套接字文件描述符
- buff:保存待传输数据的缓冲地址值
- nbytes:待传输的数据长度,以字节为单位
- flags:可选项参数,若没有则传递0
- to:存有目标地址信息的sockaddr结构体变量的地址值
- addrlen:传递给参数to的地址值结构体变量长度
上述函数与之前的TCP输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收UDP数据的函数,UDP数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回UDP数据包的发送端信息。
#include <sys/socket.h> ssize_t recvfrom (int sock, void *buff, size_t nbytes, int flags, struct sockaddr * from, socklen_t * addrlen);//成功时返回接收的字节数,失败时返回-1
- sock:用于接收数据的UDP套接字文件描述符
- buff:保存接收数据的缓冲地址值
- nbytes:可接收的最大字节数,故无法超过参数buff所指的缓冲大小
- flags:可选项参数,若没有则传递0
- from:存有发送端地址信息的sockaddr结构体变量的地址值
- addrlen:保存参数from的结构体变量长度的变量地址值
编写UDP程序时最核心的部分就在于上述两个函数,也说明二者在UDP数据传输中的地位。
2.2.7、UDP的数据传输特性
TCP传输的数据不存在数据边界,而UDP数据传输中存在数据边界。前面说过,TCP数据传输中不存在边界,这表名数据传输过程中调用I/O函数的次数不具有任何意义。相反,UDP是具有数据边界协议的,传输中调用I/O函数的次数非常重要。因此,输入函数的调用次数应和输出函数的调用次数完全一致,这样才能保证接收全部已发送的数据。
2.2.8、已连接(connected)UDP套接字与未连接(unconnected)UDP套接字
TCP套接字中需注册待传输数据的目标IP和端口号,而UDP中则无需注册。因此,通过sendto函数传输数据的过程大致可分为以下三个阶段:
第一阶段:向UDP套接字注册目标IP和端口号
第二阶段:传输数据
第三阶段:传输UDP套接字中注册的目标地址信息
每次调用sendto函数时重复上述过程,每次都变更目标地址,因此可以重复利用同一UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接connected套接字。显然,UDP套接字默认属于未连接套接字。但是,要与同一主机进行长时间通信时,将UDP套接字变为已连接套接字会提高效率,上述三个阶段中,第一个阶段和第三个阶段将占用整个通信过程的1/3的时间,缩短这部分时间将大大提高性能
2.3、协议的选择
协议的选择应该烤炉到数据的可靠性、应用的实时性和网络的可靠性。
(1)对数据可靠性要求高的应用需选择TCP,而对数据的可靠性要求不那么高的应用可选择UDP。
(2)TCP中的3次握手、重传确认等手段可以保证数据传输的可靠性,但使用TCP会有较大的延时,因此不适合对实时性要求较高的应用,而UDP则有很好的实时性。
(3)网络状况不是很好的情况下需选用TCP(如在广域网),网络状况很好的情况下选择UDP可以减少网络负荷。