使用websocket++/websocketpp库在服务重启后报[info] asio listen error: system:98 (地址已在使用)端口重用的问题

本文介绍如何在使用WebSocket++库时解决Linux下服务重启时的端口重用问题,通过设置SO_REUSEADDR选项避免端口占用,确保服务稳定重启。

项目在使用websocket++这个开源库,作为websocket的服务器端来跟网页通信,虽然可以使用,但是有个问题就是在linux下的服务每次在关闭后马上启动,就会报端口重用的错误:

[2020-07-22 11:10:13] [info] asio listen error: system:98 (地址已在使用)

如果把服务停掉等待一分多钟后启动,就不会报这个错误了,说明端口已经被释放。如果监听的套接字设置套接字选项SO_REUSEADDR,就能解决,一般而言,作为服务器监听的套接字都必须设置这个套接字选项,因为这样才能保证服务在崩溃重启之后不会出现端口占用问题。

websocket++我使用的是boost的asio作为网络通信的库,可以说websocket++封装了boost asio的东西。刚开始没时间研究websocket++这个库的源码,都是在网上百度了一下别人的示例教程就自己封装了一下,主管又急着要项目上线,能用就用,这个重启端口重用问题算是小问题,既然那么说我也赖得管这些bug,然后就用了一段时间。现在有点时间要尝试自己解决一下。

自己看了一下这个服务器类

typedef websocketpp::server<websocketpp::config::asio> server;

看有没有提供什么接口来对底层的套接字选项进行设置。看了看websocketpp的源码,listen函数有如下源码:

    void listen(lib::asio::ip::tcp::endpoint const & ep, lib::error_code & ec)
    {
        if (m_state != READY) {
            m_elog->write(log::elevel::library,
                "asio::listen called from the wrong state");
            using websocketpp::error::make_error_code;
            ec = make_error_code(websocketpp::error::invalid_state);
            return;
        }

        m_alog->write(log::alevel::devel,"asio::listen");

        lib::asio::error_code bec;

        m_acceptor->open(ep.protocol(),bec);
        if (bec) {ec = clean_up_listen_after_error(bec);return;}
        
        m_acceptor->set_option(lib::asio::socket_base::reuse_address(m_reuse_addr),bec);
        if (bec) {ec = clean_up_listen_after_error(bec);return;}
        
        // if a TCP pre-bind handler is present, run it
        if (m_tcp_pre_bind_handler) {
            ec = m_tcp_pre_bind_handler(m_acceptor);
            if (ec) {
                ec = clean_up_listen_after_error(ec);
                return;
            }
        }
        
        m_acceptor->bind(ep,bec);
        if (bec) {ec = clean_up_listen_after_error(bec);return;}
        
        m_acceptor->listen(m_listen_backlog,bec);
        if (bec) {ec = clean_up_listen_after_error(bec);return;}
        
        // Success
        m_state = LISTENING;
        ec = lib::error_code();
    }

这句 m_acceptor->set_option(lib::asio::socket_base::reuse_address(m_reuse_addr),bec);,就是设置套接字选项SO_REUSEADDR的,m_reuse_addr这是websocketpp::server<websocketpp::config::asio>类的成员变量,找了一下,这个类有个set_reuse_addr这个函数。大功告成,八九不离十,

。于是在套接字监听前加上这条语句(加粗那句):

       //这里是我定义的全局服务器对象

        //typedef websocketpp::server<websocketpp::config::asio> server;   

        //static server g_server;//全局的websocket服务器

        g_server.set_reuse_addr(true);

        g_server.listen(websocketpp::lib::asio::ip::tcp::v4(), uPort);

再编译重试了一遍,结果重启后就不会报端口重用的错误了,完美解决。

这里贴一下解决的问题的方法,希望遇到这个坑的童鞋可以找到这篇文章解决问题。

### `boost::asio::ip::tcp::acceptor` 的使用问题解析 `boost::asio::ip::tcp::acceptor` 是 Boost.Asio 提供的一个用于 TCP 服务端监听和接受连接的核心组件。它封装了底层 socket 的绑定、监听和接受连接操作,简化了服务端的实现流程。 #### 初始化与绑定 在创建 `tcp::acceptor` 实例时,通常需要指定一个 `io_context` 对象和一个本地端点(`endpoint`),后者定义了绑定的 IP 地址端口号。若未指定 IP 地址,则默认绑定到所有本地接口。例如: ```cpp boost::asio::ip::tcp::acceptor acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 8080)); ``` 该操作会将 acceptor 绑定到 IPv4 的 8080 端口,并自动调用 `listen()` 进入监听状态。如果需要更细粒度的控制,可以显式调用 `listen()` 方法,并指定 backlog 值,该值决定了等待连接队列的最大长度。 #### 接受连接 `acceptor` 的核心功能是通过 `accept()` 方法接受新的客户端连接。该方法会阻塞当前线程,直到有新的连接到达。每次调用 `accept()` 都会返回一个新的 `tcp::socket` 对象,用于与客户端通信: ```cpp boost::asio::ip::tcp::socket socket(io_context); acceptor.accept(socket); ``` 在异步编程模型中,也可以使用 `async_accept()` 启动异步接受操作。该方法接受一个回调函数作为参数,当新连接到达时触发该回调函数执行后续处理: ```cpp acceptor.async_accept([&](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) { if (!ec) { // 处理新连接 } }); ``` 需要注意的是,异步接受操作必须在 `io_context.run()` 被调用的前提下才能正常执行。 #### 常见问题与解决方案 1. **绑定失败**:若端口已被占用或地址不可用,`acceptor` 初始化时会抛出异常。可以通过捕获 `boost::system::error_code` 或 `std::exception` 来处理错误: ```cpp boost::asio::ip::tcp::acceptor acceptor(io_context, endpoint, boost::asio::socket_base::reuse_address(true)); ``` 使用 `reuse_address(true)` 可以启用地址复用选项,避免因端口处于 TIME_WAIT 状态而无法绑定的问题。 2. **并发接受连接**:在高并发场景下,单个 `acceptor` 实例可能成为性能瓶颈。可以通过在多个线程中运行 `io_context.run()` 来提高并发处理能力,但需注意同步问题。另一种方式是使用 `fork()` 创建子进程处理新连接,但这增加了进程间通信的复杂性。 3. **异步操作生命周期管理**:使用 `async_accept()` 时,必须确保 `acceptor` 和 `io_context` 的生命周期足够长,否则可能导致异步操作未完成就被销毁,引发未定义行为。 4. **IPv4 与 IPv6 兼容性**:默认情况下,`acceptor` 只监听 IPv4 连接。若需支持 IPv6,应使用 `tcp::v6()` 构造端点,并确保操作系统支持 IPv6: ```cpp boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v6(), 8080); ``` 5. **关闭 acceptor**:在服务端关闭时,应先调用 `close()` 方法释放资源,避免资源泄漏: ```cpp acceptor.close(); ``` #### 示例代码:异步 TCP 服务端 以下是一个使用 `tcp::acceptor` 实现的异步 TCP 服务端示例: ```cpp #include <boost/asio.hpp> #include <iostream> using boost::asio::ip::tcp; void handle_accept(const boost::system::error_code& ec, tcp::socket socket) { if (!ec) { std::cout << "New client connected!" << std::endl; // 可在此添加异步读写逻辑 } } int main() { try { boost::asio::io_context io_context; tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080)); for (;;) { tcp::socket socket(io_context); acceptor.async_accept([&](boost::system::error_code ec, tcp::socket socket) { handle_accept(ec, std::move(socket)); }); } io_context.run(); } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; } return 0; } ``` 此示例展示了如何使用异步方式接受连接并处理客户端请求。 --- ###
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值