Skip to content

fix: support Linux dual-stack listen for HTTP and RPC#1190

Open
shenxuebing wants to merge 15 commits into
alibaba:mainfrom
shenxuebing:main
Open

fix: support Linux dual-stack listen for HTTP and RPC#1190
shenxuebing wants to merge 15 commits into
alibaba:mainfrom
shenxuebing:main

Conversation

@shenxuebing

Copy link
Copy Markdown
Contributor

Why

Close: #1185

What is changing

Test

TEST_CASE("test server acceptor") and TEST_CASE("http listen on ipv6 any accepts ipv4 and ipv6 connections")

  - add shared IPv6-any address detection for listen endpoints
  - on Linux, bind IPv6-any listeners as IPv6-only and add a separate IPv4 acceptor
  - keep existing single-socket dual-stack behavior on Windows and other platforms
  - add HTTP dual-stack connectivity test and RPC acceptor layout test
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

for detail, goto summary download Artifacts base-ylt-cov-report(base commit coverage report) and ylt-cov-report(current pull request coverage report)

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

for detail, goto summary download Artifacts base-ylt-cov-report(base commit coverage report) and ylt-cov-report(current pull request coverage report)

@qicosmos

qicosmos commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

@milestone-17 也帮忙review一下,看看之前的pr漏掉了什么路径。

shenxuebing and others added 6 commits June 2, 2026 11:39
  - add shared IPv6-any address detection for listen endpoints
  - on Linux, bind IPv6-any listeners as IPv6-only and add a separate IPv4 acceptor
  - keep existing single-socket dual-stack behavior on Windows and other platforms
  - add HTTP dual-stack connectivity test and RPC acceptor layout test
@qicosmos

qicosmos commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

一些代码格式化暂时不要改,影响review代码,可以先不改动格式,代码review没问题后统一format就行了。

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

for detail, goto summary download Artifacts base-ylt-cov-report(base commit coverage report) and ylt-cov-report(current pull request coverage report)

@qicosmos

qicosmos commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

PR #1190 Review

1. 代码 Bug

1.1 RPC 的 port=0 场景没有真正启用双 acceptor

这个 PR 的目标是 Linux 下监听 IPv6 any 地址时,不只依赖 IPv4-mapped IPv6,而是创建两个独立 socket:

IPv6: ::
IPv4: 0.0.0.0

这对 WSL2 场景是必要的,因为 WSL2 的 IPv4 NAT 流量不一定能正确转成 IPv4-mapped IPv6 投递到 IPv6 socket。

但是当前 RPC 初始化逻辑只有在 port > 0 时才创建两个 acceptor:

void init_acceptors(std::string_view address, uint16_t port) {
#if defined(__linux__)
  if (port > 0 && coro_io::detail::is_ipv6_any_address(address)) {
    add_acceptor(address, port, true);
    add_acceptor("0.0.0.0", port);
    return;
  }
#endif
  add_acceptor(address, port);
}

因此下面这个场景仍然只有一个 IPv6 acceptor:

coro_rpc_server server(1, static_cast<unsigned short>(0), "::");

PR 新增的 RPC 测试正好使用 port=0,所以它的 IPv4 连接通过并不能证明这个 PR 新增的“双 socket”路径正确。它仍然可能只是通过原来的 v6_only(false) 单 socket 路径通过。

这个问题不能通过简单删除 port > 0 修复。因为两个 port=0 acceptor 如果在构造阶段同时创建并分别 listen,会拿到两个不同的临时端口。正确做法应该是:

  1. 先创建 IPv6 acceptor。
  2. IPv6 acceptor listen 后取得实际端口。
  3. 再用这个实际端口创建 IPv4 acceptor。
  4. 两个 acceptor 都使用同一个端口。

1.2 RPC 的字符串地址构造入口绕过了双 acceptor 初始化

RPC 还有一个构造入口接收完整地址字符串,例如:

coro_rpc_server server(1, "[::]:8827");

当前这个入口直接构造一个 tcp_server_acceptor,没有调用统一的 init_acceptors()

coro_rpc_server_base(size_t thread_num, std::string address,
                     std::chrono::steady_clock::duration conn_timeout_duration,
                     bool is_enable_tcp_no_delay)
    : pool_(thread_num),
      flag_{stat::init},
      is_enable_tcp_no_delay_(is_enable_tcp_no_delay),
      conn_timeout_duration_(conn_timeout_duration) {
  acceptors_.push_back(
      std::make_unique<coro_io::tcp_server_acceptor>(address));
}

这会导致 "[::]:8827" 在 Linux 下仍然只有一个 acceptor,没有额外创建 0.0.0.0:8827

所有构造入口都应该走同一套地址解析和双 acceptor 初始化逻辑,否则这个修复只覆盖部分用户用法。

1.3 RPC 启动失败后会保留已经成功监听的 IPv6 socket

双 acceptor 启动时存在一个失败路径:

  1. IPv6 acceptor bind/listen 成功。
  2. IPv4 acceptor bind/listen 失败。
  3. async_start() 返回错误。

当前 RPC 代码只设置错误状态并退出 listen 循环:

for (auto& acceptor : acceptors_) {
  acceptor->set_io_threads_pool(&pool_);
  auto ec = acceptor->listen();
  if (ec != coro_io::listen_errc::ok) {
    errc_ = ...;
  }
  if (errc_) {
    break;
  }
}

失败后又把状态设置为 stop

else {
  flag_ = stat::stop;
}

stop() 遇到 stop 状态会直接返回:

if (flag_ == stat::stop) {
  return;
}

结果是:启动失败的 server 对象仍然持有已经 listen 成功的 IPv6 socket,直到对象析构才释放。

这会导致应用看到启动失败后,端口仍然被当前 server 对象占用。

1.4 HTTP 启动失败后也会保留已经成功监听的 IPv6 socket

HTTP 的双 acceptor 初始化也有同类问题。

当前逻辑先初始化 primary IPv6 acceptor,再初始化 IPv4 acceptor:

if (need_dual_stack) {
  acceptor_v4_.emplace(acceptor_.get_executor());
  if (auto init_ec = init_acceptor(
          *acceptor_v4_, coro_io::detail::make_ipv4_any_endpoint(port_));
      init_ec) {
    acceptor_v4_.reset();
    return init_ec;
  }
}

如果 IPv4 acceptor 初始化失败,代码只 reset 了 IPv4 acceptor,没有关闭已经成功 listen 的 IPv6 acceptor。

因此 HTTP 在 async_start() 返回失败后,也可能在 server 对象存活期间继续占用 IPv6 端口。

2. 重复代码和复用问题

2.1 HTTP 和 RPC 重复实现了 socket 初始化流程

HTTP 和 RPC 都实现了下面这套流程:

open socket
set reuse_address
set IPV6_V6ONLY
bind
listen
failure cleanup

这类底层 socket 初始化逻辑应该抽到公共 helper。否则后续修复失败清理、reuse_address 顺序、IPV6_V6ONLY 策略时,很容易 HTTP 修了、RPC 漏了,或者反过来。

建议抽象成:

enum class ipv6_only_mode {
  keep,
  enable,
  disable,
};

asio::error_code init_tcp_acceptor(
    asio::ip::tcp::acceptor& acceptor,
    const asio::ip::tcp::endpoint& endpoint,
    ipv6_only_mode mode);

HTTP/RPC 各自只保留:

  • 日志格式
  • 错误码映射
  • accept loop
  • connection 处理

2.2 set_ipv6_only_false()set_ipv6_only(..., bool) 重复

当前有两个语义重叠的函数:

inline asio::error_code set_ipv6_only_false(
    asio::ip::tcp::acceptor& acceptor,
    const asio::ip::tcp::endpoint& endpoint);

inline asio::error_code set_ipv6_only(
    asio::ip::tcp::acceptor& acceptor,
    const asio::ip::tcp::endpoint& endpoint,
    bool enabled);

建议只保留一个通用版本。如果要保留 set_ipv6_only_false(),也应该写成 wrapper:

inline asio::error_code set_ipv6_only_false(
    asio::ip::tcp::acceptor& acceptor,
    const asio::ip::tcp::endpoint& endpoint) {
  return set_ipv6_only(acceptor, endpoint, false);
}

2.3 HTTP 和 RPC 的地址解析规则不一致

RPC 的地址解析使用 rfind(':'),并处理 bracket IPv6 地址。

HTTP 的地址解析使用 find(':') + is_ip_v6()

这会让下面这些输入在两个模块里的行为不完全一致:

::
[::]:9001
0.0.0.0:9001
localhost:9001

建议抽公共解析函数:

struct listen_address {
  std::string address;
  uint16_t port;
};

listen_address parse_listen_address(std::string_view address,
                                    uint16_t default_port);

然后 HTTP 和 RPC 都使用同一个函数,避免用户传相同地址时两个模块行为不同。

2.4 IPv6 any 判断不应该维护字符串白名单

当前判断 IPv6 any 地址使用字符串白名单:

inline bool is_ipv6_any_address(std::string_view address) {
  return address == "::" || address == "::0" || address == "::0.0.0.0" ||
         address == "0:0:0:0:0:0:0:0" || address == "[::]";
}

这个方式容易漏写等价表达,也受地址是否已经去 bracket、是否已经规范化影响。

更稳的做法是基于解析后的 endpoint 判断:

inline bool is_ipv6_any_endpoint(const asio::ip::tcp::endpoint& endpoint) {
  return endpoint.address().is_v6() &&
         endpoint.address().to_v6().is_unspecified();
}

如果仍需要接受原始字符串,也应该先统一解析成 endpoint,再判断 endpoint,而不是维护字符串列表。

3. 完整测试代码

3.1 测试代码 1:证明 RPC port=0 和字符串构造没有创建双 acceptor

#include <iostream>
#include <string>

#include "ylt/coro_rpc/coro_rpc_server.hpp"
#include "ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp"

namespace {

int expect_two_acceptors(std::string_view name,
                         const coro_rpc::coro_rpc_server& server) {
  const auto& acceptors = server.get_acceptors();
  std::cout << name << ": acceptors=" << acceptors.size();
  for (const auto& acceptor : acceptors) {
    std::cout << " [" << acceptor->address() << ":" << acceptor->port() << "]";
  }
  std::cout << "\n";

#if defined(__linux__)
  if (acceptors.size() != 2) {
    std::cerr << "BUG: Linux IPv6-any listen should create separate IPv6 and "
                 "IPv4 acceptors for this path.\n";
    return 1;
  }
#endif
  return 0;
}

}  // namespace

int main() {
  int failures = 0;

  coro_rpc::coro_rpc_server fixed_port(1, static_cast<unsigned short>(8826),
                                       std::string("::"));
  failures += expect_two_acceptors("fixed port constructor", fixed_port);

  coro_rpc::coro_rpc_server ephemeral_port(1, static_cast<unsigned short>(0),
                                           std::string("::"));
  failures += expect_two_acceptors("port 0 constructor", ephemeral_port);

  coro_rpc::coro_rpc_server address_string(1, std::string("[::]:8827"));
  failures += expect_two_acceptors("address string constructor", address_string);

  return failures == 0 ? 0 : 1;
}

我实测输出:

BUG: Linux IPv6-any listen should create separate IPv6 and IPv4 acceptors for this path.
BUG: Linux IPv6-any listen should create separate IPv6 and IPv4 acceptors for this path.
fixed port constructor: acceptors=2 [:::8826] [0.0.0.0:8826]
port 0 constructor: acceptors=1 [:::0]
address string constructor: acceptors=1 [:::8827]

这个测试证明:

  • 固定端口构造入口能创建两个 acceptor。
  • port=0 构造入口不能创建两个 acceptor。
  • "[::]:port" 字符串构造入口不能创建两个 acceptor。

3.2 测试代码 2:证明 RPC 启动失败后没有关闭 primary IPv6 acceptor

#include <iostream>
#include <string>

#include "asio/io_context.hpp"
#include "asio/ip/tcp.hpp"
#include "asio/ip/v6_only.hpp"
#include "ylt/coro_rpc/coro_rpc_server.hpp"
#include "ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp"

namespace {

asio::error_code bind_ipv6_only_probe(uint16_t port) {
  asio::io_context ctx;
  asio::ip::tcp::acceptor probe(ctx);
  asio::error_code ec;
  probe.open(asio::ip::tcp::v6(), ec);
  if (ec) {
    return ec;
  }
  probe.set_option(asio::ip::v6_only(true), ec);
  if (ec) {
    return ec;
  }
  probe.bind({asio::ip::address_v6::any(), port}, ec);
  return ec;
}

}  // namespace

int main() {
#if !defined(__linux__)
  std::cout << "This counterexample is Linux-specific.\n";
  return 0;
#else
  asio::io_context ctx;
  asio::ip::tcp::acceptor ipv4_blocker(ctx);
  asio::error_code ec;
  ipv4_blocker.open(asio::ip::tcp::v4(), ec);
  if (ec) {
    std::cerr << "failed to open IPv4 blocker: " << ec.message() << "\n";
    return 2;
  }
  ipv4_blocker.bind({asio::ip::address_v4::any(), 0}, ec);
  if (ec) {
    std::cerr << "failed to bind IPv4 blocker: " << ec.message() << "\n";
    return 2;
  }
  ipv4_blocker.listen(asio::socket_base::max_listen_connections, ec);
  if (ec) {
    std::cerr << "failed to listen IPv4 blocker: " << ec.message() << "\n";
    return 2;
  }

  const auto port = ipv4_blocker.local_endpoint().port();
  std::cout << "blocked IPv4 port " << port << "\n";

  {
    coro_rpc::coro_rpc_server server(1, port, std::string("::"));
    const auto start_error = server.async_start().get();
    std::cout << "server start error: " << start_error.message() << "\n";
    if (!start_error) {
      std::cerr << "setup failed: expected IPv4 acceptor bind to fail\n";
      server.stop();
      return 2;
    }

    const auto probe_error = bind_ipv6_only_probe(port);
    std::cout << "IPv6 probe while failed server is alive: "
              << (probe_error ? probe_error.message() : "ok") << "\n";
    if (probe_error) {
      std::cerr << "BUG: failed startup left the primary IPv6 acceptor bound.\n";
    }
    else {
      std::cerr << "No leak detected; expected current PR to fail here.\n";
      return 1;
    }
  }

  const auto probe_after_destruct = bind_ipv6_only_probe(port);
  std::cout << "IPv6 probe after server destruction: "
            << (probe_after_destruct ? probe_after_destruct.message() : "ok")
            << "\n";
  return probe_after_destruct ? 1 : 0;
#endif
}

我实测输出:

blocked IPv4 port 42393
server start error: address in used
IPv6 probe while failed server is alive: Address already in use
IPv6 probe after server destruction: ok
BUG: failed startup left the primary IPv6 acceptor bound.

这个测试证明:

  • 第二个 IPv4 acceptor 失败后,async_start() 返回错误。
  • 但是失败 server 对象存活期间,IPv6 端口仍然被占用。
  • server 析构后,IPv6 端口才释放。

3.3 测试代码 3:证明 HTTP 启动失败后没有关闭 primary IPv6 acceptor

#include <iostream>
#include <string>

#include "asio/io_context.hpp"
#include "asio/ip/tcp.hpp"
#include "asio/ip/v6_only.hpp"
#include "ylt/coro_http/coro_http_server.hpp"

namespace {

asio::error_code bind_ipv6_only_probe(uint16_t port) {
  asio::io_context ctx;
  asio::ip::tcp::acceptor probe(ctx);
  asio::error_code ec;
  probe.open(asio::ip::tcp::v6(), ec);
  if (ec) {
    return ec;
  }
  probe.set_option(asio::ip::v6_only(true), ec);
  if (ec) {
    return ec;
  }
  probe.bind({asio::ip::address_v6::any(), port}, ec);
  return ec;
}

}  // namespace

int main() {
#if !defined(__linux__)
  std::cout << "This counterexample is Linux-specific.\n";
  return 0;
#else
  asio::io_context ctx;
  asio::ip::tcp::acceptor ipv4_blocker(ctx);
  asio::error_code ec;
  ipv4_blocker.open(asio::ip::tcp::v4(), ec);
  if (ec) {
    std::cerr << "failed to open IPv4 blocker: " << ec.message() << "\n";
    return 2;
  }
  ipv4_blocker.bind({asio::ip::address_v4::any(), 0}, ec);
  if (ec) {
    std::cerr << "failed to bind IPv4 blocker: " << ec.message() << "\n";
    return 2;
  }
  ipv4_blocker.listen(asio::socket_base::max_listen_connections, ec);
  if (ec) {
    std::cerr << "failed to listen IPv4 blocker: " << ec.message() << "\n";
    return 2;
  }

  const auto port = ipv4_blocker.local_endpoint().port();
  std::cout << "blocked IPv4 port " << port << "\n";

  {
    coro_http::coro_http_server server(1, port, std::string("::"), false);
    const auto start_error = server.async_start().get();
    std::cout << "server start error: "
              << (start_error ? start_error.message() : "ok") << "\n";
    if (!start_error) {
      std::cerr << "setup failed: expected IPv4 acceptor bind to fail\n";
      server.stop();
      return 2;
    }

    const auto probe_error = bind_ipv6_only_probe(port);
    std::cout << "IPv6 probe while failed server is alive: "
              << (probe_error ? probe_error.message() : "ok") << "\n";
    if (probe_error) {
      std::cerr << "BUG: failed startup left the primary IPv6 acceptor bound.\n";
    }
    else {
      std::cerr << "No leak detected; expected current PR to fail here.\n";
      return 1;
    }
  }

  const auto probe_after_destruct = bind_ipv6_only_probe(port);
  std::cout << "IPv6 probe after server destruction: "
            << (probe_after_destruct ? probe_after_destruct.message() : "ok")
            << "\n";
  return probe_after_destruct ? 1 : 0;
#endif
}

我实测输出:

BUG: failed startup left the primary IPv6 acceptor bound.
blocked IPv4 port 41645
server start error: Address already in use
IPv6 probe while failed server is alive: Address already in use
IPv6 probe after server destruction: ok

这个测试证明 HTTP 和 RPC 有相同的失败清理问题。

4. 修复和完善代码建议

4.1 抽公共 socket 初始化 helper

建议新增公共 helper:

namespace coro_io::detail {

enum class ipv6_only_mode {
  keep,
  enable,
  disable,
};

inline asio::error_code set_ipv6_only(asio::ip::tcp::acceptor& acceptor,
                                      const asio::ip::tcp::endpoint& endpoint,
                                      ipv6_only_mode mode) {
  asio::error_code ec;
  if (endpoint.protocol() == asio::ip::tcp::v6() &&
      mode != ipv6_only_mode::keep) {
    acceptor.set_option(asio::ip::v6_only(mode == ipv6_only_mode::enable), ec);
  }
  return ec;
}

inline void close_acceptor_now(asio::ip::tcp::acceptor& acceptor) {
  asio::error_code ec;
  acceptor.cancel(ec);
  acceptor.close(ec);
}

inline asio::error_code init_tcp_acceptor(
    asio::ip::tcp::acceptor& acceptor,
    const asio::ip::tcp::endpoint& endpoint,
    ipv6_only_mode mode) {
  using asio::ip::tcp;
  asio::error_code ec;

  acceptor.open(endpoint.protocol(), ec);
  if (ec) {
    return ec;
  }

#ifdef __GNUC__
  acceptor.set_option(tcp::acceptor::reuse_address(true), ec);
#endif

  if (auto opt_ec = set_ipv6_only(acceptor, endpoint, mode); opt_ec) {
    close_acceptor_now(acceptor);
    return opt_ec;
  }

  acceptor.bind(endpoint, ec);
  if (ec) {
    close_acceptor_now(acceptor);
    return ec;
  }

#ifdef _MSC_VER
  acceptor.set_option(tcp::acceptor::reuse_address(true), ec);
#endif

  acceptor.listen(asio::socket_base::max_listen_connections, ec);
  if (ec) {
    close_acceptor_now(acceptor);
  }
  return ec;
}

inline asio::ip::tcp::endpoint make_ipv4_any_endpoint(uint16_t port) {
  return {asio::ip::address_v4::any(), port};
}

inline bool is_ipv6_any_endpoint(const asio::ip::tcp::endpoint& endpoint) {
  return endpoint.address().is_v6() &&
         endpoint.address().to_v6().is_unspecified();
}

}  // namespace coro_io::detail

4.2 RPC acceptor 支持启动失败时立即关闭

启动失败时不能调用会等待 accept loop 的关闭函数,因为 accept loop 可能还没有启动。

建议增加一个不等待 accept loop 的关闭接口:

struct server_acceptor_base {
  virtual void close() = 0;
  virtual void close_now() = 0;
  virtual listen_errc listen() = 0;

  bool ipv6_dual_stack() const noexcept { return ipv6_dual_stack_; }
};

对应实现:

struct tcp_server_acceptor : public server_acceptor_base {
  void close_now() override {
    if (!acceptor_) {
      return;
    }
    coro_io::detail::close_acceptor_now(*acceptor_);
  }

  void close() override {
    close_now();
    if (accept_started_) {
      acceptor_close_future_.wait();
    }
  }

  async_simple::coro::Lazy<
      ylt::expected<coro_io::socket_wrapper_t, std::error_code>>
  accept() override {
    accept_started_ = true;

    assert(acceptor_ != std::nullopt);
    auto executor = pool_->get_executor();
    asio::ip::tcp::socket socket(executor->get_asio_executor());
    auto error = co_await coro_io::async_accept(*acceptor_, socket);
    if (error) {
      if (error == asio::error::operation_aborted ||
          error == asio::error::bad_descriptor) {
        acceptor_close_waiter_.set_value();
      }
      co_return ylt::expected<coro_io::socket_wrapper_t, std::error_code>{
          ylt::unexpected<std::error_code>{error}};
    }

    co_return coro_io::socket_wrapper_t{std::move(socket), executor};
  }

  bool accept_started_ = false;
  std::promise<void> acceptor_close_waiter_;
  std::future<void> acceptor_close_future_ =
      acceptor_close_waiter_.get_future();
};

4.3 RPC port=0 双 acceptor 修复

构造阶段只创建 IPv6 acceptor。启动时 IPv6 acceptor listen 成功后,再用实际端口补 IPv4 acceptor。

void init_acceptors(std::string_view address, uint16_t port) {
  auto parsed = coro_io::detail::parse_listen_address(address, port);

#if defined(__linux__)
  if (coro_io::detail::is_ipv6_any_address(parsed.address)) {
    add_acceptor(parsed.address, parsed.port, true);
    if (parsed.port > 0) {
      add_acceptor("0.0.0.0", parsed.port);
    }
    return;
  }
#endif

  add_acceptor(parsed.address, parsed.port);
}

启动阶段:

for (size_t i = 0; i < acceptors_.size(); ++i) {
  auto& acceptor = acceptors_[i];
  acceptor->set_io_threads_pool(&pool_);

  auto ec = acceptor->listen();
  if (ec != coro_io::listen_errc::ok) {
    errc_ = map_listen_error(ec);
    for (auto& opened : acceptors_) {
      opened->close_now();
    }
    break;
  }

#if defined(__linux__)
  if (acceptor->ipv6_dual_stack() && acceptor->port() > 0 &&
      acceptors_.size() == 1) {
    add_acceptor("0.0.0.0", acceptor->port());
    acceptors_.back()->set_io_threads_pool(&pool_);
  }
#endif
}

4.4 RPC 字符串地址入口也走统一初始化

把字符串地址构造入口改成:

coro_rpc_server_base(size_t thread_num, std::string address,
                     std::chrono::steady_clock::duration
                         conn_timeout_duration = std::chrono::seconds(0),
                     bool is_enable_tcp_no_delay = true)
    : pool_(thread_num),
      flag_{stat::init},
      is_enable_tcp_no_delay_(is_enable_tcp_no_delay),
      conn_timeout_duration_(conn_timeout_duration) {
  init_acceptors(address, 0);
}

这样 "[::]:port""0.0.0.0:port""localhost:port" 等字符串入口和普通 (address, port) 入口使用同一套逻辑。

4.5 HTTP v4 初始化失败时关闭 primary acceptor

HTTP 至少需要在 IPv4 acceptor 初始化失败时关闭 primary IPv6 acceptor:

if (need_dual_stack) {
  acceptor_v4_.emplace(acceptor_.get_executor());
  if (auto init_ec = init_acceptor(
          *acceptor_v4_, coro_io::detail::make_ipv4_any_endpoint(port_));
      init_ec) {
    coro_io::detail::close_acceptor_now(acceptor_);
    acceptor_v4_.reset();
    return init_ec;
  }
}

如果采用公共 init_tcp_acceptor(),HTTP 的 init_acceptor() 也可以删除或变成薄 wrapper。

4.6 应补充的单元测试

RPC 应至少补:

SUBCASE("ipv6 any port 0 creates ipv4 acceptor after start") {
  coro_rpc_server server(1, static_cast<unsigned short>(0), "::");
  server.register_handler<hello>();

  auto res = server.async_start();
  REQUIRE(!res.hasResult());
  CHECK(server.get_acceptors().size() == 2);

  server.stop();
}

SUBCASE("ipv6 any address string creates dual acceptors") {
  coro_rpc_server server(1, "[::]:8827");
  CHECK(server.get_acceptors().size() == 2);
}

SUBCASE("failed second acceptor closes primary acceptor") {
  // 先占用 0.0.0.0:port。
  // 再启动 :: 同端口。
  // start 应失败。
  // 随后 bind IPv6-only ::port 应成功。
}

HTTP 也应该补同类失败清理测试。

结论

这个 PR 的方向是对的:Linux/WSL2 下监听 :: 时,独立 IPv4 acceptor 比只依赖 IPV6_V6ONLY=false 更可靠。

但当前实现还没有闭合:

  1. RPC port=0 入口没有真正创建双 acceptor。
  2. RPC 字符串地址入口绕过了双 acceptor 初始化。
  3. RPC/HTTP 在第二个 acceptor 初始化失败时,没有关闭已经成功监听的 primary acceptor。
  4. HTTP/RPC 重复实现 socket 初始化流程,导致失败清理问题两边同时存在。

建议先修复入口覆盖和失败清理,再把底层 socket 初始化、地址解析、IPv6 any 判断抽到公共 helper,最后补上上面的反例测试。

# Conflicts:
#	include/ylt/coro_io/listen_endpoint.hpp
#	include/ylt/coro_io/server_acceptor.hpp
#	include/ylt/coro_rpc/impl/coro_rpc_server.hpp
#	include/ylt/standalone/cinatra/coro_http_server.hpp
#	src/coro_http/tests/test_coro_http_server.cpp
#	src/coro_rpc/tests/test_acceptor.cpp
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

for detail, goto summary download Artifacts base-ylt-cov-report(base commit coverage report) and ylt-cov-report(current pull request coverage report)

@qicosmos qicosmos self-requested a review June 6, 2026 01:04

@qicosmos qicosmos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

把格式修一下

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

for detail, goto summary download Artifacts base-ylt-cov-report(base commit coverage report) and ylt-cov-report(current pull request coverage report)

@poor-circle

Copy link
Copy Markdown
Collaborator

@shenxuebing 这个冲突麻烦有时间看下?

@shenxuebing

Copy link
Copy Markdown
Contributor Author

@shenxuebing 这个冲突麻烦有时间看下?

哪个文件

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants