这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记
在抖音服务器项目中,计算机网络是不可缺少的一环,传输层协议更是计算机网络中的重中之重。我将通过两篇文章22个问题带你领略传输层协议的风采。
1.UDP 和 TCP 的特点与区别
用户数据报协议 UDP(User Datagram Protocol)
是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。
传输控制协议 TCP(Transmission Control Protocol)
是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。
2.UDP 、TCP 首部格式
UDP首部
在计算检验和时,要在 UDP 用户数据报之前增加 12 个字节的伪首部。
伪首部共12个字节,包括:源IP地址、目的IP地址、UDP数据长度、协议类型和填补一个字节的0x0
UDP校验方法:
- 发送方首先把全零放入校验和字段并添加伪首部,然后把UDP数据报视为许多16位的字连接起来。
- 若UDP数据报的数据部分不是偶数个字节,则要在数据部分末尾增加一个全零字节(但此字节不发送)。
- 接下来按二进制反码计算出这些16位字的和,并将此和的二进制反码写入校验和字段。
- 接收方把收到的UDP数据报加上伪首部(如果不为偶数个字节,那么还需要补上全零字节)后,按二进制反码计算出这些16位字的和。
- 当无差错时其结果应全为1,否则表明有差错出现,接收方就应该丢弃这个UDP数据报。
TCP首部
序号:用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
确认号:期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
数据偏移:指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
控制位:八位从左到右分别是 CWR,ECE,URG,ACK,PSH,RST,SYN,FIN。
CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小;
ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设为 1;
URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关;
ACK:该位设为 1,确认应答的字段有效,TCP规定除了最初建立连接时的 SYN 包之外该位必须设为 1;
PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议,若设为 0,则先将数据进行缓存;
RST:该位设为 1,表示 TCP 连接出现异常必须强制断开连接;
SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定;
FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。
每个主机又对对方的 FIN 包进行确认应答之后可以断开连接。不过,主机收到 FIN 设置为 1 的 TCP 段之后不必马上回复一个 FIN 包,而是可以等到缓冲区中的所有数据都因为已成功发送而被自动删除之后再发 FIN 包;
窗口:窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
3.TCP三次握手
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手:
- 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND 状态。首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
- 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
- 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN +1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行 被动打开(passive open)。 在socket编程中,客户端执行connect()时,将触发三次握手
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
4.为什么需要三次握手?
两次握⼿在收到服务端的响应后开始发⽣数据,不能判断当前连接是否是历史连接。
5.三次握手过程中可以携带数据吗?
其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据.为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病.
6.listen()队列剖析
监听套接字的队列
对于一个调用listen()进行监听的套接字,操作系统会给这个套接字 维护两个队列;
-
未完成连接队列 【保存连接用的】 当客户端 发送tcp连接三次握手的第一次【syn包】给服务器的时候,服务器就会在未完成队列中创建一个 跟这个 syn包对应的一项,其实,我们可以把这项看成是一个半连接【因为连接还没建立起来呢】,这个半连接的状态会从LISTEN变成SYN_RCVD状态,同时给客户端返回第二次握手包【syn,ack】这个时候,其实服务器是在等待完成第三次握手;
-
已完成连接队列 【保存连接用的】 当第三次握手完成了,这个连接就变成了ESTABLISHED状态,每个已经完成三次握手的客户端 都放在这个队列中作为一项;backlog曾经的含义:已完成队列和未完成队列里边条目之和 不能超过 backlog;
(1)客户端这个connect()什么时候返回,其实是收到三次握手的第二次握手包(也就是收到服务器发回来的syn/ack)之后就返回了;
(2)RTT是未完成队列中任意一项在未完成队列中留存的时间,这个时间取决于客户端和服务器;对于客户端,这个RTT时间是第一次和第二次握手加起来的时间;对于服务器,这个RTT时间实际上是第二次和第三次握手加起来的时间;如果这三次握手包传递速度特别快的话,大概187毫秒能够建立起来这个连接;这个时间挺慢,所以感觉建立TCP连接的成本挺高;【短连接游戏-挺恶心的】
(3)如果一个恶意客户,迟迟不发送三次握手的第三个包。那么这个连接就建立不起来,那么这个处于SYN_RCVD的这一项【服务器端的未完成队列中】,就会一致停留在服务器的未完成队列中,这个停留时间大概是75秒,如果超过这个时间,这一项会被操作系统干掉;
7.什么是半连接队列
服务器第一次收到客户端的SYN之后,就会处于SYN_RCVD(同步已接收)状态,此时双方还没有完全建立连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称为半连接队列。
8.accept()函数
accept()函数,就使用来 从 已完成连接队列 中 的队首【队头】位置取出来一项【每一项都是一个已经完成三路握手的TCP连接】,返回给进程;
如果已完成连接队列是空的呢?那么咱们这个范例中accept()会一致卡在这里【休眠】等待,一直到已完成队列中有一项时才会被唤醒;所以,从编程角度,我们要尽快的用accept()把已完成队列中的数据【TCP连接】取走,大家必须有这个认识; accept()返回的是个套接字,这个套接字就代表那个已经用三次握手建立起来的那个tcp连接,因为accept()是从 已完成队列中取的数据;
换句话来说,我们服务器程序,必须要严格区分两个套接字:
- 监听9000端口这个套接字,这个东西叫“监听套接字【listenfd】”,只要服务器程序在运行,这个套接字就应该一直存在;
- 当客户端连接进来,操作系统会为每个成功建立三次握手的客户端再创建一个套接字【当然是一个已经连接套接字】,accept()返回的就是这种套接字;也就是从已完成连接队列中取得的一项。随后,服务器使用这个accept()返回的套接字和客户端通信的;
9.SYN攻击
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击.
10.四次挥手
- 第一次挥手:客户端发送一个FIN,用来关闭客户端到服务器的数据传送,客户端进入fin_wait_1状态
- 第二次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1,服务端进入Close_wait状态。此时TCP连接处于半关闭状态,即客户端已经没有要发送的数据了,但服务端若发送数据,则客户端仍要接受
- 第三次挥手:服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入Last_ack状态
- 第四次挥手:客户端收到FIN后,客户端进入Time_wait状态,接着发送一个ACK给服务端,确认后,服务端进入Closed状态,完成四次挥手
11.挥手为什么要四次?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,"你发的FIN报文我收到了"。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。