文章记录我学习C++实现websocket服务端与客户端
准备
工具准备
libwebsockets库下载
通过git下载libwebsockets库
git clone https://github.com/warmcat/libwebsockets.git
CMake工具下载
为了可以进行多平台编译,websockets项目采用CMake作为编译工具,所以如果机器上没有CMake需要去安装CMake
官网下载地址:https://cmake.org/download/
编译准备
CMake安装完成之后我们需要进入libwebsockets源码目录下的build目录,然后执行cmake …即开始源码的编译。
客户端代码实现
初始化websocket
int __stdcall LoadCfg(const char* cfg, int devId)
{
CString str;
USES_CONVERSION;
// 用于创建vhost或者context的参数
ctx_info.port = CONTEXT_PORT_NO_LISTEN;
ctx_info.iface = NULL;
ctx_info.protocols = protocols;
ctx_info.gid = -1;
ctx_info.uid = -1;
//ctx_info.pt_serv_buf_size = 4096 * 2;
//ssl支持(指定CA证书、客户端证书及私钥路径,打开ssl支持)
ctx_info.client_ssl_ca_filepath = Ca_path;//如果服务器有CA证书则需要这行代码
//#endif
ctx_info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
// 创建一个WebSocket处理器
char *address = Ip_addr;//地址
int port = atoi(Sev_Port);//端口
char addr_port[256] = { 0 };
sprintf(addr_port, "%s:%u", address, port & 65535);
// 客户端连接参数
context = lws_create_context(&ctx_info);
conn_info.context = context;
conn_info.address = address;
conn_info.port = port;
conn_info.ssl_connection = LCCSCF_USE_SSL | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK | LCCSCF_ALLOW_EXPIRED;//ca证书连接属性,如果没CA证书则屏蔽
conn_info.path = "./";
conn_info.host = addr_port;
conn_info.origin = addr_port;
conn_info.protocol = protocols[0].name;
// 下面的调用触发LWS_CALLBACK_PROTOCOL_INIT事件
// 创建一个客户端连接
Client_wsi = lws_client_connect_via_info(&conn_info);
DWORD threadId;
exit_sig = 0;
DWORD dwExitCode = 0;
GetExitCodeThread(hStart, &dwExitCode);
if (dwExitCode == STILL_ACTIVE) {
LOG(INFO) << "线程存在";
return 0;
}
else {
hStart = CreateThread(NULL, 0, ConnectFunc, 0, 0, &threadId); // 创建线程
}
return 0;
}
DWORD WINAPI ConnectFunc(LPVOID p)
{
int n = 0;
was_closed = 0;
while (n >= 0 && !was_closed /*&& !exit_sig*/) {
// 执行一次事件循环(Poll),最长等待1000毫秒
n = lws_service(context, 0);
if (Client_wsi)
continue;
Client_wsi = lws_client_connect_via_info(&conn_info);
if (Client_wsi == NULL) {
break;
}
}
// 销毁上下文对象
return 0;
}
在这我使用了线程来实现长连接
客户端回调函数
/**
* 某个协议下的连接发生事件时,执行的回调函数
* wsi:指向WebSocket实例的指针
* reason:导致回调的事件
* user 库为每个WebSocket会话分配的内存空间
* in 某些事件使用此参数,作为传入数据的指针
* len 某些事件使用此参数,说明传入数据的长度
*/
int callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) {
HANDLE hThread;
DWORD threadId;
struct session_data* data = (struct session_data*) user;
int rlen = 0;
switch (reason) {
case LWS_CALLBACK_CLIENT_CLOSED:
lwsl_notice("Closed to server ok!\n");
was_closed = 1;
break;
case LWS_CALLBACK_CLIENT_ESTABLISHED: // 连接到服务器后的回调
lwsl_notice("Connected to server ok!\n");
//lws_callback_on_writable(wsi);
ConnectServer = 2;
break;
case LWS_CALLBACK_CLIENT_RECEIVE: //接收到服务器数据后的回调,数据为in,其长度为len
lwsl_notice("Rx: %s\n", (char*)in);
char c_res[10256];
memcpy(c_res, in, len);
memcpy(ResBuf, U2G((char*)c_res, len), len);
ResLen = len;
break;
case LWS_CALLBACK_CLIENT_WRITEABLE:
// CallBackSendMsg(wsi);
b_IsWriteAble = TRUE;
break;
}
return 0;
}
/**
* 支持的WebSocket子协议数组
* 子协议即JavaScript客户端WebSocket(url, protocols)第2参数数组的元素
* 你需要为每种协议提供回调函数
*/
struct lws_protocols protocols[] = {
{
//协议名称,协议回调,接收缓冲区大小
"ws", callback, sizeof(struct session_data), MAX_PAYLOAD_SIZE,
},
{
NULL, NULL, 0 // 最后一个元素固定为此格式
}
};
在这里我会在回调函数里面有标记是否连接,因为用的线程
报文发送与接收
Json::Value CmdFuntion(Json::Value js_CmdSendBuf,int SendLen){
js_CallBackSendMsg = js_CmdSendBuf;
LOG(INFO) << "发送长度:" << SendLen;
b_IsWriteAble = FALSE;
if (Client_wsi != NULL){
lws_callback_on_writable(Client_wsi);
}
Json::Value ReturnBuf;
int i,j =0;
for (i = atoi(Wait_Time);i > 0;i--) {
if (b_IsWriteAble && !j){
CallBackSendMsg(Client_wsi);
j = 1;
}
if (ResLen) {
ResLen = 0;
Json::Reader reader;
ReturnBuf.empty();
LOG(INFO) << "接收报文:" << ResBuf;
string req_str((char*)ResBuf);
memset(ResBuf, 0x00, sizeof(ResBuf));
if (!reader.parse(req_str, ReturnBuf)) {
ReturnBuf["error"] = "-1";
}
break;
}
Sleep(100);
}
if (i == 0)
{
LOG(INFO) << "接收超时:";
ReturnBuf["error"] = "-1";
}
js_CallBackSendMsg.empty();
return ReturnBuf;
}
下面是发送实现
static void CallBackSendMsg(struct lws* Client_wsi)
{
USES_CONVERSION;
js_CallBackSendMsg["id"] = "101";
string a,b;
a = G2U(js_CallBackSendMsg.toStyledString().c_str(), js_CallBackSendMsg.toStyledString().length());
///
if (js_CallBackSendMsg["method"] == "printCard")
{
Json::Value js_FirstMsg;
CString length;
//int alen= a.length();
int alen = js_CallBackSendMsg.toStyledString().length();
js_FirstMsg["len"] = alen;
js_FirstMsg["id"] = "101";
b = G2U(js_FirstMsg.toStyledString().c_str(), js_FirstMsg.toStyledString().length());
memset(SendBuf_b, 0x00, sizeof(SendBuf_b));
memcpy(SendBuf_b, b.c_str(), b.length());
LOG(INFO) << "发送报文:" << SendBuf_b;/*
int befor = js_CallBackSendMsg.toStyledString().length();
LOG(INFO) << "发送报文之前长度:" << befor << ",之后长度" << b.length();*/
lws_write(Client_wsi, SendBuf_b, b.length(), LWS_WRITE_TEXT);
Sleep(200);
}
//
memset(SendBuf, 0x00, sizeof(SendBuf));
memcpy(SendBuf, a.c_str(), a.length());
//CString qa(SendBuf);
LOG(INFO) << "发送报文:" << SendBuf << ",length:" << a.length();
lws_write(Client_wsi, SendBuf, a.length(), LWS_WRITE_TEXT);
}
因为系统会对过长的websocket报文进行剪切,所以在有长报文发送之前我会发送一个长度的报文,所以这里就是连续发送两次报文,而我服务器则对带长度的报文进行处理并不反回成功或失败的报文。
客户端简单调用
int __stdcall Open(int devId)
{
LOG(INFO) << "开始执行打开设备操作";
LOG(INFO) << "执行打开操作";
int ret = -1;
Json::Value js_SendMsg, js_Return;
js_SendMsg.clear();
js_SendMsg["method"] = "connect";
js_SendMsg["params"]["uuid"] = "1001";
isHaveCard = TRUE;
if (js_SendMsg.isMember("method")) {
js_Return = CmdFuntion(js_SendMsg, js_SendMsg.toStyledString().length());
if (js_Return.isMember("error")) {
ret = -1;
LOG(INFO) << "执行打开操作出错";
}
else if (js_Return.isMember("result")) {
ret = 0;
LOG(INFO) << "打开设备成功";
}
}
else
{
ret = 0;
LOG(INFO) << "打开设备成功";
}
isHaveCard = FALSE;
return ret;
}
服务器代码实现
初始化