2. 传输层
传输层
TCP协议
面向连接、可靠的、面向字节流的传输层通信协议。具体来说:面向连接就是必须是一对一,不能是一对多也不能是多对多;可靠的就是当网络发生变化,比如网络延迟,阻塞等情况,TCP都可以保证一个报文可以达到对端;面向字节流就是TCP协议的接受方必须知道消息的边界,这是因为一个消息是有可能被分为多个TCP报文分组,如果接受方不知道消息的边界,那么它就不能读到一个有效的用户消息。
三次握手
四次挥手
重传机制与超时处理
重传机制是通过序列号和确认应答来处理的。这里了解一个RTT(一次往返时间),一般设置超时重传时间RTO略大于RTT比较合适,因为远小于RTT时,有可能包没有丢失就重传了,增加网络拥塞。如果RTO远大于RTT时会导致包已经丢失了,但是真正重传还需要等待一段时间,导致网络通信效率低。重传有以下三种机制:
- 普通超时重传:某个数据包发送,设置定时器(超时时间倒计时),当定时器触发后重传改数据包。
- 快速重传:当发送端收到三个相同的ACK报文(ACK报文表示下一个要接受那个序列号的包),则说明这个序列号的包已经丢失,立即重传改包。
- 快速重传:重传不再是重传丢失包之后所有的包,而是只重传丢失的那个包。
TCP头部结构
Socket编程
TCP连接管理
具体流程如下:
- 服务器和客服端初始化socket,得到各自的文件描述符fd。
- 服务器调用bind,将socket绑定到指定的IP地址和端口。
- 服务器调用listen进行监听,等待客服端连接。
- 客服端调用connect,向服务器的地址和端口发起连接请求。服务器accept返回用于传输的socket的文件描述符。
- 后续服务器和客服端使用这个用于传输的文件描述符进行通信(write/read)。
- 使用close关闭连接。
半连接和全连接队列:
服务器收到客户端的SYN请求,内核会把连接存储到半连接队列,并向客户端返回SYN+ACK,接着客户端会返回ACK,当服务器收到第三次握手的ACK报文时,内核会把连接从半连接队列移除,然后创建新连接并将其添加到accept队列(全连接队列)。
为什么维护两个队列:
半连接队列主要用于存储SYN_RECV状态的请求,也就是客户端第一次SYN请求,但是此时还没有完成三次握手。当服务器收到客服端的SYN报文时,并不能立即确定改连接是否合法,这次连接有可能是网络延迟导致的过期连接请求,所有让这些请求先暂时存储到半连接队列,当客服端发起第三次ACK报文时才确定连接是合法的,此时才将半连接队列的请求移除,并且建立新的连接到全连接队列中。
SYN Flood 攻击及防御机制
SYN防洪攻击:在TCP连接的三次连接,假如一个用户向服务器发送了SYN报文后突然死机或掉线,那么服务器在发出SYN+ACK应答报文后是无法收到客户端的ACK报文的(第三次握手无法完成),这种情况服务器一般会重新发送一次(SYN+ACK)给客服端,并且等待一段时间丢弃这个未完成的连接,这段时间我称为SYN Timeout。并且这个时间是分钟的数量级(30s-120s)。此时如果有攻击者恶意的执行上述操作,服务器为了维护这么大的半连接列表而消耗非常多的资源。
如何防御:简单的方法有两种,第一种就是直接缩小SYN Timeout时间。比如设置为20s,但是这样子有可能影响客服端的正常连接,降低服务器的负荷。第二种就是设置SYN Cookie,就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内同时接收到同一个IP的重复SYN报文,就认为受到攻击,以后从这个IP地址来的包就会被丢弃,但是这种攻击这可以通过SOCK_RAW随机改写报文中的源地址,从而使得改防御方法无效。
滑动窗口协议
先来了解一下发送包成功的流程。一般来说发送方发送一个包,要等到接受发回复ACK才可以说这个包发送成功。但是这样子我们就只能发送一个包,等待回复才可以发送下一个包。这样子效率就比较低,那么我们考虑一次性发送多个包会怎么样了?比如我们一次性发送3个包,我们不需要等待第一个和第二个包的ACK就可以发送第三个包。这就引出滑动窗口。发送方对于滑动窗口分为了4个部分。
首先是已经发送并且已经收到ACK回复部分,第二是以发送未接收ACK部分,第三是未发送可发送部分,第四是未发生不可发送部分。并且接收方会回复一个接收窗口,这个窗口大小等于发送方的已发送未接收ACK部分加上未发送可发送部分。注意此时如果出现丢包现象,选择重传机制即可。
拥塞控制(慢启动、拥塞避免、快速重传、快速恢复)
注意一个拥塞窗口,这个窗口是发送方自己的内部参数,一般是根据网络的拥塞情况定义的一个窗口大小值。发送方的具体发送大小其实是取接收方的接收窗口和发送方拥塞窗口的最小值。
AIMD(加法增大乘法减小)
慢启动:是指拥塞窗口按照指数级别增长。一直达到慢开始门限值(ssthresh)。
拥塞避免:此时按照依次累加窗口大小,一直到发送窗口大小。如果出现超时,那么就将拥塞窗口(cawd)设置为1,慢开始门限值设置为超时的cawd的一般。
快重传和快恢复:
当发送方连接接收到三个确认时,就执行乘法减小算法,将慢开始门限值减半,但是此时不执行慢开始算法,而是直接使用加法增大(每次增加一个)使拥塞窗口缓慢增大。
粘包与拆包问题及解决方法
为什么会出现黏包,这一般是出现在TCP传输时,因为TCP是面向字节流,没有边界。OS在发送消息时,会通过缓冲区进行优化。如果一次请求发送数据量比较小,没有达到缓冲区大小,那么TCP会将多个请求合并为同一个请求进行发送,这就形成了黏包问题。
为什么会出现拆包问题,如果一次数据量太大,超过缓冲区大小。那么TCP会将其拆分进行多次发送,这就是拆包。
如何解决以上问题,常见的方法有以下:
- 发送端规定每个包的大小,不足通过填充0来解决。
- 在每个包的开始和结尾选择固定的分隔符。
- 使用消息头和消息体。消息头可以包含该次消息大小长度。(推荐使用)。
UDP协议
用户数据报协议。作为一种无连接、尽最大努力交互。由于没有可靠性和保证、顺序保证和流量控制,导致可靠性比不上TCP传输。但这是由于没有这些限制,使用UDP传输延迟更小,数据传输率更高。特别是一些对于可靠性要求不高并且网络状况较好的场景下UDP比TCP更加实用。
UDP可靠性增强
一些对于延迟要求低,并且具备可靠性的场景。我们可以基于UDP,在应用层添加一些限制达到可靠性。这样子我们既可以拥有UDP的实时性,还具备TCP一般的可靠性。具体实现需要在应用层进行自定义。可以通过模仿TCP的序列号以及超时重传机制实现,但是我们可以DIY一下,比如根据业务场景自定义超时时间是多少,重传选择快重传等等。