文章目錄
- 1、簡介
- 2、客戶端設計
- 3、服務器設計
- 3.1、session函數
- 3.2、StartListen函數
- 3、總體設計
- 4、效果測試
- 5、遇到的問題
- 5.1、服務器遇到的問題
- 5.1.1、不用顯示調用bind綁定和listen監聽函數
- 5.1.2、出現 Error occured!Error code : 10009 .Message: 提供的文件句柄無效。 [system:10009]
- 5.2、 發送普通的消息如數字12或者字符串可以 如果發送結構體協議之類的為啥要用protobuf
- 5.2.1、修改字符串或者數字消息改成類或者更為復雜的對象
- 5.3、Error occured!Error code : 10054 .Message: 遠程主機強迫關閉了一個現有的連接。 [system:10054]
- 5.4、std::shared_ptr<std::thread> t = std::make_shared<std::thread>()與()中加上函數區別以及用法
- 5.5、void Server::Session(std::shared_ptr<<boost::asio::ip::tcp::socket>> socket,uint32_t user_id)為啥read_some要寫在for循環里面
- 6、std::make_shared以及std::shared_ptr
- 6.1、shared_ptr對象創建方法
- 6.1.2、這兩種方法都有哪些不同的特性
- 6.1.3、這兩種創建方式有什么區別
- 6.1.4、std::make_shared的三個優點
- 6.1.5、使用make_shared的缺點
- 7、總結:同步讀寫的優劣
1、簡介
前面我們介紹了boost::asio同步讀寫的api函數,現在將前面的api串聯起來,做一個能跑起來的客戶端和服務器。客戶端和服務器采用阻塞的同步讀寫方式完成通信。
2、客戶端設計
客戶端設計基本思路是根據服務器對端的ip和端口創建一個endpoint,然后創建socket連接這個endpoint,之后就可以用同步讀寫的方式發送和接收數據了。
- 創建端點 (ip+端口) 。
- 創建socket。
- socket連接端點。
- 發送或者接收數據。
client.h:
#pragma once
#ifndef __CLIENT_H_2023_8_16__
#define __CLIENT_H_2023_8_16__#include<iostream>
#include<boost/asio.hpp>
#include<string>#define Ip "127.0.0.1"
#define Port 9273
#define Buffer 1024class Client {
public:Client();bool StartConnect();private:std::string ip_;uint16_t port_;
};#endif
client.cpp:
#include"client.h"Client::Client() {ip_ = Ip;port_ = Port;
}bool Client::StartConnect() {try {//Step1: create endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(ip_), port_);//Step2: create socketboost::asio::io_context context;boost::asio::ip::tcp::socket socket(context, ep.protocol());//Step3: socket connect endpointboost::system::error_code error = boost::asio::error::host_not_found;socket.connect(ep, error);if (error) {std::cout << "connect failed,error code is: " << error.value() << " .error message is:" << error.message() << std::endl;return false;}else {std::cout << "connect successed!" << std::endl;}while (true) {//Step4: send messagestd::cout << "Enter message:";char req[Buffer];std::cin.getline(req, Buffer);size_t req_length = strlen(req);socket.send(boost::asio::buffer(req, req_length));//Step5: receive messagechar ack[Buffer];size_t ack_length = socket.receive(boost::asio::buffer(ack, req_length));std::cout << "receive message: " << ack << std::endl;}}catch (boost::system::system_error& e) {std::cout << "Error occured!Error code: " << e.code().value() << ". Message: " << e.what() << std::endl;return e.code().value();}return true;
}
- 這段代碼是一個用于客戶端的C++程序,使用了之前定義的 Client 類來與服務器建立連接并進行通信。下面逐行解釋代碼的作用:
-
#include “client.h”: 包含了你的客戶端類的頭文件,以便在此文件中使用該類。
-
Client::Client(): 這是客戶端類的構造函數,初始化了 ip_ 和 port_ 成員變量。
-
bool Client::StartConnect(): 這是一個成員函數,用于開始連接服務器并執行通信。
-
try: 開始一個異常處理塊,用于捕獲可能出現的異常。
-
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(ip_), port_);: 創建一個 TCP 端點,指定連接的服務器 IP 地址和端口號。
-
boost::asio::io_context context;: 創建一個 I/O 上下文對象,它用于管理異步 I/O 操作。
-
boost::asio::ip::tcp::socket socket(context, ep.protocol());: 創建一個 TCP 套接字,使用之前創建的 I/O 上下文和指定的協議。
-
boost::system::error_code error = boost::asio::error::host_not_found;: 創建一個錯誤代碼對象,并初始化為主機未找到的錯誤代碼,作為連接的初始狀態。
-
socket.connect(ep, error);: 嘗試連接服務器,如果連接失敗,錯誤代碼將被更新以反映連接錯誤的信息。
-
if (error): 檢查錯誤代碼,如果不為 0,表示連接失敗。
-
std::cout << “connect failed,error code is: " << error.value() << " .error message is:” << error.message() << std::endl;: 輸出連接失敗的錯誤代碼和錯誤消息。
-
else: 如果連接成功,進入此分支。
-
while (true): 無限循環,用于不斷地進行消息發送和接收。
-
std::cout << “Enter message:”;: 提示用戶輸入消息。
-
char req[Buffer];: 創建一個字符數組,用于存儲用戶輸入的消息。
-
std::cin.getline(req, Buffer);: 從用戶輸入讀取一行消息。
-
size_t req_length = strlen(req);: 獲取用戶輸入消息的長度。
-
socket.send(boost::asio::buffer(req, req_length));: 將用戶輸入的消息發送給服務器。
-
char ack[Buffer];: 創建一個字符數組,用于接收服務器返回的消息。
-
size_t ack_length = socket.receive(boost::asio::buffer(ack, req_length));: 接收服務器返回的消息。
-
std::cout << "receive message: " << ack << std::endl;: 輸出接收到的消息。
-
catch (boost::system::system_error& e): 捕獲異常,如果發生異常,進入此分支。
-
std::cout << "Error occured!Error code: " << e.code().value() << ". Message: " << e.what() << std::endl;: 輸出異常信息,包括錯誤代碼和錯誤消息。
-
return e.code().value();: 返回異常中的錯誤代碼。
-
return true;: 如果沒有異常,返回 true,表示通信成功。
-
綜上所述,這段代碼創建一個客戶端對象,連接到服務器并實現了一個簡單的循環,允許用戶輸入消息并將其發送給服務器,然后接收并顯示服務器返回的消息。同時,它還能處理連接和通信過程中可能出現的異常情況。
main.cpp:
#include"client.h"int main() {Client client;if (client.StartConnect()) {;}return 0;
}
-
這段代碼是一個使用你之前編寫的客戶端類的主函數。讓我為你解釋每個部分的作用:
-
#include “client.h”: 這一行包含了你的客戶端類的頭文件,使得你可以在主函數中使用該類。
-
int main(): 這是程序的主函數,它是程序的入口點。所有的代碼將從這里開始執行。
-
Client client;: 在這一行,你創建了一個名為 client 的客戶端對象,使用了之前你定義的 Client 類的構造函數。
-
if (client.StartConnect()) { … }: 這一行開始一個條件語句。client.StartConnect() 被調用,它會嘗試與服務器建立連接并執行通信。如果連接成功并且通信正常,StartConnect() 函數將會返回 true,進入條件成立的分支。
-
;: 這是一個空語句,什么也不做。在你的代碼中似乎沒有實際操作需要執行,因此這里用一個空語句表示。
-
return 0;: 這一行是主函數的最后一行,它告訴程序在主函數結束后返回狀態碼 0,表示程序正常退出。
-
綜上所述,這段代碼創建了一個客戶端對象并調用其 StartConnect() 函數來連接服務器并進行通信。然后程序會以狀態碼 0 正常退出。如果連接或通信出現問題,你可以在適當的位置添加錯誤處理代碼。
3、服務器設計
3.1、session函數
創建session函數,該函數為服務器處理客戶端請求,每當我們獲取客戶端連接后就調用該函數。在session函數里里進行echo方式的讀寫,所謂echo就是應答式的處理(請求和響應)。
void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id) {try {for (;;) {char ack[Buffer];memset(ack, '\0', Buffer);boost::system::error_code error;size_t length = socket->read_some(boost::asio::buffer(ack, Buffer), error);if (error == boost::asio::error::eof) {std::cout << "the usred_id "<<user_id<<"connect close by peer!" << std::endl;socket->close();break;}else if (error) {throw boost::system::system_error(error);}else {if (socket->is_open()) {std::cout << "the usre_id " << user_id << " ip " << socket->remote_endpoint().address();std::cout << " send message: " << ack << std::endl;socket->send(boost::asio::buffer(ack, length));}}}}catch (boost::system::system_error& e) {std::cout << "Error occured ! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;}
}
3.2、StartListen函數
StartListen函數根據服務器ip和端口創建服務器acceptor用來接收數據,用socket接收新的連接,然后為這個socket創建session。
bool Server::StartListen(boost::asio::io_context& context) {//create endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);//create acceptorboost::asio::ip::tcp::acceptor accept(context, ep);//acceptor bind endport//accept.bind(ep);//acceptor listen/*accept.listen(30);*/std::cout << "start listen:" << std::endl;for (;;) {std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));accept.accept(*socket);user_id_ = user_id_ + 1;std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;//auto t = std::make_shared<std::thread>([&]() {// this->Session(socket);// });auto t = std::make_shared<std::thread>([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;
}
創建線程調用session函數可以分配獨立的線程用于socket的讀寫,保證acceptor不會因為socket的讀寫而阻塞。
3、總體設計
server.h:
#pragma once
#ifndef __SERVER_H_2023_8_16__
#define __SERVER_H_2023_8_16__#include<iostream>
#include<boost/asio.hpp>
#include<string>
#include<set>#define Port 9273
#define Buffer 1024
#define SIZE 30class Server {
public:Server();bool StartListen(boost::asio::io_context& context);void Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id);std::set<std::shared_ptr<std::thread>>& GetSet() {return thread_set_;}
private:uint16_t port_;uint32_t user_id_;std::set<std::shared_ptr<std::thread>> thread_set_;
};#endif
server.cp:
#include"server.h"Server::Server() {port_ = Port;user_id_ = 0;thread_set_.clear();
}void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id) {try {for (;;) {char ack[Buffer];memset(ack, '\0', Buffer);boost::system::error_code error;size_t length = socket->read_some(boost::asio::buffer(ack, Buffer), error);if (error == boost::asio::error::eof) {std::cout << "the usred_id "<<user_id<<"connect close by peer!" << std::endl;socket->close();break;}else if (error) {throw boost::system::system_error(error);}else {if (socket->is_open()) {std::cout << "the usre_id " << user_id << " ip " << socket->remote_endpoint().address();std::cout << " send message: " << ack << std::endl;socket->send(boost::asio::buffer(ack, length));}}}}catch (boost::system::system_error& e) {std::cout << "Error occured ! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;}
}bool Server::StartListen(boost::asio::io_context& context) {//create endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);//create acceptorboost::asio::ip::tcp::acceptor accept(context, ep);//acceptor bind endport//accept.bind(ep);//acceptor listen/*accept.listen(30);*/std::cout << "start listen:" << std::endl;for (;;) {std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));accept.accept(*socket);user_id_ = user_id_ + 1;std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;//auto t = std::make_shared<std::thread>([&]() {// this->Session(socket);// });auto t = std::make_shared<std::thread>([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;
}
main.cpp:
#include"server.h"int main() {try {boost::asio::io_context context;Server server;server.StartListen(context);for (auto& t : server.GetSet()) {t->join();}}catch (std::exception& e) {std::cerr << "Exception " << e.what() << "\n";}return 0;
}
每次對端連接,服務器就會觸發accept的回調函數,從而創建session。至于session的讀寫事件觸發和server的accept觸發都是asio底層多路復用模型判斷事件就緒后幫我們回調的,目前是單線程模式,所以都是在主線程里觸發。
另外sever不退出,并不是因為sever存在循環,而是我們調用了iocontext的run函數,這個函數是asio底層提供的會循環派發就緒事件,
4、效果測試
5、遇到的問題
5.1、服務器遇到的問題
5.1.1、不用顯示調用bind綁定和listen監聽函數
兩種方式,早期boost acceptor可以綁定端口,后期boost優化了,初始化acceptor時直接指定端口就可以實現綁定和監聽。
StartListen函數:
bool Server::StartListen(boost::asio::io_context& context) {//create endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);//create acceptorboost::asio::ip::tcp::acceptor accept(context, ep);//acceptor bind endport//accept.bind(ep);//acceptor listen/*accept.listen(30);*/std::cout << "start listen:" << std::endl;for (;;) {std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));accept.accept(*socket);user_id_ = user_id_ + 1;std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;//auto t = std::make_shared<std::thread>([&]() {// this->Session(socket);// });auto t = std::make_shared<std::thread>([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;
}
-
bool Server::StartListen(boost::asio::io_context& context): 這是 Server 類的成員函數,用于啟動服務器的監聽過程。
-
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);: 創建一個 TCP 端點,使用 IPv4 地址和指定的端口號。
-
boost::asio::ip::tcp::acceptor accept(context, ep);: 創建一個 TCP 接收器,使用之前創建的 I/O 上下文和端點。
-
std::cout << “start listen:” << std::endl;: 輸出啟動監聽的消息。
-
for (;; ) { … }: 無限循環,用于不斷地等待客戶端連接并處理會話。
-
std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));: 創建一個指向 tcp::socket 的智能指針,用于處理與客戶端的連接。
-
*accept.accept(socket);: 等待并接受客戶端連接,將連接套接字賦給之前創建的 socket 對象。
-
user_id_ = user_id_ + 1;: 增加用戶ID,用于標識不同的連接。
-
std::cout << “the user_id “<<user_id_<<” client connect,the ip:” << socket->remote_endpoint().address() << std::endl;: 輸出客戶端連接的消息,包括用戶ID和客戶端的IP地址。
-
auto t = std::make_shared <std::thread> ([this, socket] { … });: 創建一個線程,用于處理客戶端會話。在線程中,通過 lambda 表達式調用 Session 函數,傳遞了當前的 socket 和用戶ID。
-
thread_set_.insert(t);: 將創建的線程添加到線程集合中,以便在主線程結束前等待它們完成。
-
return true;:返回 true,表示監聽過程已經啟動成功。
-
關于 accept.bind(ep) 和 accept.listen(30) 的注釋:
-
accept.bind(ep):在上面的代碼中,沒有調用這個方法,因為 accept 對象在創建時已經傳入了端點 ep,所以不需要再顯式綁定。綁定是指將套接字與特定 IP 地址和端口綁定,但在此情況下,已經在創建接收器時完成了綁定。
-
accept.listen(30): 同樣,在上述代碼中,沒有調用這個方法。listen() 方法用于將套接字置于監聽狀態,參數表示最大排隊連接數。但在此代碼中,調用 accept() 方法自動將套接字置于監聽狀態,等待客戶端連接,因此不需要顯式調用 listen() 方法。
-
5.1.2、出現 Error occured!Error code : 10009 .Message: 提供的文件句柄無效。 [system:10009]
start listen:
have client connect,the ip:127.0.0.1
Error occured!Error code : 10009 .Message: 提供的文件句柄無效。 [system:10009]
-
代碼仍然存在一些問題,導致在客戶端連接后發生 “提供的文件句柄無效” 錯誤。由于我無法直接在您的環境中運行代碼,以下是一些可能的原因和解決方法:
-
資源競爭: 由于多個線程同時訪問 socket 對象,可能會導致資源競爭和套接字狀態不一致。確保在對套接字進行讀取和寫入操作時進行適當的同步,使用互斥鎖等機制。
-
套接字生命周期: 確保在使用完 socket 后將其正確關閉。檢查您的代碼,確保每個線程在使用完套接字后都關閉了套接字。不要在一個線程中關閉套接字,然后在另一個線程中繼續使用。
-
句柄復用: 確保您的套接字沒有被多次使用或復用。如果在一個套接字已關閉的情況下再次嘗試使用它,可能會導致 “提供的文件句柄無效” 錯誤。
-
線程同步: 確保您的線程在執行完畢之前等待其他線程完成。在 main 函數中使用 t->join() 來等待所有線程完成執行。
-
其他錯誤情況:10009 錯誤可能有多種可能的情況,例如使用無效的套接字、套接字被關閉但仍在使用等。您可能需要詳細檢查錯誤代碼的上下文以了解更多信息。
-
綜上所述,問題可能是在多線程環境中正確管理套接字的生命周期和狀態所導致的。 仔細檢查您的代碼,確保在每個線程中正確使用和關閉套接字,并使用適當的同步機制來避免競爭條件。如果問題仍然存在,您可能需要更詳細地檢查每個線程中的代碼,以找出問題所在。
auto t = std::make_shared<std::thread>([this, socket]() {Session(socket);}); 為啥 auto t = std::make_shared<std::thread>([&]() {this->Session(socket);});傳引用不行
在代碼中,使用 [&] 來傳遞引用,但由于您正在使用異步線程來處理連接,引用的內容可能在后臺線程執行時已經失效,從而導致訪問無效的資源。這可能是導致錯誤的原因。
正確的做法是在 lambda 函數中捕獲參數 socket 通過值傳遞(而不是引用),這樣可以確保在線程執行時 socket 對象仍然有效。這就是代碼中的第一個示例所做的。
auto t = std::make_shared<std::thread>([this, socket]() {Session(socket);
});
-
這個問題可能與線程間的競爭條件有關。在C++中,當你在多線程環境下訪問共享的變量時,需要確保不會出現競爭條件,其中一個線程修改了資源,而另一個線程在訪問該資源時導致未定義行為。
-
在上述的兩種寫法中,都有可能遇到懸垂引用的問題。這是因為在Lambda函數中引用了外部變量(socket),但是在Lambda函數執行時,這個外部變量的生命周期可能已經結束,導致訪問無效的資源。
-
第一個寫法中:
- 通過捕獲socket的方式,socket對象會被復制到Lambda函數內部,因此不會出現失效的問題。
auto t = std::make_shared<std::thread>([this, socket]() {Session(socket);
});
- 第二個寫法中:
- 通過捕獲引用方式,socket對象的引用被傳遞到Lambda函數內部。但是,在后臺線程執行時,主線程可能已經結束或銷毀了socket對象,導致訪問無效的資源。
為了避免這些問題,通常建議在多線程編程中,要確保在線程訪問外部資源時,外部資源的生命周期不會在線程執行期間結束。可以通過合適的同步機制、生命周期管理和避免懸垂引用的方式來解決這類問題。
5.2、 發送普通的消息如數字12或者字符串可以 如果發送結構體協議之類的為啥要用protobuf
-
在網絡通信中,數據的傳輸需要考慮多個因素,包括數據的格式、序列化與反序列化、網絡字節順序等。當您只需要傳輸普通的消息、簡單的數據類型(如整數、字符串)時,可以直接使用原始的數據格式進行傳輸。但是,當您需要傳輸復雜的數據結構、對象、類、嵌套的數據等時,使用一種序列化協議可以更加方便、安全和高效。
-
Protocol Buffers(protobuf) 是一種流行的序列化庫,由Google開發,用于在不同平臺上進行結構化數據的序列化和反序列化。protobuf 提供了一種機制,可以將結構化的數據序列化為二進制格式,然后在不同的系統之間進行傳輸和解析。它具有以下優勢:
-
跨平臺和語言支持: Protocol Buffers 支持多種編程語言,包括 C++、Java、Python、C# 等,使得不同平臺上的應用可以方便地進行數據交換。
-
高效的序列化和反序列化: Protocol Buffers 的序列化和反序列化過程是高效的,生成的二進制數據較小,傳輸效率高。
-
版本兼容性: 當數據結構變化時,Protocol Buffers 提供了向后和向前兼容的機制,可以更容易地進行協議的演化和升級。
-
強類型支持: Protocol Buffers 使用明確定義的消息結構,強制使用者在編碼和解碼時遵循特定的消息格式,避免了一些錯誤。
-
如果您需要傳輸復雜的數據結構,特別是需要跨平臺和語言交換數據,使用 Protocol Buffers 是一個不錯的選擇。它提供了清晰的消息定義語法、高效的二進制序列化和反序列化,以及多種語言的支持。
5.2.1、修改字符串或者數字消息改成類或者更為復雜的對象
#include"server.h"Server::Server() {port_ = Port;user_id_ = 0;thread_set_.clear();
}void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id) {try {for (;;) {char ack[Buffer];memset(ack, '\0', Buffer);boost::system::error_code error;size_t length = socket->read_some(boost::asio::buffer(ack, Buffer), error);if (error == boost::asio::error::eof) {std::cout << "the usred_id "<<user_id<<"connect close by peer!" << std::endl;socket->close();break;}else if (error) {throw boost::system::system_error(error);}else {if (socket->is_open()) {std::cout << "the usre_id " << user_id << " ip " << socket->remote_endpoint().address();std::cout << " send message: " << ack << std::endl;socket->send(boost::asio::buffer(ack, length));}}}}catch (boost::system::system_error& e) {std::cout << "Error occured ! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;}
}bool Server::StartListen(boost::asio::io_context& context) {boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);boost::asio::ip::tcp::acceptor accept(context, ep);std::cout << "start listen:" << std::endl;for (;;) {std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));accept.accept(*socket);user_id_ = user_id_ + 1;std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;//auto t = std::make_shared<std::thread>([&]() {// this->Session(socket);// });auto t = std::make_shared<std::thread>([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;
}
- 要發送結構體或類的實例,您需要使用一種序列化庫,如 **Protocol Buffers(protobuf)**來將結構體或類序列化為字節流,然后在網絡中傳輸。下面是您如何將您的代碼修改為支持發送結構體或類實例:
- 定義結構體或類: 首先,您需要定義要發送的結構體或類。讓我們以一個示例結構體為例:
struct Message {int id;std::string content;
};
- 使用 Protocol Buffers: 在發送和接收數據時,使用 Protocol Buffers 進行序列化和反序列化。首先,定義一個 .proto 文件來描述消息的結構:
syntax = "proto3";message Message {int32 id = 1;string content = 2;
}
- 然后使用 Protocol Buffers 編譯器生成 C++ 代碼:
- 修改會話函數: 修改 Server::Session 函數,以支持序列化和反序列化結構體消息。
#include "message.pb.h" // Generated header from Protocol Buffers compiler// ...void Server::Session(socket_ptr socket) {try {for (;;) {Message received_message;char buffer[Buffer];memset(buffer, '\0', Buffer);boost::system::error_code error;size_t length = socket->read_some(boost::asio::buffer(buffer, Buffer), error);if (error == boost::asio::error::eof) {// 客戶端連接關閉std::cout << "connect close by peer!" << std::endl;break;}else if (error) {// 發生了其他錯誤throw boost::system::system_error(error);}else {// 成功讀取length個字節received_message.ParseFromArray(buffer, static_cast<int>(length));std::cout << "Received message from: " << socket->remote_endpoint().address() << std::endl;std::cout << "ID: " << received_message.id() << std::endl;std::cout << "Content: " << received_message.content() << std::endl;// 做出響應// ...// 將消息序列化并發送回客戶端std::string serialized_message;received_message.SerializeToString(&serialized_message);socket->send(boost::asio::buffer(serialized_message.c_str(), serialized_message.size()));}}}catch (boost::system::system_error& e) {std::cout << "Error occured! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;}
}
這樣,您的服務器會將接收到的序列化消息解析為 Message 結構體,并在接收到消息后將響應的序列化消息發送回客戶端。
請注意,上述示例代碼假定您已經使用 Protocol Buffers 定義了消息結構,并生成了相應的 C++ 代碼。確保包含正確的頭文件路徑,并根據您的實際結構體和消息格式進行適當的修改。
5.3、Error occured!Error code : 10054 .Message: 遠程主機強迫關閉了一個現有的連接。 [system:10054]
出現錯誤代碼 10054 “遠程主機強迫關閉了一個現有的連接”,通常是由于遠程主機(客戶端)關閉了與服務器的連接。這種情況可能是由于客戶端主動關閉連接,或者在網絡上發生了意外問題,導致連接意外中斷。
在代碼中,當客戶端關閉連接時,在 Session 函數中捕獲了 boost::asio::error::eof 錯誤,然后嘗試關閉 socket,并跳出循環。這部分的邏輯是正確的,應該導致服務器端關閉連接并正確處理。
然而,錯誤代碼 10054 可能是由多個因素引起的,包括網絡問題、超時、操作系統配置等。如果你確定代碼中處理連接關閉的邏輯正確,那么問題可能出在其他地方。
-
以下是一些可能的解決方案和調試方法:
-
檢查網絡連接: 確保你的網絡連接是穩定的,沒有丟包或者其他問題。
-
檢查客戶端: 如果問題只出現在特定的客戶端上,檢查客戶端的網絡配置和狀態,確保沒有異常。
-
檢查防火墻和安全軟件: 防火墻或安全軟件可能會干擾網絡連接,確保它們沒有阻止連接。
-
檢查超時設置: 如果服務器設置了超時,確保它合理且不會過早地關閉連接。
-
檢查服務器端資源: 如果服務器端連接數量過多,可能會導致資源耗盡。確保服務器有足夠的資源來處理連接。
-
捕獲異常: 在捕獲異常時,嘗試打印更多詳細信息,以便更好地了解問題所在。你可以輸出錯誤碼和錯誤信息來更好地排查問題。
-
日志和調試: 使用日志和調試工具來監視網絡連接和交互,以便更詳細地了解連接關閉的原因。
-
最終,錯誤代碼 10054 可能會有多種原因,需要進行綜合性的調查和排查。如果問題仍然存在,可能需要進一步考慮網絡配置、服務器端資源、連接超時設置等方面來進行排查。
5.4、std::shared_ptrstd::thread t = std::make_sharedstd::thread()與()中加上函數區別以及用法
auto t = std::make_shared<std::thread>();
-
這個代碼片段會嘗試創建一個 std::thread 對象,但沒有指定要執行的函數,因此它不會實際上創建一個新的線程。
-
在使用 std::make_shared 時,您通常用于創建智能指針,比如 std::shared_ptr。在這個上下文中,std::make_shared 創建一個std::thread對象并返回一個智能指針,但它需要指定要構造的對象的類型和構造參數。
-
對于 std::thread,它需要指定要執行的函數作為參數,以便在創建線程時開始執行。如果沒有指定要執行的函數,創建的 std::thread 對象沒有有效的工作任務,只是創建了一個std::thread對象。
-
auto t = std::make_shared<std::thread>(); 創建了一個名為 t 的 std::shared_ptr<std::thread> 對象,但是這里并沒有傳遞任何參數給 std::make_shared,因此沒有為線程指定要執行的函數。
通常情況下,創建一個線程需要指定一個可調用的函數或函數對象(例如函數指針、lambda 函數、類成員函數、普通函數等),以便在線程中執行。但是在這個代碼片段中,沒有提供這樣的可調用對象,因此這個線程實際上沒有有效的工作任務。這樣創建的線程對象是空閑的,沒有任何實際的工作內容。
std::shared_ptr<std::thread> t = std::make_shared<std::thread>([this, socket] {
Session(socket, user_id_);
});
-
每個客戶端的會話 (Session) 都在一個單獨的線程中運行,通過 std::thread 來創建。這意味著每個客戶端的會話都在獨立的線程中處理,互相之間不會阻塞。
-
當一個客戶端連接并發送消息時,會執行 Session 函數,其中的循環會不斷嘗試從客戶端的 socket 中讀取數據(消息)。如果沒有數據可讀,read_some 函數將會阻塞,直到有數據可讀為止。但是由于每個客戶端的會話都在獨立的線程中運行,因此一個客戶端的阻塞不會影響其他客戶端的會話。
-
這就是為什么單個客戶端不關閉并且一直發送消息時,其他客戶端的會話不會被阻塞的原因。每個會話在獨立的線程中運行,互相之間不受影響。當一個客戶端的會話在等待數據時,其他客戶端的會話仍然可以繼續執行。
-
需要注意的是,盡管每個客戶端的會話都在獨立的線程中運行,但線程之間仍然可能存在競爭條件和線程安全問題。在多線程環境中,必須謹慎處理共享資源,以避免潛在的問題。
-
盡管 Session 函數被放置在一個無限循環中,但是您的代碼中是在不同的線程中調用不同的 Session 函數。每當一個新的客戶端連接進來,都會創建一個新的線程并調用 Session 函數,在這個線程中執行循環。因此,盡管每個 Session 函數都有一個無限循環,這些循環在不同的線程中運行,彼此之間是獨立的。
-
這就是為什么不同客戶端的會話不會相互阻塞的原因:每個客戶端的會話都在不同的線程中獨立運行,因此一個客戶端的會話在等待數據時,不會影響其他客戶端的會話。雖然在 StartListen 函數中的循環內部調用了 Session 函數,但由于每個 Session 函數在不同的線程中運行,它們互相之間的執行是并行的,因此不會相互阻塞。
-
在這段代碼中,使用了 C++11 中的 lambda 表達式來創建一個新線程。lambda 表達式中的內容會在新線程中執行。在這里,[this, socket] 是 lambda 表達式的形式,表示捕獲當前對象 (this) 和 socket 變量,并將它們傳遞給新線程中的代碼。
-
在新線程中,會調用 Session 函數,執行客戶端的會話邏輯。由于每個客戶端連接都會在獨立的線程中執行 Session 函數,因此不同客戶端之間的會話可以并行處理,不會相互阻塞。
-
總結起來,您的代碼通過創建多個線程來并發處理不同客戶端的會話,從而實現了同時處理多個客戶端連接的能力。這種并發處理可以提高服務器的性能和響應能力。
5.5、void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id)為啥read_some要寫在for循環里面
-
read_some 是一個阻塞的函數,當沒有數據可讀時,它會等待直到有數據到達或發生錯誤。在這個代碼中,盡管 read_some 函數在 for 循環中被調用,但它會阻塞等待直到Buffer緩沖區有數據可以讀取。如果客戶端發送了消息,那么 read_some 會返回并讀取數據,然后進入下一次循環。
-
即使在循環內,read_some 函數在等待數據到達時不會阻塞整個線程,而只會阻塞當前調用的線程,從而允許其他線程繼續執行。這使得服務器能夠同時處理多個客戶端連接,因為每個客戶端連接的阻塞等待不會影響其他連接的處理。
-
因此,盡管 read_some 在循環內被調用,但它并不會阻塞整個循環,而是在沒有數據可讀時阻塞等待,等到有數據到達時才會返回。這使得服務器能夠持續接收來自多個客戶端的消息。
-
不寫在循環里面客戶端這時只能發送一次消息:
-
是的,如果你將 read_some 放在循環外面,那么每個客戶端連接只能接收并處理一次消息。一旦服務器從客戶端接收到一條消息后,read_some 將不會再阻塞,因為緩沖區中有可讀數據。但是,一旦 read_some 不再阻塞,循環中沒有等待新的數據到達的代碼,因此服務器將不會繼續從客戶端接收消息。
-
如果你想要實現連續接收消息的功能,你可以將整個讀取消息的邏輯放在一個循環內。這樣,服務器會在每次循環中等待并接收來自客戶端的新消息。在你的代碼中,你可以將 for (; ; )) { … } 部分取消注釋,這樣服務器就會持續循環接收消息,直到客戶端斷開連接或發生錯誤。
-
這是因為在代碼中,read_some 函數是在循環外部調用的。一旦客戶端發送一條消息并且服務器成功地讀取了這條消息,read_some 將不再阻塞,因為緩沖區內有可讀數據。而在循環外部沒有邏輯來等待新的消息到達,因此服務器就不會繼續讀取和處理后續的消息。
-
要實現客戶端能夠多次發送消息并且服務器能夠持續地接收和處理這些消息,你需要將 read_some 放在循環內部,這樣服務器就能在每次循環迭代中嘗試讀取客戶端發送的消息,從而實現持續通信。 這樣服務器就能夠在一個連接上持續接收和處理多個消息,直到客戶端關閉連接。
-
6、std::make_shared以及std::shared_ptr
shared_ptr<string> p1 = make_shared<string>(10, '9'); shared_ptr<string> p2 = make_shared<string>("hello"); shared_ptr<string> p3 = make_shared<string>();
C++11 中引入了智能指針, 同時還有一個模板函數 std::make_shared 可以返回一個指定類型的 std::shared_ptr:
// make_shared example
#include <iostream>
#include <memory>int main () {std::shared_ptr<int> foo = std::make_shared<int> (10);// same as:std::shared_ptr<int> foo2 (new int(10));auto bar = std::make_shared<int> (20);auto baz = std::make_shared<std::pair<int,int>> (30,40);std::cout << "*foo: " << *foo << '\n';std::cout << "*bar: " << *bar << '\n';std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';return 0;
}
std::make_shared 是 C++ 標準庫中的一個函數模板,用于創建智能指針(std::shared_ptr)所管理的對象。它的作用是將對象的創建和智能指針的管理結合在一起,以便更安全、更方便地管理對象的生命周期。
-
具體來說,std::make_shared 的作用和意義如下:
-
簡化對象創建和管理: 在創建智能指針時,如果直接使用 std::shared_ptr 構造函數來創建,需要同時分配內存給智能指針對象和被管理的對象。而 std::make_shared(args…) 可以一次性分配內存給智能指針對象和被管理的對象,更加高效和簡潔。
-
減少內存分配次數: std::make_shared 在內存中一次性分配了智能指針對象和被管理的對象所需的內存,這可以減少內存分配次數,提高性能,同時減少內存碎片。
-
避免資源泄漏: std::make_shared 使用的是智能指針,它會自動管理對象的生命周期,確保在不再需要對象時,對象會被適時銷毀,避免資源泄漏。
-
#include <memory>int main() {// 創建智能指針并初始化為一個 int 對象std::shared_ptr<int> num_ptr = std::make_shared<int>(42);// 創建智能指針并初始化為一個動態分配的數組std::shared_ptr<int[]> array_ptr = std::make_shared<int[]>(10);return 0;
}
總之,std::make_shared 是一種推薦的方式來創建和管理智能指針所管理的對象,它不僅簡化了代碼,還提供了更好的性能和資源管理。
6.1、shared_ptr對象創建方法
- 通常我們有兩種方法去初始化一個std::shared_ptr:
- ①通過它自己的構造函數。
- ②通過std::make_shared。
6.1.2、這兩種方法都有哪些不同的特性
shared_ptr是非侵入式的,即計數器的值并不存儲在shared_ptr內,它其實是存在在其他地方——在堆上的,當一個shared_ptr由一塊內存的原生指針創建的時候(原生內存:代指這個時候還沒有其他shared_ptr指向這塊內存),這個計數器也就隨之產生,這個計數器結構的內存會一直存在——直到所有的shared_ptr和weak_ptr都被銷毀的時候,這個時候就比較巧妙了,當所有shared_ptr都被銷毀時,這塊內存就已經被釋放了,但是可能還有weak_ptr存在——也就是說計數器的銷毀有可能發生在內存對象銷毀后很久才發生。
class Object
{
private:int value;
public:Object(int x = 0):value(x) {}~Object() {}void Print() const {cout << value << endl; }
};
int main()
{std::shared_ptr<Object> op1(new Object(10)); //①std::shared_ptr<Object> op2 = std::make_shared<Object>(10); //②return 0;
}
6.1.3、這兩種創建方式有什么區別
-
當使用第①種方式,op1有三個成員,op1._Ptr、op1._Rep、op1._mD,op1._Ptr指針指向Object對象,op1._Rep指向引用計數結構,引用計數結構有也有三個成員:_Ptr、_Uses、_Weaks,_Ptr指向Object對象,_Uses和**_Weaks值都為1,實際上是對堆區構建了兩次,一次是構建Object**對象,另一次是構建 引用計數 結構
-
當使用第②種方式,對堆區只構建了一次,它是計算出了引用計數結構的大小和Object對象的大小,一次開辟了它們大小這么大的空間,_Ptr指針指向Object對象,_Uses 和 _Weaks 值都為1
6.1.4、std::make_shared的三個優點
-
①對堆區只開辟一次,減少了對堆區開辟和釋放的次數:
- 使用make_ptr最大的好處就是減少單次內存分配的次數,如果我們馬上要提到的壞影響不是那么重要的話,這幾乎就是我們使用make_shared的唯一理由
另一個好處就是可以增加大Cache局部性 (Cache Locality) :使用 make_shared,計數器的內存和原生內存就在堆上排排坐,這樣的話我們所有要訪問這兩個內存的操作就會比另一種方案減少一半的cache misses,所以,如果cache miss對你來說是個問題的話,你確實要好好考慮一下make_shared。
- 使用make_ptr最大的好處就是減少單次內存分配的次數,如果我們馬上要提到的壞影響不是那么重要的話,這幾乎就是我們使用make_shared的唯一理由
-
②為了提高命中率,讓對象和引用計數結構在同一個空間:
- 在Cache塊中能夠很快的命中它,因為空間局部性就導致在訪問對象之后,對對象前后的內存塊還要訪問,這樣的命中率就很高,因為對象和引用計數結構是挨著的。
- 引入Cache的理論基礎是程序局部性原理,包括時間局部性和空間局部性,即最近被CPU訪問的數據,短期內CPU還要訪問(時間);被CPU訪問的數據附近的數據,CPU短期內還要訪問(空間),因此如果將剛剛訪問過的數據緩存在Cache中,那下次訪問時,可以直接從Cache中取,其速度可以得到數量級的提高,CPU要訪問的數據在Cache中有緩存,稱為命中(Hit),反之則稱為缺失(Miss)。
執行順序以及異常安全性也是一個應該考慮的問題:
struct Object
{int i;
};
void doSomething(double d,std::shared_ptr<Object> pt)
double couldThrowException();
int main()
{doSomething(couldThrowException(),std::shared_ptr<Object> (new Object(10));return 0;
}
分析上面的代碼,在dosomething函數被調用之前至少有三件事被完成:
- ①構造并給Object分配內存。
- ②構造shared_ptr。
- ③couldThrowException()。
C++17中引入了更加嚴格的鑒別函數參數構造順序的方法,但是在那之前,上邊三件事情的執行順序應該是這樣的:
- ①new Object()。
- ②調用couldThrowException()函數。
- ③構造shared_ptr 并管理步驟1開辟的內存。
上面的問題就是一旦步驟二拋出異常,步驟三就永遠都不會發生, 因此沒有智能指針去管理步驟一開辟的內存——內存泄露了,但是智能指針說它很無辜,它都還沒來得及到這個世上看一眼。
這也是為什么我們要盡可能的使用std::make_shared來讓步驟一和步驟三緊挨在一起,因為你不知道中間可能會發生什么事
- ③在一些調用次序不定的情況下, 依然能夠管理對象:
- 如果使用的是doSomething(couldThrowException(),std::make_shared (10)); 來構建的話,構建的時候對象和引用計數結構會一起被構建,就算后面拋出異常了,這個對象也會被析構掉。
6.1.5、使用make_shared的缺點
使用make_shared,首先最可能遇到的問題就是make_shared函數必須能夠調用目標類型構造函數或構造方法,然而這個時候即使把make_shared設成類的友元恐怕都不夠用, 因為其實目標類型的構造是通過一個輔助函數調用的——不是make_shared這個函數
另一個問題就是我們目標內存的生存周期問題(我說的不是目標對象的生存周期),正如上邊說過的,即使被shared_ptr管理的目標都被釋放了,shared_ptr的計數器還會一直持續存在,直到最后一個指向目標內存的weak_ptr被銷毀,這個時候,如果我們使用make_shared函數。
問題就來了:程序自動的把被管理對象占用的內存和計數器占用的堆上內存視作一個整體來管理,這就意味著,即使被管理的對象被析構了,空間還在,內存可能并沒有歸還——它在等著所有的weak_ptr都被清除后和計數器所占用的內存一起被歸還,假如你的對象有點大,那就意味著一個相當可觀的內存被無意義的鎖定了一段時間
陰影區域就是被shared_ptr管理對象的內存,它在等待著weak_ptr的計數器變為0,和上邊淺橙色區域(計數器的內存)一起被釋放。
7、總結:同步讀寫的優劣
- 同步讀寫的缺陷在于讀寫是阻塞的,如果客戶端對端不發送數據服務器的read操作是阻塞的,這將導致服務器處于阻塞等待狀態。
- 可以通過開辟新的線程為新生成的連接處理讀寫,但是一個進程開辟的線程是有限的,約為2048個線程,在Linux環境可以通過unlimit增加一個進程開辟的線程數,但是線程過多也會導致切換消耗的時間片較多。
- 該服務器和客戶端為應答式,實際場景為全雙工通信模式,發送和接收要獨立分開。
- 該服務器和客戶端未考慮粘包處理。
綜上所述,是我們這個服務器和客戶端存在的問題,為解決上述問題,我在接下里的文章里做不斷完善和改進,主要以異步讀寫改進上述方案。
當然同步讀寫的方式也有其優點,比如客戶端連接數不多,而且服務器并發性不高的場景,可以使用同步讀寫的方式。使用同步讀寫能簡化編碼難度。