From 2e426f78dc4cccef2114272cac8ae4cda738ff89 Mon Sep 17 00:00:00 2001 From: wang-guannan <136193543@qq.com> Date: Wed, 28 Oct 2026 13:55:16 +0800 Subject: [PATCH 1/2] feat(http): add http_client_set_local_addr to bind the source address before connecting On a multi-NIC host the kernel picks the source address of outgoing packets by the routing table, so the peer may see an unexpected source address. hv::TcpClient already offers bind(local_port, local_host), but hv::HttpClient is a thin wrapper over the opaque C API http_client_t, so there is no way to bind a local address before connecting. Add ConnectTimeoutWithLocalAddr() in base/hsocket, expose it through http_client_set_local_addr() and hv::HttpClient::setLocalAddr(), and add a unit test verifying that the peer sees the bound source address. Behaviour is unchanged unless setLocalAddr() is called. --- Makefile | 1 + base/hsocket.c | 53 ++++++++++++ base/hsocket.h | 5 ++ docs/cn/HttpClient.md | 6 ++ http/client/HttpClient.cpp | 22 ++++- http/client/HttpClient.h | 15 ++++ unittest/CMakeLists.txt | 4 + unittest/http_client_local_addr_test.cpp | 101 +++++++++++++++++++++++ 8 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 unittest/http_client_local_addr_test.cpp diff --git a/Makefile b/Makefile index 34817430b..baef2ead4 100644 --- a/Makefile +++ b/Makefile @@ -403,6 +403,7 @@ unittest: prepare libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil -o bin/threadpool_test unittest/threadpool_test.cpp -pthread $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil -o bin/objectpool_test unittest/objectpool_test.cpp -pthread $(CXX) -g -Wall -O0 -std=c++11 -Ihttp/server -o bin/http_router_test unittest/http_router_test.cpp + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/http_client_local_addr_test unittest/http_client_local_addr_test.cpp -Llib -lhv -pthread $(LDFLAGS) $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Ievpp -Icpputil -Ihttp -Ihttp/client -Ihttp/server -o bin/sizeof_test unittest/sizeof_test.cpp $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Issl -Ievpp -o bin/tlv_test unittest/tlv_test.cpp $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/nslookup unittest/nslookup_test.c protocol/dns.c base/hsocket.c base/htime.c diff --git a/base/hsocket.c b/base/hsocket.c index 1dcc8ae1c..08a48a0b0 100644 --- a/base/hsocket.c +++ b/base/hsocket.c @@ -377,6 +377,59 @@ int ConnectTimeout(const char* host, int port, int ms) { return ConnectFDTimeout(connfd, remaining); } +int ConnectTimeoutWithLocalAddr(const char* host, int port, int ms, const char* local_host, int local_port) { +#ifdef OS_WIN + WSAInit(); +#endif + unsigned int start_time = gettick_ms(); + + sockaddr_u peeraddr; + memset(&peeraddr, 0, sizeof(peeraddr)); + int ret = sockaddr_set_ipport(&peeraddr, host, port); + if (ret != 0) { + return NABS(ret); + } + + sockaddr_u localaddr; + memset(&localaddr, 0, sizeof(localaddr)); + ret = sockaddr_set_ipport(&localaddr, local_host, local_port); + if (ret != 0) { + return NABS(ret); + } + + int connfd = socket(peeraddr.sa.sa_family, SOCK_STREAM, 0); + if (connfd < 0) { + return socket_errno_negative(connfd); + } + + // bind the local address before connect, so that the source address of + // the outgoing packets is the one we asked for. + if (bind(connfd, &localaddr.sa, sockaddr_len(&localaddr)) < 0) { + closesocket(connfd); + return socket_errno_negative(connfd); + } + + nonblocking(connfd); + + ret = connect(connfd, &peeraddr.sa, sockaddr_len(&peeraddr)); +#ifdef OS_WIN + if (ret < 0 && socket_errno() != WSAEWOULDBLOCK) { +#else + if (ret < 0 && socket_errno() != EINPROGRESS) { +#endif + closesocket(connfd); + return socket_errno_negative(connfd); + } + + unsigned int elapsed = gettick_ms() - start_time; + int remaining = ms - (int)elapsed; + if (remaining <= 0) { + closesocket(connfd); + return -ETIMEDOUT; + } + return ConnectFDTimeout(connfd, remaining); +} + #ifdef ENABLE_UDS int BindUnix(const char* path, int type) { sockaddr_u localaddr; diff --git a/base/hsocket.h b/base/hsocket.h index 47169e3f9..94038bb3b 100644 --- a/base/hsocket.h +++ b/base/hsocket.h @@ -165,6 +165,11 @@ HV_EXPORT int ConnectNonblock(const char* host, int port); // Connect(host, port, 1) -> select -> blocking #define DEFAULT_CONNECT_TIMEOUT 10000 // ms HV_EXPORT int ConnectTimeout(const char* host, int port, int ms DEFAULT(DEFAULT_CONNECT_TIMEOUT)); +// ConnectTimeout with a local address bound before connecting, so that the +// source address of the outgoing packets is fixed. +// @param local_host local IP address to bind, e.g. "192.168.1.10" or "fe80::1" +// @param local_port local port to bind, 0 means let the kernel choose one +HV_EXPORT int ConnectTimeoutWithLocalAddr(const char* host, int port, int ms, const char* local_host, int local_port); #ifdef ENABLE_UDS HV_EXPORT int BindUnix(const char* path, int type DEFAULT(SOCK_STREAM)); diff --git a/docs/cn/HttpClient.md b/docs/cn/HttpClient.md index 2eb480bc0..40171a3ff 100644 --- a/docs/cn/HttpClient.md +++ b/docs/cn/HttpClient.md @@ -7,6 +7,12 @@ class HttpClient { // 设置超时 int setTimeout(int timeout); + // 设置本地地址,连接前先 bind,出包的源地址固定为该地址 + // 多网卡场景下内核会按路由表选源地址,这里可以显式指定 + // @param host 本地 IP 地址,如 "192.168.1.10" 或 "fe80::1" + // @param port 本地端口,0 表示由内核分配 + int setLocalAddr(const char* host, int port = 0); + // 设置SSL/TLS int setSslCtx(hssl_ctx_t ssl_ctx); // 新建SSL/TLS diff --git a/http/client/HttpClient.cpp b/http/client/HttpClient.cpp index eff80957b..f0c215e56 100644 --- a/http/client/HttpClient.cpp +++ b/http/client/HttpClient.cpp @@ -27,6 +27,10 @@ struct http_client_s { unsigned char https; // 0/1 unsigned char http_version; // intended HTTP version for the next connect (1 or 2) int timeout; // s + // local address to bind before connecting + std::string local_host; + int local_port; + bool has_local_addr; http_headers headers; // http_proxy std::string http_proxy_host; @@ -60,6 +64,8 @@ struct http_client_s { port = DEFAULT_HTTP_PORT; https = 0; timeout = DEFAULT_HTTP_TIMEOUT; + local_port = 0; + has_local_addr = false; http_proxy_port = DEFAULT_HTTP_PORT; https_proxy_port = DEFAULT_HTTP_PORT; #ifdef WITH_CURL @@ -116,6 +122,14 @@ int http_client_set_timeout(http_client_t* cli, int timeout) { return 0; } +int http_client_set_local_addr(http_client_t* cli, const char* host, int port) { + if (cli == NULL || host == NULL) return ERR_NULL_POINTER; + cli->local_host = host; + cli->local_port = port; + cli->has_local_addr = true; + return 0; +} + int http_client_set_ssl_ctx(http_client_t* cli, hssl_ctx_t ssl_ctx) { cli->ssl_ctx = ssl_ctx; return 0; @@ -378,7 +392,13 @@ int http_client_connect(http_client_t* cli, HttpRequest* req) { int connect_port = req->IsProxy() ? req->proxy_port : req->port; unsigned int start_time = gettick_ms(); - int connfd = ConnectTimeout(connect_host, connect_port, blocktime); + int connfd; + if (cli->has_local_addr) { + connfd = ConnectTimeoutWithLocalAddr(connect_host, connect_port, blocktime, + cli->local_host.c_str(), cli->local_port); + } else { + connfd = ConnectTimeout(connect_host, connect_port, blocktime); + } if (connfd < 0) { hloge("connect %s:%d failed!", connect_host, connect_port); return connfd; diff --git a/http/client/HttpClient.h b/http/client/HttpClient.h index 0fc9fe760..3b4eb3ec2 100644 --- a/http/client/HttpClient.h +++ b/http/client/HttpClient.h @@ -36,6 +36,13 @@ HV_EXPORT const char* http_client_strerror(int errcode); // timeout: s HV_EXPORT int http_client_set_timeout(http_client_t* cli, int timeout); +// Set the local address to bind before connecting, so that the source address +// of the outgoing packets is fixed. Useful on multi-NIC hosts, where the +// kernel would otherwise pick the source address by the routing table. +// @param host local IP address to bind, e.g. "192.168.1.10" or "fe80::1" +// @param port local port to bind, 0 means let the kernel choose one +HV_EXPORT int http_client_set_local_addr(http_client_t* cli, const char* host, int port); + // SSL/TLS HV_EXPORT int http_client_set_ssl_ctx(http_client_t* cli, hssl_ctx_t ssl_ctx); // hssl_ctx_new(opt) -> http_client_set_ssl_ctx @@ -94,6 +101,14 @@ class HttpClient { return http_client_set_timeout(client_.get(), timeout); } + // Set the local address to bind before connecting, so that the source + // address of the outgoing packets is fixed. + // @param host local IP address to bind, e.g. "192.168.1.10" or "fe80::1" + // @param port local port to bind, 0 means let the kernel choose one + int setLocalAddr(const char* host, int port = 0) { + return http_client_set_local_addr(client_.get(), host, port); + } + // SSL/TLS int setSslCtx(hssl_ctx_t ssl_ctx) { return http_client_set_ssl_ctx(client_.get(), ssl_ctx); diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 6e17408b3..fe5c587b7 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -90,6 +90,10 @@ target_include_directories(sendmail PRIVATE .. ../base ../protocol ../util) add_executable(http_router_test http_router_test.cpp) target_include_directories(http_router_test PRIVATE ../http/server) +add_executable(http_client_local_addr_test http_client_local_addr_test.cpp) +target_include_directories(http_client_local_addr_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/client ../http/server) +target_link_libraries(http_client_local_addr_test ${HV_LIBRARIES}) + if(WITH_HTTP) add_executable(websocket_parser_test websocket_parser_test.cpp) target_include_directories(websocket_parser_test PRIVATE .. ../base ../http) diff --git a/unittest/http_client_local_addr_test.cpp b/unittest/http_client_local_addr_test.cpp new file mode 100644 index 000000000..196779c56 --- /dev/null +++ b/unittest/http_client_local_addr_test.cpp @@ -0,0 +1,101 @@ +// 验证 http_client_set_local_addr 能把出包的源地址固定住 +// +// 判定依据:绑定 127.0.0.1 之后去连本机的非 loopback 地址,服务端看到的 +// 源地址仍然是 127.0.0.1(不绑定时会是本机主地址),说明绑定确实生效。 +// +// 手动运行: +// make unittest && bin/http_client_local_addr_test <本机非loopback地址> +// +// 注意:需要占用一个 TCP 端口,且第二个用例要求传入本机的非 loopback 地址, +// 因此不加入 scripts/unittest.sh 的自动运行列表。 + +#include "http/client/HttpClient.h" +#include "http/server/HttpServer.h" +#include "http/httpdef.h" + +#include +#include +#include +#include +#include + +static int g_port = 18099; + +static void* server_thread(void* userdata) { + HttpService router; + router.GET("/whoami", [](HttpRequest* req, HttpResponse* resp) { + resp->body = req->client_addr.ip; + return 200; + }); + http_server_t server; + server.port = g_port; + server.service = &router; + http_server_run(&server); + return NULL; +} + +// 发起一次请求,返回服务端看到的客户端地址;失败返回空串 +static std::string request_client_addr(const char* bind_ip, const char* connect_ip) { + http_client_t* cli = http_client_new(connect_ip, g_port, 0); + if (bind_ip) { + if (http_client_set_local_addr(cli, bind_ip, 0) != 0) { + http_client_del(cli); + return ""; + } + } + + HttpRequest req; + req.method = HTTP_GET; + req.url = std::string("http://") + connect_ip + ":" + std::to_string(g_port) + "/whoami"; + HttpResponse resp; + int ret = http_client_send(cli, &req, &resp); + std::string addr; + if (ret == 0) { + addr = resp.body; + } + http_client_del(cli); + return addr; +} + +int main(int argc, char** argv) { + // 本机主地址,用于验证"绑定生效"这个语义 + const char* host_ip = argc > 1 ? argv[1] : "127.0.0.1"; + + // 第二个用例要靠"源地址被绑成 loopback、目标却是主地址"来判定, + // 如果本机主地址本身就是 loopback,这个用例没有意义 + if (strcmp(host_ip, "127.0.0.1") == 0) { + printf("SKIP: pass a non-loopback local address as argv[1]\n"); + return 0; + } + + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + usleep(1000000); + + // 1) 不绑定,连接 loopback:源地址应为 127.0.0.1 + std::string addr = request_client_addr(NULL, "127.0.0.1"); + printf("no-bind -> 127.0.0.1 : client_addr=[%s]\n", addr.c_str()); + if (addr != "127.0.0.1") { + printf("FAILED: expected 127.0.0.1\n"); + return 1; + } + + // 2) 绑定 loopback,连接本机主地址:源地址仍应是 127.0.0.1 + addr = request_client_addr("127.0.0.1", host_ip); + printf("bind 127.0.0.1 -> %s : client_addr=[%s]\n", host_ip, addr.c_str()); + if (addr != "127.0.0.1") { + printf("FAILED: the local address is not applied\n"); + return 1; + } + + // 3) 绑定一个本机不存在的地址:应当失败 + addr = request_client_addr("10.255.255.253", host_ip); + printf("bind 10.255.255.253 -> %s : client_addr=[%s] (expected to fail)\n", host_ip, addr.c_str()); + if (!addr.empty()) { + printf("FAILED: binding an unavailable address should fail\n"); + return 1; + } + + printf("PASS\n"); + return 0; +} From bce66588ffd005d44da75ecee858a4cf80288f77 Mon Sep 17 00:00:00 2001 From: gnwang <136193543@qq.com> Date: Thu, 29 Oct 2026 17:58:33 +0800 Subject: [PATCH 2/2] feat(http): support setLocalAddr on the asynchronous client path PR #917 added http_client_set_local_addr() and wired it into the synchronous connect path (http_client_connect). The asynchronous path was left unchanged, so hv::HttpClient::sendAsync() silently ignored the local address and the kernel still picked the source address by the routing table. This extends the feature to the async path: - HttpClientTask carries the local address from the http_client_t to the connection setup. - AsyncHttpClient::startConnect() binds the socket to that address before connecting. The bind happens before hio_get() so that a failure can close the socket without racing the hloop cleanup. A failed bind fails the connection, matching the behaviour of the synchronous path. - http_client_exec_async() forwards cli->local_host/local_port. Adds unittest/http_client_local_addr_async_test.cpp, which checks that the peer sees the bound source address and that binding an unavailable address fails. --- Makefile | 1 + http/client/AsyncHttpClient.cpp | 32 ++++- http/client/AsyncHttpClient.h | 11 +- http/client/HttpClient.cpp | 6 +- http/client/HttpClient.h | 3 + unittest/CMakeLists.txt | 4 + .../http_client_local_addr_async_test.cpp | 122 ++++++++++++++++++ 7 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 unittest/http_client_local_addr_async_test.cpp diff --git a/Makefile b/Makefile index baef2ead4..5003ab831 100644 --- a/Makefile +++ b/Makefile @@ -404,6 +404,7 @@ unittest: prepare libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil -o bin/objectpool_test unittest/objectpool_test.cpp -pthread $(CXX) -g -Wall -O0 -std=c++11 -Ihttp/server -o bin/http_router_test unittest/http_router_test.cpp $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/http_client_local_addr_test unittest/http_client_local_addr_test.cpp -Llib -lhv -pthread $(LDFLAGS) + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/http_client_local_addr_async_test unittest/http_client_local_addr_async_test.cpp -Llib -lhv -pthread $(LDFLAGS) $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Ievpp -Icpputil -Ihttp -Ihttp/client -Ihttp/server -o bin/sizeof_test unittest/sizeof_test.cpp $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Issl -Ievpp -o bin/tlv_test unittest/tlv_test.cpp $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/nslookup unittest/nslookup_test.c protocol/dns.c base/hsocket.c base/htime.c diff --git a/http/client/AsyncHttpClient.cpp b/http/client/AsyncHttpClient.cpp index efe8a3816..ec16ae9c0 100644 --- a/http/client/AsyncHttpClient.cpp +++ b/http/client/AsyncHttpClient.cpp @@ -2,13 +2,17 @@ namespace hv { -int AsyncHttpClient::send(const HttpRequestPtr& req, HttpResponseCallback resp_cb) { +int AsyncHttpClient::send(const HttpRequestPtr& req, HttpResponseCallback resp_cb, const char* local_host, int local_port) { hloop_t* loop = EventLoopThread::hloop(); if (loop == NULL) return -1; auto task = std::make_shared(); task->req = req; task->cb = std::move(resp_cb); task->start_time = hloop_now_hrtime(loop); + if (local_host && *local_host) { + task->local_host = local_host; + task->local_port = local_port; + } if (req->retry_count > 0 && req->retry_delay > 0) { req->retry_count = MIN(req->retry_count, req->timeout * 1000 / req->retry_delay - 1); } @@ -122,8 +126,34 @@ int AsyncHttpClient::startConnect(const HttpClientTaskPtr& task, const sockaddr_ perror("socket"); return -30; } + + // 绑定出包源地址(可选):多网卡环境下内核按路由表选源 IP 不可控, + // 绑定后出包一定从指定地址出去,对端看到的源 IP 才是预期的。 + // 必须在 hio_get() 之前做:socket 尚未交给 hloop,此时失败可以直接关闭; + // 交给 hloop 之后再 close 会和 hloop 自己的清理撞车(double close)。 + // 绑定失败与同步路径 http_client_connect() 保持一致:直接让本次连接失败。 + sockaddr_u localaddr; + memset(&localaddr, 0, sizeof(localaddr)); + bool bind_localaddr = false; + if (!task->local_host.empty()) { + if (sockaddr_set_ipport(&localaddr, task->local_host.c_str(), task->local_port) != 0) { + closesocket(connfd); + return -1; + } + if (::bind(connfd, &localaddr.sa, sockaddr_len(&localaddr)) < 0) { + // errno 必须在 closesocket 之前取:close 可能改写 errno。 + int err = socket_errno(); + closesocket(connfd); + return -err; + } + bind_localaddr = true; + } + hio_t* connio = hio_get(EventLoopThread::hloop(), connfd); assert(connio != NULL); + if (bind_localaddr) { + hio_set_localaddr(connio, &localaddr.sa, sockaddr_len(&localaddr)); + } hio_set_peeraddr(connio, &peeraddr.sa, sockaddr_len(&peeraddr)); const SocketChannelPtr& channel = addChannel(connio); channel->getContext()->conn_key = HttpConnKey(*req); diff --git a/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index 56ba97f17..18ac920e6 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -52,7 +52,12 @@ class ConnPool { struct HttpClientTask { HttpRequestPtr req; HttpResponseCallback cb; - uint64_t start_time; + uint64_t start_time = 0; + // 出包源地址(可选):连接前 bind 到该地址,固定出包的源 IP。 + // 多网卡环境下内核按路由表选源地址,绑定后出包一定从指定地址出去。 + // 由 http_client_set_local_addr() 填入,为空表示不绑定。 + std::string local_host; + int local_port = 0; }; typedef std::shared_ptr HttpClientTaskPtr; @@ -185,7 +190,9 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } // thread-safe - int send(const HttpRequestPtr& req, HttpResponseCallback resp_cb); + // local_host 非空时,连接前 bind 到 local_host:local_port,固定出包源 IP; + // 为空(默认)表示不绑定,由内核按路由表选择源地址。 + int send(const HttpRequestPtr& req, HttpResponseCallback resp_cb, const char* local_host = NULL, int local_port = 0); int send(const HttpClientTaskPtr& task) { EventLoopThread::loop()->queueInLoop(std::bind(&AsyncHttpClient::sendInLoop, this, task)); return 0; diff --git a/http/client/HttpClient.cpp b/http/client/HttpClient.cpp index f0c215e56..487c43e99 100644 --- a/http/client/HttpClient.cpp +++ b/http/client/HttpClient.cpp @@ -129,7 +129,6 @@ int http_client_set_local_addr(http_client_t* cli, const char* host, int port) { cli->has_local_addr = true; return 0; } - int http_client_set_ssl_ctx(http_client_t* cli, hssl_ctx_t ssl_ctx) { cli->ssl_ctx = ssl_ctx; return 0; @@ -970,6 +969,11 @@ static int http_client_exec_async(http_client_t* cli, HttpRequestPtr req, HttpRe cli->mutex_.unlock(); } + // 源地址绑定:异步路径也要和同步路径(http_client_connect)一样固定出包源 IP。 + // cli->local_host/local_port 由 http_client_set_local_addr() 设置。 + if (cli->has_local_addr) { + return cli->async_client_->send(req, std::move(resp_cb), cli->local_host.c_str(), cli->local_port); + } return cli->async_client_->send(req, std::move(resp_cb)); } diff --git a/http/client/HttpClient.h b/http/client/HttpClient.h index 3b4eb3ec2..db119f578 100644 --- a/http/client/HttpClient.h +++ b/http/client/HttpClient.h @@ -39,6 +39,9 @@ HV_EXPORT int http_client_set_timeout(http_client_t* cli, int timeout); // Set the local address to bind before connecting, so that the source address // of the outgoing packets is fixed. Useful on multi-NIC hosts, where the // kernel would otherwise pick the source address by the routing table. +// Applies to both the synchronous and the asynchronous client paths. +// NOTE: set it once, before sending any request -- connections that were +// already established keep the source address they were created with. // @param host local IP address to bind, e.g. "192.168.1.10" or "fe80::1" // @param port local port to bind, 0 means let the kernel choose one HV_EXPORT int http_client_set_local_addr(http_client_t* cli, const char* host, int port); diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index fe5c587b7..6aada0fc3 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -94,6 +94,10 @@ add_executable(http_client_local_addr_test http_client_local_addr_test.cpp) target_include_directories(http_client_local_addr_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/client ../http/server) target_link_libraries(http_client_local_addr_test ${HV_LIBRARIES}) +add_executable(http_client_local_addr_async_test http_client_local_addr_async_test.cpp) +target_include_directories(http_client_local_addr_async_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/client ../http/server) +target_link_libraries(http_client_local_addr_async_test ${HV_LIBRARIES}) + if(WITH_HTTP) add_executable(websocket_parser_test websocket_parser_test.cpp) target_include_directories(websocket_parser_test PRIVATE .. ../base ../http) diff --git a/unittest/http_client_local_addr_async_test.cpp b/unittest/http_client_local_addr_async_test.cpp new file mode 100644 index 000000000..b426cd071 --- /dev/null +++ b/unittest/http_client_local_addr_async_test.cpp @@ -0,0 +1,122 @@ +// 验证 http_client_set_local_addr 在异步路径上同样能把出包的源地址固定住 +// +// 判定依据:绑定 127.0.0.1 之后去连本机的非 loopback 地址,服务端看到的 +// 源地址仍然是 127.0.0.1(不绑定时会是本机主地址),说明绑定确实生效。 +// +// 与 http_client_local_addr_test.cpp(同步路径)的区别只在于请求是异步发出的: +// 绑定发生在 AsyncHttpClient::startConnect() 里,socket 创建之后、connect 之前。 +// +// 手动运行: +// make unittest && bin/http_client_local_addr_async_test <本机非loopback地址> +// +// 注意:需要占用一个 TCP 端口,且第二个用例要求传入本机的非 loopback 地址, +// 因此不加入 scripts/unittest.sh 的自动运行列表。 + +#include "http/client/HttpClient.h" +#include "http/server/HttpServer.h" +#include "http/httpdef.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static int g_port = 18099; + +static void* server_thread(void* userdata) { + HttpService router; + router.GET("/whoami", [](HttpRequest* req, HttpResponse* resp) { + resp->body = req->client_addr.ip; + return 200; + }); + http_server_t server; + server.port = g_port; + server.service = &router; + http_server_run(&server); + return NULL; +} + +// 异步发起一次请求,返回服务端看到的客户端地址;失败返回空串 +static std::string request_client_addr(const char* bind_ip, const char* connect_ip) { + http_client_t* cli = http_client_new(connect_ip, g_port, 0); + if (bind_ip) { + if (http_client_set_local_addr(cli, bind_ip, 0) != 0) { + http_client_del(cli); + return ""; + } + } + + auto req = std::make_shared(); + req->method = HTTP_GET; + req->url = std::string("http://") + connect_ip + ":" + std::to_string(g_port) + "/whoami"; + + std::mutex mtx; + std::condition_variable cv; + bool done = false; + std::string addr; + + http_client_send_async(cli, req, [&](const HttpResponsePtr& resp) { + { + std::lock_guard lock(mtx); + if (resp) addr = resp->body; + done = true; + } + cv.notify_all(); + }); + + std::unique_lock lock(mtx); + bool finished = cv.wait_for(lock, std::chrono::seconds(10), [&] { return done; }); + if (!finished) { + // 回调可能仍在执行,这里释放 client 会与它撞车;进程退出时由系统回收。 + return ""; + } + http_client_del(cli); + return addr; +} + +int main(int argc, char** argv) { + // 本机主地址,用于验证"绑定生效"这个语义 + const char* host_ip = argc > 1 ? argv[1] : "127.0.0.1"; + + // 第二个用例要靠"源地址被绑成 loopback、目标却是主地址"来判定, + // 如果本机主地址本身就是 loopback,这个用例没有意义 + if (strcmp(host_ip, "127.0.0.1") == 0) { + printf("SKIP: pass a non-loopback local address as argv[1]\n"); + return 0; + } + + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + usleep(1000000); + + // 1) 不绑定,连接 loopback:源地址应为 127.0.0.1 + std::string addr = request_client_addr(NULL, "127.0.0.1"); + printf("no-bind -> 127.0.0.1 : client_addr=[%s]\n", addr.c_str()); + if (addr != "127.0.0.1") { + printf("FAILED: expected 127.0.0.1\n"); + return 1; + } + + // 2) 绑定 loopback,连接本机主地址:源地址仍应是 127.0.0.1 + addr = request_client_addr("127.0.0.1", host_ip); + printf("bind 127.0.0.1 -> %s : client_addr=[%s]\n", host_ip, addr.c_str()); + if (addr != "127.0.0.1") { + printf("FAILED: the local address is not applied\n"); + return 1; + } + + // 3) 绑定一个本机不存在的地址:应当失败 + addr = request_client_addr("10.255.255.253", host_ip); + printf("bind 10.255.255.253 -> %s : client_addr=[%s] (expected to fail)\n", host_ip, addr.c_str()); + if (!addr.empty()) { + printf("FAILED: binding an unavailable address should fail\n"); + return 1; + } + + printf("PASS\n"); + return 0; +}