[Boost network library from bronze to king] Part 7: Asynchronous echo server in asio network programming, focusing on response

1 Introduction

The asynchronous operation API has been introduced before . Today I will write a simple asynchronous echo server, focusing on responses.

2. Echo mode responds to asynchronous server

2.1. Session class

The Session class is mainly a session class that handles the receiving and sending of client messages. For the sake of simplicity, we do not consider the problem of sticky packets, nor do we consider the interface that supports manual calls to send. We only send and receive fixed length (1024 bytes in length) in the form of responses . ) data.

“session.h” :

#pragma once
#include<iostream>
#include<string>
#include<boost/asio.hpp>

class Session
{
    
    
public:
	Session(boost::asio::io_context& ioc);
public:
	boost::asio::ip::tcp::socket& GetSocket();
	void Start();

protected:
	//接收数据回调 tcp接收缓冲区有数据
	void HandlerRead(const boost::system::error_code& err, size_t bytes_transferred);
	//发送数据回调 tcp发送缓冲区有空闲空间,就会从用户缓冲区拷贝到tcp发送缓冲区,然后发送数据
	void HandleSend(const boost::system::error_code& err);
protected:
	enum {
    
    
		max_length = 1024
	};
	//数组接收数据
	char data_[max_length];

private:
	boost::asio::ip::tcp::socket socket_;
};

This code is a C++ class definition implemented using the Boost.Asio library for network communication sessions. You interpret this code:

  • The header file contains:
    • These lines include the necessary header files. #pragma once is a preprocessing directive that ensures that included header files will only be included once, preventing multiple inclusions and potential problems.
#pragma once
#include<iostream>
#include<string>
#include<boost/asio.hpp>
  • Class declaration:
    • The code defines a C++ class called Session . It has a constructor that accepts a reference to a boost::asio::io_context object, and has two public member functions: GetSocket and Start .
class Session
{
    
    
public:
    Session(boost::asio::io_context& ioc);
public:
    boost::asio::ip::tcp::socket& GetSocket();
    void Start();
protected:
    // ...
private:
    // ...
};

  • Public member functions:

    • Session(boost::asio::io_context& ioc): This is the constructor of the Session class. It accepts a reference to io_context as a parameter and is usually used to manage asynchronous I/O operations.

    • boost::asio::ip::tcp::socket& GetSocket() : This function returns a reference to a TCP socket object. It may allow external code to access the socket associated with this session.

    • void Start(): This function is expected to start the session. The code snippet does not provide its actual implementation, but it may initiate some asynchronous operations for network communication.

  • Protected member functions:

    • These are protected member functions. They may be overridden by derived classes or used internally to handle read and send operations.

      • HandlerRead(const boost::system::error_code& err, size_t bytes_transferred): This function appears to be a callback for handling data received over a TCP connection. It accepts an error code and the number of bytes transferred as parameters.

      • HandleSend(const boost::system::error_code& err): This function seems to be a callback used to handle the completion of sending data. It also accepts an error code as a parameter.

protected:
    void HandlerRead(const boost::system::error_code& err, size_t bytes_transferred);
    void HandleSend(const boost::system::error_code& err);

  • Private member variables:
private:
    boost::asio::ip::tcp::socket socket_;

This private member variable socket_ is an instance of a TCP socket and is used for network communication within the session.

  • constant:
enum {
    
    
    max_length = 1024
};

This defines a constant called max_length with a value of 1024 . This is most likely the maximum length of the buffer used to receive data.

Overall, this code is the basic outline of a network session class using the Boost.Asio library. It provides the structure and components needed to manage network communications, but does not provide the actual implementation of session behavior in this fragment.

“session.cpp” :

#include "Session.h"

Session::Session(boost::asio::io_context& io_context)
	:socket_(io_context)
{
    
    
	memset(data_, 0, sizeof(data_));
}

boost::asio::ip::tcp::socket& Session::GetSocket() {
    
    
	return socket_;
}

void Session::Start() {
    
    
	memset(data_, 0, max_length);
	socket_.async_read_some(boost::asio::buffer(data_, max_length),
		std::bind(&Session::HandlerRead, this, std::placeholders::_1, std::placeholders::_2));
}

void Session::HandlerRead(const boost::system::error_code& err, std::size_t bytes_transferred) {
    
    
	if (0 != err.value()) {
    
    
		std::cout << "read data failed!err_code is: " << err.value() << " .message: " << err.what() << std::endl;
		delete this;
	}else {
    
    
		std::cout << "receive data is: " << data_ << std::endl;

		//大部分服务器这样设计全双工通信
		memset(data_, 0, sizeof(data_));
		//继续让接收/读数据监听,这样就会造成删除bug
		socket_.async_read_some(boost::asio::buffer(data_, sizeof(data_))
			, std::bind(&Session::HandlerRead, this, std::placeholders::_1, std::placeholders::_2));

		socket_.async_send(boost::asio::buffer(data_, max_length),
			std::bind(&Session::HandleSend, this, std::placeholders::_1));
	}
}

void Session::HandleSend(const boost::system::error_code& err) {
    
    
	if (0 != err.value()) {
    
    
		std::cout << "send data failed!err code is: " << err.value() << " .message: " << err.what() << std::endl;
	}
	else {
    
    
		memset(data_, 0, sizeof(data_));
		//继续让接收/读数据监听
		socket_.async_read_some(boost::asio::buffer(data_, sizeof(data_))
			, std::bind(&Session::HandlerRead, this, std::placeholders::_1, std::placeholders::_2));
	}

}

This is the implementation file related to the Session class provided earlier . Explain this code:

  • Constructor:
    • This is the implementation of the constructor of the Session class. It accepts a reference to boost::asio::io_context as a parameter and initializes socket_ with that context . Additionally, it uses the memset function to initialize the contents of the data_array to zero.
Session::Session(boost::asio::io_context& io_context)
    : socket_(io_context)
{
    
    
    memset(data_, 0, sizeof(data_));
}

  • GetSocket function:
    • This function returns a reference to socket_ , allowing external code to access the TCP socket associated with the session .
boost::asio::ip::tcp::socket& Session::GetSocket() {
    
    
    return socket_;
}

  • Start function, in the Start method we call an asynchronous read operation and listen to the messages sent by the peer. When the peer sends data, the HandlerRead callback function is triggered:
    • The Start function seems to be the function that starts the session. It first uses memset to initialize the contents of the data_ array to zero, and then calls the async_read_some function of socket_ to start an asynchronous read operation. When data arrives, the HandlerRead callback function is called to process the read data.
void Session::Start() {
    
    
    memset(data_, 0, max_length);
    socket_.async_read_some(boost::asio::buffer(data_, max_length),
        std::bind(&Session::HandlerRead, this, std::placeholders::_1, std::placeholders::_2));
}

  • HandlerRead function, the HandlerRead function sends the received data to the peer, and when the sending is completed, the HandleSend callback function is triggered:
    • The HandlerRead function is a callback used to handle the completion of an asynchronous read operation. It checks if an error occurred, and if so, it prints an error message and deletes the session object. If there are no errors, it outputs the received data and continues listening for receiving/reading data by calling async_read_some again. At the same time, it also starts an asynchronous send operation to send data back to the client.
void Session::HandlerRead(const boost::system::error_code& err, std::size_t bytes_transferred) {
    
    
    if (0 != err.value()) {
    
    
        std::cout << "read data failed! err_code is: " << err.value() << " . message: " << err.what() << std::endl;
        delete this;
    }
    else {
    
    
        std::cout << "receive data is: " << data_ << std::endl;

        // 大部分服务器这样设计全双工通信
        memset(data_, 0, sizeof(data_));
        // 继续让接收/读数据监听, 这样就会造成删除bug
        socket_.async_read_some(boost::asio::buffer(data_, sizeof(data_)),
            std::bind(&Session::HandlerRead, this, std::placeholders::_1, std::placeholders::_2));

        socket_.async_send(boost::asio::buffer(data_, max_length),
            std::bind(&Session::HandleSend, this, std::placeholders::_1));
    }
}

  • HandleSend function:
    • The HandleSend function is used to handle the callback when the asynchronous send operation is completed. It checks if an error occurred, and if so, it prints an error message and deletes the session object. If there are no errors, it continues to listen for receiving/reading data by calling async_read_some again . The read event is monitored again in the HandleSend function. If the peer has data sent, HandlerRead is triggered , and we then send the received data back. In order to achieve the effect of responsive service.
void Session::HandleSend(const boost::system::error_code& err) {
    
    
    if (0 != err.value()) {
    
    
        std::cout << "send data failed! err code is: " << err.value() << " . message: " << err.what() << std::endl;
        delete this;
    }
    else {
    
    
        memset(data_, 0, sizeof(data_));
        // 继续让接收/读数据监听
        socket_.async_read_some(boost::asio::buffer(data_, sizeof(data_)),
            std::bind(&Session::HandlerRead, this, std::placeholders::_1, std::placeholders::_2));
    }
}

In general, this code is the implementation of the Session class, which handles asynchronous data reading and sending operations, and provides some error handling and data cleaning logic. This is an implementation of a basic network session class, used to handle network communication.

2.2. The Server class is the management class for the server to receive connections.

The Server class is the management class for the server to receive connections.

“server.h":

#pragma once
#include<iostream>
#include<boost/asio.hpp>
#include"Session.h"

class Server
{
    
    
public:
	Server(boost::asio::io_context& io_context, int16_t port);
private:
	void StartAccept();
	void HandleAccept(Session* session, const boost::system::error_code& err);
private:
	boost::asio::io_context& io_context_;
	boost::asio::ip::tcp::acceptor acceptor_;
};

This code defines a C++ class called Server, which appears to be used to create and manage a TCP server based on the Boost.Asio library . Here's an explanation of this code:

  • The header file contains:
    • These lines include the necessary header files. #pragma once is used to ensure that the header file is only included once to avoid the problem of multiple inclusions.
#pragma once
#include <iostream>
#include <boost/asio.hpp>
#include "Session.h"
  • Class declaration:
    • This code defines a **C++** class named Server , which has a public constructor, two private member functions, and two private member variables.
class Server
{
    
    
public:
    Server(boost::asio::io_context& io_context, int16_t port);
private:
    void StartAccept();
    void HandleAccept(Session* session, const boost::system::error_code& err);
private:
    boost::asio::io_context& io_context_;
    boost::asio::ip::tcp::acceptor acceptor_;
};

  • Constructor:
    • This is the implementation of the constructor of the Server class. The constructor accepts a reference to a boost::asio::io_context object and a port number as parameters. Inside the constructor, it initializes the io_context_ and acceptor_ member variables. io_context_ is used to manage asynchronous operations, while acceptor_ is used to listen for incoming connections. The constructor also calls the StartAccept function to start accepting connections.
Server::Server(boost::asio::io_context& io_context, int16_t port)
    : io_context_(io_context), acceptor_(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
{
    
    
    StartAccept();
}

  • Private member function: StartAccept binds the acceptor to receive the connection to the service. Internally, it binds the socket descriptor corresponding to the accpeptor to the epoll or iocp model to achieve event drive. HandleAccept is the callback function triggered when a new connection arrives.
    • void StartAccept(): The StartAccept function creates a new Session object, and then uses the async_accept function of acceptor_ to asynchronously wait and accept the incoming connection. When the connection is accepted, the HandleAccept function is called to handle the connection.
void Server::StartAccept() {
    
    
    Session* new_session = new Session(io_context_);
    acceptor_.async_accept(new_session->GetSocket(),
        std::bind(&Server::HandleAccept, this, new_session, std::placeholders::_1));
}

  • void HandleAccept(Session session, const boost::system::error_code& err):*
    • The HandleAccept function is used to handle callbacks for accepting connection operations. If no errors occur, it calls the Start method of the Session object to start the session. If an error occurs, it deletes the Session object. Then, no matter what, it continues calling the StartAccept function in order to wait for the next connection.
void Server::HandleAccept(Session* session, const boost::system::error_code& err) {
    
    
    if (!err) {
    
    
        session->Start();
    } else {
    
    
        delete session;
    }
    StartAccept(); // 继续等待下一个连接
}

In summary, this code defines a TCP server class Server based on Boost.Asio , which initializes the necessary member variables in the constructor and starts accepting incoming connections. It uses asynchronous operations to handle connection requests and starts a new session ( Session ) after each connection is accepted.

“server.cpp":

#include"server.h"

Server::Server(boost::asio::io_context& io_context,int16_t port)
	:io_context_(io_context)
	,acceptor_(io_context,boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(),port))
{
    
    
	StartAccept();
}

void Server::StartAccept() {
    
    
	Session* new_session = new Session(io_context_);
	acceptor_.async_accept(new_session->GetSocket(),
		std::bind(&Server::HandleAccept, this, new_session, std::placeholders::_1));
}

void Server::HandleAccept(Session* new_session, const boost::system::error_code& err)
{
    
    
	if (err.value() != 0)
	{
    
    
		std::cout << "acceptor session failed.error_code is: " << err.value() << " .message: " << err.what() << std::endl;
		delete new_session;
	}
	else {
    
    
		std::cout << "accept new session success!" << std::endl;
		std::cout << "client connect,the ip:" << new_session->GetSocket().remote_endpoint().address() << std::endl;

		new_session->Start();
	}
	//继续监听新的客户端连接
	StartAccept();
}

This code is the C++ server.cpp file , which implements a member function of the TCP server class Server based on the Boost.Asio library . Here's a detailed explanation of the code:

  • Constructor:
    • This is the implementation of the constructor of the Server class. The constructor accepts a reference to a boost::asio::io_context object and a port number as parameters. Inside the constructor, it initializes the io_context_ and acceptor_ member variables. io_context_ is used to manage asynchronous operations, while acceptor_ is used to listen for incoming connections. The constructor also calls the StartAccept function to start accepting connections.
Server::Server(boost::asio::io_context& io_context, int16_t port)
    : io_context_(io_context)
    , acceptor_(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
{
    
    
    StartAccept();
}

  • StartAccept function:
    • The StartAccept function creates a new Session object, and then uses the async_accept function of acceptor_ to asynchronously wait for and accept incoming connections. When the connection is accepted, the HandleAccept function is called to handle the connection.
void Server::StartAccept() {
    
    
    Session* new_session = new Session(io_context_);
    acceptor_.async_accept(new_session->GetSocket(),
        std::bind(&Server::HandleAccept, this, new_session, std::placeholders::_1));
}

  • HandleAccept function:
    • The HandleAccept function is a callback used to handle accepting connection operations. If no error occurs (err.value() == 0), it prints a successful connection message and displays the client's IP address. Then, it calls the Start method of the Session object to start the session. If an error occurs, it prints an error message and deletes the Session object. Anyway, in the end it will continue to call the StartAccept function in order to wait for the next connection.
void Server::HandleAccept(Session* new_session, const boost::system::error_code& err)
{
    
    
    if (err.value() != 0)
    {
    
    
        std::cout << "acceptor session failed. error_code is: " << err.value() << " . message: " << err.what() << std::endl;
        delete new_session;
    }
    else {
    
    
        std::cout << "accept new session success!" << std::endl;
        std::cout << "client connect, the IP: " << new_session->GetSocket().remote_endpoint().address() << std::endl;

        new_session->Start();
    }
    // 继续监听新的客户端连接
    StartAccept();
}

In general, this code is the implementation of the member function of the Server class, which is used to accept the client's connection request and start the session after accepting the connection. This is the part of a basic TCP server that handles incoming connection requests.

“main.cpp”:

#include"server.h"

int main() {
    
    
	try {
    
    
		boost::asio::io_context io_context;
		Server server(io_context, 9273);
		io_context.run();
	}
	catch (std::exception& e) {
    
    
		std::cout << "exception: " << e.what() << std::endl;
	}
	return 0;
}

This code is the main function of a C++ program , which creates a TCP server based on the Boost.Asio library and runs it. Here is a detailed explanation of the code:

  • The header file contains:
    • This line of code contains a header file named "server.h" , which should contain the declaration of the Server class and other necessary header files.
#include "server.h"

  • main function:
    • boost::asio::io_context io_context; : Creates an io_context object, which is used to manage asynchronous operations. The Boost.Asio library usually requires an io_context object to coordinate and manage asynchronous operations.

    • Server server(io_context, 9273); creates an object server of the Server class and passes the io_context object and a port number ( 9273 in this example ) as parameters. Doing so will start the server and start listening on the specified port.

    • i o_context.run(); Call the run method of the io_context object to start running the event loop, which will continue to run until there are no pending asynchronous operations. Here, it will be running all the time to listen and handle client connection requests.

    • catch (std::exception& e) { … }: This is an exception handling block used to catch any exception that may be thrown. If an exception occurs, it will print a description of the exception.

int main() {
    
    
    try {
    
    
        boost::asio::io_context io_context;
        Server server(io_context, 9273);
        io_context.run();
    }
    catch (std::exception& e) {
    
    
        std::cout << "exception: " << e.what() << std::endl;
    }
    return 0;
}

In summary, this main function creates a TCP server, starts the server by calling io_context.run() and enters the event loop, waiting for client connection requests. If an exception occurs, it catches the exception and prints an error message. This is a simple server entry point for starting a server application.

3. Client

The client design can use the previous synchronous mode. The client does not need an asynchronous method because the client is not mainly concurrency. Of course, it is better to write asynchronous reception and reception. The code will not be shown here. If you are interested, check out the previous article.

After running the server, run the client. After entering the string, you can receive the string responded by the server.
Insert image description here
Insert image description here
echo response mode:
Insert image description here

4. Hidden dangers

This demo example is written after the asio official website. There is a hidden danger. When the server is about to send data (before calling async_write ), the client is interrupted at this moment. When the server calls async_write at this time , it will trigger the send callback function. It will judge that ec is non-0 and then execute it. delete this logically recycles the session. But it should be noted that after the client is closed, the read ready event will be triggered at the tcp level, and the server will trigger the read event callback function. In the read event callback function, it is determined that the error code ec is non-0, and then the delete operation is performed again, resulting in a secondary destruction, which is extremely dangerous.
Insert image description here

5. Summary

This article introduces the design of an asynchronous response server, but this kind of server will not be used in actual production, mainly for two reasons:

  • Because the server's sending and receiving interact in the form of responses, it cannot achieve the purpose of random sending by the application layer, that is, it does not achieve complete separation of sending and receiving (full-duplex logic).
  • The server does not handle issues such as sticky packets, serialization, and decoupling of logic and sending and receiving threads.
  • The server is at risk of secondary destruction.

Guess you like

Origin blog.csdn.net/qq_44918090/article/details/133349652