文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-08-30 13:47

关注

前面我们介绍了boost::asio同步读写的api函数,现在将前面的api串联起来,做一个能跑起来的客户端服务器客户端服务器采用阻塞的同步读写方式完成通信。

客户端设计基本思路是根据服务器对端的ip和端口创建一个endpoint,然后创建socket连接这个endpoint,之后就可以用同步读写的方式发送和接收数据了。

client.h:

#pragma once#ifndef __CLIENT_H_2023_8_16__#define __CLIENT_H_2023_8_16__#include#include#include#define Ip "127.0.0.1"#define Port 9273#define Buffer 1024class 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 endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(ip_), port_);//Step2: create socketboost::asio::io_context context;boost::asio::ip::tcp::socket socket(context, ep.protocol());//Step3: socket connect endpointboost::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 messagestd::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 messagechar 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;}

综上所述,这段代码创建一个客户端对象,连接到服务器并实现了一个简单的循环,允许用户输入消息并将其发送给服务器,然后接收并显示服务器返回的消息。同时,它还能处理连接和通信过程中可能出现的异常情况。

main.cpp:

#include"client.h"int main() {Client client;if (client.StartConnect()) {;}return 0;}

综上所述,这段代码创建了一个客户端对象并调用其 StartConnect() 函数来连接服务器并进行通信。然后程序会以状态码 0 正常退出。如果连接或通信出现问题,你可以在适当的位置添加错误处理代码。

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 endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);//create acceptorboost::asio::ip::tcp::acceptor accept(context, ep);//acceptor bind endport//accept.bind(ep);//acceptor listenstd::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([&]() {//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#include#include#include#define Port 9273#define Buffer 1024#define SIZE 30class 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 endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);//create acceptorboost::asio::ip::tcp::acceptor accept(context, ep);//acceptor bind endport//accept.bind(ep);//acceptor listenstd::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([&]() {//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底层提供的会循环派发就绪事件,

在这里插入图片描述

5.1、服务器遇到的问题

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

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

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

bool Server::StartListen(boost::asio::io_context& context) {//create endpointboost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);//create acceptorboost::asio::ip::tcp::acceptor accept(context, ep);//acceptor bind endport//accept.bind(ep);//acceptor listenstd::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([&]() {//this->Session(socket);//});auto t = std::make_shared<std::thread>([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;}

5.1.2、出现 Error occured!Error code : 10009 .Message: 提供的文件句柄无效。 [system:10009]

start listen:have client connect,the ip:127.0.0.1Error occured!Error code : 10009 .Message: 提供的文件句柄无效。 [system:10009]

综上所述,问题可能是在多线程环境中正确管理套接字的生命周期和状态所导致的。 仔细检查您的代码,确保在每个线程中正确使用和关闭套接字,并使用适当的同步机制来避免竞争条件。如果问题仍然存在,您可能需要更详细地检查每个线程中的代码,以找出问题所在。

auto t = std::make_shared([this, socket]() {Session(socket);}); 为啥auto t = std::make_shared([&]() {this->Session(socket);});传引用不行

在代码中,使用 [&] 来传递引用,但由于您正在使用异步线程来处理连接,引用的内容可能在后台线程执行时已经失效,从而导致访问无效的资源。这可能是导致错误的原因。

正确的做法是在 lambda 函数中捕获参数 socket 通过值传递(而不是引用),这样可以确保在线程执行时 socket 对象仍然有效。这就是代码中的第一个示例所做的。

auto t = std::make_shared<std::thread>([this, socket]() {    Session(socket);});
auto t = std::make_shared<std::thread>([this, socket]() {    Session(socket);});

为了避免这些问题,通常建议在多线程编程中,要确保在线程访问外部资源时,外部资源的生命周期不会在线程执行期间结束。可以通过合适的同步机制、生命周期管理和避免悬垂引用的方式来解决这类问题。

5.2、 发送普通的消息如数字12或者字符串可以 如果发送结构体协议之类的为啥要用protobuf

如果您需要传输复杂的数据结构,特别是需要跨平台和语言交换数据,使用 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([&]() {//this->Session(socket);//});auto t = std::make_shared<std::thread>([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;}
struct Message {    int id;    std::string content;};
syntax = "proto3";message Message {    int32 id = 1;    string content = 2;}
#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();

通常情况下,创建一个线程需要指定一个可调用的函数或函数对象(例如函数指针、lambda 函数、类成员函数、普通函数等),以便在线程中执行。但是在这个代码片段中,没有提供这样的可调用对象,因此这个线程实际上没有有效的工作任务。这样创建的线程对象是空闲的,没有任何实际的工作内容。

std::shared_ptr t = std::make_shared([this, socket] {
Session(socket, user_id_);
});

5.5、void Server::Session(std::shared_ptr socket,uint32_t user_id)为啥read_some要写在for循环里面

 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 #include  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)所管理的对象。它的作用是将对象的创建和智能指针的管理结合在一起,以便更安全、更方便地管理对象的生命周期。

#include 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对象创建方法

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、这两种创建方式有什么区别

6.1.4、std::make_shared的三个优点

执行顺序以及异常安全性也是一个应该考虑的问题:

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函数被调用之前至少有三件事被完成:

C++17中引入了更加严格的鉴别函数参数构造顺序的方法,但是在那之前,上边三件事情的执行顺序应该是这样的:

上面的问题就是一旦步骤二抛出异常,步骤三就永远都不会发生, 因此没有智能指针去管理步骤一开辟的内存——内存泄露了,但是智能指针说它很无辜,它都还没来得及到这个世上看一眼。

这也是为什么我们要尽可能的使用std::make_shared来让步骤一和步骤三紧挨在一起,因为你不知道中间可能会发生什么事

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,和上边浅橙色区域(计数器的内存)一起被释放。

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

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

来源地址:https://blog.csdn.net/qq_44918090/article/details/132341589

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-服务器
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯