服务器设计原则总述

我们写的是:通用的服务框架:将来,稍加改造甚至不用改造就可以把它直接应用在很多的具体开发工作中;

我们的工作重点就可以聚焦在业务逻辑上; 相当于你自带框架【自带源码】入职;甚至你可以挑战高级程序员/主程序这种职业;

收发包格式问题提出

游戏服务器:第一条命令出拳【1abc2】,第二条加血【1def2|30】;

1abc21def2|30,服务器如何识别出这是两条命令?

TCP粘包、缺包

Nagle优化算法

TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):

(1)如果包长度达到MSS,则允许发送;

(2)如果该包含有FIN,则允许发送;

(3)设置了TCP_NODELAY选项,则允许发送;

(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if there is new data to send #有数据要发送
# 发送窗口缓冲区和队列数据 >=mss,队列数据(available data)为原有的队列数据加上新到来的数据
# 也就是说缓冲区数据超过mss大小,nagle算法尽可能发送足够大的数据包
if the window size >= MSS and available data is >= MSS
send complete MSS segment now # 立即发送
else
if there is unconfirmed data still in the pipe # 前一次发送的包没有收到ack
# 将该包数据放入队列中,直到收到一个ack再发送缓冲区数据
enqueue data in the buffer until an acknowledge is received
else
send data immediately # 立即发送
end if
end if
end if 

tcp粘包问题

client发送abc,def,hij,三个数据包发出去;

a)客户端粘包现象

客户端因为有一个Nagle优化算法;

send(“abc”); write()也可以
send(“def”);
send(“hij”);

因为Nagle算法存在的,这三个数据包被Nagle优化算法直接合并一个数据包发送出去;这就属于客户端粘包;

如果你关闭Nagle优化算法,那么你调用几次send()就发送出去几个包;那客户端的粘包问题就解决了;

b)服务器端粘包现象

不管你客户端是否粘包,服务器端都存在粘包的问题:就算你客户端不粘包,但是,仍然避免不了服务器端粘包的问题;因此客户端解决粘包不是很有必要。

服务器端两次 recv之间可能间隔100毫秒,那可能在这100毫秒内,客户端这三个包都到了,这三个包都被保存到了服务器端的。针对该TCP连接收数据缓冲中【abcdefhij】;你再次recv一次,就可能拿到了全部的“abcdefhij”,这就叫服务器端的 粘包;

tcp缺包问题

再举一例:

send(“abc…….”); //8000字节;这个可能被操作系统拆成6个包发送出去了;

网络可能出现延迟或者阻塞,

服务器端第一次recv() = “ab”
第二次recv = “c…”,
recv………
recv() = “…..de”…. [缺包]

TCP粘包、缺包解决

粘包,要解决的就是吧这几个包拆出来,一个是一个;

解决粘包的方案很多;提供简单、严谨、有效的一种解决方案;

例如abc$def$hij ,很多服务器程序员不考虑 恶意数据包;
服务器程序员不能假设收到的数据包都是善意的,合理的,构造畸形数据包 abc#def-hij,没法处理了

如何解决拆包问题:给收发的数据包定义一个统一的格式[规则];c/s都按照这个格式来,就能够解决粘包问题;

包格式: 包头+包体 的格式;其中 包头 是固定长度【10字节】,在包头中,有一个成员变量会记录整个包【包头+包体】的长度;
这样的话,先收包头,从包头中,我知道了整个包的长度,然后 用整个包的长度 - 10个字节 = 包体的长度。
我再收 “包体的长度”这么多的字节; 收满了包体的长度字节数,我就认为,一个完整的数据包【包头+包体】收完;

image-20220211153421282

收包总结:
(1)先收固定长度包头 10字节;
(2)收满后,根据包头中的内容,计算出包体的长度:整个长度-10
(3)我再收包体长度这么多的数据,收完了,一个包就完整了;
我们就认为受到了一个完整的数据包;从而解决了粘包的问题;

官方的nginx的代码主要是用来处理web服务器【一种专用的服务器】,代码写的很庞杂;不太适合咱们这种固定数据格式【包头+包体】的服务器【通用性强的服务器,可以应用于各种领域】。