前言
本文主要讲述linux下C++语言socket.io库的编译及使用
一、什么是socket.io
socket.io是一个基于WebSocket的CS的实时通信库,可以在客户端和服务器之间实现实时,双向和基于事件的通信。它底层基于engine.io。engine.io使用WebSocket和xhr-polling(或jsonp)封装了一套自己的协议,在不支持WebSocket的低版本浏览器中使用了长轮询(long polling)来代替。socket.io在engine.io的基础上增加了namespace,room,自动重连等特性。Websocket可以说是socket.io的一个子集。
二、版本特点
- Socket.io目前已经发展到v5版本。
- 从Socket.IO 3.1.0 开始,v3 服务器现在能够与 v2 客户端通信。但是v3 客户端仍然无法连接到 v2 服务器。
- Socket.IO 4.0.0 重大更改仅影响服务器端的 API。Socket.IO 协议本身没有更新,因此 v3 客户端将能够访问 v4 服务器,反之亦然。此外,当 allowEIO3:true 标志存在,则Socket.IO v2 客户端和 Socket.IO v4 服务器之间仍然可以使用兼容模式 。
三、版本判断
Socket.io依赖Engine.IO版本,关系如下: Engine.IO v2 包含在 Socket.IO v0.9 中, v3 包含在 Socket.IO v1/v2 中,v4包含在 Socket.IO v3 中。通常拿到一个数据包怎么判断Socket.io版本,通过看EIO版本号,如下所示,Socket.IO请求报文中EIO=3表示Engine.IO版本为3,则Socket.io的版本为v1/v2,不能与Socket.IO v3服务器通信。四、socket.io库相关资源
- 官网:https://socket.io/
- 中文介绍网站:https://www.w3cschool.cn/socket/
- C++库源码下载地址: https://github.com/socketio/socket.io-client-cpp,根据版本特点介绍的内容,在release版本中选择合适的源代下载。 -如需要对接v3版本服务器,则需要下载3.X.X版本,否则下载2.X.X版本。
五、socket.io库编译
-
Socket.io client源码下载好后,需要下载依赖的库资源,这里以下载 socket.io-client-cpp-2.1.0版本为例,在主干中点击lib, 则可以看到依赖的库资源及版本,如下图所示,点击下载即可,然后把资源解压到socket.io的源码lib目录下。(除以上库之外,socket.io也依赖openssl库,openssl下载和编译参考我的其他文章)
-
Socket.io使用Cmake编译,创建交叉编译文件toolChain.cmake,这里放到与源码同一级目录,如下图
toolChain.cmake内容参考如下,注意修改交叉编译工具链及openssl依赖:
#PROJECT_SOURCE_DIR为执行cmake命令后顶层CMakeLists.txt所处路径
SET(TOOLCHAIN_FILE_PATH "${PROJECT_SOURCE_DIR}")
message(STATUS "TOOLCHAIN_FILE_PATH=${TOOLCHAIN_FILE_PATH}")
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
#设置交叉编译工具链
set(compilerPrefix /opt/arm-linux-)
set(CMAKE_C_COMPILER ${compilerPrefix}gcc)
set(CMAKE_CXX_COMPILER ${compilerPrefix}g++)
#选择编译模式:Debug、Release
set(CMAKE_BUILD_TYPE Release CACHE STRING "set build type manually")
#设置Openssl库位置
set(OPENSSL_INCLUDE_DIR "${TOOLCHAIN_FILE_PATH}/../../../openssl-1.0.2t/openssl-1.0.2t/install/include")
set(OPENSSL_ROOT_DIR "${TOOLCHAIN_FILE_PATH}/openssl-1.0.2t-ai3/openssl-1.0.2t/install")
set(OPENSSL_CRYPTO_LIBRARY "${OPENSSL_ROOT_DIR}/libs/libcrypto.a")
set(OPENSSL_SSL_LIBRARY "${OPENSSL_ROOT_DIR}/libs/libssl.a")
#指定处目标系统搜索目录外的搜索目录,这里暂时用不到
#set(CMAKE_FIND_ROOT_PATH "")
# ONLY表示只在目标系统目录查找,NEVER表示不查找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
- 切到socket.io-client-cpp/build目录后执行,执行完成后会在build目录下生成Makefile文件。
cmake -DCMAKE_TOOLCHAIN_FILE=../toolChain.cmake .. -DCMAKE_INSTALL_PREFIX=../build/
- 然后执行make命令,会在build目录下生成libsioclient.a、libsioclient_tls.a两个静态库(libsioclient_tls.a为支持TLS的库,另一个不支持TLS,根据服务器是否使用TLS加密传输选择),make DESTDIR=./install/ install,在install下会生成用户使用的头文件及lib库。
- 完成以上内容就完成了socket.io库的编译工作。
六、socket.io程序编译调试
编写一个测试程序 sio_test.cpp,文件内容如下所示:
#include <iostream>
#include "string.h"
#include <unistd.h>
#include "posixWrapper.h"
#include "sio_client.h" //socket.io库编译生成的头文件
using namespace std;
using namespace sio;
class connection_listener
{
sio::client &handler;
public:
connection_listener(sio::client& h):
handler(h)
{
}
void on_connected()
{
std::cout<<"[SOCKET.IO]sio con "<<std::endl;
}
void on_close(client::close_reason const& reason)
{
std::cout<<"[SOCKET.IO]sio closed "<<std::endl;
}
void on_fail()
{
std::cout<<"[SOCKET.IO]sio failed "<<std::endl;
}
};
void OnMessage_deal_example(const std::string& name, message::ptr const& data, bool isAck, message::list& ack_resp)
{
std::cout<<"[SOCKET.IO]OnMessage_deal_example:" << name <<std::endl;
if(isAck)
{
std::string msg = "{success}";
ack_resp.push(msg);
}
if (sio::message::flag_integer == data->get_flag())
{
std::cout<< "int:" << data->get_int() <<std::endl;
}
else if (sio::message::flag_double == data->get_flag())
{
std::cout<< "double:%lf\n" << data->get_double() <<std::endl;
}
else if (sio::message::flag_string == data->get_flag())
{
std::cout<< "string:" << data->get_string() <<std::endl;
}
else if (sio::message::flag_binary == data->get_flag())
{
std::cout<<"[SOCKET.IO]binary!"<<std::endl;
}
else if (sio::message::flag_object == data->get_flag())
{
std::cout<<"[SOCKET.IO]object!"<<std::endl;
/* 以解析消息内容为: {“deviceIds”:“test12345”}为例 */
std::map<std::string, message::ptr> MessageObject;
map<std::string, message::ptr>::iterator iter;
MessageObject = data->get_map();
iter = MessageObject.find("deviceIds");
if(iter != MessageObject.end())
{
if (sio::message::flag_string == iter->second->get_flag())
{
std::cout<< "deviceIds:" << iter->second->get_string() <<std::endl;
}
else
{
std::cout<< "[SOCKET.IO]unsupport data type:" << iter->second->get_flag() <<std::endl;
}
}
else
{
std::cout<<"[SOCKET.IO]Do not Find deviceIds" <<std::endl;
}
}
else
{
std::cout<<"[SOCKET.IO]unsupport data type!"<<std::endl;
}
}
int socketio_thread(void)
{
sio::client h;
connection_listener l(h);
std::map<std::string, std::string> header;
std::map<std::string, std::string> query;
pair<map<int, string>::iterator, bool> retPair;
char devId[32] = {0};
char NewServerAddr[256] = {0};
char buf[512] = {0};
int isFirstRun = 1;
int isNeedReconn = 0;
snprintf(devId, sizeof(devId), "%s", "test12345");
/* 绑定状态监听函数 */
h.set_open_listener(std::bind(&connection_listener::on_connected, &l));
h.set_close_listener(std::bind(&connection_listener::on_close, &l,std::placeholders::_1));
h.set_fail_listener(std::bind(&connection_listener::on_fail, &l));
/* 监听namespace为/entrance的消息, 消息名为:push_message */
h.socket("/entrance")->on("push_message", &OnMessage_deal_example);
/* 设置连接参数,连接服务器 */
/* 添加报文头 */
header.insert(std::pair<std::string, std::string>("Origin", "null"));
header.insert(std::pair<std::string, std::string>("Accept", "*/*"));
/* 添加请求报文体(URL之后的内容,如 http://xxxxxxx?devId) */
query.insert(std::pair<std::string, std::string>("deviceid", devId));
while(1)
{
if(1 == isFirstRun || (0 == isFirstRun && 1 == isNeedReconn))
{
std::cout<<"[SOCKET.IO]socketio run!" <<std::endl;
snprintf(NewServerAddr, sizeof(NewServerAddr), "%s", "http://10.6.124.181:10000");
snprintf(buf, sizeof(buf), "%s/entrance", NewServerAddr);
/* 连接socket.io服务器 */
h.connect(buf, query, header);
isFirstRun = 0;
isNeedReconn = 0;
}
/* 使用emit发送消息 */
//h.socket("/entrance")->emit("ping");
sleep(5);
}
h.socket("/entrance")->close();
h.close();
h.clear_con_listeners();
h.clear_socket_listeners();
return 0;
}
编译测试程序 sio_test.cpp,arm-linux-g++为本文所用编译工具链,看开发者情况修改,~/nfs/lib为本文测试存放libpthread、libm、libdl等基础库的路径,看开发者情况修改, ./lib 为存放libssl、libcrypto库路径,看开发者情况修改:
(1)支持ssl加密的编译链接方式(即链接libsioclient_tls.a及openssl库相关资源):
arm-linux-g++ sio_test.cpp -o sio_test -lsioclient_tls -lssl -lcrypto -lpthread -lm -ldl -I~/xxx/ssl/include -I~/xxx/ssl/include/openssl -I./include -L~/nfs/lib -L./lib
(2)非加密的编译链接方式(即链接libsioclient.a):
arm-linux-g++ sio_test.cpp -o sio_test -lsioclient -lpthread -lm -ldl -L/lib -L./lib
这里以python socket.io脚本作为陪测服务器,由于本文使用的是V2.X.X socket.io版本,选用 python-socketio-1.9.0.zip 作为陪测,不同版本使用的 python-socketio 版本不同,需要注意,调试步骤如下:
- 解压 python-socketio-1.9.0.zip 文件,进入python-socketio-1.9.0\examples\wsgi目录,打开app.py脚本,修改脚本内容如下:
# set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to
# force a mode else, the best mode is selected automatically from what's
# installed
async_mode = None
import time
from flask import Flask, render_template
import socketio
sio = socketio.Server(logger=True, async_mode=async_mode)
app = Flask(__name__)
app.wsgi_app = socketio.Middleware(sio, app.wsgi_app)
app.config['SECRET_KEY'] = 'secret!'
thread = None
def background_thread():
"""Example of how to send server generated events to clients."""
count = 0
while True:
sio.sleep(10)
count += 1
sio.emit('my response', {'data': 'Server generated event'},
namespace='/test')
@app.route('/')
def index():
global thread
if thread is None:
thread = sio.start_background_task(background_thread)
return render_template('index.html')
@sio.on('connect', namespace=None)
def connect(sid, message):
print('connect')
@sio.on('disconnect', namespace=None)
def disconnect(sid, message):
print('Client disconnected')
@sio.on('ping', namespace='/entrance')
def ping(sid, message):
print('ping')
sio.emit('push_message', {"deviceIds":"test12345"}, room=sid, namespace='/entrance')
if __name__ == '__main__':
if sio.async_mode == 'eventlet':
print('eventlet')
# deploy with eventlet
import eventlet
import eventlet.wsgi
eventlet.wsgi.server(eventlet.listen(('', 10000)), app)
else:
print('Unknown async_mode: ' + sio.async_mode)
- 运行app.py脚本,启动socket.io服务器,服务器此时订阅了namespace为 "/entrance"消息名为"ping"的消息,如果收到此消息,则发送"namespace为 "/entrance"消息名为"push_message"的消息到客户端,为了完成测试,所以需要客户端增加每隔几秒发送"ping"消息功能,上文客户端代码未添加,自己加一下即可完成测试。