【音视频】WebRTC 一对一通话-部署到公网

一、编译nginx

具体的步骤如下:

  1. 安装依赖库
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
  1. 下载nginx1.15.8版本
shell 复制代码
wget http://nginx.org/download/nginx‐1.15.8.tar.gz
  1. 解压、编译、安装

安装后的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

二、生成证书

由于使用httpswss需要使用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是不安全的连接,类似httpwss是安全的连接,类似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 服务器

  • coturnGoogle开源的服务器,我之前的博客有介绍如何搭建

  • coturn服务器提供了stunturn两个功能,分别用于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.asiossl/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;
}

更多资料:https://github.com/0voice