网站建设 备案什么意思,哪里有做效果图的网站,动漫制作专业课程,成都的做网站公司文章目录 1、简介2、客户端设计3、服务器设计3.1、session函数3.2、StartListen函数3、总体设计 4、效果测试5、遇到的问题5.1、服务器遇到的问题5.1.1、不用显示调用bind绑定和listen监听函数5.1.2、出现 Error occured!Error code : 10009 .Message: 提供的文件句柄无效。 [s… 文章目录 1、简介2、客户端设计3、服务器设计3.1、session函数3.2、StartListen函数3、总体设计 4、效果测试5、遇到的问题5.1、服务器遇到的问题5.1.1、不用显示调用bind绑定和listen监听函数5.1.2、出现 Error occured!Error code : 10009 .Message: 提供的文件句柄无效。 [system:10009] 5.2、 发送普通的消息如数字12或者字符串可以 如果发送结构体协议之类的为啥要用protobuf5.2.1、修改字符串或者数字消息改成类或者更为复杂的对象 5.3、Error occured!Error code : 10054 .Message: 远程主机强迫关闭了一个现有的连接。 [system:10054]5.4、std::shared_ptrstd::thread t std::make_sharedstd::thread()与()中加上函数区别以及用法5.5、void Server::Session(std::shared_ptrboost::asio::ip::tcp::socket socket,uint32_t user_id)为啥read_some要写在for循环里面 6、std::make_shared以及std::shared_ptr6.1、shared_ptr对象创建方法6.1.2、这两种方法都有哪些不同的特性6.1.3、这两种创建方式有什么区别6.1.4、std::make_shared的三个优点6.1.5、使用make_shared的缺点 7、总结同步读写的优劣 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__#includeiostream
#includeboost/asio.hpp
#includestring#define Ip 127.0.0.1
#define Port 9273
#define Buffer 1024class Client {
public:Client();bool StartConnect();private:std::string ip_;uint16_t port_;
};#endifclient.cpp:
#includeclient.hClient::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;
}
这段代码是一个用于客户端的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:
#includeclient.hint 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_ptrboost::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_idconnect 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 listen/*accept.listen(30);*/std::cout start listen: std::endl;for (;;) {std::shared_ptrboost::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_sharedstd::thread([]() {// this-Session(socket);// });auto t std::make_sharedstd::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__#includeiostream
#includeboost/asio.hpp
#includestring
#includeset#define Port 9273
#define Buffer 1024
#define SIZE 30class Server {
public:Server();bool StartListen(boost::asio::io_context context);void Session(std::shared_ptrboost::asio::ip::tcp::socket socket,uint32_t user_id);std::setstd::shared_ptrstd::thread GetSet() {return thread_set_;}
private:uint16_t port_;uint32_t user_id_;std::setstd::shared_ptrstd::thread thread_set_;
};#endifserver.cp:
#includeserver.hServer::Server() {port_ Port;user_id_ 0;thread_set_.clear();
}void Server::Session(std::shared_ptrboost::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_idconnect 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 listen/*accept.listen(30);*/std::cout start listen: std::endl;for (;;) {std::shared_ptrboost::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_sharedstd::thread([]() {// this-Session(socket);// });auto t std::make_sharedstd::thread([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;
}
main.cpp:
#includeserver.hint 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的读写事件触发和server的accept触发都是asio底层多路复用模型判断事件就绪后帮我们回调的目前是单线程模式所以都是在主线程里触发。
另外sever不退出并不是因为sever存在循环而是我们调用了iocontext的run函数这个函数是asio底层提供的会循环派发就绪事件
4、效果测试 5、遇到的问题
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 listen/*accept.listen(30);*/std::cout start listen: std::endl;for (;;) {std::shared_ptrboost::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_sharedstd::thread([]() {// this-Session(socket);// });auto t std::make_sharedstd::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_ptrboost::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_sharedstd::thread([this, socket]() {Session(socket);}); 为啥 auto t std::make_sharedstd::thread([]() {this-Session(socket);});传引用不行在代码中使用 [] 来传递引用但由于您正在使用异步线程来处理连接引用的内容可能在后台线程执行时已经失效从而导致访问无效的资源。这可能是导致错误的原因。
正确的做法是在 lambda 函数中捕获参数 socket 通过值传递而不是引用这样可以确保在线程执行时 socket 对象仍然有效。这就是代码中的第一个示例所做的。
auto t std::make_sharedstd::thread([this, socket]() {Session(socket);
}); 这个问题可能与线程间的竞争条件有关。在C中当你在多线程环境下访问共享的变量时需要确保不会出现竞争条件其中一个线程修改了资源而另一个线程在访问该资源时导致未定义行为。 在上述的两种写法中都有可能遇到悬垂引用的问题。这是因为在Lambda函数中引用了外部变量socket但是在Lambda函数执行时这个外部变量的生命周期可能已经结束导致访问无效的资源。 第一个写法中 通过捕获socket的方式socket对象会被复制到Lambda函数内部因此不会出现失效的问题。
auto t std::make_sharedstd::thread([this, socket]() {Session(socket);
});
第二个写法中 通过捕获引用方式socket对象的引用被传递到Lambda函数内部。但是在后台线程执行时主线程可能已经结束或销毁了socket对象导致访问无效的资源。
为了避免这些问题通常建议在多线程编程中要确保在线程访问外部资源时外部资源的生命周期不会在线程执行期间结束。可以通过合适的同步机制、生命周期管理和避免悬垂引用的方式来解决这类问题。
5.2、 发送普通的消息如数字12或者字符串可以 如果发送结构体协议之类的为啥要用protobuf 在网络通信中数据的传输需要考虑多个因素包括数据的格式、序列化与反序列化、网络字节顺序等。当您只需要传输普通的消息、简单的数据类型如整数、字符串时可以直接使用原始的数据格式进行传输。但是当您需要传输复杂的数据结构、对象、类、嵌套的数据等时使用一种序列化协议可以更加方便、安全和高效。 Protocol Buffersprotobuf 是一种流行的序列化库由Google开发用于在不同平台上进行结构化数据的序列化和反序列化。protobuf 提供了一种机制可以将结构化的数据序列化为二进制格式然后在不同的系统之间进行传输和解析。它具有以下优势 跨平台和语言支持 Protocol Buffers 支持多种编程语言包括 C、Java、Python、C# 等使得不同平台上的应用可以方便地进行数据交换。 高效的序列化和反序列化 Protocol Buffers 的序列化和反序列化过程是高效的生成的二进制数据较小传输效率高。 版本兼容性 当数据结构变化时Protocol Buffers 提供了向后和向前兼容的机制可以更容易地进行协议的演化和升级。 强类型支持 Protocol Buffers 使用明确定义的消息结构强制使用者在编码和解码时遵循特定的消息格式避免了一些错误。
如果您需要传输复杂的数据结构特别是需要跨平台和语言交换数据使用 Protocol Buffers 是一个不错的选择。它提供了清晰的消息定义语法、高效的二进制序列化和反序列化以及多种语言的支持。
5.2.1、修改字符串或者数字消息改成类或者更为复杂的对象
#includeserver.hServer::Server() {port_ Port;user_id_ 0;thread_set_.clear();
}void Server::Session(std::shared_ptrboost::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_idconnect 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_ptrboost::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_sharedstd::thread([]() {// this-Session(socket);// });auto t std::make_sharedstd::thread([this, socket]() {Session(socket,user_id_);});thread_set_.insert(t);}return true;
}
要发送结构体或类的实例您需要使用一种序列化库如 **Protocol Buffersprotobuf**来将结构体或类序列化为字节流然后在网络中传输。下面是您如何将您的代码修改为支持发送结构体或类实例 定义结构体或类 首先您需要定义要发送的结构体或类。让我们以一个示例结构体为例
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_castint(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_sharedstd::thread(); 这个代码片段会尝试创建一个 std::thread 对象但没有指定要执行的函数因此它不会实际上创建一个新的线程。 在使用 std::make_shared 时您通常用于创建智能指针比如 std::shared_ptr。在这个上下文中std::make_shared 创建一个std::thread对象并返回一个智能指针但它需要指定要构造的对象的类型和构造参数。 对于 std::thread它需要指定要执行的函数作为参数以便在创建线程时开始执行。如果没有指定要执行的函数创建的 std::thread 对象没有有效的工作任务,只是创建了一个std::thread对象。 auto t std::make_sharedstd::thread(); 创建了一个名为 t 的 std::shared_ptrstd::thread 对象但是这里并没有传递任何参数给 std::make_shared因此没有为线程指定要执行的函数。
通常情况下创建一个线程需要指定一个可调用的函数或函数对象例如函数指针、lambda 函数、类成员函数、普通函数等以便在线程中执行。但是在这个代码片段中没有提供这样的可调用对象因此这个线程实际上没有有效的工作任务。这样创建的线程对象是空闲的没有任何实际的工作内容。
std::shared_ptrstd::thread t std::make_sharedstd::thread([this, socket] { Session(socket, user_id_); }); 每个客户端的会话 Session 都在一个单独的线程中运行通过 std::thread 来创建。这意味着每个客户端的会话都在独立的线程中处理互相之间不会阻塞。 当一个客户端连接并发送消息时会执行 Session 函数其中的循环会不断尝试从客户端的 socket 中读取数据消息。如果没有数据可读read_some 函数将会阻塞直到有数据可读为止。但是由于每个客户端的会话都在独立的线程中运行因此一个客户端的阻塞不会影响其他客户端的会话。 这就是为什么单个客户端不关闭并且一直发送消息时其他客户端的会话不会被阻塞的原因。每个会话在独立的线程中运行互相之间不受影响。当一个客户端的会话在等待数据时其他客户端的会话仍然可以继续执行。 需要注意的是尽管每个客户端的会话都在独立的线程中运行但线程之间仍然可能存在竞争条件和线程安全问题。在多线程环境中必须谨慎处理共享资源以避免潜在的问题。 尽管 Session 函数被放置在一个无限循环中但是您的代码中是在不同的线程中调用不同的 Session 函数。每当一个新的客户端连接进来都会创建一个新的线程并调用 Session 函数在这个线程中执行循环。因此尽管每个 Session 函数都有一个无限循环这些循环在不同的线程中运行彼此之间是独立的。 这就是为什么不同客户端的会话不会相互阻塞的原因每个客户端的会话都在不同的线程中独立运行因此一个客户端的会话在等待数据时不会影响其他客户端的会话。虽然在 StartListen 函数中的循环内部调用了 Session 函数但由于每个 Session 函数在不同的线程中运行它们互相之间的执行是并行的因此不会相互阻塞。 在这段代码中使用了 C11 中的 lambda 表达式来创建一个新线程。lambda 表达式中的内容会在新线程中执行。在这里[this, socket] 是 lambda 表达式的形式表示捕获当前对象 (this) 和 socket 变量并将它们传递给新线程中的代码。 在新线程中会调用 Session 函数执行客户端的会话逻辑。由于每个客户端连接都会在独立的线程中执行 Session 函数因此不同客户端之间的会话可以并行处理不会相互阻塞。 总结起来您的代码通过创建多个线程来并发处理不同客户端的会话从而实现了同时处理多个客户端连接的能力。这种并发处理可以提高服务器的性能和响应能力。
5.5、void Server::Session(std::shared_ptrboost::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_ptrstring p1 make_sharedstring(10, 9); shared_ptrstring p2 make_sharedstring(hello); shared_ptrstring p3 make_sharedstring(); C11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr:
// make_shared example
#include iostream
#include memoryint main () {std::shared_ptrint foo std::make_sharedint (10);// same as:std::shared_ptrint foo2 (new int(10));auto bar std::make_sharedint (20);auto baz std::make_sharedstd::pairint,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 memoryint main() {// 创建智能指针并初始化为一个 int 对象std::shared_ptrint num_ptr std::make_sharedint(42);// 创建智能指针并初始化为一个动态分配的数组std::shared_ptrint[] array_ptr std::make_sharedint[](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_ptr和weak_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_ptrObject op1(new Object(10)); //①std::shared_ptrObject op2 std::make_sharedObject(10); //②return 0;
}
6.1.3、这两种创建方式有什么区别 当使用第①种方式op1有三个成员op1._Ptr、op1._Rep、op1._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_ptrObject pt)
double couldThrowException();
int main()
{doSomething(couldThrowException(),std::shared_ptrObject (new Object(10));return 0;
}
分析上面的代码在dosomething函数被调用之前至少有三件事被完成
①构造并给Object分配内存。②构造shared_ptr。③couldThrowException()。
C17中引入了更加严格的鉴别函数参数构造顺序的方法但是在那之前上边三件事情的执行顺序应该是这样的
①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增加一个进程开辟的线程数但是线程过多也会导致切换消耗的时间片较多。该服务器和客户端为应答式实际场景为全双工通信模式发送和接收要独立分开。该服务器和客户端未考虑粘包处理。
综上所述是我们这个服务器和客户端存在的问题为解决上述问题我在接下里的文章里做不断完善和改进主要以异步读写改进上述方案。
当然同步读写的方式也有其优点比如客户端连接数不多而且服务器并发性不高的场景可以使用同步读写的方式。使用同步读写能简化编码难度。