一、编译nginx
具体的步骤如下:
- 安装依赖库
shell
sudo apt update
sudo apt install build-essential libtool
sudo apt install libpcre3 libpcre3‐dev
sudo apt install zlib1g‐dev
sudo apt get install openssl
- 下载
nginx1.15.8
版本
shell
wget http://nginx.org/download/nginx‐1.15.8.tar.gz
- 解压、编译、安装
安装后的nginx
位置为:/usr/local/nginx/sbin/nginx
shell
tar -xvzf nginx‐1.15.8.tar.gz
cd nginx‐1.15.8/
./configure ‐‐with‐http_ssl_module
make && sudo make install
相关nginx
命令:
- 启动:sudo /usr/local/nginx/sbin/nginx
- 停止:sudo /usr/local/nginx/sbin/nginx s stop
- 重新加载配置文件:sudo /usr/local/nginx/sbin/nginx s reload
二、生成证书
由于使用https
和wss
需要使用tls/ssl
加密,因此我们需要使用openssl
库生成自签名证书和私钥
shell
mkdir -p ~/cert
cd ~/cert
openssl genrsa ‐out key.pem 2048
openssl req ‐new ‐x509 ‐key key.pem ‐out cert.pem ‐days 1095
二、升级Web服务器 Https
由于Web
中如果需要打开摄像头,只允许是localhost
或者网站通过https
加密,否则打开摄像头会失败,因此我们需要搭建一个https服务器,这里我们使用nginx
搭建我们我们的https
服务器,具体的配置文件:
json
server {
listen 443 ssl;
ssl_certificate /home/liuhang/cert/cert.pem; # 证书
ssl_certificate_key /home/liuhang/cert/key.pem; # 私钥
charset utf-8;
# ip地址或者域名
server_name 192.168.10.251;
location / {
add_header 'Access‐Control‐Allow‐Origin' '*';
add_header 'Access‐Control‐Allow‐Credentials' 'true';
add_header 'Access‐Control‐Allow‐Methods' '*';
add_header 'Access‐Control‐Allow‐Headers' 'Origin, X‐Requested‐With, Content‐Type, Accept';
# web页面所在目录
root /home/liuhang/web;
index index.php index.html index.htm;
}
}
三、nginx代理WebSocket服务器
ws
是不安全的连接,类似http
,wss
是安全的连接,类似wss
,因此我们的https
不能访问ws
,我们这里可以使用nginx
代理wss
连接,然后nginx
和我们的WebSocket
服务器是ws
连接,解密后转发给我们的服务器
客户端 nginx服务器代理 WebSocket Server
添加配置文件:
json
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
server 192.168.10.251:9002; # WebSocket服务器
}
server {
listen 8098 ssl;
#ssl on;
ssl_certificate /home/liuhang/cert/cert.pem; # 证书
ssl_certificate_key /home/liuhang/cert/key.pem; # 私钥
server_name 192.168.10.251; # 服务器ip
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_connect_timeout 4s;
proxy_read_timeout 6000s;
proxy_send_timeout 6000s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
在配置文件中:/usr/local/nginx/conf/nginx.conf
,添加上这一句,其中这个是你的配置文件的地址:
conf
include /usr/local/nginx/conf/conf.d/*.conf

同时,Web端的js代码需要连接wss
到代理的服务器:
shell
rtcEngine = new RTCEngine("wss://192.168.10.251:8098/ws");
四、添加couturn 服务器
-
coturn
是Google
开源的服务器,我之前的博客有介绍如何搭建 -
coturn
服务器提供了stun
和turn
两个功能,分别用于p2p
打洞和中继转发,我们可以在前端js
代码中,初始化RTCPeerConnection
的时候配置我们的服务器,配置我们的服务器,具体代码如下:
js
//创建和远端的连接
function createPeerConnection() {
//stun服务器配置信息
var defaultConfiguration = {
bundlePolicy: "max-bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy: "all",
iceServers: [
{
"urls": [
"turn:192.168.10.251:3478?transport=udp",
"turn:192.168.10.251:3478?transport=tcp" // 可以插入多个进行备选
],
"username": "lh",
"credential": "123456"
},
{
"urls": [
"stun:192.168.10.251:3478"
]
}
]
};
//创建RTCPeerConnection对象
pc = new RTCPeerConnection(defaultConfiguration);
// pc = new RTCPeerConnection(null);
//设置回调函数
pc.onicecandidate = handleIceCandidate; //设置IceCandidate回调函数
pc.ontrack = handleRemoteStreamAdd; //设置远端流回调函数
//遍历本地流,加入流中
localStream.getTracks().forEach(function (track) {
pc.addTrack(track, localStream);
});
console.log("创建RTCPeerConnection对象成功");
}
注意,上述的用户名和密码需要和我们启动coturn
服务器一致,端口默认是3478,选择all
代表优先p2p
,然后才是relay
,也可以指定relay
表示只使用中继服务器
五、启动测试
5.1 启动coturn
启动coturn
,用户名和密码要对应上:
shell
sudo nohup turnserver -L 0.0.0.0 -a -u lh:123456 -v -f -r nort.gov

5.2 启动nginx
shell
sudo /usr/local/nginx/sbin/nginx
5.3 启动信令服务器
shell
cd ~/C++/websocket/webrtc_server/build
./webrtc_server

5.4 客户端访问
客户端访问https
地址
http
https://192.168.10.251/index.html
由于是自签名证书,所以浏览器会提示不安全,我们选择接受它

任意加入一个房间后,成功打开本地摄像头

我们使用Android也打开这个地址,可以发现已经可以连接上了

六、不使用代理
如果不想使用nginx
代理我们的websocket
服务器,也可以实现一个wss
服务器,在我们之前的服务器前提上加上ssl/tls
的握手,完整代码如下,可以自己看看,主要用到的就是boost.asio
的ssl/tls
部分
cpp
#include <functional>
#include <iostream>
#include <list>
#include <mutex>
#include <websocketpp/config/asio.hpp> // 修改为支持TLS的配置
#include <websocketpp/server.hpp>
#include <string>
#include <nlohmann/json.hpp>
#include "signal_type.h"
using json = nlohmann::json;
// 使用TLS服务器配置
typedef websocketpp::server<websocketpp::config::asio_tls> server;
using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
typedef server::message_ptr message_ptr;
typedef websocketpp::lib::shared_ptr<boost::asio::ssl::context> context_ptr;
const int LISTEN_PORT = 9002; // 建议使用标准HTTPS端口
std::list<websocketpp::connection_hdl> vgdl;
std::mutex vgdl_mutex;
struct Client {
std::string uid;
int roomId;
websocketpp::connection_hdl hdl;
};
std::map<int, std::vector<Client>> room_map;
server webSocket_server;
int totalUser = 0;
context_ptr on_tls_init() {
namespace asio_ssl = boost::asio::ssl;
// context_ptr ctx = std::make_shared<asio_ssl::context>(asio_ssl::context::tlsv12_server);
context_ptr ctx = std::make_shared<asio_ssl::context>(asio_ssl::context::tls);
try {
// 设置安全协议选项
ctx->set_options(
asio_ssl::context::default_workarounds |
asio_ssl::context::no_sslv2 |
asio_ssl::context::no_sslv3 |
asio_ssl::context::no_tlsv1 |
asio_ssl::context::no_tlsv1_1 |
asio_ssl::context::single_dh_use
);
// 加载证书和私钥
ctx->use_certificate_chain_file("/home/liuhang/cert/cert.pem");
ctx->use_private_key_file("/home/liuhang/cert/key.pem", asio_ssl::context::pem);
// 验证密钥匹配
if (!SSL_CTX_check_private_key(ctx->native_handle())) {
std::cerr << "错误: 证书和私钥不匹配" << std::endl;
throw std::runtime_error("证书/私钥不匹配");
}
// 设置密码套件
SSL_CTX_set_cipher_list(ctx->native_handle(), "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4");
std::cout << "TLS上下文初始化成功" << std::endl;
} catch (std::exception& e) {
std::cerr << "TLS初始化失败: " << e.what() << std::endl;
exit(1);
}
return ctx;
}
void send_msg(server *s, message_ptr msg)
{
for (auto it = vgdl.begin(); it != vgdl.end();)
{
if (!it->expired())
{
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())
{
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 handleJoin(server* s, websocketpp::connection_hdl hdl, json JMsg) {
// 解析JSON
std::string uid = JMsg["uid"];
std::string roomStr = JMsg["roomId"];
int roomId = stoi(roomStr);
std::cout << "uid = " << uid << " try to join roomId = " << roomId << std::endl;
// 获取房间信息
// 房间不存在
if (!room_map.count(roomId))
{
Client client = {uid, roomId, hdl};
room_map[roomId].push_back(client);
}
else
{
// 房间人数>=2,不允许加入
if (room_map[roomId].size() >= 2)
{
std::cout << "roomId = " << roomId << "is full" << std::endl;
return;
}
// 房间人数==1,加入房间,通知对端
else if (room_map[roomId].size() == 1)
{
Client client = {uid, roomId, hdl};
room_map[roomId].push_back(client);
// 处理信令
Client remoteClient = room_map[roomId][0];
// 告知加入者对端的信息
json sendMsg;
sendMsg["cmd"] = SIGNAL_TYPE_RESP_JOIN;
sendMsg["remoteUid"] = remoteClient.uid;
std::string sendMsgStr = sendMsg.dump();
s->send(client.hdl, sendMsgStr, websocketpp::frame::opcode::text);
std::cout << "resp_join uid = " << remoteClient.uid << std::endl;
std::cout << "sendMsgStr = " << sendMsgStr << std::endl;
// 告知对端加入者的身份
json sendMsg2;
sendMsg2["cmd"] = SIGNAL_TYPE_NEW_PEER;
sendMsg2["remoteUid"] = uid;
std::string sendMsgStr2 = sendMsg2.dump();
s->send(remoteClient.hdl, sendMsgStr2, websocketpp::frame::opcode::text);
std::cout << "new_peer uid = " << uid << std::endl;
std::cout << "sendMsgStr = " << sendMsgStr2 << std::endl;
}
}
}
void handleLeave(server* s, websocketpp::connection_hdl hdl, json JMsg) {
std::string roomStr = JMsg["roomId"];
int roomId = stoi(roomStr);
std::string uid = JMsg["uid"];
std::cout << "uid = " << uid << " try to leave roomId = " << roomId << std::endl;
// 找不到房间
if (!room_map.count(roomId))
{
std::cout << "房间不存在 !" << std::endl;
return;
}
else
{
// 房间内只有一个人,删除房间
if (room_map[roomId].size() == 1)
{
room_map.erase(roomId);
std::cout << "erase roomId = " << roomId << "success" << std::endl;
}
// 房间有两个人,通知对端离开
else if (room_map[roomId].size() == 2)
{
// 删除用户信息
auto iter = std::find_if(room_map[roomId].begin(), room_map[roomId].end(), [&uid](const Client &client)
{ return client.uid == uid; });
if (iter != room_map[roomId].end())
{
room_map[roomId].erase(iter);
std::cout << "erase uid = " << uid << "success" << std::endl;
}
// 发送JSON消息
json sendMsg;
sendMsg["cmd"] = SIGNAL_TYPE_PEER_LEAVE;
sendMsg["remoteUid"] = uid;
std::string sendMsgStr = sendMsg.dump();
// 只有一个人了,使用room_map[roomId][0]
s->send(room_map[roomId][0].hdl, sendMsgStr, websocketpp::frame::opcode::text);
std::cout << "resp_leave uid = " << uid << std::endl;
std::cout << "sendMsgStr = " << sendMsgStr << std::endl;
}
}
}
// offer
void handleOffer(server *s, websocketpp::connection_hdl hdl, json JMsg)
{
std::string roomStr = JMsg["roomId"];
int roomId = stoi(roomStr);
std::string uid = JMsg["uid"];
std::string remoteUid = JMsg["remoteUid"];
std::cout << "uid = " << uid << " try to send offer to remoteUid = " << remoteUid << std::endl;
// 房间号不存在
if (!room_map.count(roomId))
{
std::cout << "roomId = " << roomId << "not exist" << std::endl;
return;
}
// 房间没人
else if (room_map[roomId].size() == 0)
{
std::cout << "roomId = " << roomId << "is empty" << std::endl;
return;
}
else
{
// 转发offer到对端
auto remoteClientIter = std::find_if(room_map[roomId].begin(), room_map[roomId].end(), [&remoteUid](const Client &client)
{ return client.uid == remoteUid; });
if (remoteClientIter != room_map[roomId].end())
{
std::cout << "send offer from " << uid << " to " << remoteUid << std::endl;
s->send(remoteClientIter->hdl, JMsg.dump(), websocketpp::frame::opcode::text);
}
else
{
std::cout << "remoteUid = " << remoteUid << "not exist" << std::endl;
}
}
}
// answer
void handleAnswer(server *s, websocketpp::connection_hdl hdl, json JMsg)
{
std::string roomStr = JMsg["roomId"];
int roomId = stoi(roomStr);
std::string uid = JMsg["uid"];
std::string remoteUid = JMsg["remoteUid"];
std::cout << "uid = " << uid << " try to send answer to remoteUid = " << remoteUid << std::endl;
// 房间号不存在
if (!room_map.count(roomId))
{
std::cout << "roomId = " << roomId << "not exist" << std::endl;
return;
}
// 房间没人
else if (room_map[roomId].size() == 0)
{
std::cout << "roomId = " << roomId << "is empty" << std::endl;
return;
}
else
{
// 转发answer到对端
auto remoteClientIter = std::find_if(room_map[roomId].begin(), room_map[roomId].end(), [&remoteUid](const Client &client)
{ return client.uid == remoteUid; });
if (remoteClientIter != room_map[roomId].end())
{
std::cout << "send answer from " << uid << " to " << remoteUid << std::endl;
s->send(remoteClientIter->hdl, JMsg.dump(), websocketpp::frame::opcode::text);
}
else
{
std::cout << "remoteUid = " << remoteUid << "not exist" << std::endl;
}
}
}
// candidate
void handleCandidate(server *s, websocketpp::connection_hdl hdl, json JMsg)
{
std::string roomStr = JMsg["roomId"];
int roomId = stoi(roomStr);
std::string uid = JMsg["uid"];
std::string remoteUid = JMsg["remoteUid"];
std::cout << "uid = " << uid << " try to send candidate to remoteUid = " << remoteUid << std::endl;
// 房间号不存在
if (!room_map.count(roomId))
{
std::cout << "roomId = " << roomId << "not exist" << std::endl;
return;
}
// 房间没人
else if (room_map[roomId].size() == 0)
{
std::cout << "roomId = " << roomId << "is empty" << std::endl;
return;
}
else
{
// 转发candidate到对端
auto remoteClientIter = std::find_if(room_map[roomId].begin(), room_map[roomId].end(), [&remoteUid](const Client &client)
{ return client.uid == remoteUid; });
if (remoteClientIter != room_map[roomId].end())
{
std::cout << " send candidate from " << uid << " to " << remoteUid << std::endl;
s->send(remoteClientIter->hdl, JMsg.dump(), websocketpp::frame::opcode::text);
}
else
{
std::cout << "remoteUid = " << remoteUid << "not exist" << std::endl;
}
}
}
void on_message(server* s, websocketpp::connection_hdl hdl, message_ptr msg) {
// 解析客户端的json消息
json JMsg;
try
{
JMsg = json::parse(msg->get_payload());
std::cout << "on_message called with hdl: " << hdl.lock().get()
<< " and message: " << JMsg.dump() << std::endl;
std::string cmd = JMsg["cmd"];
if (cmd == SIGNAL_TYPE_JOIN)
{
handleJoin(s, hdl, JMsg); // 加入
}
else if (cmd == SIGNAL_TYPE_LEAVE)
{
handleLeave(s, hdl, JMsg); // 离开
}
else if (cmd == SIGNAL_TYPE_OFFER)
{
handleOffer(s, hdl, JMsg); // ice候选
}
else if (cmd == SIGNAL_TYPE_ANSWER)
{
handleAnswer(s, hdl, JMsg);
}
else if (cmd == SIGNAL_TYPE_CANDIDATE)
{
handleCandidate(s, hdl, JMsg);
}
}
catch (const std::exception &e)
{
std::cout << "JSON解析失败: " << e.what() << std::endl;
return;
}
}
void on_open(server* s, websocketpp::connection_hdl hdl) {
vgdl.push_back(hdl);
std::cout << "on_open called with hdl: " << hdl.lock().get() << std::endl;
}
void on_close(server* s, websocketpp::connection_hdl hdl) {
std::string msg = "close OK";
printf("%s\n", msg.c_str());
std::cout << "vgdl size = " << vgdl.size() << std::endl;
// 清理连接列表
for (auto it = vgdl.begin(); it != vgdl.end();)
{
std::cout << "it = " << it->lock() << std::endl;
if (it->expired() || it->lock() == hdl.lock()) //断开自己
{
it = vgdl.erase(it);
std::cout << "vgdl erase" << std::endl;
}
else
{
++it;
}
}
// 遍历 room_map,删除对应客户端信息
for (auto roomIt = room_map.begin(); roomIt != room_map.end();)
{
auto &clients = roomIt->second;
bool isErase = false;
for (auto clientIt = clients.begin(); clientIt != clients.end();)
{
if (clientIt->hdl.expired() || clientIt->hdl.lock() == hdl.lock())
{ // 连接过期
std::cout << "client uid = " << clientIt->uid << " has been removed from roomid = "
<< clientIt->roomId << std::endl;
clientIt = clients.erase(clientIt);
isErase = true;
}
else
{
++clientIt;
}
}
if(!isErase){
continue;
}
// 如果房间为空,删除房间
if (clients.empty())
{
std::cout << "roomId = " << (roomIt->first) << " has been removed" << std::endl;
roomIt = room_map.erase(roomIt);
}
else
{
//向对端发送离开消息
json sendMsg;
sendMsg["cmd"] = SIGNAL_TYPE_PEER_LEAVE;
sendMsg["remoteUid"] = roomIt->second[0].uid;
send_msg(&webSocket_server, sendMsg.dump());
++roomIt;
}
}
}
int main() {
try {
webSocket_server.set_access_channels(websocketpp::log::alevel::none);
webSocket_server.clear_access_channels(websocketpp::log::alevel::all);
// 初始化ASIO
webSocket_server.init_asio();
// 设置TLS处理器
webSocket_server.set_tls_init_handler(bind(&on_tls_init));
// 注册回调函数
webSocket_server.set_open_handler(bind(&on_open, &webSocket_server, ::_1));
webSocket_server.set_close_handler(bind(&on_close, &webSocket_server, ::_1));
webSocket_server.set_message_handler(bind(&on_message, &webSocket_server, ::_1, ::_2));
// 监听端口
webSocket_server.listen(LISTEN_PORT);
webSocket_server.start_accept();
std::cout << "WSS服务器运行在端口 " << LISTEN_PORT << std::endl;
webSocket_server.run();
}
catch (const std::exception& e) {
std::cerr << "服务器异常: " << e.what() << std::endl;
}
return 0;
}