Skip to content

Commit 8ad6406

Browse files
committed
[feat] HttpRequest 支持获取对端地址
之前 handler 里拿不到客户端 IP,限流、审计、白名单这些场景都没法做。给 HttpRequest 加了 peerAddr(),连接层 accept 后取一次 remote_endpoint 注入,handler、中间件、WS/SSE 握手链路都能读到。InetAddress 顺带补了 isValid()。 另外修了个遗留问题:RateLimiter 默认 key 读 hical.remote_addr 属性但一直没人往里写,默认限流所有请求共用一个桶。改成优先用 peerAddr(),删掉那个半成品的 kRemoteAddrKey 常量。
1 parent 96b8d49 commit 8ad6406

15 files changed

Lines changed: 182 additions & 47 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- **HttpRequest 获取对端地址**`HttpRequest::peerAddr()` 返回客户端对端 `InetAddress``toIp()`/`toIpPort()`/`port()` 自取),`InetAddress` 新增 `isValid()` 判断地址有效性。连接层 `handleSession` 在连接建立时取一次 `remote_endpoint()` 注入到本连接所有请求,handler、中间件、WS/SSE 握手链路都能拿到真实对端地址([#16](https://github.com/Hical61/Hical/issues/16)
12+
13+
### Fixed
14+
- **RateLimiter 默认限流 key 失效**:默认 key 提取之前读 `"hical.remote_addr"` 请求属性,但连接层从未注入该属性,导致所有请求共享一个桶(限流形同虚设)。改为优先取 `peerAddr()`、无效时回退 `X-Forwarded-For`、再兜底 `"global"`
15+
16+
### Changed
17+
- **移除 `kRemoteAddrKey` 常量**`"hical.remote_addr"` 属性键随上一条修复退场。此前它只被 RateLimiter 默认 key 提取读取,从未有连接层写入,属于半成品接线;现在对端地址走 `peerAddr()` 一等接口,不再用魔法字符串属性传递
18+
819
## [2.6.8] - 2026-08-09
920

1021
### Added

docs/api_reference.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ HTTP 请求封装,对原生 HTTP 解析结果的 hical 风格封装。
318318
| `jsonBody()` || `const boost::json::value&` | 将消息体解析为 JSON(多次调用返回缓存引用) |
319319
| `readJson<T>()` || `T` | 将消息体反序列化为 T(需 `HICAL_JSON`|
320320
| `contentType()` || `std::string_view` | 获取 Content-Type 头 |
321+
| `peerAddr()` || `const InetAddress&` | 获取客户端对端地址(未注入时为无效地址) |
321322
| `param(name)` | name: 参数名 | `const std::string&` | 获取路径参数 |
322323
| `hasParam(name)` | name: 参数名 | `bool` | 是否有指定路径参数 |
323324
| `native()` || `NativeRequest&` | 获取底层原生请求引用 |
@@ -367,13 +368,14 @@ HTTP 请求封装,对原生 HTTP 解析结果的 hical 风格封装。
367368

368369
#### 构建请求的方法
369370

370-
| 方法 | 参数 | 返回值 | 说明 |
371-
| ------------------------ | -------------------------------------------------- | ------ | ---------------------------------- |
372-
| `setMethod(method)` | method: HTTP 方法 | `void` | 设置 HTTP 方法 |
373-
| `setTarget(target)` | target: 目标 URI | `void` | 设置请求路径 |
374-
| `setHeader(name, value)` | name: 字段名<br>value: 字段值 (`std::string_view`) | `void` | 设置头部字段(拒绝含 CR/LF 的值) |
375-
| `setBody(body)` | body: 消息体 | `void` | 设置消息体(不动 Content-Type 头) |
376-
| `setParam(name, value)` | name: 参数名<br>value: 参数值 | `void` | 设置路径参数(由 Router 内部调用) |
371+
| 方法 | 参数 | 返回值 | 说明 |
372+
| ------------------------ | -------------------------------------------------- | ------ | --------------------------------------------- |
373+
| `setMethod(method)` | method: HTTP 方法 | `void` | 设置 HTTP 方法 |
374+
| `setTarget(target)` | target: 目标 URI | `void` | 设置请求路径 |
375+
| `setHeader(name, value)` | name: 字段名<br>value: 字段值 (`std::string_view`) | `void` | 设置头部字段(拒绝含 CR/LF 的值) |
376+
| `setBody(body)` | body: 消息体 | `void` | 设置消息体(不动 Content-Type 头) |
377+
| `setParam(name, value)` | name: 参数名<br>value: 参数值 | `void` | 设置路径参数(由 Router 内部调用) |
378+
| `setPeerAddr(addr)` | addr: `const InetAddress&` | `void` | 设置对端地址(由连接层注入,测试/构建请求用) |
377379

378380
#### 示例
379381

docs/examples_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
| ---------- | ------------------------------------------------ |
3636
| C++ 编译器 | GCC 14+ / Clang 20+ / MSVC 2022+(需支持 C++20) |
3737
| CMake | 3.20+ |
38-
| Boost | 1.82+(Asio、JSON);DB 中间件 >= 1.85 |
38+
| Boost | 1.82+(Asio、JSON);DB 中间件 >= 1.85 |
3939
| OpenSSL | 3.0+ |
4040

4141
**MSYS2 MINGW64 快速安装:**

docs/performance_report.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,12 @@ strace + perf 火焰图实测 10K 并发下发现的浪费点,逐一消除:
142142

143143
strace 实测 10K 并发下,每次 keep-alive 请求的 `async_wait(wait_read)` 都触发一次多余的 `epoll_ctl(MOD)`——EPOLLIN 早在注册连接时就加了,反复 MOD 纯属浪费。改法:循环顶用 256B 栈缓冲 `async_read_some` 替代 `async_wait`,走 Asio 投机路径,有数据直接返回、没数据挂起等 epoll 通知,全程不碰 epoll_ctl。
144144

145-
| 指标 | 优化前 | 优化后 | 变化 |
146-
|------|:------:|:------:|:----:|
147-
| epoll_ctl 调用数 | 32,563 | 9,173 | ↓71.8% |
148-
| epoll_ctl 时间占比 | 26.51% | 5.57% | ↓20.94 p.p. |
149-
| 空闲连接额外内存 | 0 | +256B(协程帧栈) | 可忽略 |
150-
| 总系统调用数 | 99,579 | 79,504 | ↓20.2% |
145+
| 指标 | 优化前 | 优化后 | 变化 |
146+
| ------------------ | :----: | :---------------: | :---------: |
147+
| epoll_ctl 调用数 | 32,563 | 9,173 | ↓71.8% |
148+
| epoll_ctl 时间占比 | 26.51% | 5.57% | ↓20.94 p.p. |
149+
| 空闲连接额外内存 | 0 | +256B(协程帧栈) | 可忽略 |
150+
| 总系统调用数 | 99,579 | 79,504 | ↓20.2% |
151151

152152
---
153153

ports/hical61-hical/portfile.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ vcpkg_from_github(
44
OUT_SOURCE_PATH SOURCE_PATH
55
REPO Hical61/Hical
66
REF "v${VERSION}"
7-
SHA512 feeebad5debd254b564b13822f5ef69b76d03f817f046b6ebf201c438b65cb7a19bc3e81bd8248b3edebc2d43e5d6f90d60d4de60c7a50dc4aefd2ca2bbb8057
7+
SHA512 244f87004d6a4c9ca2e04d555ee0c20faf3db8623b90c4c73f4c5de1dffff0a24d2a5d0d8bd633e89363d63204845583af30c1139e50d09c7c24657d66ca3a06
88
HEAD_REF main
99
)
1010

src/core/HttpRequest.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ namespace hical
146146
}
147147
}
148148

149+
const InetAddress& HttpRequest::peerAddr() const
150+
{
151+
return peerAddr_;
152+
}
153+
154+
void HttpRequest::setPeerAddr(const InetAddress& addr)
155+
{
156+
peerAddr_ = addr;
157+
}
158+
149159
// ============ 路径参数 ============
150160

151161
const std::string& HttpRequest::param(std::string_view name) const

src/core/HttpRequest.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "HeaderMap.h"
99
#include "HttpTypes.h"
10+
#include "InetAddress.h"
1011
#include <any>
1112
#include <array>
1213
#include <boost/json.hpp>
@@ -211,6 +212,18 @@ namespace hical
211212
void setHeader(const std::string& name, const std::string& value);
212213
void setBody(const std::string& body);
213214

215+
/**
216+
* @brief 获取对端网络地址
217+
* @return 对端地址引用(未设置时为默认构造的无效地址)
218+
*/
219+
[[nodiscard]] const InetAddress& peerAddr() const;
220+
221+
/**
222+
* @brief 设置对端网络地址
223+
* @param addr 对端地址
224+
*/
225+
void setPeerAddr(const InetAddress& addr);
226+
214227
// ============ 路径参数 ============
215228

216229
/**
@@ -296,6 +309,7 @@ namespace hical
296309
void parseFormParams() const;
297310

298311
NativeRequest req_;
312+
InetAddress peerAddr_;
299313
// setter 专用拥有存储(测试/构建请求场景,不在热路径)
300314
std::string ownedTarget_;
301315
HeaderMap ownedHeaders_;

src/core/HttpSessionImpl.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "HttpServer.h"
99
#include "FixedBuffer.h"
10+
#include "InetAddress.h"
1011
#include "MemoryPool.h"
1112
#include "ReadBufferPool.h"
1213
#include "SseSession.h"
@@ -554,6 +555,11 @@ namespace hical
554555
// 期间设置的 O_NONBLOCK 是实现细节,不能依赖它保持生效。
555556
socket.non_blocking(true);
556557

558+
// 对端地址是连接级信息,取一次供本连接所有请求复用。
559+
// socket 刚 accept 必然已连接,getpeername() 不会失败,与 TcpServer::acceptLoop 同款写法。
560+
auto remoteEp = socket.remote_endpoint();
561+
InetAddress peerAddr(remoteEp.address().to_string(), remoteEp.port());
562+
557563
// entry 在协程栈上,Guard 析构时自动注销
558564
// 声明在 SocketGuard 后面 → 先析构(先 unregister 再关 socket)
559565
IdleScanner::Entry idleEntry;
@@ -1016,6 +1022,7 @@ namespace hical
10161022

10171023
// ====== 阶段 D:构建 HttpRequest 并分发 ======
10181024
HttpRequest req = HttpRequest::fromParsed(std::move(nativeReq));
1025+
req.setPeerAddr(peerAddr);
10191026

10201027
// 检查 WebSocket 升级请求
10211028
if (req.native().isUpgrade())

src/core/InetAddress.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ namespace hical
8181
return isIpV6_;
8282
}
8383

84+
bool InetAddress::isValid() const
85+
{
86+
// 默认构造只 memset 了 addr6_ 且 isIpV6_=false,此时 addr_.sin_family 为 0
87+
// 按 isIpV6_ 标志读取对应 union 成员的 sin_family,避免读错成员
88+
return isIpV6_ ? (addr6_.sin6_family == AF_INET6) : (addr_.sin_family == AF_INET);
89+
}
90+
8491
const struct sockaddr* InetAddress::getSockAddr() const
8592
{
8693
return isIpV6_ ? reinterpret_cast<const struct sockaddr*>(&addr6_)

src/core/InetAddress.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ namespace hical
7070
*/
7171
bool isIpV6() const;
7272

73+
/**
74+
* @brief 地址是否有效
75+
* 默认构造或解析失败时地址族为 0,返回 false。
76+
* @return true 如果已解析为有效的 IPv4 或 IPv6 地址
77+
*/
78+
[[nodiscard]] bool isValid() const;
79+
7380
/**
7481
* @brief 获取底层 sockaddr 结构
7582
* @return sockaddr 指针

0 commit comments

Comments
 (0)