【boost网络库从青铜到王者】第五篇:asio网络编程中的同步读写的客户端和服务器示例

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 1024

class 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 endpoint
		boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(ip_), port_);

		//Step2: create socket
		boost::asio::io_context context;
		boost::asio::ip::tcp::socket socket(context, ep.protocol());

		//Step3: socket connect endpoint
		boost::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 message
			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));

			//Step5: receive message
			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;
}

  • 这段代码是一个用于客户端的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 endpoint
	boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);

	//create acceptor
	boost::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 30

class 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 endpoint
	boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);

	//create acceptor
	boost::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的读写事件触发和serveraccept触发都是asio底层多路复用模型判断事件就绪后帮我们回调的,目前是单线程模式,所以都是在主线程里触发。

另外sever不退出,并不是因为sever存在循环,而是我们调用了iocontextrun函数,这个函数是asio底层提供的会循环派发就绪事件,

4、效果测试

在这里插入图片描述

5、遇到的问题

5.1、服务器遇到的问题

5.1.1、不用显示调用bind绑定和listen监听函数

两种方式,早期boost acceptor可以绑定端口,后期boost优化了,初始化acceptor时直接指定端口就可以实现绑定和监听。

StartListen函数:
在这里插入图片描述

bool Server::StartListen(boost::asio::io_context& context) {
    
    
	//create endpoint
	boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);

	//create acceptor
	boost::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_ptrweak_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._Ptrop1._Repop1._mDop1._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
  • ②为了提高命中率,让对象和引用计数结构在同一个空间:

    • 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增加一个进程开辟的线程数,但是线程过多也会导致切换消耗的时间片较多。
  • 该服务器和客户端为应答式,实际场景为全双工通信模式,发送和接收要独立分开。
  • 该服务器和客户端未考虑粘包处理。

综上所述,是我们这个服务器和客户端存在的问题,为解决上述问题,我在接下里的文章里做不断完善和改进,主要以异步读写改进上述方案。

当然同步读写的方式也有其优点,比如客户端连接数不多,而且服务器并发性不高的场景,可以使用同步读写的方式。使用同步读写能简化编码难度。

猜你喜欢

转载自blog.csdn.net/qq_44918090/article/details/132341589