基本介绍
上一篇博客我们介绍了通过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异步服务器和客户端系统搭建完成,实际上真正的异步通信远不如这么简单,要实现一个完整的异步通信需要进行大量的思考和复杂的编程。