网络编程(2)——同步服务器设计

发布于:2024-10-12 ⋅ 阅读:(12) ⋅ 点赞:(0)

三、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);
    }
}

客户端操作过程:

  1. 绑定服务器ip和端口号为端点->
  2. 创建socket->
  3. socket尝试连接服务器端点->
  4. 如果连接成功,通过boost::asio::write(sock,buffer)函数进行数据的发送,第一个参数是客户端创建的与服务器端点连接成功的socket,第二个参数是客户端要发送的数据,通过buffer函数进行类型转换->
  5. 发送后,等待回传消息,使用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()保证子线程的程序结束后,才会进行下一行命令

总结:

  1. 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 避免了这个问题,因为它是一个原子操作,要么成功创建对象并管理它,要么什么都不做。