基本介紹
上一篇博客我們介紹了通過Boost.asio搭建一個TCP同步服務器和客戶端,這次我們再通過asio搭建一個異步通信的服務器和客戶端系統,由于這是一個簡單異步服務器,所以我們的異步特指異步服務器而不是異步客戶端,同步服務器在處理一個請求時會阻塞其他請求,而異步服務器可以同時處理多個請求,不會阻塞其他請求的處理,客戶端一般是不會處理其他客戶端請求的,所以客戶端仍舊使用同步模式。(本次博客使用的Boost庫版本是1.84.0)
服務器端
main.cpp
#include<boost/asio.hpp>
#include"Server.h"
#include<iostream>
int main()
{try{boost::asio::io_context ioc;Server s(ioc, 56789);ioc.run();}catch (const std::exception& e){std::cout << e.what() << std::endl;}return 0;
}
其中ioc是boost.asio的核心類對象,用于管理和調度異步操作,負責處理事件循環和IO事件的分派,尤其對于異步通信模式來說更為重要,56789就是我們要監聽的端口號,至于Server類就是用來接收客戶端連接的,之所以將ioc和端口號傳給Server,是因為我們要在Server類中初始化一個acceptor套接字,用來接收客戶端的連接,而創建套接字需要使用上下文對象,這是必要條件,要使得服務器能夠監聽客戶端的請求,就需要創建端點對象endpoint,并將它綁定到acceptor,而創建端點對象,不就需要我們的端口號和IP地址嘛,接下來我們會把它實現。
ioc.run()這句話是異步通信模式的核心,同步通信模式并不會通過ioc對象調用run函數,因為同步通信模式是阻塞式的,它會一直等待操作完成后再繼續執行后續代碼,相反,異步通信模式中的操作是非阻塞的,需要通過調用上下文對象的run()函數來啟動事件循環,以便處理異步操作的完成事件和回調函數,run函數會啟動io_context的事件循環,處理代處理的異步操作,直到沒有更多的客戶端響應要處理為止,其實就是類似一個循環的效果,可以使服務器同時不斷處理不同客戶端的請求。
Server.h
#pragma once
#include<boost/asio.hpp>
#include"Session.h"
class Server
{
public:Server(boost::asio::io_context& ioc, int port);void accept_handle(Session* s, const boost::system::error_code&error);void start_accept();boost::asio::io_context &ioc;boost::asio::ip::tcp::acceptor act;
};
Server類用來接收客戶端的連接,實際上異步和同步之間差的就是一個封裝,同步通信中我們同樣要接收客戶端的連接,同樣要使用到acceptor套接字,但是我們是直接使用的,不用再創建一個類什么的去封裝這個acceptor套接字,到了異步中,這就相當必要了,因為存在回調函數的原因,所以通過Server類將acceptor套接字進行封裝,可以使我們的思路更加清晰,不至于被一推回調函數繞暈。
Server.cpp
#include"Server.h"
#include<iostream>
Server::Server(boost::asio::io_context& ioc, int port) :ioc(ioc), act(ioc, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
{start_accept();
}
void Server::start_accept()
{try{Session* s = new Session(ioc);act.async_accept(s->get_socket(), std::bind(&Server::accept_handle, this, s, std::placeholders::_1));}catch (boost::system::system_error &e){std::cout << e.what() << std::endl;}
}
void Server::accept_handle(Session* s, const boost::system::error_code& error)
{if (!error){s->Start();}else{delete s;}start_accept();
}
Server類的構造函數,可以用來幫助我們初始化acceptor套接字act,以及上下文對象ioc,Server類中有一個上下文對象的成員變量,這是用來創建客戶端處理套接字的,我們知道服務器本身的acceptor套接字不會直接處理客戶端發來的請求,它接收了客戶端的連接后,就會新創建一個套接字專門用來處理這個客戶端的請求,而創建套接字就要用到上下文對象,因此我們也要通過構造函數初始化這個上下文成員變量,初始化完了這些變量后,就調用start_accept()函數開始接收客戶端的連接。
start_accept函數,之前我們就說了,異步相比于同步,最大的區別就是封裝,start_accept函數就是對async_accept函數的封裝,async_accept函數是Boost.Asio庫中用于異步接受傳入連接的函數,它的第一個參數其實就是我們要接收的客戶端處理套接字,而第二個參數就是一async_accept回調函數的函數對象。
void async_accept(basic_socket<Protocol, Executor>& socket,AcceptHandler&& handler);
//socket:表示服務器偵聽的套接字對象。
//handler:是一個回調函數,當接受操作完成時將被調用。回調函數必須具有以下簽名:void handler(const boost::system::error_code& error)
回調函數可以使用std::bind()來創建一個函數對象,用于作為異步操作完成后的回調處理函數,std::bind()函數可以將成員函數與指定的對象綁定,以及在調用時傳遞其他參數,我們使用std::bind()綁定Server類的成員函數handle_accept(),并將當前對象指針(this)、new_session參數(作為客戶端處理對象的指針,里面包含了客戶端處理套接字)以及placeholders::_1(表示接受操作的錯誤代碼參數)作為參數進行綁定。
this關鍵字表示指向當前Server對象的指針。由于回調函數需要訪問Server類的成員函數(start_accept())和成員變量,因此將this作為第一個參數傳遞給std::bind()來綁定成員函數handle_accept()
std::placeholders::_1是一個占位符,用于在使用std::bind()函數時表示第一個參數的位置。它是C++標準庫中的一部分,可以用于綁定函數的參數。在給定的代碼中,std::placeholders::_1被用作異步操作完成后回調函數的參數位置的占位符。具體來說,它代表了async_accept()函數的回調函數中的錯誤代碼參數,即接受操作的結果。通過使用std::placeholders::_1,可以將回調函數與一個參數進行綁定,而不需要提供實際的值。當異步操作完成后,實際的錯誤代碼將傳遞給回調函數,并填充到占位符的位置上,從而在回調函數中可以訪問和處理該值。因此,std::placeholders::_1在這里充當了待綁定參數的占位符,以便在異步操作完成后正確地傳遞相應的參數給回調函數。
如果服務器接收到了客戶端的連接,那么接下來就會調用回調函數accept_handle,用來處理連接后的操作。
Session.h
#pragma once
#include<boost/asio.hpp>
class Session
{public:Session(boost::asio::io_context& ioc);boost::asio::ip::tcp::socket &get_socket();void Start();void handle_send(const::boost::system::error_code &error);void handle_recive(const::boost::system::error_code& error,size_t recived_len);boost::asio::ip::tcp::socket soc;int max_len = 1024;char data[1024];
};
Sesion類用來處理客戶端的連接,包括接收和發送數據給客戶端等操作,它里面封裝了客戶端處理套接字socket soc。
Session.cpp
#include"Session.h"
#include<iostream>
Session::Session(boost::asio::io_context& ioc):soc(ioc)
{}
boost::asio::ip::tcp::socket& Session::get_socket()
{return soc;
}
void Session::Start()
{memset(data, 0, max_len);soc.async_read_some(boost::asio::buffer(data, max_len),std::bind(&Session::handle_recive, this, std::placeholders::_1, std::placeholders::_2));}
void Session::handle_recive(const::boost::system::error_code& error, size_t recived_len)
{if (!error){std::cout << "收到的數據是: " << data<<std::endl;soc.async_write_some(boost::asio::buffer(data, recived_len),std::bind(&Session::handle_send, this, std::placeholders::_1));}else{delete this;}
}
void Session::handle_send(const::boost::system::error_code& error)
{if (!error){memset(data, 0, max_len);soc.async_read_some(boost::asio::buffer(data, max_len), std::bind(&Session::handle_recive, this, std::placeholders::_1, std::placeholders::_2));}else{delete this;}
}
Start函數用來開啟服務器對客戶端請求的處理,我們知道服務器連接后對客戶端的第一個操作都是接收客戶端的數據或請求,所以我們在這個函數里面調用了async_read_some函數用來接收客戶端的請求,并且將這個函數綁定了一個回調函數handle_recive。
std::bind(&Session::handle_recive, this, std::placeholders::_1, std::placeholders::_2)綁定了handle_recive成員函數作為回調函數。當讀取操作完成時,會調用該回調函數,并將錯誤碼和實際傳輸的字節數作為參數傳遞給該函數,placeholders的作用和之前的一樣,只是一個函數參數的占位符。
handle_recive和handle_send函數分別是異步讀和異步寫的回調函數,這兩個函數其實互相封裝了對方的異步操作函數,handle_recive封裝的是異步寫,而handle_send封裝的是異步讀,你會發現兩個回調函數封裝的異步操作函數和它們本身是相反的。
handle_recive函數和handle_send函數是相互調用的原因是為了實現一個基本的回顯服務器(echo server)的功能。當客戶端發送數據到服務器時,服務器會先讀取接收到的數據并打印出來(在handle_recive函數中),然后將相同的數據寫回給客戶端(在handle_send函數中)。調用handle_send函數后,當寫操作完成時,又會調用handle_recive函數,以便繼續等待下一個來自客戶端的數據。這種循環的設計方式可以保持與客戶端的持續通信,并確保服務器能夠及時處理客戶端發送的新數據。通過在讀取和寫入操作之間相互調用,可以實現數據的來回傳輸。
客戶端
客戶端采用同步的通信模式,所以代碼相當簡單。
main.cpp
#include<boost/asio.hpp>
#include<iostream>
int main()
{boost::asio::io_context ioc;boost::asio::ip::tcp::socket soc(ioc);boost::asio::ip::tcp::endpoint ed(boost::asio::ip::address::from_string("127.0.0.1"), 56789);char buf[1024]="";try{soc.connect(ed);std::cout << "請輸入發送的消息:";std::cin >> buf;soc.send(boost::asio::buffer(buf, strlen(buf)));char rec[1024]="";soc.receive(boost::asio::buffer(rec, 1024));std::cout << "收到了消息:" << rec << std::endl;}catch (boost::system::system_error &e){std::cout << e.what()<<std::endl;}return 0;
}
代碼運行
首先運行服務器端的代碼,然后再兩次運行客戶端的代碼,在兩個客戶端窗口中輸入要發送的消息,先不要回車。
先在二號客戶端進行回車,我們發現比1號客戶端晚一步運行的二號客戶端既然可以在一號客戶端的前面向服務器發送消息,要知道,1號客戶端雖然沒有回車,但是沒報異常就是說明1號客戶端是成功連接上了服務器的,而且比二號客戶端要早連接上,這說明了1號并沒有阻塞2號的請求發送,這就是異步通信,如果是同步通信,只要1號客戶端不會車,服務器就會一直等待1號回車,等1號回車完了服務器才會釋放1號的連接,這時候2號回車的消息才會被服務器接收到,也就是說2號被1號阻塞了。
?將1號也回車,正常執行,至此一個簡單的TCP異步服務器和客戶端系統搭建完成,實際上真正的異步通信遠不如這么簡單,要實現一個完整的異步通信需要進行大量的思考和復雜的編程。