Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ 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 -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
Expand Down
53 changes: 53 additions & 0 deletions base/hsocket.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions base/hsocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
6 changes: 6 additions & 0 deletions docs/cn/HttpClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion http/client/AsyncHttpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpClientTask>();
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);
}
Expand Down Expand Up @@ -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<HttpClientContext>()->conn_key = HttpConnKey(*req);
Expand Down
11 changes: 9 additions & 2 deletions http/client/AsyncHttpClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpClientTask> HttpClientTaskPtr;

Expand Down Expand Up @@ -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;
Expand Down
26 changes: 25 additions & 1 deletion http/client/HttpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -116,6 +122,13 @@ 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;
Expand Down Expand Up @@ -378,7 +391,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;
Expand Down Expand Up @@ -950,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));
}

Expand Down
18 changes: 18 additions & 0 deletions http/client/HttpClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ 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.
// 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);

// 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
Expand Down Expand Up @@ -94,6 +104,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);
Expand Down
8 changes: 8 additions & 0 deletions unittest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ 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})

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)
Expand Down
Loading