因爲咱們的即時通信軟件的用戶存在用戶狀態問題,即用戶登陸成功之後能夠在他的好友列表中看到哪些好友在線,因此客戶端和服務器須要保持長鏈接狀態。另外即時通信軟件通常要求信息準確、有序、完整地到達對端,而這也是TCP協議的特色之一。綜合這兩個因此這裏咱們選擇TCP協議,而不是UDP協議。java
因爲TCP協議是流式協議,所謂流式協議即通信的內容是無邊界的字節流:如A給B連續發送了三個數據包,每一個包的大小都是100個字節,那麼B可能會一次性收到300個字節;也可能先收到100個字節,再收到200個字節;也可能先收到100個字節,再收到50個字節,再收到150個字節;或者先收到50個字節,再收到50個字節,再收到50個字節,最後收到150個字節。也就是說,B可能以任何組合形式收到這300個字節。即像水流同樣無明確的邊界。爲了能讓對端知道如何給包分界,目前通常有三種作法:linux
以固定大小字節數目來分界,上文所說的就是屬於這種類型,如每一個包100個字節,對端每收齊100個字節,就當成一個包來解析;編程
以特定符號來分界,如每一個包都以特定的字符來結尾(如\n),當在字節流中讀取到該字符時,則代表上一個包到此爲止。windows
固定包頭+包體結構,這種結構中通常包頭部分是一個固定字節長度的結構,而且包頭中會有一個特定的字段指定包體的大小。這是目前各類網絡應用用的最多的一種包格式。數組
上面三種分包方式各有優缺點,方法1和方法2簡單易操做,可是缺點也很明顯,就是很不靈活,如方法一當包數據不足指定長度,只能使用佔位符如0來湊,比較浪費;方法2中包中不能有包界定符,不然就會引發歧義,也就是要求包內容中不能有某些特殊符號。而方法3雖然解決了方法1和方法2的缺點,可是操做起來就比較麻煩。咱們的即時通信協議就採用第三種分包方式。因此咱們的協議包的包頭看起來像這樣:bash
struct package_header
{
int32_t bodysize;
};
複製代碼
一個應用中,有許多的應用數據,拿咱們這裏的即時通信來講,有註冊、登陸、獲取好友列表、好友消息等各類各樣的協議數據包,而每一個包由於業務內容不同可能數據內容也不同,因此各個包可能看起來像下面這樣:服務器
struct package_header
{
int32_t bodysize;
};
//登陸數據包
struct register_package
{
package_header header;
//命令號
int32_t cmd;
//註冊用戶名
char username[16];
//註冊密碼
char password[16];
//註冊暱稱
char nickname[16];
//註冊手機號
char mobileno[16];
};
//登陸數據包
struct login_package
{
package_header header;
//命令號
int32_t cmd;
//登陸用戶名
char username[16];
//密碼
char password[16];
//客戶端類型
int32_t clienttype;
//上線類型,如在線、隱身、忙碌、離開等
int32_t onlinetype;
};
//獲取好友列表
struct getfriend_package
{
package_header header;
//命令號
int32_t cmd;
};
//聊天內容
struct chat_package
{
package_header header;
//命令號
int32_t cmd;
//發送人userid
int32_t senderid;
//接收人userid
int32_t targetid;
//消息內容
char chatcontent[8192];
};
複製代碼
看到沒有?因爲每個業務的內容不同,定義的結構體也不同。若是業務比較多的話,咱們須要定義各類各樣的這種結構體,這簡直是一場噩夢。那麼有沒有什麼方法能夠避免這個問題呢?有,我受jdk中的流對象的WriteInt3二、WriteByte、WriteInt6四、WriteString,這樣的接口的啓發,也發明了一套這樣的協議,並且這套協議基本上是通用協議,可用於任何場景。咱們的包仍是分爲包頭和包體兩部分,包頭和上文所說的同樣,包體是一個不固定大小的二進制流,其長度由包頭中的指定包體長度的字段決定。網絡
struct package_protocol
{
int32_t bodysize;
//注意:C/C++語法不能這麼定義結構體,
//這裏只是爲了說明含義的僞代碼
//bodycontent即爲一個不固定大小的二進制流
char binarystream[bodysize];
};
複製代碼
接下來的核心部分就是如何操做這個二進制流,咱們將流分爲二進制讀和二進制寫兩種流,下面給出接口定義:數據結構
//寫
class BinaryWriteStream
{
public:
BinaryWriteStream(string* data);
const char* GetData() const;
size_t GetSize() const;
bool WriteCString(const char* str, size_t len);
bool WriteString(const string& str);
bool WriteDouble(double value, bool isNULL = false);
bool WriteInt64(int64_t value, bool isNULL = false);
bool WriteInt32(int32_t i, bool isNULL = false);
bool WriteShort(short i, bool isNULL = false);
bool WriteChar(char c, bool isNULL = false);
size_t GetCurrentPos() const{ return m_data->length(); }
void Flush();
void Clear();
private:
string* m_data;
};
//讀
class BinaryReadStream : public IReadStream
{
private:
const char* const ptr;
const size_t len;
const char* cur;
BinaryReadStream(const BinaryReadStream&);
BinaryReadStream& operator=(const BinaryReadStream&);
public:
BinaryReadStream(const char* ptr, size_t len);
const char* GetData() const;
size_t GetSize() const;
bool IsEmpty() const;
bool ReadString(string* str, size_t maxlen, size_t& outlen);
bool ReadCString(char* str, size_t strlen, size_t& len);
bool ReadCCString(const char** str, size_t maxlen, size_t& outlen);
bool ReadInt32(int32_t& i);
bool ReadInt64(int64_t& i);
bool ReadShort(short& i);
bool ReadChar(char& c);
size_t ReadAll(char* szBuffer, size_t iLen) const;
bool IsEnd() const;
const char* GetCurrent() const{ return cur; }
public:
bool ReadLength(size_t & len);
bool ReadLengthWithoutOffset(size_t &headlen, size_t & outlen);
};
複製代碼
這樣若是是上文的一個登陸數據包,咱們只要寫成以下形式就能夠了:app
std::string outbuf;
BinaryWriteStream stream(&outbuf);
stream.WriteInt32(cmd);
stream.WriteCString(username, 16);
stream.WriteCString(password, 16);
stream.WriteInt32(clienttype);
stream.WriteInt32(onlinetype);
//最終數據就存儲到outbuf中去了
stream.Flush();
複製代碼
接着咱們再對端,解得正確的包體後,咱們只要按寫入的順序依次讀出來便可:
BinaryWriteStream stream(outbuf.c_str(), outbuf.length());
int32_t cmd;
stream.WriteInt32(cmd);
char username[16];
stream.ReadCString(username, 16, NULL);
char password[16];
stream.WriteCString(password, 16, NULL);
int32_t clienttype;
stream.WriteInt32(clienttype);
int32_t onlinetype;
stream.WriteInt32(onlinetype);
複製代碼
這裏給出BinaryReadStream和BinaryWriteStream的完整實現:
//計算校驗和
unsigned short checksum(const unsigned short *buffer, int size)
{
unsigned int cksum = 0;
while (size > 1)
{
cksum += *buffer++;
size -= sizeof(unsigned short);
}
if (size)
{
cksum += *(unsigned char*)buffer;
}
//將32位數轉換成16
while (cksum >> 16)
cksum = (cksum >> 16) + (cksum & 0xffff);
return (unsigned short)(~cksum);
}
bool compress_(unsigned int i, char *buf, size_t &len)
{
len = 0;
for (int a = 4; a >= 0; a--)
{
char c;
c = i >> (a * 7) & 0x7f;
if (c == 0x00 && len == 0)
continue;
if (a == 0)
c &= 0x7f;
else
c |= 0x80;
buf[len] = c;
len++;
}
if (len == 0)
{
len++;
buf[0] = 0;
}
//cout << "compress:" << i << endl;
//cout << "compress len:" << len << endl;
return true;
}
bool uncompress_(char *buf, size_t len, unsigned int &i)
{
i = 0;
for (int index = 0; index < (int)len; index++)
{
char c = *(buf + index);
i = i << 7;
c &= 0x7f;
i |= c;
}
//cout << "uncompress:" << i << endl;
return true;
}
BinaryReadStream::BinaryReadStream(const char* ptr_, size_t len_)
: ptr(ptr_), len(len_), cur(ptr_)
{
cur += BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN;
}
bool BinaryReadStream::IsEmpty() const
{
return len <= BINARY_PACKLEN_LEN_2;
}
size_t BinaryReadStream::GetSize() const
{
return len;
}
bool BinaryReadStream::ReadCString(char* str, size_t strlen, /* out */ size_t& outlen)
{
size_t fieldlen;
size_t headlen;
if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
return false;
}
// user buffer is not enough
if (fieldlen > strlen) {
return false;
}
// 偏移到數據的位置
//cur += BINARY_PACKLEN_LEN_2;
cur += headlen;
if (cur + fieldlen > ptr + len)
{
outlen = 0;
return false;
}
memcpy(str, cur, fieldlen);
outlen = fieldlen;
cur += outlen;
return true;
}
bool BinaryReadStream::ReadString(string* str, size_t maxlen, size_t& outlen)
{
size_t headlen;
size_t fieldlen;
if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
return false;
}
// user buffer is not enough
if (maxlen != 0 && fieldlen > maxlen) {
return false;
}
// 偏移到數據的位置
//cur += BINARY_PACKLEN_LEN_2;
cur += headlen;
if (cur + fieldlen > ptr + len)
{
outlen = 0;
return false;
}
str->assign(cur, fieldlen);
outlen = fieldlen;
cur += outlen;
return true;
}
bool BinaryReadStream::ReadCCString(const char** str, size_t maxlen, size_t& outlen)
{
size_t headlen;
size_t fieldlen;
if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
return false;
}
// user buffer is not enough
if (maxlen != 0 && fieldlen > maxlen) {
return false;
}
// 偏移到數據的位置
//cur += BINARY_PACKLEN_LEN_2;
cur += headlen;
//memcpy(str, cur, fieldlen);
if (cur + fieldlen > ptr + len)
{
outlen = 0;
return false;
}
*str = cur;
outlen = fieldlen;
cur += outlen;
return true;
}
bool BinaryReadStream::ReadInt32(int32_t& i)
{
const int VALUE_SIZE = sizeof(int32_t);
if (cur + VALUE_SIZE > ptr + len)
return false;
memcpy(&i, cur, VALUE_SIZE);
i = ntohl(i);
cur += VALUE_SIZE;
return true;
}
bool BinaryReadStream::ReadInt64(int64_t& i)
{
char int64str[128];
size_t length;
if (!ReadCString(int64str, 128, length))
return false;
i = atoll(int64str);
return true;
}
bool BinaryReadStream::ReadShort(short& i)
{
const int VALUE_SIZE = sizeof(short);
if (cur + VALUE_SIZE > ptr + len) {
return false;
}
memcpy(&i, cur, VALUE_SIZE);
i = ntohs(i);
cur += VALUE_SIZE;
return true;
}
bool BinaryReadStream::ReadChar(char& c)
{
const int VALUE_SIZE = sizeof(char);
if (cur + VALUE_SIZE > ptr + len) {
return false;
}
memcpy(&c, cur, VALUE_SIZE);
cur += VALUE_SIZE;
return true;
}
bool BinaryReadStream::ReadLength(size_t & outlen)
{
size_t headlen;
if (!ReadLengthWithoutOffset(headlen, outlen)) {
return false;
}
//cur += BINARY_PACKLEN_LEN_2;
cur += headlen;
return true;
}
bool BinaryReadStream::ReadLengthWithoutOffset(size_t& headlen, size_t & outlen)
{
headlen = 0;
const char *temp = cur;
char buf[5];
for (size_t i = 0; i<sizeof(buf); i++)
{
memcpy(buf + i, temp, sizeof(char));
temp++;
headlen++;
//if ((buf[i] >> 7 | 0x0) == 0x0)
if ((buf[i] & 0x80) == 0x00)
break;
}
if (cur + headlen > ptr + len)
return false;
unsigned int value;
uncompress_(buf, headlen, value);
outlen = value;
/*if ( cur + BINARY_PACKLEN_LEN_2 > ptr + len ) {
return false;
}
unsigned int tmp;
memcpy(&tmp, cur, sizeof(tmp));
outlen = ntohl(tmp);*/
return true;
}
bool BinaryReadStream::IsEnd() const
{
assert(cur <= ptr + len);
return cur == ptr + len;
}
const char* BinaryReadStream::GetData() const
{
return ptr;
}
size_t BinaryReadStream::ReadAll(char * szBuffer, size_t iLen) const
{
size_t iRealLen = min(iLen, len);
memcpy(szBuffer, ptr, iRealLen);
return iRealLen;
}
//=================class BinaryWriteStream implementation============//
BinaryWriteStream::BinaryWriteStream(string *data) :
m_data(data)
{
m_data->clear();
char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
m_data->append(str, sizeof(str));
}
bool BinaryWriteStream::WriteCString(const char* str, size_t len)
{
char buf[5];
size_t buflen;
compress_(len, buf, buflen);
m_data->append(buf, sizeof(char)*buflen);
m_data->append(str, len);
//unsigned int ulen = htonl(len);
//m_data->append((char*)&ulen,sizeof(ulen));
//m_data->append(str,len);
return true;
}
bool BinaryWriteStream::WriteString(const string& str)
{
return WriteCString(str.c_str(), str.length());
}
const char* BinaryWriteStream::GetData() const
{
return m_data->data();
}
size_t BinaryWriteStream::GetSize() const
{
return m_data->length();
}
bool BinaryWriteStream::WriteInt32(int32_t i, bool isNULL)
{
int32_t i2 = 999999999;
if (isNULL == false)
i2 = htonl(i);
m_data->append((char*)&i2, sizeof(i2));
return true;
}
bool BinaryWriteStream::WriteInt64(int64_t value, bool isNULL)
{
char int64str[128];
if (isNULL == false)
{
#ifndef _WIN32
sprintf(int64str, "%ld", value);
#else
sprintf(int64str, "%lld", value);
#endif
WriteCString(int64str, strlen(int64str));
}
else
WriteCString(int64str, 0);
return true;
}
bool BinaryWriteStream::WriteShort(short i, bool isNULL)
{
short i2 = 0;
if (isNULL == false)
i2 = htons(i);
m_data->append((char*)&i2, sizeof(i2));
return true;
}
bool BinaryWriteStream::WriteChar(char c, bool isNULL)
{
char c2 = 0;
if (isNULL == false)
c2 = c;
(*m_data) += c2;
return true;
}
bool BinaryWriteStream::WriteDouble(double value, bool isNULL)
{
char doublestr[128];
if (isNULL == false)
{
sprintf(doublestr, "%f", value);
WriteCString(doublestr, strlen(doublestr));
}
else
WriteCString(doublestr, 0);
return true;
}
void BinaryWriteStream::Flush()
{
char *ptr = &(*m_data)[0];
unsigned int ulen = htonl(m_data->length());
memcpy(ptr, &ulen, sizeof(ulen));
}
void BinaryWriteStream::Clear()
{
m_data->clear();
char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
m_data->append(str, sizeof(str));
}
複製代碼
這裏詳細解釋一下上面的實現原理,即如何把各類類型的字段寫入這種所謂的流中,或者怎麼從這種流中讀出各類類型的數據。上文的字段在流中的格式以下圖:
這裏最簡便的方式就是每一個字段的長度域都是固定字節數目,如4個字節。可是這裏咱們並無這麼作,而是使用了一個小小技巧去對字段長度進行了一點壓縮。對於字符串類型的字段,咱們將表示其字段長度域的整型值(int32類型,4字節)按照其數值的大小壓縮成1~5個字節,對於每個字節,若是咱們只用其低7位。最高位爲標誌位,爲1時,表示其左邊的還有下一個字節,反之到此結束。例如,對於數字127,咱們二進制表示成01111111,因爲最高位是0,那麼若是字段長度是127及如下,一個字節就能夠存儲下了。若是一個字段長度大於127,如等於256,對應二進制100000000,那麼咱們按照剛纔的規則,先填充最低字節(從左往右依次是從低到高),因爲最低的7位放不下,還有後續高位字節,因此咱們在最低字節的最高位上填1,即10000000,接着次高位爲00000100,因爲次高位後面沒有更高位的字節了,因此其最高位爲0,組合起來兩個字節就是10000000 0000100。對於數字50000,其二進制是1100001101010000,根據每7個一拆的原則是:11 0000110 1010000再加上標誌位就是:10000011 10000110 01010000。採用這樣一種策略將原來佔4個字節的整型值根據數值大小壓縮成了1~5個字節(因爲咱們對數據包最大長度有限制,因此不會出現長度須要佔5個字節的情形)。反過來,解析每一個字段的長度,就是先取出一個字節,看其最高位是否有標誌位,若是有繼續取下一個字節當字段長度的一部分繼續解析,直到遇到某個字節最高位不爲1爲止。
對一個整形壓縮和解壓縮的部分從上面的代碼中摘錄以下:
壓縮:
//將一個四字節的整形數值壓縮成1~5個字節
bool compress_(unsigned int i, char *buf, size_t &len)
{
len = 0;
for (int a = 4; a >= 0; a--)
{
char c;
c = i >> (a * 7) & 0x7f;
if (c == 0x00 && len == 0)
continue;
if (a == 0)
c &= 0x7f;
else
c |= 0x80;
buf[len] = c;
len++;
}
if (len == 0)
{
len++;
buf[0] = 0;
}
//cout << "compress:" << i << endl;
//cout << "compress len:" << len << endl;
return true;
}
複製代碼
解壓:
//將一個1~5個字節的值還原成四字節的整形值
bool uncompress_(char *buf, size_t len, unsigned int &i)
{
i = 0;
for (int index = 0; index < (int)len; index++)
{
char c = *(buf + index);
i = i << 7;
c &= 0x7f;
i |= c;
}
//cout << "uncompress:" << i << endl;
return true;
}
複製代碼
因爲咱們的即時通信同時涉及到Java和C++兩種編程語言,且有windows、linux、安卓三個平臺,而咱們爲了保障學習的質量和效果,因此咱們不用第三跨平臺庫(其實咱們也是在學習如何編寫這些跨平臺庫的原理),因此咱們須要學習如下如何在Java語言中去解析C++的網絡數據包或者反過來。安卓端發送的數據使用Java語言編寫,pc與服務器發送的數據使用C++編寫,這裏以在Java中解析C++網絡數據包爲例。 這對於不少人來講是一件很困難的事情,因此只能變着法子使用第三方的庫。其實只要你掌握了必定的基礎知識,利用一些現成的字節流抓包工具(如tcpdump、wireshark)很容易解決這個問題。咱們這裏使用tcpdump工具來嘗試分析和解決這個問題。 首先,咱們須要明確字節序列這樣一個概念,即咱們說的大端編碼(big endian)和小端編碼(little endian),x86和x64系列的cpu使用小端編碼,而數據在網絡上傳輸,以及Java語言中,使用的是大端編碼。那麼這是什麼意思呢? 咱們舉個例子,看一個x64機器上的32位數值在內存中的存儲方式:
i在內存中的地址序列是0x003CF7C4~0x003CF7C8,值爲40 e2 01 00。
十六進制0001e240正好等於10進制123456,也就是說小端編碼中權重高的的字節值存儲在內存地址高(地址值較大)的位置,權重值低的字節值存儲在內存地址低(地址值較小)的位置,也就是所謂的高高低低。 相反,大端編碼的規則應該是高低低高,也就是說權值高字節存儲在內存地址低的位置,權值低的字節存儲在內存地址高的位置。 因此,若是咱們一個C++程序的int32值123456不做轉換地傳給Java程序,那麼Java按照大端編碼的形式讀出來的值是:十六進制40E20100 = 十進制1088553216。 因此,咱們要麼在發送方將數據轉換成網絡字節序(大端編碼),要麼在接收端再進行轉換。
下面看一下若是C++端傳送一個以下數據結構,Java端該如何解析(因爲Java中是沒有指針的,也沒法操做內存地址,致使不少人無從下手),下面利用tcpdump來解決這個問題的思路。 咱們客戶端發送的數據包:
其結構體定義以下:
利用tcpdump抓到的包以下:
放大一點:咱們白色標識出來就是咱們收到的數據包。這裏我想說明兩點:
item 若是咱們知道發送端發送的字節流,再比照接收端收到的字節流,咱們就能檢測數據包的完整性,或者利用這個來排查一些問題;
item 對於Java程序只要按照這個順序,先利用java.net.Socket的輸出流java.io.DataOutputStream對象readByte、readInt3二、readInt3二、readBytes、readBytes方法依次讀出一個char、int3二、int3二、16個字節的字節數組、63個字節數組便可,爲了還原像int32這樣的整形值,咱們須要作一些小端編碼向大端編碼的轉換。
歡迎關注公衆號『easyserverdev』。若是有任何技術或者職業方面的問題可經過這個公衆號與我取得聯繫,一塊兒交流。