(参考链接:计算机网络协议(三)——UDP、TCP、Socket)

TCP与UDP区别总结

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的。UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
综上所述,UDP适用于对数据完整性要求不高,实时性要求高,多客户端的场景。

基于UDP的实际应用:
网页或者APP的访问,访问网页和手机APP都是基于HTTP协议(基于TCP)的,建立连接需要多次交互,比较耗时,Google提出了QUIC实现快速连接建立、减少重传时延,自适应拥塞控制;
流媒体的协议,直播协议多使用RTMP(基于TCP),当数据丢包或者网络不好,影响直播的实时性,很多直播应用,都基于UDP实现了自己的视频传输协议;
实时游戏,采用自定义的可靠UDP协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响;
IoT物联网,物联网通信协议Thread,就是基于UDP协议的,解决了物联网领域终端资源少,实时性要求高的问题;
移动通信领域:4G网络里,移动流量上网的数据面对的协议GTP-U是基于UDP的;
ps:它可以用在环境简单、需要多播、应用层自己控制传输的地方。例如DHCP、VXLAN、QUIC等。

UDP的首部(8字节):
2.png
TCP的首部(20字节):
3.png
源端口号和目标端口号:知道谁发的和发给谁的;
序号:编号是为了解决乱序问题;
确认序号:发出去的包应该有确认,没有收到就应该重新发送,直到送达;
状态位:SYN是发起一个连接、ACK是回复、RST是重新连接、FIN是结束连接;
窗口大小:TCP要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力。(后文会讲到)

ACK:TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1
SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1。因此,SYN置1就表示这是一个连接请求或连接接受报文。
FIN(finis)即完,终结的意思,用来释放一个连接。当 FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
seq(序号)是数据包本身的序列号;
ack(确认号)是期望对方继续发送的那个数据包的序列号。(这个ack并不是ACK)

※TCP/IP三次握手

4.png
第一次握手:建立连接,客户端向服务器发送连接请求,状态由CLOSED主动打开。此时ACK=0,表示连接未建立;SYN=1,表示这是一个连接请求;seq表示当前数据包序号为x。(每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个32位的计数器,每4ms加一)客户端进入SYN-SENT状态,服务器收到连接请求,状态由CLOSED变为LISTEN。
第二次握手:服务器向客户端发送连接接受报文。此时SYN=1,ACK=1,表示连接接收;seq=y表示当前数据包为y,ack确认号为x+1(收到的包的序号加一)。服务器进入SYN-RCVD状态;
第三次握手:客户端向服务器发送确认包。此时SYN=0,因为不是连接器请求也不是连接接收;ACK=1,连接有效;seq=x+1表示当前数据包序号为x+1,ack为确认号。

为什么要三次握手,二次不行?
如果只进行两次握手,那么服务器区分不了一个连接请求到底是由于网络延时滞留的上一次的失效请求还是新发出的请求。
5.png

也有一种通俗的解释,三次握手第一次服务器端知道了客户端的发送能力是ok的,第二次握手客户端知道自己的发送能力、服务端的接收能力、服务无端的发送能力都是ok的,但是服务器还不确定客户端接收能力;第三次握手就是确保服务器知道了客户端接收到了。这样双方都知道对方和自己的收发能力都是ok的,连接建立。

※TCP/IP四次挥手

6.png
关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,”你发的FIN报文我收到了”。只有等到Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

※TIME-WAIT
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL(MSL:最大报文段生存时间)。这么做有两个理由:
1)确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A等待一段时间就是为了处理这种情况的发生。
2)等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

7.png

面试总结之time_wait状态产生的原因,危害,如何避免
1. time_wait状态如何产生?
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。

2.time_wait状态产生的原因
1)为实现TCP全双工连接的可靠释放
由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。
2)为使旧的数据包在网络因过期而消失
为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。
3)总结
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。

3.time_wait状态如何避免
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。

状态转移

TCP状态机:加黑加粗的部分,是上面说到的主要流程,其中阿拉伯数字的序号,是连接过程中的顺序,而大写中文数字的序号,是连接断开过程中的顺序。
加粗的实线是客户端A的状态变迁,加粗的虚线是服务端B的状态变迁;
8.png

拥塞控制

(参考链接:TCP的拥塞控制
计算机网络中的带宽、交换结点中的缓存和处理机等,都是网络的资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏。这种情况就叫做拥塞。拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制是一个全局性的过程,和流量控制不同,流量控制指点对点通信量的控制。
拥塞控制主要算法:慢开始、拥塞避免、快重传、快恢复(任何问题的解决方法都是这样,发生前的预防避免:慢开始、拥塞避免;以及发生后的补救止损:快重传、快恢复)
首先要引入一个概念:拥塞窗口(cwnd)。

可靠工作原理

(参考链接:我是如何讲清楚TCP/IP是如何实现可靠传输的

Socket编程

在通信之前,双方都要建立一个Socket。Socket编程进行的是端到端的通信,也只能是端到端协议之上网络层和传输层的。
在网络层中,Socket函数需要指定到底是IPv4还是IPv6,分别对应设置为AF_INET和AF_INET6。还要指定到底是TCP还是UDP,TCP协议是基于数据流的,所以设置为SOCK_STREAM,而UDP是基于数据报的,因而设置为SOCK_DGRAM。

1)基于TCP协议

两端创建Socket之后,TCP的服务端调用bind函数监听一个端口, 给这个Socket赋予一个IP地址和端口;

当服务端有了IP和端口号,就可以调用listen函数进行监听。此时的客户端就可以发起连接请求了;

在内核中为每个Socket维护两个队列,分别是已经建立了连接、完成三次握手后处于established状态的队列;一个是还没有完全建立连接的队列,三次握手还没完成,处于syn_rcvd的状态。

接下来,服务端调用accept函数,拿出一个已经完成的连接进行处理。

在服务端等待的时候,客户端可以通过connect函数发起连接。先在参数中指明要连接的IP地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。

连接建立之后双方开始通过read和write函数来读写数据,下图是基于TCP协议的Socket程序函数调用过程:
9.png
对于客户端来说,比较简单,首先创建一个socket,然后向服务器发起连接,连接上就可以发送/接收数据了:
11.png
客户端一般是内核自动分配IP以及端口,所以不用自己定,connect函数即是与服务器端的三次握手。
接下来看服务端的socket编程伪代码:
12.png
bind为绑定IP以及端口;因为服务器端是被动的,所以需要listen函数来监听客户端发起的连接,并且服务器是一直工作的,所以用while(1)的无限循环;但是一个服务器可能连接了多个客户端,所以服务器要会区分每个socket,这里的listenfd相当于创建一条tcp链路,有多个socket共用这一条链路,所以每一个connfd相当于创建每一个socket描述符。这里要注意的是虽然创建了多个connfd描述符,但是并没有创建新的端口,服务器通过不同的客户端IP、端口来区分不同的socket。

2)基于UDP协议

UDP是没有连接的,所以不需要三次握手,也就不需要调用listen和connect,但是,UDP的的交互仍然需要IP和端口号,因而也需要bind函数;但正是没有连接状态,每次通信的时候,都调用sendto和recvfrom,都可以传入IP地址和端口;

下图就是基于UDP协议的Socket程序函数调用过程:
10.png