网络编程(2)——同步服务器设计
三、day3
将前面学习的boost::asio同步读写的api函数串联起来,做一个客户端和服务器,客户端和服务器采用阻塞的同步读写方式完成通信。
1)客户端设计
客户端设计基本思路是根据服务器对端的ip和端口创建一个endpoint,然后创建socket连接这个endpoint,之后就可以用同步读写的方式发送和接收数据
#include <boost/asio.hpp>
#include <iostream>
using namespace boost::asio::ip;
using std::cout;
using std::endl;
const int MAX_LENGTH = 1024; // 发送和接收的长度为1024字节
int main()
{
try {
boost::asio::io_context ioc; // 创建上下文服务
// 127.0.0.1是本机的回路地址,也就是服务器和客户端在一个机器上
tcp::endpoint remote_ep(address::from_string("127.0.0.1"), 10086); // 构造endpoint
tcp::socket sock(ioc);
boost::system::error_code error = boost::asio::error::host_not_found; // 错误:主机未找到
sock.connect(remote_ep, error);
if (error) {
cout << "connect failed, code is " << error.value() << " error msg is " << error.message() << endl;;
}
cout << "Enter message: "; // 连接成功,请输入发送的信息
char request[MAX_LENGTH];
std::cin.getline(request, MAX_LENGTH);
size_t request_length = strlen(request);
boost::asio::write(sock, boost::asio::buffer(request, request_length)); // 一次性发送数据
char reply[MAX_LENGTH]; // 记录对端回复的信息
size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply, request_length));
cout << "Reply is: ";
cout.write(reply, reply_length);
cout << "\n";
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
return 0;
}
2)服务器设计
1. session设计
创建session函数,该函数为服务器处理客户端请求,每当我们获取客户端连接后就调用该函数。在session函数里里进行echo方式的读写,所谓echo就是应答式的处理。
void session(socket_ptr sock) {
try {
for (;;) {
char data[MAX_LENGTH]; // 缓存收到的数据
memset(data, '\0', MAX_LENGTH); // 将char数组中的数据全部设为\0
boost::system::error_code error;
// 服务器可能不会一次读完,而用read或receive可能会造成堵塞
// 因为这里是独立的线程而不是主程序,所以这里可以用read,一直等,但直到读到最大长度才会返回
//size_t length = boost::asio::read(sock, boost::asio::buffer(data, MAX_LENGTH), error);
// read_some实际只会读收到的长度,在没有粘包的情况下用read_some比较好
size_t length = sock->read_some(boost::asio::buffer(data, MAX_LENGTH), error);
if (error == boost::asio::error::eof) { // 连接被关闭
std::cout << "connection closed by peer" << endl;
break;
}
else if (error) // 读取失败
throw boost::system::system_error(error);
cout << "receive from " << sock->remote_endpoint().address().to_string() << endl;
cout << "receive message is " << data << endl;
// 回传给对方
boost::asio::write(*sock, boost::asio::buffer(data, length));
}
}
catch (std::exception& e) {
cout << "Exception in thread: " << e.what() << "\n" << endl;
}
}
2. server设计
server函数根据服务器ip和端口创建服务器acceptor用来接收客户端的请求,然后创建一个新的socket去处理这个请求,并为此开辟一个线程,新的socket对客户端在该线程中执行session任务。
void server(boost::asio::io_context& io_context, unsigned short port) {
// 生成一个用于接收客户端连接的acceptor,绑定的端点是服务器的地址(用ipv4的方式绑定)以及自定义的端口
tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port));
while (1) {
// 生成一个socket来处理客户端的请求,相当于服务员
socket_ptr socket(new tcp::socket(io_context));
// accept接收到请求后,用socket对请求进行处理
a.accept(*socket);
// 生成一个线程,相当于服务员将客户带到包间,大厅仍进行客人的招待
// 线程中也就是包间中,用session作为招待服务,session就是服务器的读和写
// 如果不创建线程,而是直接session,那么如果对端不发送数据,程序就会阻塞一直不运行下一行,其他的连接也会无法接收
// 之所以将线程存储到智能指针中,是为了管理其生命周期,防止线程对象被提前销毁
auto t = std::make_shared<std::thread>(session, socket);
// thread_set是全局变量而不是局部变量,当server函数结束后,thread_set仍会保留
thread_set.insert(t);
}
}
3. 整体结构
#include <iostream>
#include <boost/asio.hpp>
#include <set>
#include <memory>
using boost::asio::ip::tcp;
using std::cout;
using std::endl;
const int MAX_LENGTH = 1024; // 发送和接收的长度为1024字节
typedef std::shared_ptr<tcp::socket> socket_ptr;
std::set<std::shared_ptr<std::thread>> thread_set;
void session(socket_ptr sock); // 处理某个客户端连接之后数据的读和写(收发)
void server(boost::asio::io_context& io_context, unsigned short port); // 接收客户端的连接
int main()
{
try {
cout << "服务器已启动!\n";
boost::asio::io_context ioc;
server(ioc, 10086);
// 主线程必须等到子线程结束之后,主线程才会结束
for (auto& t : thread_set) {
if (t->joinable()) // t.joinable()检查t(即当前遍历到的std::thread对象)是否可以被连接
t->join(); // 调用join()的线程将阻塞,直到t所代表的线程执行完毕才会继续下一行
}
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
return 0;
}
void session(socket_ptr sock) {
try {
// 该段代码是循环的,当第一次接收到数据后,程序走到最后又重回第一行,如果第二次没有
// 读到任何信息,那么length==0,此时error未eof,相当于客户端不在发送信息,连接中止
for (;;) {
char data[MAX_LENGTH]; // 缓存收到的数据
memset(data, '\0', MAX_LENGTH); // 将char数组中的数据全部设为\0
boost::system::error_code error;
// 服务器可能不会一次读完,而用read或receive可能会造成堵塞
// 因为这里是独立的线程而不是主程序,所以这里可以用read,一直等,但直到读到最大长度才会返回
//size_t length = boost::asio::read(sock, boost::asio::buffer(data, MAX_LENGTH), error);
// read_some实际只会读收到的长度,在没有粘包的情况下用read_some比较好
size_t length = sock->read_some(boost::asio::buffer(data, MAX_LENGTH), error);
if (error == boost::asio::error::eof) { // 连接被关闭
std::cout << "connection closed by peer" << endl;
break;
}
else if (error) // 读取失败
throw boost::system::system_error(error);
cout << "receive from " << sock->remote_endpoint().address().to_string() << endl;
cout << "receive message is " << data << endl;
// 回传给对方
boost::asio::write(*sock, boost::asio::buffer(data, length));
}
}
catch (std::exception& e) {
cout << "Exception in thread: " << e.what() << "\n" << endl;
}
}
void server(boost::asio::io_context& io_context, unsigned short port) {
// 生成一个用于接收客户端连接的acceptor,绑定的端点是服务器的地址(用ipv4的方式绑定)以及自定义的端口
tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port));
while (1) {
// 生成一个socket来处理客户端的请求,相当于服务员
socket_ptr socket(new tcp::socket(io_context));
// accept接收到请求后,用socket对请求进行处理
a.accept(*socket);
// 生成一个线程,相当于服务员将客户带到包间,大厅仍进行客人的招待
// 线程中也就是包间中,用session作为招待服务,session就是服务器的读和写
// 如果不创建线程,而是直接session,那么如果对端不发送数据,程序就会阻塞一直不运行下一行,其他的连接也会无法接收
// 之所以将线程存储到智能指针中,是为了管理其生命周期,防止线程对象被提前销毁
auto t = std::make_shared<std::thread>(session, socket);
// thread_set是全局变量而不是局部变量,当server函数结束后,thread_set仍会保留
thread_set.insert(t);
}
}
客户端操作过程:
- 绑定服务器ip和端口号为端点->
- 创建socket->
- socket尝试连接服务器端点->
- 如果连接成功,通过boost::asio::write(sock,buffer)函数进行数据的发送,第一个参数是客户端创建的与服务器端点连接成功的socket,第二个参数是客户端要发送的数据,通过buffer函数进行类型转换->
- 发送后,等待回传消息,使用boost::asio::read(sock,buffer),进行读取服务器传回的信息,第一个参数是与服务器端点连接成功的socket,第二个参数buffer,buffer中有一个容器,用于存储服务器传回的信息。
服务器操作过程:
1)进入server函数,首先生成一个用于接收客户端连接的acceptor,将服务器地址用ipv4的方式与自定义端口号绑定,然后进入循环,创建一个元素成员类型为tcp::socket的智能指针容器(该容器可以不断的生成socket类型,相当于生成多个服务员应对不同的线程请求),如果acceptor接收到客户端请求,用智能指针容器中的socket去处理该请求,并为此创建一个元素成员类型是线程的智能指针容器,该容器中的第一个元素(线程)是第一个socket以及socket的任务session;最后,将该容器插入至全局变量集中,管理其声明周期。
2)socket在新线程中处理客户端的请求,首先,用这个socket读取客户端发送的信息,并将其保存至自定义的容器(缓冲区)中,并对该数据进行处理;然后,boost::asio::write(sock,buffer)函数进行数据回传,第一个参数就是服务员一开始的socket,第二个参数是想要回传的数据。
3)为了防止子线程的内容还未结束,主程序就停止,使用t->join()保证子线程的程序结束后,才会进行下一行命令
总结:
- make_shared为什么可以管理其声明周期,防止线程对象被提前销毁?
1)工作原理:
std::shared_ptr
是一种引用计数的智能指针。当你使用std::make_shared
创建一个对象时,它不仅会分配内存并构造该对象,还会创建一个控制块(control block),这个控制块包含:- 一个指向实际对象的指针。
- 一个引用计数(reference count),用于跟踪有多少个
std::shared_ptr
指向这个对象。 - 一个弱引用计数(weak reference count),用于跟踪有多少个
std::weak_ptr
指向这个对象。 - 每当创建一个新的
std::shared_ptr
指向同一个对象时,引用计数会增加;每当一个std::shared_ptr
被销毁或重置(reset)时,引用计数会减少。当引用计数变为零时(即没有任何std::shared_ptr
指向该对象时),对象会自动被销毁,其占用的内存也会被释放
2)与new的比较
- 内存效率:
std::make_shared
在一个单一的内存分配中同时分配对象和控制块。这通常比使用new
和随后创建一个std::shared_ptr
更加高效,因为后者可能需要两次内存分配(一次为对象,一次为控制块)。- 简化代码,防止内存泄漏:
- 使用
new
时,如果在创建对象和赋值给智能指针之间发生异常,可能导致内存泄漏。std::make_shared
避免了这个问题,因为它是一个原子操作,要么成功创建对象并管理它,要么什么都不做。
原文地址:https://blog.csdn.net/m0_63086198/article/details/142577678
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!