上一篇介紹線程架構(gòu),現(xiàn)在介紹網(wǎng)絡(luò)底層是怎么在這個架構(gòu)上工作的
首先網(wǎng)絡(luò)io在windows下我們選擇select,linux情況下我們使用epoll,這篇文章主要是使用epoll
首先聊聊內(nèi)存對齊
內(nèi)存對齊
#pragma pack(push)
#pragma pack(4)
struct PacketHead {
unsigned short MsgId;
};
#pragma pack(pop)
這段代碼的含義是:自定義的協(xié)議頭,并且這個PacketHead的結(jié)構(gòu)體是4字節(jié)對齊,服務(wù)端和客戶端都遵從這個協(xié)議頭的結(jié)構(gòu)。
在網(wǎng)絡(luò)傳輸過程中,連續(xù)發(fā)送5條10KB的消息,在邏輯上認(rèn)為它是一條一條發(fā)送的,但在真實的網(wǎng)絡(luò)傳輸過程中卻不是嚴(yán)格按照一條一條數(shù)據(jù)到達(dá)接收端的,可能一次收到5KB,也可能一次就收到15KB,這就是網(wǎng)絡(luò)編程中常說的粘包問題。那么我們?nèi)绾闻袛嗍盏搅艘粋€完整協(xié)議呢?為了解決這個問題,需要在邏輯層手動為它加上一個協(xié)議頭,這個協(xié)議頭的定義是自由的,但最重要的一個數(shù)據(jù)是size,表示本協(xié)議的大小。
我們規(guī)定這個協(xié)議的格式遵循:
2字節(jié)(unsigned short) + 4字節(jié)(PacketHead) +body
其中開始的2字節(jié)是代表本協(xié)議的大?。ㄖ劣诒緟f(xié)議的大小包括不包括自己 看設(shè)計者怎么考慮了,兩種都行,我們選擇包含自己)。
收到網(wǎng)絡(luò)數(shù)據(jù)的時候,首先緩沖區(qū)中年將已收到的數(shù)據(jù)大小與一個協(xié)議頭的大小(2字節(jié))相比較,如果小于一個協(xié)議頭,就不處理,等到大于等于一個協(xié)議頭的時候,再把協(xié)議頭讀出來,然后取到協(xié)議的size,如果緩沖區(qū)的size還沒大于等于這個size就說明數(shù)據(jù)還沒接受完,等到數(shù)據(jù)大于等于這個size了,就說明一個完整的協(xié)議發(fā)送完畢了,如果這個數(shù)據(jù)解析完了還剩下數(shù)據(jù) ,那么剩下的數(shù)據(jù)就是下一個協(xié)議頭,然后依舊按照之前的邏輯反復(fù)操作就行。

這篇文章講pragma的 不懂的可以看看 https://www.cnblogs.com/yangguang-it/p/7392726.html
協(xié)議體
在早期的游戲編程中,協(xié)議內(nèi)容一般是由程序員自定義一個結(jié)構(gòu)類型。以登錄為例,它的結(jié)構(gòu)類型的定義可能如下:
struct AccountCheck{
unsigned short Version;
char Account[128];
char Password[128];
};
結(jié)構(gòu)類型定義完成之后,在代碼中實現(xiàn)序列化,并轉(zhuǎn)化為二進(jìn)制串。這個結(jié)構(gòu)類型一旦定義,客戶端和服務(wù)端就必須使用相同的格式。當(dāng)數(shù)據(jù)從客戶端到達(dá)服務(wù)端時,以同樣的規(guī)則反序列化,生成一個結(jié)構(gòu)體(Struct)。
自定義結(jié)構(gòu)類型有一定的優(yōu)勢,執(zhí)行效率相對來說比較高,序列化與反序列化都是清晰可見的。但自定義結(jié)構(gòu)類型有一個致命的缺點,當(dāng)客戶端和服務(wù)端協(xié)議結(jié)構(gòu)不一致時,容易引起異?;蛘咤礄C(jī),必須解決這類兼容問題。特別是對于在線游戲,有人對協(xié)議進(jìn)行分析試探的時候,傳來的協(xié)議可能是錯誤的。我們必須有一個根本的認(rèn)識,從網(wǎng)絡(luò)傳來的協(xié)議任何時候都是不可靠的,它有可能是一個偽客戶端。
另一方面,在上面自定義的結(jié)構(gòu)類型的結(jié)構(gòu)體中加了一個Version字段,隨著游戲上線的時間增長,我們要修改原來的協(xié)議變得十分煩瑣。因為既要考慮到舊的結(jié)構(gòu)體,又要處理新的結(jié)構(gòu)體。常用的辦法就是增加Version字段,同一個協(xié)議的每一個不同的版本都需要處理。
現(xiàn)在不需要這么復(fù)雜的步驟了,有了一個可替代方案,就是Google提供的Protocol Buffer開源項目,簡稱Protobuf。Protobuf是跨平臺的,并提供多種語言版本,也就是說,服務(wù)端和客戶端的編程語言可以不一致,數(shù)據(jù)卻可以通用。序列化和反序列化功能Protobuf都已經(jīng)完成了,不需要我們過多關(guān)心,這樣可以把編碼的重心放在游戲邏輯上。
Packet具體實現(xiàn)
class Packet : public Buffer {
public:
//Packet();
Packet(const int msgId, SOCKET socket);
~Packet();
template<class ProtoClass>
ProtoClass ParseToProto()
{
ProtoClass proto;
proto.ParsePartialFromArray(GetBuffer(), GetDataLength());
return proto;
}
template<class ProtoClass>
void SerializeToBuffer(ProtoClass& protoClase)
{
auto total = protoClase.ByteSizeLong();
while (GetEmptySize() < total)
{
ReAllocBuffer();
}
protoClase.SerializePartialToArray(GetBuffer(), total);
FillData(total);
}
void Dispose() override;
void CleanBuffer();
char* GetBuffer() const;
unsigned short GetDataLength() const;
int GetMsgId() const;
void FillData(unsigned int size);
void ReAllocBuffer();
SOCKET GetSocket() const;
private:
int _msgId;
SOCKET _socket;
};
先聊成員:
1._msgId: protobuf對應(yīng)的msgId
2._socket:packet主要是接收網(wǎng)絡(luò)數(shù)據(jù),那么必定是某個connect函數(shù)返回的socket。這個socket成員就與之對應(yīng)
在講成員函數(shù)之前,先看看這個類繼承于Buffer對象,大概理解就是一個緩沖區(qū)。至于緩沖區(qū)干了什么,我們再看Buffer類是啥:
Buffer類
class Buffer :public IDisposable
{
public:
virtual unsigned int GetEmptySize();
void ReAllocBuffer(unsigned int dataLength);
unsigned int GetEndIndex() const
{
return _endIndex;
}
unsigned int GetBeginIndex() const
{
return _beginIndex;
}
unsigned int GetTotalSize() const
{
return _bufferSize;
}
protected:
char* _buffer{ nullptr };
unsigned int _beginIndex{ 0 }; //
unsigned int _endIndex{ 0 };
unsigned int _bufferSize{ 0 }; //
};
看關(guān)鍵成員:char* _buffer;
原來這個緩沖區(qū)就是一個char字符數(shù)組
并且有一個開始index,和結(jié)束index,同時還有一個已經(jīng)存儲數(shù)據(jù)的大小記錄字段_bufferSize;再仔細(xì)看,原來這個是個環(huán)形的緩沖區(qū),為什么是環(huán)形,后面仔細(xì)說明。
再看關(guān)鍵函數(shù)ReAllocBuffer
void Buffer::ReAllocBuffer(const unsigned int dataLength)
{
if (_bufferSize >= MAX_SIZE) {
std::cout << "Buffer::Realloc except!! " << std::endl;
}
char* tempBuffer = new char[_bufferSize + ADDITIONAL_SIZE];
unsigned int _newEndIndex;
if (_beginIndex < _endIndex)
{
::memcpy(tempBuffer, _buffer + _beginIndex, _endIndex - _beginIndex);
_newEndIndex = _endIndex - _beginIndex;
}
else
{
if (_beginIndex == _endIndex && dataLength <= 0)
{
_newEndIndex = 0;
}
else
{
::memcpy(tempBuffer, _buffer + _beginIndex, _bufferSize - _beginIndex);
_newEndIndex = _bufferSize - _beginIndex;
if (_endIndex > 0)
{
::memcpy(tempBuffer + _newEndIndex, _buffer, _endIndex);
_newEndIndex += _endIndex;
}
}
}
_bufferSize += ADDITIONAL_SIZE;
delete[] _buffer;
_buffer = tempBuffer;
_beginIndex = 0;
_endIndex = _newEndIndex;
//std::cout << "Buffer::Realloc. _bufferSize:" << _bufferSize << std::endl;
}
這是buffer長度再分配函數(shù)。
1.首先判斷——bufferSize是否超出了最大長度MAX_SIZE(假設(shè)是1024)。
2.生成一個新的長為 _bufferSize + ADDITIONAL_SIZE(設(shè)定為10)的char類型數(shù)組
3.聲明一個新的名為:_newEndIndex變量 (先不說原因 下面會講)
4.如果beginIndex < endIndex
如圖所示

那么就將beginIndex到endIndex的數(shù)據(jù)拷貝到新的char*數(shù)組中,此時上面的newIndex就應(yīng)該是:
_newEndIndex = _endIndex - _beginIndex;

原來第三點為什么要聲明新的endIndex的原因是:因為在重新拷貝數(shù)據(jù)的時候,將內(nèi)存重新整理過了
5.如果beginIndex >= endIndex
如圖所示:

這樣就可以看出 原來這是環(huán)形的緩沖區(qū),那么這個時候也需要進(jìn)行數(shù)據(jù)的拷貝以及內(nèi)存的重新的對齊

這里的處理方式是分段進(jìn)行拷貝,先拷貝beginIndex的,再拷貝endIndex的。
最終結(jié)果和上面的一致:

剩下的步驟就不仔細(xì)說了,挺簡單的。
這樣我們就學(xué)習(xí)到了環(huán)形緩沖區(qū)的寫法,學(xué)會之后,再回到pakcet類當(dāng)中來。
packet類方法解析:
Packet::Packet(const int msgId, SOCKET socket)
{
_socket = socket;
_msgId = msgId;
CleanBuffer();
_bufferSize = DEFAULT_PACKET_BUFFER_SIZE;
_beginIndex = 0;
_endIndex = 0;
_buffer = new char[_bufferSize];
}
這是構(gòu)造函數(shù)主要干這幾件事:注冊msgId,賦值connectSocket,初始化buffer。
再看別的函數(shù)
Packet::~Packet()
{
CleanBuffer();
}
void Packet::Dispose()
{
_msgId = 0;
_beginIndex = 0;
_endIndex = 0;
}
void Packet::CleanBuffer()
{
if (_buffer != nullptr)
delete[] _buffer;
_beginIndex = 0;
_endIndex = 0;
_bufferSize = 0;
}
char* Packet::GetBuffer() const
{
return _buffer;
}
unsigned short Packet::GetDataLength() const
{
return _endIndex - _beginIndex;
}
int Packet::GetMsgId() const
{
return _msgId;
}
void Packet::FillData(const unsigned int size)
{
_endIndex += size;
}
void Packet::ReAllocBuffer()
{
Buffer::ReAllocBuffer(_endIndex - _beginIndex);
}
SOCKET Packet::GetSocket() const
{
return _socket;
}
看完會發(fā)現(xiàn),很簡單。甚至沒有讀數(shù)據(jù)的邏輯,這是為什么呢?
那么就需要來介紹protobuf了
最關(guān)鍵的兩個模板函數(shù):
template<class ProtoClass>
ProtoClass ParseToProto()
{
ProtoClass proto;
proto.ParsePartialFromArray(GetBuffer(), GetDataLength());
return proto;
}
template<class ProtoClass>
void SerializeToBuffer(ProtoClass& protoClase)
{
auto total = protoClase.ByteSizeLong();
while (GetEmptySize() < total)
{
ReAllocBuffer();
}
protoClase.SerializePartialToArray(GetBuffer(), total);
FillData(total);
}
說這兩個方法之前 就需要說到protobuff的用法了。
首先假設(shè)和客戶端需要規(guī)定一個消息,這個消息數(shù)據(jù)結(jié)構(gòu)名叫TestMsg,里面有兩個成員 一個msg,一個index

并且這個消息的msgId為1

那么在pakcet的封裝中,MsgId就是對應(yīng)的這個msgId,如果是1 那么packet的數(shù)據(jù)就可以解析成TestMsg的格式。但是如果不解析的話,那么這條數(shù)據(jù)將會是二進(jìn)制的,怎么解析數(shù)據(jù)呢?
就用到了上面兩個模板函數(shù)
當(dāng)用戶拿到了這個MsgId為1的數(shù)據(jù) 需要將這個packet反序列化成我們需要的TestMsg結(jié)構(gòu),只需要調(diào)用

這樣protoObj就可以獲取到 TestMsg的 id和index字段供我們使用了。
于此同時 另外一個函數(shù)是在發(fā)送方,想要發(fā)一個數(shù)據(jù)的時候,將這個數(shù)據(jù)打包成packet,并進(jìn)行序列化

這樣我們就不需要關(guān)心這個數(shù)據(jù)是怎么變成二進(jìn)制的,全靠protobuf幫助就行了。