游戏服务器中对于发包/收包的个人理解
作者:互联网
TCP
- 发包: tcp有自动重传机制,所以一般的包体结构基本是
包体长度 | 包体数据 |
---|
······很简单明了,也是我们初学网络编程是所用的结构。那么思考一下,我们发包需要什么信息呢?
其实我们只需两个信息,已发送长度和数据包大小。那么结构就变成了
包体信息(已发送长度 + 数据包大小) | 包体数据(包长 + 数据) |
---|
······有些人可能这里就疑惑了,不是包体数据里面有个包长数据吗?为什么还要多一个数据包大小,呵呵,不要着急。我们接下来慢慢说
- 包体数据内结构
······我在网上看到很多人都是用 包头标识符 + 包长 + 数据 + 包尾标识符 包头标识符 + 包长 + 数据或者包长 + 数据 + 包尾标识符 这样的结构来处理数据包的。包括我身边朋友实际项目中也是用的 包长 + 数据 + 包尾标识符这样的结构。
······但是这样会有个问题,那就是如果发生的断包的情况(极少发生,可能发生的情况有:恶意改包,截断数据包。或者对端的代码逻辑错误,或者其他不知名的情况下),我们收到了一个不完整包,但之后的包都是正确的包,里面有包长信息和 部分数据,假设包长为16字节,我们收到包长2字节 + 部分数据 4字节,这时候后面发送的包来了,这时候底层就会把前面的不完整包和后面发来的完整包进行拼接,会导致后面的数据一直是混乱的。
······这时候有人出来说了。我们不是有包头包尾标识吗?这样就可以判断是不是有效包啊。那我们来看看。如果是包头标识符 + 包长 + 数据这样的结构。假设这时候我们收到了一个被截断的包,里面有5个字节。1个字节表示包头标识,2个字节表示包长(假设16字节),剩下的2个字节表示数据。然后这时候后面的正常包数据来了。但是这个包只有8个字节,包头标识和包长占3个,剩下5个字节全是数据,假设这个8字节的包就是一个完整包的情况下,那我们又经过了一轮接收,这时候终于接收到了前面那个截断包(包长 + 1)的长度(为何要 + 1?因为我们需要后面一个包的包头标识符验证是否是截断包,如果后面的包过很久才发也是一个问题)。这时候我们检查到 package[pack_len + 1] 的位置却不是本该在那儿的包头标识符,这时候我们才发现之前的数据里包含了截断包,只能把它丢弃。里面还包括了一个完好的包。。。
而包长 + 数据 + 包尾标识符的结构异同。就不多讲了。
可能是没有想过或者也是不在乎那几个数据包吧。我可能有点强迫症。。说多了
我自己定的包体结构是这样的
namespace network::tcp {
#pragma pack(push)
#pragma pack(1)
/*
包体信息
存储结构:包体信息 + 报文结构
报文结构: 包头结构 + 包体 + 包尾结构
*/
struct pack_info {
slen_t slen; //发送长度
slen_t len; //包长
};
/*
包头结构
报文结构: 包头结构 + 包体 + 包尾结构
*/
struct head_pack {
char head; //包头标识符
slen_t len; //包长
};
/*
包尾结构
报文结构: 包头结构 + 包体 + 包尾结构
*/
struct tail_pack {
slen_t len; //包长
char tail; //包尾标识符
};
#pragma pack(pop)
}
至于为何包尾也要加个包长,还有为什么需要头尾两端的标识符,大家思考一下?
- 合包机制
为了减少服务器压力,通常的做法是将小包合成为大包,本来需要发送几次才能发送完成的数据只需要一次就能完成,这样可以减少部分服务器的压力,但是合成的大包不能超过MTU(最大传输单元),那么MTU到底有多大呢?
其实根据宽带连接方式的不同,MTU可能不尽相同,如下所示:
(1). PPPoE/ADSL: 1360-1492
(2). PPTP VPN: 1400-1460
(3). L2TP VPN: 1400-1460
(4). Fixed IP: 1400-1500
(5). DHCP: 1400-1492
保险起见,这里我自己定的合包大小为1024字节。那么我们合包之后结构就变成了这样
包体信息(已发送长度 + 数据包大小) | 包体数据([包头+ 包体 + 包尾] + [包头+ 包体 + 包尾] + …) |
---|
怎么样,这下就知道我们最开始为什么需要包体信息中的数据包大小了吧,因为它是代表整个数据包大小的数据,而不是单个数据包。虽然单个数据包的包头信息中包含了包长数据。但是它只代表了自身的大小。
- 发送优先级
在游戏中,游戏数据包会有优先级划分,比如自己的移动数据优先级 > 其他玩家移动数据的优先级,其实这个优先级发送机制很多游戏都有(难怪玩撸啊撸的时候卡了都是自己卡,看队友一点都不卡,可能就用了发送优先级吧)。那么我们为了能让先发送的包优先级更大。我的方法是使用最大堆存储消息队列(deque),我们插入发送数据需要三种信息:- 优先级;
- 发送数据;
- 发送长度;
······我们就是根据优先级来获取最大堆中指定的优先级消息队列,如果没有则创建一个新的消息队列并加入最大堆。再向指定的优先级消息队列中添加我们插入的发送数据。等到发送的时候再取出使用,发送完成后再删除消息队列中的发送数据。如果消息队列为空了,那么就把它从最大堆中移除。这样就能保证最大堆的顶部一直都是消息优先级最高的数据。
- 对于环形缓冲区
我个人是对其产生了不少的疑问,所以并没有在项目使用,我用的是自己写的不定长内存池来申请发送数据使用的内存。申请内存的时候,可以直接申请sizeof(包体信息) + sizeof(包头) + 数据长度 + sizeof(包尾)大小的内存,设置数据的时候直接使用指针指向对应的位置就好了。上面的#pragma pack也是为了我们能利用好每一个字节的空间;
udp
还没开始写呢。别着急,等个十年八年就出来了
标签:收包,标识符,包长,包尾,发包,包头,服务器,数据包,包体 来源: https://blog.csdn.net/qq_28398301/article/details/106324867