一、选择TCP还是UDP协议
由于我们的即时通讯软件的用户存在用户状态问题,即用户登录成功以后可以在他的好友列表中看到哪些好友在线,所以客户端和服务器需要保持长连接状态。另外即时通讯软件一般要求信息准确、有序、完整地到达对端,而这也是TCP协议的特点之一。综合这两个所以这里我们选择TCP协议,而不是UDP协议。
二、协议的结构
由于TCP协议是流式协议,所谓流式协议即通讯的内容是无边界的字节流:如A给B连续发送了三个数据包,每个包的大小都是100个字节,那么B可能会一次性收到300个字节;也可能先收到100个字节,再收到200个字节;也可能先收到100个字节,再收到50个字节,再收到150个字节;或者先收到50个字节,再收到50个字节,再收到50个字节,最后收到150个字节。也就是说,B可能以任何组合形式收到这300个字节。即像水流一样无明确的边界。为了能让对端知道如何给包分界,目前一般有三种做法:
-
以固定大小字节数目来分界,上文所说的就是属于这种类型,如每个包100个字节,对端每收齐100个字节,就当成一个包来解析;
-
以特定符号来分界,如每个包都以特定的字符来结尾(如\n),当在字节流中读取到该字符时,则表明上一个包到此为止。
-
固定包头+包体结构,这种结构中一般包头部分是一个固定字节长度的结构,并且包头中会有一个特定的字段指定包体的大小。这是目前各种网络应用用的最多的一种包格式。
上面三种分包方式各有优缺点,方法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这样的整形值,我们需要做一些小端编码向大端编码的转换。
推荐阅读
-
TCP 协议如何解决粘包、半包问题
-
罗永浩,是条汉子!
-
一口气说出“分布式追踪系统”原理!
-
实例:一个服务器程序的架构介绍
-
one thread one loop 思想
-
业务数据处理一定要单独开线程吗
-
网络通信中收发数据的正确姿势
-
日志系统的设计
-
C++ 高性能服务器网络框架设计细节
-
一个 WebSocket 服务器是如何开发出来的?
-
如何设计断线自动重连机制
-
心跳包机制设计详解
欢迎关注公众号『高性能服务器开发』,本公众号推崇基础学习与原理理解,不谈大而空的架构与技术术语,分享接地气的服务器开发实战技巧与项目经验,实实在在分享可用于实际编码的编程知识。如果对后端开发感兴趣,想加入 高性能服务器开发微信交流群 进行交流,可以先加我微信 easy_coder,备注”加微信群”,我拉你入群,备注不对不加哦。
如果觉得受益
请别吝啬你的在看
转载请注明:爱学习爱分享 » 服务器开发通信协议设计介绍