一、实现思路
- 
Web端就是使用html + JavaScript来实现页面,通过WebSocket长连接和服务器保持通讯,协议的payload使用JSON格式封装 - 
服务端使用
C++配合第三方库WebSocket++和nlonlohmann库来实现 
二、Web端
2.1 界面显示
首先,使用html来设计一个简单的静态框架:
- 有一个聊天室的标题
 - 然后一个文本框加上一个发送按钮
 
            
            
              html
              
              
            
          
          <body>
    <h1>WebSocket简易聊天室</h1>
    <div id="app">
        <input id="sendMsg" type="text" />
        <button id="sendBtn">发送</button>
    </div>
</body>
        然后我们还需要显示聊天信息,我们可以利用脚本每次有信息的时候,就把这个信息嵌入到<body>里面,这个信息本身存放在<div>里面,如下:
- 加入房间显示蓝色
 - 离开房间显示红色
 - 聊天信息显示黑色
 
            
            
              js
              
              
            
          
          function showMessage(str, type) {
	var div = document.createElement("div");
	div.innerHTML = str;
	if (type == "enter") div.style.color = "blue";
	else if (type == "leave") div.style.color = "red";
	document.body.appendChild(div);
}
        2.2 WebSocket 连接
接下来我们配合JavaScript脚本,先连接服务端的WebSocket服务器
            
            
              js
              
              
            
          
          var websocket = new WebSocket('ws://192.168.217.128:9002');
        我们需要实现WebSocket的几个回调函数:
- onopen
 
- 这个回调函数在连接服务器成功时触发,我们在连接成功的时候,绑定上发送按钮的点击事件
 
            
            
              js
              
              
            
          
          // 连接成功
websocket.onopen = function () {
	console.log("连接服务器成功");
	document.getElementById("sendBtn").onclick = function () {
		var msg = document.getElementById("sendMsg").value;
		if (msg) {
			websocket.send(msg);
		}
	};
};
        - onmessage
 
- 这个回调函数在接收到服务端的消息后触发,这里是
JSON格式,我们服务端定义了data为key,对应的消息就是data后面的value了 
            
            
              js
              
              
            
          
          websocket.onmessage = function (e) {
	var mes = JSON.parse(e.data);
	showMessage(mes.data, mes.type);
};
        - onclose
 
- 这个回调函数在连接断开的时候触发,在这里我们简单的打印一下即可:
 
            
            
              js
              
              
            
          
          // 连接关闭
websocket.onclose = function (event) {
	console.log("连接已关闭", "代码:", event.code, "原因:", event.reason);
};
        2.3 完整代码
完整的Web代码如下:
            
            
              html
              
              
            
          
          <!DOCTYPE html>
<html>
<body>
    <h1>WebSocket简易聊天室</h1>
    <div id="app">
        <input id="sendMsg" type="text" />
        <button id="sendBtn">发送</button>
    </div>
</body>
<script>
    function showMessage(str, type) {
        var div = document.createElement("div");
        div.innerHTML = str;
        if (type == "enter") div.style.color = "blue";
        else if (type == "leave") div.style.color = "red";
        document.body.appendChild(div);
    }
    var websocket = new WebSocket('ws://192.168.217.128:9002');
    // 连接成功
    websocket.onopen = function () {
        console.log("连接服务器成功");
        document.getElementById("sendBtn").onclick = function () {
            var msg = document.getElementById("sendMsg").value;
            if (msg) {
                websocket.send(msg);
            }
        };
    };
    // 接收消息
    websocket.onmessage = function (e) {
        var mes = JSON.parse(e.data);
        showMessage(mes.data, mes.type);
    };
    // 连接关闭
    websocket.onclose = function (event) {
        console.log("连接已关闭", "代码:", event.code, "原因:", event.reason);
    };
    // 错误处理
    websocket.onerror = function (error) {
        console.error("WebSocket错误:", error);
    };
</script>
</html>
        三、服务端代码
确保你已经配置好了第三方库,下面我们开始讲解服务端代码:
3.1 echo_server改造
首先这里我们是使用WebSocket++这个第三方库的配套示例echo_server.cpp改造的,因此我们只讲解改造的部分,未修改源代码echo_server.cpp如下:
            
            
              cpp
              
              
            
          
          // examples目录是官方的一些例子 本次使用的是echo_server\echo_server.cpp
// 该原程序只支持一对一发送后回复
// 改造后可以通知所有连接上来的客户端。
// 编译 g++ main.cpp -o main -lboost_system -lboost_chrono
 
#include <websocketpp/config/asio_no_tls.hpp>
 
#include <websocketpp/server.hpp>
 
#include <iostream>
#include <list>
 
#include <functional> 
 
typedef websocketpp::server<websocketpp::config::asio> server;
 
using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
 
// pull out the type of messages sent by our config
typedef server::message_ptr message_ptr;
 
std::list<websocketpp::connection_hdl> vgdl;
 
// Define a callback to handle incoming messages
void on_message(server *s, websocketpp::connection_hdl hdl, message_ptr msg)
{
    // std::cout << "on_message called with hdl: " << hdl.lock().get()
    //           << " and message: " << msg->get_payload()
    //           << std::endl;
 
    // check for a special command to instruct the server to stop listening so
    // it can be cleanly exited.
    if (msg->get_payload() == "stop-listening")
    {
        s->stop_listening();
        return;
    }
 
    for (auto it = vgdl.begin(); it != vgdl.end(); it++)
    {
        if (it->expired())//移除连接断开的
        {
            it = vgdl.erase(it);
            continue;
        }
        if (it != vgdl.end())
            s->send(*it, msg->get_payload(), msg->get_opcode());
        // t.wait();
    }
 
    // try {
    //     s->send(hdl, msg->get_payload()+std::string("aaaaa"), msg->get_opcode());
    // } catch (websocketpp::exception const & e) {
    //     std::cout << "Echo failed because: "
    //               << "(" << e.what() << ")" << std::endl;
    // }
}
//将每个连接存入容器
void on_open(websocketpp::connection_hdl hdl)
{
    std::string msg = "link OK";
    printf("%s\n", msg.c_str());
    // printf("fd %d\n",(int)hdl._M_ptr());
    vgdl.push_back(hdl);
}
 
void on_close(websocketpp::connection_hdl hdl)
{
    std::string msg = "close OK";
    printf("%s\n", msg.c_str());
}
int main()
{
    // Create a server endpoint
    server echo_server;
 
    try
    {
        // Set logging settings 设置log
        echo_server.set_access_channels(websocketpp::log::alevel::all);
        echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);
 
        // Initialize Asio 初始化asio
        echo_server.init_asio();
 
        // Register our message handler
        // 绑定收到消息后的回调
        echo_server.set_message_handler(bind(&on_message, &echo_server, ::_1, ::_2));
        //当有客户端连接时触发的回调
        std::function<void(websocketpp::connection_hdl)> f_open;
        f_open = on_open;
        echo_server.set_open_handler(websocketpp::open_handler(f_open));
        //关闭是触发
        std::function<void(websocketpp::connection_hdl)> f_close(on_close);
        echo_server.set_close_handler(f_close);
        // Listen on port 9002
        echo_server.listen(9002);//监听端口
 
        // Start the server accept loop
        echo_server.start_accept();
 
        // Start the ASIO io_service run loop
        echo_server.run();
    }
    catch (websocketpp::exception const &e)
    {
        std::cout << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "other exception" << std::endl;
    }
}
        3.2 管理用户名
- 
对于每一个连接上来的用户,对应唯一的
connection_hdl类型的值,是一个用于标识和跟踪 WebSocket 连接的句柄类型,其本质是对连接对象的弱引用(封装了std::weak_ptr) - 
因此我们可以把它映射到每一个用户名,这样我们就知道发送过来的连接句柄对应是哪个用户了,实际这里我们每次连接都分配了一个用户名:
 
            
            
              cpp
              
              
            
          
          std::string username = "user:" + std::to_string(++totalUser);
        这里我们使用std::map进行映射,由于websocketpp::connection_hdl类型不具备运算符<,因此我们自己写一个仿函数,里面用它内部的weak_ptr进行比较
            
            
              cpp
              
              
            
          
          struct ConnectionHdlCompare {
    bool operator()(const websocketpp::connection_hdl& a, const websocketpp::connection_hdl& b) const {
        // 通过获取底层的弱指针的原始指针进行比较
        return a.lock() < b.lock();
    }
};
        然后这样定义std::map:
            
            
              cpp
              
              
            
          
          std::map<websocketpp::connection_hdl, std::string, ConnectionHdlCompare> user_map; 
        3.3 连接事件
- 每个用户连接的时候,需要分配一个唯一的用户名,然后广播给全部用户,该用户加入房间了,我们使用
JSON进行序列化,类型是enter,同时,把这个连接句柄加入全局的链表中 
            
            
              cpp
              
              
            
          
          void on_open(websocketpp::connection_hdl hdl)
{
    std::string msg = "link OK";
    printf("%s\n", msg.c_str());
    
    vgdl.push_back(hdl);
    // 生成唯一用户名
    std::string username = "user:" + std::to_string(++totalUser);
    user_map[hdl] = username;  // 记录连接对应的用户名
    // 广播用户加入消息
    json j;
    j["type"] = "enter";
    j["data"] = username + "加入房间";
    std::string json_str = j.dump();
    std::cout << "json_str = " << json_str << std::endl;
    send_msg(&echo_server, json_str);
}
        - 
广播函数
send_msg我们重载了两个版本,其中一个版本是字符串std::string发送,另一个则是使用message_ptr类型,其内部封装了消息帧的数据,比如payload负载、opcode操作码,它本质也是一个weak_ptr - 
创建文本帧如下,我们发送
JSON,使用的是text0x0:延续帧(用于分片传输大消息,当前帧是消息的中间部分);0x1:文本帧(payload 是 UTF-8 编码的文本数据);0x2:二进制帧(payload 是任意二进制数据,如图片、protobuf 等);0x8:关闭帧(通知对方关闭连接,payload 可包含关闭原因);0x9:Ping 帧(心跳检测,用于确认连接活性);0xA:Pong 帧(响应 Ping 帧的心跳回复)。
 - 
遍历的时候,需要判断是否指针失效了,因为
weak_ptr本身并不能管理对象,需要转换为shared_ptr来查看,可以调用expire()函数来看看是否为nullptr,我们只对有效的连接发送消息,无效的连接直接从链表删除这个句柄 
            
            
              cpp
              
              
            
          
          void send_msg(server *s, message_ptr msg){
    for (auto it = vgdl.begin(); it != vgdl.end(); )
    {
        if (it->expired())
        {
            it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开
        }
        else
        {
            try {
                s->send(*it, msg->get_payload(), msg->get_opcode());
            } catch (websocketpp::exception const & e) {
                std::cout << "Broadcast failed because: " << e.what() << std::endl;
            }
            ++it;  // 只有在未删除元素时才递增迭代器
        }
    }
}
void send_msg(server *s,std::string msg){
    for (auto it = vgdl.begin(); it != vgdl.end(); )
    {
        if (it->expired())
        {
            it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开
        }
        else
        {
            try {
                s->send(*it, msg, websocketpp::frame::opcode::text);
            } catch (websocketpp::exception const & e) {
                std::cout << "Broadcast failed because: " << e.what() << std::endl;
            }
            ++it;  // 只有在未删除元素时才递增迭代器
        }
    }
}
        3.4 关闭事件
在触发关闭连接的回调函数中,我们要删除对应map里面的用户名,并且我们将这个用户离开房间的消息转发给所有人,JSON序列化的type为leave
            
            
              cpp
              
              
            
          
          void on_close(websocketpp::connection_hdl hdl)
{
	for (auto it = vgdl.begin(); it != vgdl.end(); )
    {
        if (it->expired())
        {
            it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开
        }
    }
    std::string msg = "close OK";
    printf("%s\n", msg.c_str());
    
    // 清理用户映射表并广播离开消息
    if (user_map.find(hdl) != user_map.end()) {
        std::string username = user_map[hdl];
        user_map.erase(hdl);  // 删除映射记录
        json j;
        j["type"] = "leave";
        j["data"] = username + "离开房间";
        std::string json_str = j.dump();
        send_msg(&echo_server, json_str);
    }
}
        3.5 发送消息事件
- 
在收到客户端的消息之后,我们需要将这个消息转发给所有人,
JSON序列化的type为message,发送的时候需要加上用户名,这样前端显示才有用户名: - 
这里是通过修改
message_ptr的payload的形式来发送消息的,因此我们调用send_msg是第一个重载版本 
            
            
              cpp
              
              
            
          
          void on_message(server *s, websocketpp::connection_hdl hdl, message_ptr msg)
{
    std::cout << "on_message called with hdl: " << hdl.lock().get()
              << " and message: " << msg->get_payload()
              << std::endl;
 
    // check for a special command to instruct the server to stop listening so
    // it can be cleanly exited.
    if (msg->get_payload() == "stop-listening")
    {
        s->stop_listening();
        return;
    }
    //转发消息
    json j;
    j["type"] = "message";
    j["data"] =  user_map[hdl] + "说:" + msg->get_payload();
    std::string json_str = j.dump();
    msg->set_payload(json_str);
    std::cout << "msg = " << msg->get_payload() << std::endl;
    send_msg(s, msg);
}
        3.6 完整代码
完整的服务端代码如下:
            
            
              cpp
              
              
            
          
          // examples目录是官方的一些例子 本次使用的是echo_server\echo_server.cpp
// 该原程序只支持一对一发送后回复
// 改造后可以通知所有连接上来的客户端。
// 编译 g++ main.cpp -o main -lboost_system -lboost_chrono
 
#include <websocketpp/config/asio_no_tls.hpp>
 
#include <websocketpp/server.hpp>
 
#include <iostream>
#include <list>
#include <functional> 
#include <mutex>  // 添加互斥锁头文件
//json解析
#include<nlohmann/json.hpp>
using json = nlohmann::json;
typedef websocketpp::server<websocketpp::config::asio> server;
 
using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
 
// pull out the type of messages sent by our config
typedef server::message_ptr message_ptr;
 
std::list<websocketpp::connection_hdl> vgdl;
std::mutex vgdl_mutex;  // 添加互斥锁保护连接列表
// 自定义比较函数对象
struct ConnectionHdlCompare {
    bool operator()(const websocketpp::connection_hdl& a, const websocketpp::connection_hdl& b) const {
        // 通过获取底层的弱指针的原始指针进行比较
        return a.lock() < b.lock();
    }
};
// 使用自定义比较函数对象的连接-用户名映射表
std::map<websocketpp::connection_hdl, std::string, ConnectionHdlCompare> user_map;  // 新增:连接-用户名映射表
 
// Define a callback to handle incoming messages
// Create a server endpoint
server echo_server;
int totalUser = 0;
void send_msg(server *s, message_ptr msg){
    for (auto it = vgdl.begin(); it != vgdl.end(); )
    {
        if (it->expired())
        {
            it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开
        }
        else
        {
            try {
                s->send(*it, msg->get_payload(), msg->get_opcode());
            } catch (websocketpp::exception const & e) {
                std::cout << "Broadcast failed because: " << e.what() << std::endl;
            }
            ++it;  // 只有在未删除元素时才递增迭代器
        }
    }
}
void send_msg(server *s,std::string msg){
    for (auto it = vgdl.begin(); it != vgdl.end(); )
    {
        if (it->expired())
        {
            it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开
        }
        else
        {
            try {
                s->send(*it, msg, websocketpp::frame::opcode::text);
            } catch (websocketpp::exception const & e) {
                std::cout << "Broadcast failed because: " << e.what() << std::endl;
            }
            ++it;  // 只有在未删除元素时才递增迭代器
        }
    }
}
void on_message(server *s, websocketpp::connection_hdl hdl, message_ptr msg)
{
    std::cout << "on_message called with hdl: " << hdl.lock().get()
              << " and message: " << msg->get_payload()
              << std::endl;
 
    // check for a special command to instruct the server to stop listening so
    // it can be cleanly exited.
    if (msg->get_payload() == "stop-listening")
    {
        s->stop_listening();
        return;
    }
    //转发消息
    json j;
    j["type"] = "message";
    j["data"] =  user_map[hdl] + "说:" + msg->get_payload();
    std::string json_str = j.dump();
    msg->set_payload(json_str);
    std::cout << "msg = " << msg->get_payload() << std::endl;
    send_msg(s, msg);
}
//将每个连接存入容器
void on_open(websocketpp::connection_hdl hdl)
{
    std::string msg = "link OK";
    printf("%s\n", msg.c_str());
    
    vgdl.push_back(hdl);
    // 生成唯一用户名
    std::string username = "user:" + std::to_string(++totalUser);
    user_map[hdl] = username;  // 记录连接对应的用户名
    // 广播用户加入消息
    json j;
    j["type"] = "enter";
    j["data"] = username + "加入房间";
    std::string json_str = j.dump();
    std::cout << "json_str = " << json_str << std::endl;
    send_msg(&echo_server, json_str);
}
 
void on_close(websocketpp::connection_hdl hdl)
{
	for (auto it = vgdl.begin(); it != vgdl.end(); )
    {
        if (it->expired())
        {
            it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开
        }
    }
    std::string msg = "close OK";
    printf("%s\n", msg.c_str());
    
    // 清理用户映射表并广播离开消息
    if (user_map.find(hdl) != user_map.end()) {
        std::string username = user_map[hdl];
        user_map.erase(hdl);  // 删除映射记录
        json j;
        j["type"] = "leave";
        j["data"] = username + "离开房间";
        std::string json_str = j.dump();
        send_msg(&echo_server, json_str);
    }
}
int main()
{
    try
    {
        // Set logging settings 设置log
        echo_server.set_access_channels(websocketpp::log::alevel::all);
        echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);
 
        // Initialize Asio 初始化asio
        echo_server.init_asio();
 
        // Register our message handler
        // 绑定收到消息后的回调
        echo_server.set_message_handler(bind(&on_message, &echo_server, ::_1, ::_2));
        //当有客户端连接时触发的回调
        std::function<void(websocketpp::connection_hdl)> f_open;
        f_open = on_open;
        echo_server.set_open_handler(websocketpp::open_handler(f_open));
        //关闭是触发
        std::function<void(websocketpp::connection_hdl)> f_close(on_close);
        echo_server.set_close_handler(f_close);
        // Listen on port 9002
        echo_server.listen(9002);//监听端口
 
        // Start the server accept loop
        echo_server.start_accept();
 
        // Start the ASIO io_service run loop
        echo_server.run();
    }
    catch (websocketpp::exception const &e)
    {
        std::cout << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "other exception" << std::endl;
    }
}
        四、运行结果
编译服务端并启动:
            
            
              shell
              
              
            
          
          g++ main.cpp -o main -lboost_system -lboost_chrono
./main
        在两个浏览器中打开我们的Web服务端,这里是本地的,所以是
            
            
              http
              
              
            
          
          http://127.0.0.1:5500/chatClient.html
        两个用户加入房间、聊天、离开的效果有不同的颜色,如下所示
