【網絡編程】WebSocket 實現簡易Web多人聊天室

一、實現思路

  • Web端就是使用html + JavaScript來實現頁面,通過WebSocket長連接和服務器保持通訊,協議的payload使用JSON格式封裝

  • 服務端使用C++配合第三方庫WebSocket++nlonlohmann庫來實現

二、Web端

2.1 界面顯示

首先,使用html來設計一個簡單的靜態框架:

  • 有一個聊天室的標題
  • 然后一個文本框加上一個發送按鈕
<body><h1>WebSocket簡易聊天室</h1><div id="app"><input id="sendMsg" type="text" /><button id="sendBtn">發送</button></div>
</body>

然后我們還需要顯示聊天信息,我們可以利用腳本每次有信息的時候,就把這個信息嵌入到<body>里面,這個信息本身存放在<div>里面,如下:

  • 加入房間顯示藍色
  • 離開房間顯示紅色
  • 聊天信息顯示黑色
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服務器

var websocket = new WebSocket('ws://192.168.217.128:9002');

我們需要實現WebSocket的幾個回調函數:

  1. onopen
  • 這個回調函數在連接服務器成功時觸發,我們在連接成功的時候,綁定上發送按鈕的點擊事件
// 連接成功
websocket.onopen = function () {console.log("連接服務器成功");document.getElementById("sendBtn").onclick = function () {var msg = document.getElementById("sendMsg").value;if (msg) {websocket.send(msg);}};
};
  1. onmessage
  • 這個回調函數在接收到服務端的消息后觸發,這里是JSON格式,我們服務端定義了datakey,對應的消息就是data后面的value
websocket.onmessage = function (e) {var mes = JSON.parse(e.data);showMessage(mes.data, mes.type);
};
  1. onclose
  • 這個回調函數在連接斷開的時候觸發,在這里我們簡單的打印一下即可:
// 連接關閉
websocket.onclose = function (event) {console.log("連接已關閉", "代碼:", event.code, "原因:", event.reason);
};

2.3 完整代碼

完整的Web代碼如下:

<!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如下:

// 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 endpointserver echo_server;try{// Set logging settings 設置logecho_server.set_access_channels(websocketpp::log::alevel::all);echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);// Initialize Asio 初始化asioecho_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 9002echo_server.listen(9002);//監聽端口// Start the server accept loopecho_server.start_accept();// Start the ASIO io_service run loopecho_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

  • 因此我們可以把它映射到每一個用戶名,這樣我們就知道發送過來的連接句柄對應是哪個用戶了,實際這里我們每次連接都分配了一個用戶名:

std::string username = "user:" + std::to_string(++totalUser);

這里我們使用std::map進行映射,由于websocketpp::connection_hdl類型不具備運算符<,因此我們自己寫一個仿函數,里面用它內部的weak_ptr進行比較

struct ConnectionHdlCompare {bool operator()(const websocketpp::connection_hdl& a, const websocketpp::connection_hdl& b) const {// 通過獲取底層的弱指針的原始指針進行比較return a.lock() < b.lock();}
};

然后這樣定義std::map

std::map<websocketpp::connection_hdl, std::string, ConnectionHdlCompare> user_map; 

3.3 連接事件

  • 每個用戶連接的時候,需要分配一個唯一的用戶名,然后廣播給全部用戶,該用戶加入房間了,我們使用JSON進行序列化,類型是enter,同時,把這個連接句柄加入全局的鏈表中
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,使用的是text

    • 0x0:延續幀(用于分片傳輸大消息,當前幀是消息的中間部分);
    • 0x1:文本幀(payload 是 UTF-8 編碼的文本數據);
    • 0x2:二進制幀(payload 是任意二進制數據,如圖片、protobuf 等);
    • 0x8:關閉幀(通知對方關閉連接,payload 可包含關閉原因);
    • 0x9:Ping 幀(心跳檢測,用于確認連接活性);
    • 0xA:Pong 幀(響應 Ping 幀的心跳回復)。
  • 遍歷的時候,需要判斷是否指針失效了,因為weak_ptr本身并不能管理對象,需要轉換為shared_ptr來查看,可以調用expire()函數來看看是否為nullptr,我們只對有效的連接發送消息,無效的連接直接從鏈表刪除這個句柄

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序列化的typeleave

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序列化的typemessage,發送的時候需要加上用戶名,這樣前端顯示才有用戶名:

  • 這里是通過修改message_ptrpayload的形式來發送消息的,因此我們調用send_msg是第一個重載版本

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 完整代碼

完整的服務端代碼如下:

// 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 設置logecho_server.set_access_channels(websocketpp::log::alevel::all);echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);// Initialize Asio 初始化asioecho_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 9002echo_server.listen(9002);//監聽端口// Start the server accept loopecho_server.start_accept();// Start the ASIO io_service run loopecho_server.run();}catch (websocketpp::exception const &e){std::cout << e.what() << std::endl;}catch (...){std::cout << "other exception" << std::endl;}
}

四、運行結果

編譯服務端并啟動:

g++ main.cpp -o main -lboost_system -lboost_chrono
./main

在兩個瀏覽器中打開我們的Web服務端,這里是本地的,所以是

http://127.0.0.1:5500/chatClient.html

兩個用戶加入房間、聊天、離開的效果有不同的顏色,如下所示

在這里插入圖片描述

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91588.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91588.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91588.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AI 驅動、設施擴展、驗證器強化、上線 EVM 測試網,Injective 近期動態全更新!

作為一個專注于金融應用、且具有高度可互操作性的高性能 Layer-1 區塊鏈&#xff0c;Injective 自誕生以來便為開發者提供有即插即用的技術模塊&#xff0c;以便開發者能夠更好地搭建新一代 Web3 金融類應用。談及項目發展的愿景和基本定位&#xff0c;創始團隊曾提到希望 Inje…

Qt-----初識

1. 什么是Qt定義&#xff1a;Qt是一個跨平臺的應用程序和用戶界面框架&#xff0c;主要用于開發具有圖形用戶界面的應用程序&#xff0c;同時也支持非GUI程序的開發。 編程語言&#xff1a;主要使用C&#xff0c;但也提供了對Python&#xff08;PyQt&#xff09;、JavaScript&a…

理解微信體系中的 AppID、OpenID 和 UnionID

前言: 在開發微信相關的服務(如小程序,公眾號,微信開放平臺等)時,很多人都會接觸到幾個看起來相似但實際用途不同的額ID: AppiD, OpenID,UnionID. 搞清楚這三者的區別,是微信生態開發中的基本功,本文將從開發者視角觸發,深入淺出地解釋它們的關系,區別以及實際應用場景一.什么是…

FFmpeg,如何插入SEI自定義數據

FFmpeg&#xff0c;如何插入SEI自定義數據 一、什么是SEI&#xff1f; SEI&#xff08;Supplemental Enhancement Information&#xff0c;補充增強信息&#xff09;是H.264/H.265視頻編碼標準中的一種元數據載體&#xff0c;它允許在視頻流中嵌入額外的信息&#xff0c;如時…

為什么分類任務偏愛交叉熵?MSE 為何折戟?

在機器學習的世界里&#xff0c;損失函數是模型的“指南針”——它定義了模型“好壞”的標準&#xff0c;直接決定了參數優化的方向。對于分類任務&#xff08;比如判斷一張圖片是貓還是狗&#xff09;&#xff0c;我們通常會選擇交叉熵作為損失函數&#xff1b;而在回歸任務&a…

[echarts]橫向柱狀圖

前言 接到一個需求&#xff0c;需要展示一個橫向的柱狀圖&#xff0c;按數量從大到小排序&#xff0c;并定時刷新 使用react配合echarts進行實現。 react引入echarts import React, { useEffect, useRef } from react; import * as echarts from echarts; import DeviceApi fro…

【開源項目】輕量加速利器 HubProxy 自建 Docker、GitHub 下載加速服務

??引言?? 如果你經常被 Docker 鏡像拉取、GitHub 文件下載的龜速折磨&#xff0c;又不想依賴第三方加速服務&#xff08;擔心穩定性或隱私&#xff09;&#xff0c;今天分享的 ??HubProxy?? 可能正是你需要的。這個開源工具用一行命令就能部署&#xff0c;以極低資源消…

java web jsp jstl練習

JSP 的學習。 核心功能模塊 1. 源代碼層 &#xff08; src &#xff09; HelloWorld &#xff1a;主程序入口領域模型 &#xff1a; domain 包含User.java和ceshi.java控制器 &#xff1a; servlet 包含登錄驗證和驗證碼相關ServletWeb表現層 &#xff08; web &#xff09; JS…

VSCode 完全指南:釋放你的編碼潛能

零、簡介 在當今的軟件開發領域&#xff0c;代碼編輯器的選擇至關重要&#xff0c;它就像是工匠手中的工具&#xff0c;直接影響著工作效率和成果質量。Visual Studio Code&#xff08;簡稱 VSCode&#xff09;自問世以來&#xff0c;迅速在全球開發者社區中嶄露頭角&#xff…

《n8n基礎教學》第一節:如何使用編輯器UI界面

在本課中&#xff0c;你將學習如何操作編輯器界面。我們將瀏覽畫布&#xff0c;向您展示每個圖標的含義&#xff0c;以及在 n8n 中構建工作流程時在哪里可以找到您需要的東西。本課程基于 n8n 最新版本 。在其他版本中&#xff0c;某些用戶界面可能有所不同&#xff0c;但這不會…

gcc g++ makefile CMakeLists.txt cmake make 的關系

gcc&#xff1a;C語言編譯器g&#xff1a;C編譯器makefile&#xff1a;定義編譯規則、依賴關系和構建目標。可以手動編寫&#xff0c;也可以由CMakeLists.txt生成cmake&#xff1a;讀取CMakeLists.txt文件&#xff0c;生成Makefilemake&#xff1a;構建工具&#xff0c;執行Mak…

SFT 訓練器

SFT 訓練器 “訓練時間到!” 我們現在終于可以創建一個監督微調訓練器的實例了: trainer = SFTTrainer( model=model, processing_class=tokenizer, args=sft_config, train_dataset=dataset, )SFTTrainer 已經對數據集進行了預處理,因此我們可以深入查看,了解每個小批次…

Android Material Components 全面解析:打造現代化 Material Design 應用

引言 在當今移動應用開發領域&#xff0c;用戶體驗(UX)已成為決定應用成功與否的關鍵因素之一。Google推出的Material Design設計語言為開發者提供了一套完整的視覺、交互和動效規范&#xff0c;而Material Components for Android(MDC-Android)則是將這些設計理念轉化為可重用…

Windows使用Powershell自動安裝SqlServer2025服務器與SSMS管理工具

安裝結果: 安裝前準備: 1.下載mssql server 2025安裝器 2.下載iso鏡像 3.下載好SSMS安裝程序,并放到iso同目錄下 4.執行腳本開始自動安裝

09 RK3568 Debian11 ES8388 模擬音頻輸出

1、設備樹配置 確認自己的i2c,使用sdk帶的驅動es8323 /SDK/kernel/sound/soc/codecs/es8323.c es8388_sound: es8388-sound {status = "okay";compatible = "rockchip,multicodecs-card"; rockchip,card-name = "rockchip,es8388-codec"; …

力扣-199.二叉樹的右視圖

題目鏈接 199.二叉樹的右視圖 class Solution {public List<Integer> rightSideView(TreeNode root) {List<Integer> res new ArrayList<>();Queue<TreeNode> queue new LinkedList<>();if (root null)return res;queue.offer(root);while …

Android Bitmap 完全指南:從基礎到高級優化

在 Android 開發中&#xff0c;圖像處理是一個核心且復雜的領域&#xff0c;而 Bitmap 作為 Android 中表示圖像的基本單位&#xff0c;貫穿了從簡單圖片顯示到復雜圖像編輯的各個場景。然而&#xff0c;Bitmap 處理不當往往會導致應用性能下降、內存溢出&#xff08;OOM&#…

unity日志過濾器

背景&#xff1a;之前做游戲的時候和同組的同事聊過說日志過濾盡量不要限制大家怎么使用日志打印的接口&#xff0c;不要加額外的參數&#xff0c;比如多加一個標簽string,或者使用特定的接口&#xff0c;枚舉。最好就是日志大家還是用Debug.Log無感去用&#xff0c;然后通過勾…

OpenGL Camera

一. lookAt函數的參數含義glm::mat4 view glm::lookAt(cameraPos, // 相機在世界坐標系中的位置&#xff08;任意值&#xff09;cameraPos cameraFront, // 相機看向的目標點&#xff08;位置朝向&#xff09;cameraUp // 相機的"上方向"&#xff08;通…

Android RTMP推送|輕量級RTSP服務同屏實踐:屏幕+音頻+錄像全鏈路落地方案

一、背景&#xff1a;從“移動終端”到“遠程協作節點”&#xff0c;同屏音頻錄像為何成剛需&#xff1f; 在數字化辦公、智慧醫療與遠程教育等快速發展的推動下&#xff0c;手機作為隨身終端&#xff0c;已不再只是“內容接收者”&#xff0c;而逐步成為遠程信息發布與可視化…