欢迎来到爱学习爱分享,在这里,你会找到许多有趣的技术 : )

服务器开发通信协议设计介绍

开发者头条 484℃

一、选择TCP还是UDP协议

由于我们的即时通讯软件的用户存在用户状态问题,即用户登录成功以后可以在他的好友列表中看到哪些好友在线,所以客户端和服务器需要保持长连接状态。另外即时通讯软件一般要求信息准确、有序、完整地到达对端,而这也是TCP协议的特点之一。综合这两个所以这里我们选择TCP协议,而不是UDP协议。

二、协议的结构

由于TCP协议是流式协议,所谓流式协议即通讯的内容是无边界的字节流:如A给B连续发送了三个数据包,每个包的大小都是100个字节,那么B可能会一次性收到300个字节;也可能先收到100个字节,再收到200个字节;也可能先收到100个字节,再收到50个字节,再收到150个字节;或者先收到50个字节,再收到50个字节,再收到50个字节,最后收到150个字节。也就是说,B可能以任何组合形式收到这300个字节。即像水流一样无明确的边界。为了能让对端知道如何给包分界,目前一般有三种做法:

  1. 以固定大小字节数目来分界,上文所说的就是属于这种类型,如每个包100个字节,对端每收齐100个字节,就当成一个包来解析;

  2. 以特定符号来分界,如每个包都以特定的字符来结尾(如\n),当在字节流中读取到该字符时,则表明上一个包到此为止。

  3. 固定包头+包体结构,这种结构中一般包头部分是一个固定字节长度的结构,并且包头中会有一个特定的字段指定包体的大小。这是目前各种网络应用用的最多的一种包格式。

上面三种分包方式各有优缺点,方法1和方法2简单易操作,但是缺点也很明显,就是很不灵活,如方法一当包数据不足指定长度,只能使用占位符如0来凑,比较浪费;方法2中包中不能有包界定符,否则就会引起歧义,也就是要求包内容中不能有某些特殊符号。而方法3虽然解决了方法1和方法2的缺点,但是操作起来就比较麻烦。我们的即时通讯协议就采用第三种分包方式。所以我们的协议包的包头看起来像这样:

1struct package_header
2{
3    int32_t bodysize;
4};

一个应用中,有许多的应用数据,拿我们这里的即时通讯来说,有注册、登录、获取好友列表、好友消息等各种各样的协议数据包,而每个包因为业务内容不一样可能数据内容也不一样,所以各个包可能看起来像下面这样:

 1struct package_header
 2{
 3    int32_t bodysize;
 4};
 5
 6//登录数据包
 7struct register_package
 8{
 9    package_header header;
10    //命令号
11    int32_t cmd;
12    //注册用户名
13    char username[16];
14    //注册密码
15    char password[16];
16    //注册昵称
17    char nickname[16];
18    //注册手机号
19    char mobileno[16];
20};
21
22//登录数据包
23struct login_package
24{
25    package_header header;
26    //命令号
27    int32_t cmd;
28    //登录用户名
29    char username[16];
30    //密码
31    char password[16];
32    //客户端类型
33    int32_t clienttype;
34    //上线类型,如在线、隐身、忙碌、离开等
35    int32_t onlinetype;
36};
37
38//获取好友列表
39struct getfriend_package
40{
41    package_header header;
42    //命令号
43    int32_t cmd;
44};
45
46//聊天内容
47struct chat_package
48{
49    package_header header;
50    //命令号
51    int32_t cmd;
52    //发送人userid
53    int32_t senderid;
54    //接收人userid
55    int32_t targetid;
56    //消息内容
57    char chatcontent[8192];
58};

看到没有?由于每一个业务的内容不一样,定义的结构体也不一样。如果业务比较多的话,我们需要定义各种各样的这种结构体,这简直是一场噩梦。那么有没有什么方法可以避免这个问题呢?有,我受jdk中的流对象的WriteInt32、WriteByte、WriteInt64、WriteString,这样的接口的启发,也发明了一套这样的协议,而且这套协议基本上是通用协议,可用于任何场景。我们的包还是分为包头和包体两部分,包头和上文所说的一样,包体是一个不固定大小的二进制流,其长度由包头中的指定包体长度的字段决定。

1struct package_protocol
2{
3    int32_t bodysize;
4    //注意:C/C++语法不能这么定义结构体,
5    //这里只是为了说明含义的伪代码
6    //bodycontent即为一个不固定大小的二进制流
7    char    binarystream[bodysize];
8};

接下来的核心部分就是如何操作这个二进制流,我们将流分为二进制读和二进制写两种流,下面给出接口定义:

 1    //写
 2    class BinaryWriteStream
 3    {
 4    public:
 5        BinaryWriteStream(string* data);
 6        const char* GetData() const;
 7        size_t GetSize() const;
 8        bool WriteCString(const char* str, size_t len);
 9        bool WriteString(const string& str);
10        bool WriteDouble(double value, bool isNULL = false);
11        bool WriteInt64(int64_t value, bool isNULL = false);
12        bool WriteInt32(int32_t i, bool isNULL = false);
13        bool WriteShort(short i, bool isNULL = false);
14        bool WriteChar(char c, bool isNULL = false);
15        size_t GetCurrentPos() const{ return m_data->length(); }
16        void Flush();
17        void Clear();
18    private:
19        string* m_data;
20    };
 1    //读
 2    class BinaryReadStream : public IReadStream
 3    {
 4    private:
 5        const char* const ptr;
 6        const size_t      len;
 7        const char*       cur;
 8        BinaryReadStream(const BinaryReadStream&);
 9        BinaryReadStream& operator=(const BinaryReadStream&);
10    public:
11        BinaryReadStream(const char* ptr, size_t len);
12        const char* GetData() const;
13        size_t GetSize() const;
14        bool IsEmpty() const;
15        bool ReadString(string* str, size_t maxlen, size_t& outlen);
16        bool ReadCString(char* str, size_t strlen, size_t& len);
17        bool ReadCCString(const char** str, size_t maxlen, size_t& outlen);
18        bool ReadInt32(int32_t& i);
19        bool ReadInt64(int64_t& i);
20        bool ReadShort(short& i);
21        bool ReadChar(char& c);
22        size_t ReadAll(char* szBuffer, size_t iLen) const;
23        bool IsEnd() const;
24        const char* GetCurrent() const{ return cur; }
25    public:
26        bool ReadLength(size_t & len);
27        bool ReadLengthWithoutOffset(size_t &headlen, size_t & outlen);
28    };

这样如果是上文的一个登录数据包,我们只要写成如下形式就可以了:

1std::string outbuf;
2BinaryWriteStream stream(&outbuf);
3stream.WriteInt32(cmd);
4stream.WriteCString(username, 16);
5stream.WriteCString(password, 16);
6stream.WriteInt32(clienttype);
7stream.WriteInt32(onlinetype);
8//最终数据就存储到outbuf中去了
9stream.Flush();

接着我们再对端,解得正确的包体后,我们只要按写入的顺序依次读出来即可:

 1BinaryWriteStream stream(outbuf.c_str(), outbuf.length());
 2int32_t cmd;
 3stream.WriteInt32(cmd);
 4char username[16];
 5stream.ReadCString(username, 16, NULL);
 6char password[16];
 7stream.WriteCString(password, 16, NULL);
 8int32_t clienttype;
 9stream.WriteInt32(clienttype);
10int32_t onlinetype;
11stream.WriteInt32(onlinetype);

这里给出BinaryReadStream和BinaryWriteStream的完整实现:

  1    //计算校验和
  2    unsigned short checksum(const unsigned short *buffer, int size)
  3    {
  4        unsigned int cksum = 0;
  5        while (size > 1)
  6        {
  7            cksum += *buffer++;
  8            size -= sizeof(unsigned short);
  9        }
 10        if (size)
 11        {
 12            cksum += *(unsigned char*)buffer;
 13        }
 14        //将32位数转换成16
 15        while (cksum >> 16)
 16            cksum = (cksum >> 16) + (cksum & 0xffff);
 17        return (unsigned short)(~cksum);
 18    }
 19
 20    bool compress_(unsigned int i, char *buf, size_t &len)
 21    {
 22        len = 0;
 23        for (int a = 4; a >= 0; a--)
 24        {
 25            char c;
 26            c = i >> (a * 7) & 0x7f;
 27            if (c == 0x00 && len == 0)
 28                continue;
 29            if (a == 0)
 30                c &= 0x7f;
 31            else
 32                c |= 0x80;
 33            buf[len] = c;
 34            len++;
 35        }
 36        if (len == 0)
 37        {
 38            len++;
 39            buf[0] = 0;
 40        }
 41        //cout << "compress:" << i << endl;
 42        //cout << "compress len:" << len << endl;
 43        return true;
 44    }
 45
 46    bool uncompress_(char *buf, size_t len, unsigned int &i)
 47    {
 48        i = 0;
 49        for (int index = 0; index < (int)len; index++)
 50        {
 51            char c = *(buf + index);
 52            i = i << 7;
 53            c &= 0x7f;
 54            i |= c;
 55        }
 56        //cout << "uncompress:" << i << endl;
 57        return true;
 58    }
 59
 60    BinaryReadStream::BinaryReadStream(const char* ptr_, size_t len_)
 61        : ptr(ptr_), len(len_), cur(ptr_)
 62    {
 63        cur += BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN;
 64    }
 65
 66    bool BinaryReadStream::IsEmpty() const
 67    {
 68        return len <= BINARY_PACKLEN_LEN_2;
 69    }
 70
 71    size_t BinaryReadStream::GetSize() const
 72    {
 73        return len;
 74    }
 75
 76    bool BinaryReadStream::ReadCString(char* str, size_t strlen, /* out */ size_t& outlen)
 77    {
 78        size_t fieldlen;
 79        size_t headlen;
 80        if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
 81            return false;
 82        }
 83        // user buffer is not enough
 84        if (fieldlen > strlen) {
 85            return false;
 86        }
 87        // 偏移到数据的位置
 88        //cur += BINARY_PACKLEN_LEN_2;    
 89        cur += headlen;
 90        if (cur + fieldlen > ptr + len)
 91        {
 92            outlen = 0;
 93            return false;
 94        }
 95        memcpy(str, cur, fieldlen);
 96        outlen = fieldlen;
 97        cur += outlen;
 98        return true;
 99    }
100
101    bool BinaryReadStream::ReadString(string* str, size_t maxlen, size_t& outlen)
102    {
103        size_t headlen;
104        size_t fieldlen;
105        if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
106            return false;
107        }
108        // user buffer is not enough
109        if (maxlen != 0 && fieldlen > maxlen) {
110            return false;
111        }
112        // 偏移到数据的位置
113        //cur += BINARY_PACKLEN_LEN_2;    
114        cur += headlen;
115        if (cur + fieldlen > ptr + len)
116        {
117            outlen = 0;
118            return false;
119        }
120        str->assign(cur, fieldlen);
121        outlen = fieldlen;
122        cur += outlen;
123        return true;
124    }
125
126    bool BinaryReadStream::ReadCCString(const char** str, size_t maxlen, size_t& outlen)
127    {
128        size_t headlen;
129        size_t fieldlen;
130        if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
131            return false;
132        }
133        // user buffer is not enough
134        if (maxlen != 0 && fieldlen > maxlen) {
135            return false;
136        }
137        // 偏移到数据的位置
138        //cur += BINARY_PACKLEN_LEN_2;    
139        cur += headlen;
140        //memcpy(str, cur, fieldlen);
141        if (cur + fieldlen > ptr + len)
142        {
143            outlen = 0;
144            return false;
145        }
146        *str = cur;
147        outlen = fieldlen;
148        cur += outlen;
149        return true;
150    }
151
152    bool BinaryReadStream::ReadInt32(int32_t& i)
153    {
154        const int VALUE_SIZE = sizeof(int32_t);
155        if (cur + VALUE_SIZE > ptr + len)
156            return false;
157        memcpy(&i, cur, VALUE_SIZE);
158        i = ntohl(i);
159        cur += VALUE_SIZE;
160        return true;
161    }
162
163    bool BinaryReadStream::ReadInt64(int64_t& i)
164    {
165        char int64str[128];
166        size_t length;
167        if (!ReadCString(int64str, 128, length))
168            return false;
169        i = atoll(int64str);
170        return true;
171    }
172
173    bool BinaryReadStream::ReadShort(short& i)
174    {
175        const int VALUE_SIZE = sizeof(short);
176        if (cur + VALUE_SIZE > ptr + len) {
177            return false;
178        }
179        memcpy(&i, cur, VALUE_SIZE);
180        i = ntohs(i);
181        cur += VALUE_SIZE;
182        return true;
183    }
184
185    bool BinaryReadStream::ReadChar(char& c)
186    {
187        const int VALUE_SIZE = sizeof(char);
188        if (cur + VALUE_SIZE > ptr + len) {
189            return false;
190        }
191        memcpy(&c, cur, VALUE_SIZE);
192        cur += VALUE_SIZE;
193        return true;
194    }
195
196    bool BinaryReadStream::ReadLength(size_t & outlen)
197    {
198        size_t headlen;
199        if (!ReadLengthWithoutOffset(headlen, outlen)) {
200            return false;
201        }
202        //cur += BINARY_PACKLEN_LEN_2;
203        cur += headlen;
204        return true;
205    }
206
207    bool BinaryReadStream::ReadLengthWithoutOffset(size_t& headlen, size_t & outlen)
208    {
209        headlen = 0;
210        const char *temp = cur;
211        char buf[5];
212        for (size_t i = 0; i<sizeof(buf); i++)
213        {
214            memcpy(buf + i, temp, sizeof(char));
215            temp++;
216            headlen++;
217            //if ((buf[i] >> 7 | 0x0) == 0x0)
218            if ((buf[i] & 0x80) == 0x00)
219                break;
220        }
221        if (cur + headlen > ptr + len)
222            return false;
223        unsigned int value;
224        uncompress_(buf, headlen, value);
225        outlen = value;
226        /*if ( cur + BINARY_PACKLEN_LEN_2 > ptr + len ) {
227        return false;
228        }
229        unsigned int tmp;
230        memcpy(&tmp, cur, sizeof(tmp));
231        outlen = ntohl(tmp);*/
232        return true;
233    }
234
235    bool BinaryReadStream::IsEnd() const
236    {
237        assert(cur <= ptr + len);
238        return cur == ptr + len;
239    }
240
241    const char* BinaryReadStream::GetData() const
242    {
243        return ptr;
244    }
245
246    size_t BinaryReadStream::ReadAll(char * szBuffer, size_t iLen) const
247    {
248        size_t iRealLen = min(iLen, len);
249        memcpy(szBuffer, ptr, iRealLen);
250        return iRealLen;
251    }
252
253    //=================class BinaryWriteStream implementation============//
254    BinaryWriteStream::BinaryWriteStream(string *data) :
255        m_data(data)
256    {
257        m_data->clear();
258        char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
259        m_data->append(str, sizeof(str));
260    }
261
262    bool BinaryWriteStream::WriteCString(const char* str, size_t len)
263    {
264        char buf[5];
265        size_t buflen;
266        compress_(len, buf, buflen);
267        m_data->append(buf, sizeof(char)*buflen);
268        m_data->append(str, len);
269        //unsigned int ulen = htonl(len);
270        //m_data->append((char*)&ulen,sizeof(ulen));
271        //m_data->append(str,len);
272        return true;
273    }
274
275    bool BinaryWriteStream::WriteString(const string& str)
276    {
277        return WriteCString(str.c_str(), str.length());
278    }
279
280    const char* BinaryWriteStream::GetData() const
281    {
282        return m_data->data();
283    }
284
285    size_t BinaryWriteStream::GetSize() const
286    {
287        return m_data->length();
288    }
289
290    bool BinaryWriteStream::WriteInt32(int32_t i, bool isNULL)
291    {
292        int32_t i2 = 999999999;
293        if (isNULL == false)
294            i2 = htonl(i);
295        m_data->append((char*)&i2, sizeof(i2));
296        return true;
297    }
298
299    bool BinaryWriteStream::WriteInt64(int64_t value, bool isNULL)
300    {
301        char int64str[128];
302        if (isNULL == false)
303        {
304        #ifndef _WIN32
305            sprintf(int64str, "%ld", value);
306        #else
307            sprintf(int64str, "%lld", value);
308        #endif
309            WriteCString(int64str, strlen(int64str));
310        }
311        else
312            WriteCString(int64str, 0);
313        return true;
314    }
315
316    bool BinaryWriteStream::WriteShort(short i, bool isNULL)
317    {
318        short i2 = 0;
319        if (isNULL == false)
320            i2 = htons(i);
321        m_data->append((char*)&i2, sizeof(i2));
322        return true;
323    }
324
325    bool BinaryWriteStream::WriteChar(char c, bool isNULL)
326    {
327        char c2 = 0;
328        if (isNULL == false)
329            c2 = c;
330        (*m_data) += c2;
331        return true;
332    }
333
334    bool BinaryWriteStream::WriteDouble(double value, bool isNULL)
335    {
336        char   doublestr[128];
337        if (isNULL == false)
338        {
339            sprintf(doublestr, "%f", value);
340            WriteCString(doublestr, strlen(doublestr));
341        }
342        else
343            WriteCString(doublestr, 0);
344        return true;
345    }
346
347    void BinaryWriteStream::Flush()
348    {
349        char *ptr = &(*m_data)[0];
350        unsigned int ulen = htonl(m_data->length());
351        memcpy(ptr, &ulen, sizeof(ulen));
352    }
353
354    void BinaryWriteStream::Clear()
355    {
356        m_data->clear();
357        char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
358        m_data->append(str, sizeof(str));
359    }

这里详细解释一下上面的实现原理,即如何把各种类型的字段写入这种所谓的流中,或者怎么从这种流中读出各种类型的数据。上文的字段在流中的格式如下图:

这里最简便的方式就是每个字段的长度域都是固定字节数目,如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    //将一个四字节的整形数值压缩成1~5个字节
 2    bool compress_(unsigned int i, char *buf, size_t &len)
 3    {
 4        len = 0;
 5        for (int a = 4; a >= 0; a--)
 6        {
 7            char c;
 8            c = i >> (a * 7) & 0x7f;
 9            if (c == 0x00 && len == 0)
10                continue;
11            if (a == 0)
12                c &= 0x7f;
13            else
14                c |= 0x80;
15            buf[len] = c;
16            len++;
17        }
18        if (len == 0)
19        {
20            len++;
21            buf[0] = 0;
22        }
23        //cout << "compress:" << i << endl;
24        //cout << "compress len:" << len << endl;
25        return true;
26    }

解压

 1    //将一个1~5个字节的值还原成四字节的整形值
 2    bool uncompress_(char *buf, size_t len, unsigned int &i)
 3    {
 4        i = 0;
 5        for (int index = 0; index < (int)len; index++)
 6        {
 7            char c = *(buf + index);
 8            i = i << 7;
 9            c &= 0x7f;
10            i |= c;
11        }
12        //cout << "uncompress:" << i << endl;
13        return true;
14    }

三、关于跨系统与跨语言之间的网络通信协议解析与识别问题

由于我们的即时通讯同时涉及到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抓到的包如下:

放大一点:

我们白色标识出来就是我们收到的数据包。这里我想说明两点:

  • 如果我们知道发送端发送的字节流,再比照接收端收到的字节流,我们就能检测数据包的完整性,或者利用这个来排查一些问题;

  • 对于Java程序只要按照这个顺序,先利用java.net.Socket的输出流java.io.DataOutputStream对象readByte、readInt32、readInt32、readBytes、readBytes方法依次读出一个char、int32、int32、16个字节的字节数组、63个字节数组即可,为了还原像int32这样的整形值,我们需要做一些小端编码向大端编码的转换。


推荐阅读

欢迎关注公众号『高性能服务器开发』,本公众号推崇基础学习与原理理解,不谈大而空的架构与技术术语,分享接地气的服务器开发实战技巧与项目经验,实实在在分享可用于实际编码的编程知识。如果对后端开发感兴趣,想加入 高性能服务器开发微信交流群 进行交流,可以先加我微信 easy_coder,备注”加微信群”,我拉你入群,备注不对不加哦。

如果觉得受益

请别吝啬你的在看

转载请注明:爱学习爱分享 » 服务器开发通信协议设计介绍

喜欢 (0)or分享 (0)