最近想用C++实现一个websocket服务器,到网上找了一下,其实已经有一些实现好的开源库(比如WebSocketPP),尝试了一下,代码实现可以说是十分简单了,基本不到100行代码就搭好了,自己只要实现三个回调函数(OnOpen,OnClose,OnMessage,)即定义接收到来自客户端的websocket连接,关闭,以及收到消息要干什么,然后绑定到各自的handle,基本就可以实现服务器的基本功能了。
这里给出一个开源库websocketpp的源码例子: https://download.csdn.net/download/weixin_34196559/10750141
但是对于开源库的使用,虽然是十分容易实现,但是前期配置那个依赖库还是十分的烦人的,比如WebSocketPP,是需要依赖于boost(也就是说你还得先配置好boost...TAT),所以想着自己利用原生的socket实现以下websocket协议,以后要嵌入就不会太麻烦配置一堆依赖了0.0。
废话不多说,开始一步步搭建自己的websocket服务器吧。
(本文默认你已经基本懂得如何使用原生的socket,如果不懂还是先补一下吧,这里提供一篇自我感觉写的很好的博客,附上链接socket编程)
1、参照socket编程创建一个服务器监听(监听连接)
2、开始接收数据(可以使用recv()函数,当然了此处也踩了不少坑,后面会详细提到),这里首先是用来接收来自客户端的协议握手(因为刚开始建立的只是基础TCP连接),收到的报文大致是这样的:
GET / HTTP/1.1
Connection:Upgrade
Host:127.0.0.1:8088
Origin:null
Sec-WebSocket-Extensions:x-webkit-deflate-frame
Sec-WebSocket-Key:puVOuWb7rel6z2AVZBKnfw==
Sec-WebSocket-Version:13
Upgrade:websocket
3、根据接收到的数据(就是上面的报文),可以先判断一下是不是握手协议(这里可以直接判断一下recv从缓冲区读到的数据是否包含“GET”,虽然这里我感觉不是特别严谨,但好像网上很多都是这么写的0.0),对这个报文进行解析,获取其中的Sec-WebSocket-Key(也就是这里的puVOuWb7rel6z2AVZBKnfw==)。然后服务端要对这个key(包含24个字符)进行解析,解析方法是:
首先加上一个"神奇"的字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"(这个是固定的),然后对其进行sha1解码跟base64编码,就可以得到一个“密码”,然后就把这个密码再次打包一下,然后发给客户端(因为客户端也是按照同样的方式处理他发出去的key的),如果服务器发给客户端的报文密码一致的话,此时就完成了协议握手,(emmmm。。。大概就是,暗号对上了,哈哈哈),然后现在就已经成功创建了一个基于webosket协议的连接了。
这里也给出一个服务端最后打包要发给客户端的报文示例:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Server:beetle websocket server
Upgrade:WebSocket
Date:Mon, 26 Nov 2012 23:42:44 GMT
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:content-type
Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q=
4、建立连接后,就可以愉快的发送数据跟接收数据了,但是!!!虽然接收数据采用的还是socket TCP的方式,但是现在客户端发送给服务器的数据就不是普通的数据了,而是websocket格式的数据,协议的格式如下:
关于这个协议的详细介绍,这里也给出一篇推荐的博客,websocket协议解析
可以明显看出,这个协议头是占用了两个字节的(16位)我简单说一下吧。
(1)、FIN是表示这个数据是不是接收完毕(如果缓冲区长度小于客户端发送的数据长度,那显然要多次接收才能得到完整的数据啦,FIN为1表示接收到的数据是完整的),
(2)、然后opcode,也就是第4-7位,表示的是数据类型,比如客户端发送的如果是一个要断开websocket连接的请求,就可以根据这个opcode判断出来,
(3)、第8位MASK是表示后面的数据是否经过掩码处理,(规定从客户端发送给服务器的数据都是需要通过掩码处理的,所以你直接用recv收到的数据肯定是乱码的啦!!!),而从服务端发给客户端的数据是不需要使用掩码加密的,但是是肯定需要加上协议头的啦(这个地方也有不少坑!!!当时也找了半天,后面再统一说一下)
(4)、后面的显然就是表示数据长度咯
关于客户端的测试工具,也没必要自己去写一个客户端啦,小白表示自己并不是特别会前端,而且要写的话还得去看一下js的websocket,比较麻烦,这里有一个websocket的在线测试网站,可以直接在这边测试就好啦
websocket测试: http://www.blue-zero.com/WebSocket/
自此,就可以愉快玩耍啦!
还是按照编程过程讲起吧!!!
1、首先是关于recv函数的,recv函数是非阻塞的,数据的发送和接收是独立的,并不是发送方执行一次send,接收方就执行以此recv。recv函数不管发送几次,都会从输入缓冲区尽可能多的获取数据。如果发送方发送了多次信息,接收方没来得及进行recv,则数据堆积在输入缓冲区中,取数据的时候会都取出来。这里借用一张图解:
还有,recv函数返回的是一个接收到的数据(从缓冲区读出)的长度,这个数据时有可能包含有0x00(‘\0’),也就是终结符,绝对不能直接用直接把从缓冲区读出的数据直接转成string类型(比如string str = buffer),这种写法是会将数据从0x00处截断的,也不能使用strlen去获取数据长度,这个strlen函数的实现机制也是会从0x00处截断,也就是说获取到的数据长度也是错的!!!当然,一般来说在协议握手那一块接收到的数据一般是没有什么问题的(不会包含0x00),以上这个坑主要是在建立连接之后,有包含协议头的数据通讯中的!(因为数据经过了掩码处理,是有可能包含0x00的!!!)
所以如果这个地方出错的话,假设你将你要发送给客户端的数据在加上协议头的时候出错(也就是错误的协议头)的时候,利用send发送过去的话,是会导致直接断开连接的(因为websocket协议一旦你数据传输出现问题,便会断开连接(TCP连接还在))
好了,问题找出来了,那肯定就能找到解决方法啦!这里给出一种解决办法:利用memcpy函数,这个函数不同于strcpy的复制(差点忘了说,strcpy也是不行的,会截断!),用memcpy的一个内存复制机制就能解决问题了!!!
2、如果你用的是VS2017的话,可能还会出现一个问题,那就是控制台显示出来的数据是会中文乱码的!!这个我刚开始就遇到了(测试连接的时候就遇到了),但是 没去理他,TAT因为我转发回客户端显示并没有问题,但是终归不好看,看着不爽啊!后面查了查,其实只需要在主函数加上一个定义就好了:
system("chcp 65001");
最后源码我就不贴啦,这里就给出sha1跟base64吧,这两个我刚开始也是有点迷,找不到0.0
sha1.h
#pragma once
/*
* sha1.h
*
* Copyright (C) 1998, 2009
* Paul E. Jones
* All Rights Reserved.
*
*****************************************************************************
* $Id: sha1.h 12 2009-06-22 19:34:25Z paulej $
*****************************************************************************
*
* Description:
* This class implements the Secure Hashing Standard as defined
* in FIPS PUB 180-1 published April 17, 1995.
*
* Many of the variable names in this class, especially the single
* character names, were used because those were the names used
* in the publication.
*
* Please read the file sha1.cpp for more information.
*
*/
#ifndef _SHA1_H_
#define _SHA1_H_
class SHA1
{
public:
SHA1();
virtual ~SHA1();
/*
* Re-initialize the class
*/
void Reset();
/*
* Returns the message digest
*/
bool Result(unsigned *message_digest_array);
/*
* Provide input to SHA1
*/
void Input(const unsigned char *message_array,
unsigned length);
void Input(const char *message_array,
unsigned length);
void Input(unsigned char message_element);
void Input(char message_element);
SHA1& operator<<(const char *message_array);
SHA1& operator<<(const unsigned char *message_array);
SHA1& operator<<(const char message_element);
SHA1& operator<<(const unsigned char message_element);
private:
/*
* Process the next 512 bits of the message
*/
void ProcessMessageBlock();
/*
* Pads the current message block to 512 bits
*/
void PadMessage();
/*
* Performs a circular left shift operation
*/
inline unsigned CircularShift(int bits, unsigned word);
unsigned H[5]; // Message digest buffers
unsigned Length_Low; // Message length in bits
unsigned Length_High; // Message length in bits
unsigned char Message_Block[64]; // 512-bit message blocks
int Message_Block_Index; // Index into message block array
bool Computed; // Is the digest computed?
bool Corrupted; // Is the message digest corruped?
};
#endif
sha1.cpp
/*
* sha1.cpp
*
* Copyright (C) 1998, 2009
* Paul E. Jones
* All Rights Reserved.
*
*****************************************************************************
* $Id: sha1.cpp 12 2009-06-22 19:34:25Z paulej $
*****************************************************************************
*
* Description:
* This class implements the Secure Hashing Standard as defined
* in FIPS PUB 180-1 published April 17, 1995.
*
* The Secure Hashing Standard, which uses the Secure Hashing
* Algorithm (SHA), produces a 160-bit message digest for a
* given data stream. In theory, it is highly improbable that
* two messages will produce the same message digest. Therefore,
* this algorithm can serve as a means of providing a "fingerprint"
* for a message.
*
* Portability Issues:
* SHA-1 is defined in terms of 32-bit "words". This code was
* written with the expectation that the processor has at least
* a 32-bit machine word size. If the machine word size is larger,
* the code should still function properly. One caveat to that
* is that the input functions taking characters and character arrays
* assume that only 8 bits of information are stored in each character.
*
* Caveats:
* SHA-1 is designed to work with messages less than 2^64 bits long.
* Although SHA-1 allows a message digest to be generated for
* messages of any number of bits less than 2^64, this implementation
* only works with messages with a length that is a multiple of 8
* bits.
*
*/
#include "sha1.h"
/*
* SHA1
*
* Description:
* This is the constructor for the sha1 class.
*
* Parameters:
* None.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
SHA1::SHA1()
{
Reset();
}
/*
* ~SHA1
*
* Description:
* This is the destructor for the sha1 class
*
* Parameters:
* None.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
SHA1::~SHA1()
{
// The destructor does nothing
}
/*
* Reset
*
* Description:
* This function will initialize the sha1 class member variables
* in preparation for computing a new message digest.
*
* Parameters:
* None.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
void SHA1::Reset()
{
Length_Low = 0;
Length_High = 0;
Message_Block_Index = 0;
H[0] = 0x67452301;
H[1] = 0xEFCDAB89;
H[2] = 0x98BADCFE;
H[3] = 0x10325476;
H[4] = 0xC3D2E1F0;
Computed = false;
Corrupted = false;
}
/*
* Result
*
* Description:
* This function will return the 160-bit message digest into the
* array provided.
*
* Parameters:
* message_digest_array: [out]
* This is an array of five unsigned integers which will be filled
* with the message digest that has been computed.
*
* Returns:
* True if successful, false if it failed.
*
* Comments:
*
*/
bool SHA1::Result(unsigned *message_digest_array)
{
int i; // Counter
if (Corrupted)
{
return false;
}
if (!Computed)
{
PadMessage();
Computed = true;
}
for (i = 0; i < 5; i++)
{
message_digest_array[i] = H[i];
}
return true;
}
/*
* Input
*
* Description:
* This function accepts an array of octets as the next portion of
* the message.
*
* Parameters:
* message_array: [in]
* An array of characters representing the next portion of the
* message.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
void SHA1::Input(const unsigned char *message_array,
unsigned length)
{
if (!length)
{
return;
}
if (Computed || Corrupted)
{
Corrupted = true;
return;
}
while (length-- && !Corrupted)
{
Message_Block[Message_Block_Index++] = (*message_array & 0xFF);
Length_Low += 8;
Length_Low &= 0xFFFFFFFF; // Force it to 32 bits
if (Length_Low == 0)
{
Length_High++;
Length_High &= 0xFFFFFFFF; // Force it to 32 bits
if (Length_High == 0)
{
Corrupted = true; // Message is too long
}
}
if (Message_Block_Index == 64)
{
ProcessMessageBlock();
}
message_array++;
}
}
/*
* Input
*
* Description:
* This function accepts an array of octets as the next portion of
* the message.
*
* Parameters:
* message_array: [in]
* An array of characters representing the next portion of the
* message.
* length: [in]
* The length of the message_array
*
* Returns:
* Nothing.
*
* Comments:
*
*/
void SHA1::Input(const char *message_array,
unsigned length)
{
Input((unsigned char *)message_array, length);
}
/*
* Input
*
* Description:
* This function accepts a single octets as the next message element.
*
* Parameters:
* message_element: [in]
* The next octet in the message.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
void SHA1::Input(unsigned char message_element)
{
Input(&message_element, 1);
}
/*
* Input
*
* Description:
* This function accepts a single octet as the next message element.
*
* Parameters:
* message_element: [in]
* The next octet in the message.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
void SHA1::Input(char message_element)
{
Input((unsigned char *)&message_element, 1);
}
/*
* operator<<
*
* Description:
* This operator makes it convenient to provide character strings to
* the SHA1 object for processing.
*
* Parameters:
* message_array: [in]
* The character array to take as input.
*
* Returns:
* A reference to the SHA1 object.
*
* Comments:
* Each character is assumed to hold 8 bits of information.
*
*/
SHA1& SHA1::operator<<(const char *message_array)
{
const char *p = message_array;
while (*p)
{
Input(*p);
p++;
}
return *this;
}
/*
* operator<<
*
* Description:
* This operator makes it convenient to provide character strings to
* the SHA1 object for processing.
*
* Parameters:
* message_array: [in]
* The character array to take as input.
*
* Returns:
* A reference to the SHA1 object.
*
* Comments:
* Each character is assumed to hold 8 bits of information.
*
*/
SHA1& SHA1::operator<<(const unsigned char *message_array)
{
const unsigned char *p = message_array;
while (*p)
{
Input(*p);
p++;
}
return *this;
}
/*
* operator<<
*
* Description:
* This function provides the next octet in the message.
*
* Parameters:
* message_element: [in]
* The next octet in the message
*
* Returns:
* A reference to the SHA1 object.
*
* Comments:
* The character is assumed to hold 8 bits of information.
*
*/
SHA1& SHA1::operator<<(const char message_element)
{
Input((unsigned char *)&message_element, 1);
return *this;
}
/*
* operator<<
*
* Description:
* This function provides the next octet in the message.
*
* Parameters:
* message_element: [in]
* The next octet in the message
*
* Returns:
* A reference to the SHA1 object.
*
* Comments:
* The character is assumed to hold 8 bits of information.
*
*/
SHA1& SHA1::operator<<(const unsigned char message_element)
{
Input(&message_element, 1);
return *this;
}
/*
* ProcessMessageBlock
*
* Description:
* This function will process the next 512 bits of the message
* stored in the Message_Block array.
*
* Parameters:
* None.
*
* Returns:
* Nothing.
*
* Comments:
* Many of the variable names in this function, especially the single
* character names, were used because those were the names used
* in the publication.
*
*/
void SHA1::ProcessMessageBlock()
{
const unsigned K[] = { // Constants defined for SHA-1
0x5A827999,
0x6ED9EBA1,
0x8F1BBCDC,
0xCA62C1D6
};
int t; // Loop counter
unsigned temp; // Temporary word value
unsigned W[80]; // Word sequence
unsigned A, B, C, D, E; // Word buffers
/*
* Initialize the first 16 words in the array W
*/
for (t = 0; t < 16; t++)
{
W[t] = ((unsigned)Message_Block[t * 4]) << 24;
W[t] |= ((unsigned)Message_Block[t * 4 + 1]) << 16;
W[t] |= ((unsigned)Message_Block[t * 4 + 2]) << 8;
W[t] |= ((unsigned)Message_Block[t * 4 + 3]);
}
for (t = 16; t < 80; t++)
{
W[t] = CircularShift(1, W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]);
}
A = H[0];
B = H[1];
C = H[2];
D = H[3];
E = H[4];
for (t = 0; t < 20; t++)
{
temp = CircularShift(5, A) + ((B & C) | ((~B) & D)) + E + W[t] + K[0];
temp &= 0xFFFFFFFF;
E = D;
D = C;
C = CircularShift(30, B);
B = A;
A = temp;
}
for (t = 20; t < 40; t++)
{
temp = CircularShift(5, A) + (B ^ C ^ D) + E + W[t] + K[1];
temp &= 0xFFFFFFFF;
E = D;
D = C;
C = CircularShift(30, B);
B = A;
A = temp;
}
for (t = 40; t < 60; t++)
{
temp = CircularShift(5, A) +
((B & C) | (B & D) | (C & D)) + E + W[t] + K[2];
temp &= 0xFFFFFFFF;
E = D;
D = C;
C = CircularShift(30, B);
B = A;
A = temp;
}
for (t = 60; t < 80; t++)
{
temp = CircularShift(5, A) + (B ^ C ^ D) + E + W[t] + K[3];
temp &= 0xFFFFFFFF;
E = D;
D = C;
C = CircularShift(30, B);
B = A;
A = temp;
}
H[0] = (H[0] + A) & 0xFFFFFFFF;
H[1] = (H[1] + B) & 0xFFFFFFFF;
H[2] = (H[2] + C) & 0xFFFFFFFF;
H[3] = (H[3] + D) & 0xFFFFFFFF;
H[4] = (H[4] + E) & 0xFFFFFFFF;
Message_Block_Index = 0;
}
/*
* PadMessage
*
* Description:
* According to the standard, the message must be padded to an even
* 512 bits. The first padding bit must be a '1'. The last 64 bits
* represent the length of the original message. All bits in between
* should be 0. This function will pad the message according to those
* rules by filling the message_block array accordingly. It will also
* call ProcessMessageBlock() appropriately. When it returns, it
* can be assumed that the message digest has been computed.
*
* Parameters:
* None.
*
* Returns:
* Nothing.
*
* Comments:
*
*/
void SHA1::PadMessage()
{
/*
* Check to see if the current message block is too small to hold
* the initial padding bits and length. If so, we will pad the
* block, process it, and then continue padding into a second block.
*/
if (Message_Block_Index > 55)
{
Message_Block[Message_Block_Index++] = 0x80;
while (Message_Block_Index < 64)
{
Message_Block[Message_Block_Index++] = 0;
}
ProcessMessageBlock();
while (Message_Block_Index < 56)
{
Message_Block[Message_Block_Index++] = 0;
}
}
else
{
Message_Block[Message_Block_Index++] = 0x80;
while (Message_Block_Index < 56)
{
Message_Block[Message_Block_Index++] = 0;
}
}
/*
* Store the message length as the last 8 octets
*/
Message_Block[56] = (Length_High >> 24) & 0xFF;
Message_Block[57] = (Length_High >> 16) & 0xFF;
Message_Block[58] = (Length_High >> 8) & 0xFF;
Message_Block[59] = (Length_High) & 0xFF;
Message_Block[60] = (Length_Low >> 24) & 0xFF;
Message_Block[61] = (Length_Low >> 16) & 0xFF;
Message_Block[62] = (Length_Low >> 8) & 0xFF;
Message_Block[63] = (Length_Low) & 0xFF;
ProcessMessageBlock();
}
/*
* CircularShift
*
* Description:
* This member function will perform a circular shifting operation.
*
* Parameters:
* bits: [in]
* The number of bits to shift (1-31)
* word: [in]
* The value to shift (assumes a 32-bit integer)
*
* Returns:
* The shifted value.
*
* Comments:
*
*/
unsigned SHA1::CircularShift(int bits, unsigned word)
{
return ((word << bits) & 0xFFFFFFFF) | ((word & 0xFFFFFFFF) >> (32 - bits));
}
base64.h
#pragma once
#ifndef BASE64_H
#define BASE64_H
#include
#include "sha1.h"
static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
const std::string MAGICSTRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
class base64
{
public:
base64();
~base64();
std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len);
private:
};
#endif //BASE64_H
base64.cpp
#include "base64.h"
#include
base64::base64()
{
}
base64::~base64()
{
}
std::string base64::base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) {
std::string ret;
int i = 0;
int j = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i)
{
for (j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; (j < i + 1); j++)
ret += base64_chars[char_array_4[j]];
while ((i++ < 3))
ret += '=';
}
return ret;
}
以上都是自己的总结,有什么写的不对的地方欢迎指出!
因为有很多人找我要源码,我已经把源码上传的我的资源里了
源码下载地址:C++简单实现一个websocket服务器