服务器基本架构
服务器有一个欢迎套接字,专门用来接收新的连接的。当每次收到新的连接的时候,欢迎套接字就会产生新的普通套接字用来处理这个连接。
这里处理每个连接的套接字都会开启一个Session去处理单个连接。
异步读写api
在写操作前,我们先封装一个Node结构,用来管理要发送和接收的数据,该结构包含数据域首地址,数据的总长度,以及已经处理的长度(已读的长度或者已写的长度)
/*管理要发送和接收的数据*/
struct MsgNode {
MsgNode(const char* msg, size_t total_len)
: total_length(total_len),
cur_length(0) {
msg_ = new char[total_len];
memcpy(msg_, msg, total_len);
}
MsgNode(size_t total_len)
: total_length(total_len),
cur_length(0) {
msg_ = new char[total_len];
}
~MsgNode() {
delete[] msg_;
}
char* msg_;//数据首地址
size_t total_length;//数据总长度
size_t cur_length;//已经处理的长度
};
异步写
async_write_some
async_write_some函数是asio中的一个异步发送函数,这个函数会立即返回已发送的字节数。下边是该函数的部分原码。
可以从函数源码看出,async_write_some的参数由两个,第一个参数是一个ConstBufferSequence结构的buffer结构,可以使用asio提供的buffer函数构造。第二个参数是一个回调函数,其上红框框部分就是这个回调函数的参数,回调函数的第一个参数是boost底层返回的错误码,第二个参数是已经发送的字节数。这个回调函数可以使用std提供的bind函数绑定。
下边是session类的结构
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> sock);
//async_write_some异步写的数据往往并不是要求写的数据长度那么多
void WriteCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteToSocket(const std::string& buff);
private:
//发送队列
std::queue<std::shared_ptr<MsgNode>> sendQueue;
//发送状态
bool send_pending;
std::shared_ptr<boost::asio::ip::tcp::socket> sock_;
std::shared_ptr<MsgNode> send_node;
std::shared_ptr<MsgNode> recv_node;
bool recv_pending;
};
这里是session成员函数的定义
void Session::WriteCallBack(const boost::system::error_code& ec, size_t bytes_transferred) {
if (ec.value()) {
std::cout << "Error code: " << ec.value()
<< "Error Message: " << ec.message() << std::endl;
return;
}
auto& send_data = sendQueue.front();
//更新已经发送的长度
send_data->cur_length += bytes_transferred;
if (send_data->cur_length < send_data->total_length) {
this->sock_->async_write_some(boost::asio::buffer(send_data->msg_ +
send_data->cur_length,send_data->total_length - send_data->cur_length),
std::bind(&Session::WriteCallBack, this, std::placeholders::_1,
std::placeholders::_2));
return;
}
sendQueue.pop();
if (sendQueue.empty()) {
send_pending = false;
}
else {
auto& send_data = sendQueue.front();
this->sock_->async_write_some(boost::asio::buffer(send_data->msg_ +
send_data->cur_length, send_data->total_length - send_data->cur_length),
std::bind(&Session::WriteCallBack, this, std::placeholders::_1,
std::placeholders::_2));
}
}
void Session::WriteToSocket(const std::string& buff) {
sendQueue.push(std::make_shared<MsgNode>(buff.c_str(), buff.size()));
//sendQueue.emplace(new MsgNode(buff.c_str(), buff.size()));
if (send_pending) {
return;
}
//第一次异步发送
this->sock_->async_write_some(boost::asio::buffer(buff.c_str(),
buff.size()), std::bind(&Session::WriteCallBack, this,
std::placeholders::_1, std::placeholders::_2));
send_pending = true;
}
异步读写的async_read_some和async_write_some返回的字节数往往并不是实际要求读写的长度。是因为用户缓冲区和tcp缓冲区不一致导致的。比如tcp缓冲区还有2字节的空余,而用户缓冲区需要发送5字节的数据,这个时候,就只能发送2字节的数据,因为用户缓冲区必须以tcp缓冲区为导向。
其次,async_read_some还有一个问题就是,boost::asio封装时,在Linux环境用的epoll模型,在Windows环境用的iocp模型,都是多路复用的模型, 当写事件就绪后就发数据,发送的数据按照async_write_some调用的顺序发送,所以回调函数内调的 async_write_some可能并没有被及时调用。这样可能会导致什么样的后果呢,就是异步读写的时序便不能保证。比如上边第二个例子,剩下的“ boost”只能再次调用async_write_some发送。如果在它调用async_write_some之前, 用户态再次在多线程或者循环调用的情况下,再次调用了async_write_some函数,并且这次调用将数据发送完成了,就会导致发送数据时序异常,对端收到的数据就是“hello hello boost boost”。其中第一个hello和最后位置的boost是第一次发送的,中间的hello boost则是中间抢占async_write_some发送的。显然这样的情况是不能接受的。这里可以采用队列来控制应用层的数据同步
async_send
async_send异步写数据并一次性写完。其内部是多次调用async_write_some。这个函数在发送的长度未达到我们要求的长度时就不会触发回调,所以触发回调函数时要么时发送出错了要么是发送完成了,其内部的实现原理就是帮我们不断的调用async_write_some直到完成发送,所以async_send不能和async_write_some混合使用。下边是async_send的部分源码。
从上边可以看出,async_send函数的参数和async_write_some函数参数基本相同。
下边是Session类结构
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> sock);
//async_write_some异步写的数据往往并不是要求写的数据长度那么多
void WriteCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteToSocket(const std::string& buff);
//async_send异步写写数据一次性写完。内部是多次调用async_write_some
void WriteAllCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteAllToSocket(const std::string& buff);
private:
//发送队列
std::queue<std::shared_ptr<MsgNode>> sendQueue;
//发送状态
bool send_pending;
std::shared_ptr<boost::asio::ip::tcp::socket> sock_;
std::shared_ptr<MsgNode> send_node;
std::shared_ptr<MsgNode> recv_node;
bool recv_pending;
};
这里是session中调用asyn_send函数的成员函数定义
void Session::WriteAllCallBack(const boost::system::error_code& ec, size_t bytes_transferred) {
if (ec.value()) {
std::cout << "Error code: " << ec.value()
<< "Error Message: " << ec.message() << std::endl;
return;
}
sendQueue.pop();
if (sendQueue.empty()) {
send_pending = false;
}
else {
auto& send_data = sendQueue.front();
this->sock_->async_send(boost::asio::buffer(
send_data->msg_, send_data->total_length),
std::bind(&Session::WriteAllCallBack, this,
std::placeholders::_1, std::placeholders::_2));
}
}
void Session::WriteAllToSocket(const std::string& buff) {
sendQueue.emplace(new MsgNode(buff.c_str(), buff.size()));
if (send_pending) {
return;
}
/*async_send内部会多次调用async_write_some,保证只调用一次回调*/
this->sock_->async_send(boost::asio::buffer(buff),
std::bind(&Session::WriteAllCallBack, this,
std::placeholders::_1, std::placeholders::_2));
send_pending = true;
}
异步读
async_read_some
async_read_some函数是asio中的一个异步接收函数,这个函数会立即返回已接收的字节数。下边是该函数的部分原码。
async_read_some和async_write_some很相似,都是立即返回自己已经读或者写的字节数。它们的参数类型也差不多。
下边是session结构
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> sock);
//async_write_some异步写的数据往往并不是要求写的数据长度那么多
void WriteCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteToSocket(const std::string& buff);
//async_send异步写写数据一次性写完。内部是多次调用async_write_some
void WriteAllCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteAllToSocket(const std::string& buff);
//async_write_read异步读的数据往往并不是要求读的数据长度那么多
void ReadFromSocket();
void ReadCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
private:
//发送队列
std::queue<std::shared_ptr<MsgNode>> sendQueue;
//发送状态
bool send_pending;
std::shared_ptr<boost::asio::ip::tcp::socket> sock_;
std::shared_ptr<MsgNode> send_node;
std::shared_ptr<MsgNode> recv_node;
bool recv_pending;
};
这里是session中调用asyn_read_some函数的成员函数定义
void Session::ReadFromSocket() {
if (recv_pending) {
return;
}
recv_node = std::make_shared<MsgNode>(RECVSIZE);
/*数据读好之后会调用传递给它的回调函数*/
this->sock_->async_read_some(boost::asio::buffer(recv_node->msg_,
recv_node->total_length), std::bind(&Session::ReadCallBack, this,
std::placeholders::_1, std::placeholders::_2));
recv_pending = true;
}
void Session::ReadCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred) {
if (ec.value()) {
std::cout << "Error code: " << ec.value()
<< "Error Message: " << ec.message() << std::endl;
return;
}
//更新已经发送的长度
recv_node->cur_length += bytes_transferred;
if (recv_node->cur_length < recv_node->total_length) {
this->sock_->async_read_some(boost::asio::buffer(recv_node->msg_ + recv_node->cur_length,
recv_node->total_length - recv_node->cur_length), std::bind(&Session::ReadCallBack,
this, std::placeholders::_1, std::placeholders::_2));
return;
}
recv_pending = false;
recv_node = nullptr;
}
async_receive
async_receive是一次性接收完数据。其内部是多次调用async_read_some函数。
写到这里,我们可以看到这里的async_write_some、async_send、async_read_some、async_receive等函数的参数,差不多参数类型都是差不多的。但是这里要注意这里的函数都是属于boost::asio::ip::tcp::socket套接字调用的,也就是使用套接字去调用异步读写函数。但是异步读写还有些boost::asio提供读写函数,其中函数也必定需要添加一个套接字参数。比如boost::asio::async_write函数,其参数就有三个,多出来的一个参数就是需要的套接字参数,其后两个的参数也就是和这里的参数差不多。
下边是Session结构
class Session
{
public:
Session(std::shared_ptr<boost::asio::ip::tcp::socket> sock);
//async_write_some异步写的数据往往并不是要求写的数据长度那么多
void WriteCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteToSocket(const std::string& buff);
//async_send异步写写数据一次性写完。内部是多次调用async_write_some
void WriteAllCallBack(const boost::system::error_code& ec, size_t bytes_transferred);
void WriteAllToSocket(const std::string& buff);
//async_write_read异步读的数据往往并不是要求读的数据长度那么多
void ReadFromSocket();
void ReadCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
//async_receive异步写写数据一次性写完。内部是多次调用async_read_some
void ReadAllFromSocket();
void ReadAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred);
private:
//发送队列
std::queue<std::shared_ptr<MsgNode>> sendQueue;
//发送状态
bool send_pending;
std::shared_ptr<boost::asio::ip::tcp::socket> sock_;
std::shared_ptr<MsgNode> send_node;
std::shared_ptr<MsgNode> recv_node;
bool recv_pending;
};
这里是session中调用asyn_read_some函数的成员函数定义
void Session::ReadAllFromSocket() {
if (recv_pending) {
return;
}
recv_node = std::make_shared<MsgNode>(RECVSIZE);
/*async_receive一次性接收我们要求的长度,只触发一次回调函数。
async_receive内部是多次调用async_read_some*/
this->sock_->async_receive(boost::asio::buffer(recv_node->msg_, recv_node->total_length),
std::bind(&Session::ReadAllCallBack, this,
std::placeholders::_1, std::placeholders::_2));
recv_pending = true;
}
void Session::ReadAllCallBack(const boost::system::error_code& ec, std::size_t bytes_transferred) {
if (ec.value()) {
std::cout << "Error code: " << ec.value()
<< "Error Message: " << ec.message() << std::endl;
return;
}
recv_node->cur_length += bytes_transferred;
recv_node = nullptr;
recv_pending = false;
}
完整异步echo服务器demo
Session.h文件
#pragma once
#include <iostream>
#include <boost/asio.hpp>
class Session
{
public:
Session(boost::asio::io_context& ioc) :sock_(ioc) {
}
boost::asio::ip::tcp::socket& Socket() {
return sock_;
}
//监听客户端的读写
void start();
private:
//读回调函数
void handle_read(const boost::system::error_code& ec, size_t bytes_transferred);
//读回调函数
void handle_write(const boost::system::error_code& ec);
//套接字
boost::asio::ip::tcp::socket sock_;
enum{max_len = 1024};
char data_[max_len];
};
class Server {
public:
Server(boost::asio::io_context& ioc, short port);
private:
void start_accept();
void handle_accept(Session* new_session, const boost::system::error_code& ec);
//服务上下文
//io_context是不被允许拷贝和构造的。引用成员变量必须使用初始化列表的方式赋值
boost::asio::io_context& ioc_;
short port_num;
boost::asio::ip::tcp::acceptor acceptor;
};
Session.cpp文件
#include "Session.h"
void Session::start()
{
memset(data_, 0, max_len);
std::cout << "client ip is : " << sock_.remote_endpoint().address() << std::endl;
//异步读
sock_.async_read_some(boost::asio::buffer(data_, max_len),
std::bind(&Session::handle_read, this,
std::placeholders::_1, std::placeholders::_2));
}
void Session::handle_read(const boost::system::error_code& ec, size_t bytes_transferred) {
if (ec) {
std::cout << "read error, error code: " << ec.value() <<
"read message: " << ec.message() << std::endl;
delete this;
return;
}
std::cout << "server receive data is : " << data_ << std::endl;
//异步写
boost::asio::async_write(sock_, boost::asio::buffer(data_, bytes_transferred),
std::bind(&Session::handle_write, this, std::placeholders::_1));
}
void Session::handle_write(const boost::system::error_code& ec) {
if (ec) {
std::cout << "read error, error code: " << ec.value() <<
"read message: " << ec.message() << std::endl;
delete this;
}
memset(data_, 0, max_len);
sock_.async_read_some(boost::asio::buffer(data_, max_len),
std::bind(&Session::handle_read, this,
std::placeholders::_1, std::placeholders::_2));
/*boost::asio::async_read(sock_, boost::asio::buffer(data_, max_len),
std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2));*/
}
Server::Server(boost::asio::io_context& ioc, short port)
:ioc_(ioc), acceptor(ioc,
boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) {
std::cout << "Server start success, on port: " << port << std::endl;
start_accept();
}
void Server::start_accept() {
Session* new_session = new Session(ioc_);
acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept,
this, new_session, std::placeholders::_1));
}
void Server::handle_accept(Session* new_session, const boost::system::error_code& ec) {
if (ec.value()) {
std::cout << "read error, error code: " << ec.value() <<
"read message: " << ec.message() << std::endl;
delete this;
}
new_session->start();
this->start_accept();
}
这个异步echo服务器的代码还是比较简短。但是其中的逻辑还是比较绕的。使用队列保证async_write_some的发送时序性,当一次发送的时候,必定会调用async_write_some函数的。但是后续的写事件就不一定会进入自己去调async_write_some函数,因为其已经加入了队列,后续有队列去管理写事件即可。还有一点就是,如何理解队列去管理写事件,asyn_write_some函数发送完之后会调用其回调函数,在它的回调函数中当队列中有数据还会继续调用自己。这里就能理解成为一个递归处理队列中的数据,这里递归的出口就是队列中的数据被处理完毕了。
这里还有一点就是上边的异步读写api都有自己的回调函数,它们的回调函数都是当各自函数完成读写的时候再去调用的自己绑定的回调函数。
echo服务器缺陷
上边的完整代码其实还是有问题的,不知道各位能不能看出来。因为上边的逻辑再发生读写异常的时候,就去的delete session的操作,当然手动释放内存是非常考验开发人员的功力的。但是这里就模拟一种可能出现问题的情况。
就是在读函数中既绑定读的回调函数和写的回调函数,因为这里仅仅是以简单的echo模式的服务器,而在实际的生产中,服务器是全双工的模式。这是出现了一个特殊情况就是,client端向server端发送一个消息后,client的用户因为某些突发情况,比如断网了,或者突然接了个电话,client挂掉了。此时便会触发读事件的异常处理去delete掉session。当然也会触发写事件的异常处理,去delete session。这里就发生了二次析构的严重错误。下边是模拟这种情况的server端代码。
问题来了?这里的问题该怎么解决呢?
我们就希望一个局部变量出了局部作用域之后,依然存在,即延长其生命周期。这里可以利用c++里的智能指针中的引用计数实现延长生命周期的效果。