云计算百科
云计算领域专业知识百科平台

IncludeOS HTTP 集成测试实战:在本地服务器上验证 HTTP 客户端与服务器

  • 操作系统
  • 云原生
  • 虚拟化
  • 网络

【免费下载链接】IncludeOS

A minimal, resource efficient unikernel for cloud services

项目地址:
https://gitcode.com/gh_mirrors/in/IncludeOS

点击查看 免费下载

导读

本文围绕 test/integration/net/http/README.md 所描述的 HTTP 集成测试展开,深入讲解 IncludeOS 如何在 unikernel 内部同时启动 http::Server 与 http::Basic_client,并借助宿主机上的 Python HTTP 服务器与 vmrunner 完成双向验证。读完本文,你将掌握:IncludeOS HTTP 服务的构建与网络配置方式、服务端请求回调与响应写入的 API 用法、客户端请求构造与错误处理机制,以及整个集成测试从启动 VM 到断言输出的完整运行链路。

原文档核心信息仅有两句话:在本地服务器上测试 HTTP Client,以及 出站(outgoing)测试需要正确的网络配置。本文以这两点为骨架,结合 test/integration/net/http/ 目录下的全部测试文件与 api/net/http/、src/net/http/ 的源码实现,展开为一份可运行的实战指南。

一、测试全景:一个 VM 内同时扮演客户端与服务器

test/integration/net/http/ 目录下共有 6 个文件,构成了完整的测试体系:

文件职责
service.cpp unikernel 服务主体:配置网络、启动 HTTP 服务器、构造 HTTP 客户端并发送请求
test.py 宿主端 Python 驱动:在 9011 端口起一个回显服务器,并用 vmrunner 启动/断言 VM
run.sh 启动脚本:创建桥接网络后运行 VM 镜像
CMakeLists.txt 构建配置:生成名为 net_http 的可执行镜像,并挂载 virtionet 网卡驱动
vm.json VM 配置:指定 virtio 网卡、MAC 地址与内存大小
README.md 本文所对应的原始测试说明

测试的核心思路是:IncludeOS 服务在 VM 内监听 8080 端口扮演 HTTP 服务器;宿主机通过 Python HTTP 服务器在 9011 端口扮演外部服务器;VM 内的 HTTP 客户端向宿主机发起出站请求。这恰好覆盖了服务端与客户端两个方向,也是原文档强调"出站测试需要正确网络配置"的原因——VM 内的客户端要访问宿主机的 9011 端口,必须先打通桥接网络。

从 test/README.md 可以看到,本测试固定占用 10.0.0.46 这一 IP 地址,与其他网络测试(如 tcp 用 10.0.0.44、dns 用 10.0.0.48)错开,避免并行运行时发生地址冲突。

二、构建配置:如何把一个 HTTP 服务编译成镜像

CMakeLists.txt 展示了 IncludeOS 服务标准的三段式构建写法:

cmake_minimum_required(VERSION 3.31.6)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

project (service)
include(os)

set(SOURCES
service.cpp
)

os_add_executable(net_http "HTTP test" ${SOURCES})
os_add_drivers(net_http virtionet)
os_add_stdout(net_http default_stdout)
configure_file(test.py ${CMAKE_CURRENT_BINARY_DIR})

要点解读:

  • os_add_executable(net_http "HTTP test" ${SOURCES}):把 service.cpp 编译链接为名为 net_http 的 unikernel 镜像,输出二进制为 net_http.elf.bin(这正是 test.py 末尾 vm.boot(20, image_name='net_http.elf.bin') 所引用的镜像名)。
  • os_add_drivers(net_http virtionet):挂载 virtio 网络驱动,使 VM 能够使用 vm.json 中声明的 virtio 网卡。
  • os_add_stdout(net_http default_stdout):将标准输出绑定到默认 stdout 驱动,确保 INFO、printf 打印的内容能够被 vmrunner 捕获,用于后续的输出匹配触发。
  • configure_file(test.py …):把测试驱动脚本复制到构建目录,方便与镜像放在一起执行。

对应的 VM 资源配置见 vm.json:

{
"net" : [{"device" : "virtio", "mac" : "c0:01:0a:00:00:2a"}],
"mem" : 256
}

这里声明了一块 virtio 网卡(MAC 为 c0:01:0a:00:00:2a,属于桥接网段 10.0.0.x 对应的地址规划)与 256 MB 内存,为后续网络交互提供硬件基础。

三、服务端代码剖析:10 行回调搭起一个 HTTP 服务器

service.cpp 在 Service::ready() 中完成了全部测试逻辑。首先为接口 0 配置静态网络:

auto& inet = net::Interfaces::get(0);
inet.network_config(
{ 10, 0, 0, 46 }, // IP
{ 255,255,255, 0 }, // Netmask
{ 10, 0, 0, 1 }, // Gateway
{ 8, 8, 8, 8 } // DNS
);

  • IP 10.0.0.46 与 test/README.md 中的地址分配表一致;
  • 网关 10.0.0.1 是桥接网络中宿主机的地址——客户端后续就向这个地址的 9011 端口发请求;
  • DNS 8.8.8.8 用于域名解析(测试中虽未真正出站访问外网,但地址仍被配置)。

3.1 服务器:on_request 回调 + Response_writer

随后创建 http::Server 并注册请求处理回调:

server_ = std::make_unique<Server>(inet.tcp());

server_->on_request([] (Request_ptr req, Response_writer_ptr writer)
{
printf("Received request:\\n%s\\n", req->to_string().c_str());
// set content type
writer->header().set_field(header::Content_Type, "text/plain");
// write body
writer->write("Hello");
});

server_->listen(8080);

这段代码对应 server.hpp 中定义的 API:

  • on_request(Request_handler handler):注册请求回调。回调签名是 delegate<void(Request_ptr, Response_writer_ptr)>,当解析到完整请求时被触发(见 server.cpp 的 Server::receive)。
  • listen(const uint16_t port):绑定端口开始监听。从 server.cpp 的实现可见,listen 会先断言 on_request_ 已设置("You must set 'on_request' on the server to receive requests!"),再调用 bind(port) 通过 tcp_.listen(port, {this, &Server::on_connect}) 注册连接事件,并打印 "Listening on port %u on CPU %d"——这正是 test.py 中 vm.on_output("Listening on port 8080", …) 用来触发宿主端断言的关键日志。
  • Response_writer 是响应写入的辅助对象(response_writer.hpp):header().set_field() 设置响应头字段,write("Hello") 写入响应体;析构或 end() 时自动完成报文发送。

3.2 服务器底层:连接复用、空闲超时与统计

服务器并非一个简单的回调壳。从 server.cpp 的实现可以读到更多细节:

  • 连接管理:connect() 优先复用 free_idx_ 中空闲的槽位,否则新建 Server_connection,并用 connected_clients() 统计在线连接数。
  • 空闲超时:构造时可通过第三个参数设置 idle_duration(默认 DEFAULT_IDLE_TIMEOUT 为 60 秒,见 server.cpp);listen() 中若超时不为 0,会注册 Timers::periodic(30s, 1min, …) 周期性地通过 timeout_clients() 关闭超时连接。
  • 请求解析:Server_connection::recv_request(见 server_connection.cpp)把 TCP 流数据累积成 Request,若请求头带有 Content-Length 则按长度判定请求完成,否则立即结束请求;解析出错时以 Bad_Request 状态码响应。
  • 统计指标:构造函数通过 Statman 注册了 http_server.connections、http_server.requests_rx、http_server.requests_bad、http_server.timeouts 四个统计项,可在运行期通过 statman 查询服务器健康状态。

四、客户端代码剖析:Basic_client 的请求构造与 HTTPS 限制

4.1 发送前回调:观察将要发出的请求

client_ = std::make_unique<Basic_client>(inet.tcp());
client_->on_send([] (Request& req, Basic_client::Options& opt, const Basic_client::Host host)
{
printf("Sending request:\\n%s\\n", req.to_string().c_str());
});

on_send 回调在请求真正写入 TCP 流之前被调用(见 basic_client.cpp 中 send() 内 if(on_send_) on_send_(*req, options, host);),适合调试或修改自动生成的请求。

4.2 HTTPS 不支持:期望抛出异常

INFO("Basic_client", "HTTPS");

try
{
client_->get("https://www.google.com", {}, [](Error err, Response_ptr res, Connection&) {});
assert(false && "Basic Client should throw exception");
}
catch(const http::Client_error& err)
{
CHECKSERT(true, "Basic Client should throw exception on https URL");
}

这是一个很有意思的负向测试:Basic_client 构造函数传入的 https_supported 标志为 false(见 basic_client.cpp),因此当 URL 的 scheme 为 https 时,validate_secure() 会抛出 Client_error{"Secured connections not supported (use the HTTPS Client)."}(见 basic_client.hpp)。CHECKSERT 宏(定义于 api/info,即 CHECK(…) + assert(…))在打印 [x] 通过标记的同时执行断言,确认异常确实被抛出。

若需要 HTTPS 客户端,仓库提供了继承自 Basic_client 的 http::Client(见 client.cpp),它在 get_secure_connection() 中通过 openssl::TLS_stream 建立 TLS 连接。测试这里刻意验证的是"普通客户端必须拒绝 https",属于 API 契约测试。

4.3 正向往本地服务器发起请求

auto req = client_->create_request();

req->set_uri(uri::URI{"/testing"});
client_->send(std::move(req), {inet.gateway(), 9011},
[] (Error err, Response_ptr res, Connection&)
{
if (err)
printf("Error: %s \\n", err.to_string().c_str());

CHECKSERT(!err, "No error");
printf("Received body: %.*s\\n", static_cast<int>(res->body().length()), res->body().data());
CHECKSERT(res->body() == "/testing", "Received body: \\"/testing\\"");

printf("SUCCESS\\n");
});

这一段的完整调用链体现了 Basic_client 的设计:

  • create_request()(默认 GET)会预填 User-Agent: IncludeOS/0.12 以及 Connection 头(默认 close),见 basic_client.cpp。测试中仅覆盖了 create_request() 配合 send() 的用法;实际上客户端还提供 get/post/request 等便捷方法,并支持以 URI 或 (Host, path) 两种方式定位目标。
  • send(Request_ptr, Host, Response_handler, secure=false, Options={}):向指定主机发送请求。实现中会自动补齐 Host 与 Origin 头(Origin 即本机 IP,见 origin()),再调用 Client_connection::send 完成发送。
  • 回调与错误:响应回调签名是 delegate<void(Error, Response_ptr, Connection&)>。Error(见 error.hpp)是一个轻量错误封装,枚举包括 NONE / RESOLVE_HOST / NO_REPLY / INVALID / TIMEOUT / CLOSING,可用 to_string() 输出人类可读信息,operator bool 用于 if (err) 判断。回调中 !err 表示成功收到响应,res->body() 返回响应体,测试断言其与请求路径 /testing 一致。
  • 4.4 客户端底层:连接复用、超时与重定向

    从 client_connection.cpp 可以看到客户端连接的核心机制:

    • 连接复用:Basic_client 内部以 Connection_mapset = std::map<Host, Connection_set> 按目标主机缓存连接;get_connection() 优先复用未占用的空闲连接,否则新建 TCP 连接(见 basic_client.cpp)。连接的可用性由 available()(on_response_ == nullptr && keep_alive_)决定,而 keep_alive_ 取决于请求头 Connection 是否为 close。
    • 响应接收:recv_response 把 TCP 分片逐步拼装成 Response,按 Content-Length 判断报文是否完整;对于 1xx/204/304 以及 HEAD 请求等"无实体"响应,收到空行即结束。
    • 超时控制:Options.timeout 默认 DEFAULT_TIMEOUT 为 5 秒(见 basic_client.cpp),超时后回调收到 Error::TIMEOUT。
    • 重定向:Options.follow_redirect 默认 0(不跟随);若大于 0,收到 3xx 且带 Location 的响应时,Client_connection::redirect 会更新 URI 并重新发送请求(见 client_connection.cpp)。
    • 关闭语义:连接被对端关闭时,若已收到完整响应头则回调 Error::NONE,否则回调 Error::CLOSING。

    五、宿主端测试驱动:Python 回显服务器与 vmrunner

    test.py 是运行在宿主 Linux 上的 Python 3 脚本,它同时承担两个角色。

    5.1 角色一:9011 端口的回显 HTTP 服务器

    HOST = ''
    PORT = 9011

    class RequestHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(s):
    s.send_response(200)
    s.send_header("Content-type", "text/plain; charset=utf-8",)
    s.end_headers()
    s.wfile.write(s.path.encode("utf-8"))

    def Client_test():
    server_class = http.server.HTTPServer
    httpd = server_class((HOST, PORT), RequestHandler)
    global DO_SERVE
    while(DO_SERVE):
    httpd.handle_request()
    DO_SERVE = False
    httpd.server_close()

    # Start web server in a separate thread
    _thread.start_new_thread(Client_test, ())

    这段逻辑设计得非常精妙:宿主机在 10.0.0.1:9011 起一个只处理一次请求的 HTTP 服务器,do_GET 直接把请求路径作为响应体返回(请求 /testing 就回 testing)。VM 内客户端恰好发送 URI 为 /testing 的请求到 {inet.gateway(), 9011}——网关就是宿主机——因此客户端回调断言 res->body() == "/testing" 时,实际上同时验证了桥接网络连通性 + TCP 连接 + HTTP 请求构造 + 响应解析四层链路。这里就是原文档"Outgoing test requires correct network configuration"所指的环节:若宿主机与 VM 之间没有正确桥接(run.sh 中 create_bridge.sh 的职责),请求将永远到达不了 9011 端口。

    5.2 角色二:vmrunner 驱动的断言脚本

    def Server_test(triggerline):
    res = urllib.request.urlopen("http://10.0.0.46:8080").read()
    assert(res.decode('utf-8') == "Hello")

    # Get an auto-created VM from the vmrunner
    vm = vmrunner.vms[0]

    # Add custom event for testing server
    vm.on_output("Listening on port 8080", Server_test)

    if len(sys.argv) > 1:
    vm.boot(image_name=str(sys.argv[1]))
    else:
    # Boot the VM, taking a timeout as parameter
    vm.boot(20,image_name='net_http.elf.bin')

    Server_test 在 VM 输出 Listening on port 8080 的那一刻触发,用标准库 urllib 向 VM 的 8080 端口发起 GET,断言响应体为 Hello——与 service.cpp 中 writer->write("Hello") 一一对应。这验证了服务端方向:外部客户端能访问到 unikernel 内的 HTTP 服务器,并收到正确的响应。

    vm.boot(20, …) 表示以 20 秒超时启动 VM(若命令行传入镜像参数则以参数为准),整个测试脚本最终通过 exit 状态码向 CI 汇报成败。

    六、运行方式与出站网络配置

    run.sh 提供了标准启动流程:

    #! /bin/bash
    set –
    INCLUDEOS_PREFIX=${INCLUDEOS_PREFIX-/usr/local}
    sudo $INCLUDEOS_PREFIX/includeos/scripts/create_bridge.sh

    FILE=$1
    shift
    source $INCLUDEOS_PREFIX/includeos/scripts/run.sh $FILE ${*}

    执行步骤:

  • 若未设置 INCLUDEOS_PREFIX,默认取 /usr/local;
  • create_bridge.sh 在宿主机上创建桥接网卡(需 sudo),使 VM 的 virtio 网卡能接入 10.0.0.0/24 网段、并可通过网关 10.0.0.1 与宿主机互通;
  • source run.sh <镜像文件> 用 QEMU 启动 net_http.elf.bin。
  • 6.1 两种测试方向的网络要求

    测试方向网络依赖失败表现
    服务端测试(宿主机访问 VM:8080) VM 与宿主机桥接互通即可,无需外网 urllib 连接超时,assert 失败
    客户端出站测试(VM 访问宿主机:9011) 桥接互通 + 宿主机 9011 端口可被 VM 访问 客户端回调收到错误,CHECKSERT(!err) 失败

    原文档的"出站测试需要正确的网络配置"正是针对第二行:VM 内的 TCP 连接要到达宿主机监听端口,除桥接外还要求宿主机侧防火墙/路由允许该流量;若要访问外网(如某些测试访问真实公网服务),还需配置 NAT 或端口转发规则。本测试用"宿主机回显服务器"替代外网目标,把外部依赖降到最低,这是它适合作为 CI 回归测试的重要原因。

    七、测试预期输出与结果判定

    整个测试成功时,日志链路大致如下(按时间顺序):

    [ HTTP Server ] Listening on port 8080 on CPU 0 ← VM 服务端就绪,触发 Server_test
    [ Basic_client ] Testing against local server ← 客户端开始出站请求
    Sending request: ← on_send 回调打印
    GET /testing HTTP/1.1
    Host: 10.0.0.1:9011
    …
    Received body: /testing ← 回调收到回显
    SUCCESS ← 客户端测试通过

    若某个环节失败:

    • CHECKSERT(!err, "No error") 失败说明出站请求出错,可结合 Error::to_string() 定位(Unable to resolve host / No reply / Request timed out 等);
    • CHECKSERT(res->body() == "/testing", …) 失败说明响应内容异常,通常是宿主机回显服务器行为与预期不符;
    • 服务端方向的 assert(res.decode('utf-8') == "Hello") 失败说明 8080 端口响应体错误。

    由于 CHECKSERT 宏在打印 [ ] 失败标记后会触发 assert(见 api/info),VM 会中止运行,vmrunner 随之判定测试失败——这正是集成测试"断言即哨兵"的设计思路。

    八、小结:从测试文档到源码契约

    尽管原 README 只有寥寥数语,但 test/integration/net/http/ 目录实际上浓缩了 IncludeOS HTTP 栈最重要的两条使用路径:

    • 服务端:Server + on_request 回调 + Response_writer,配合连接复用、空闲超时与 Statman 统计,见 server.hpp 与 server.cpp;
    • 客户端:Basic_client 的请求构造、连接缓存、超时/重定向与错误模型,以及"不支持 HTTPS 必须抛异常"的 API 契约,见 basic_client.hpp 与 basic_client.cpp。

    对想要在 IncludeOS 上开发 HTTP 服务的开发者而言,这套测试就是现成的"最小可运行范例":照抄 service.cpp 的服务器三段式(创建 Server → 注册回调 → listen),再配合 CMakeLists.txt 的构建脚手架,即可快速产出自己的 HTTP 服务镜像;而客户端的 create_request / get / post / send / on_send 组合,则覆盖了在 unikernel 内主动发起 HTTP 请求的全部常见场景。运行测试前,务必像原文档提醒的那样确认桥接与出站网络配置正确——这是整套链路能够跑通的前提。

    赞

    分享

    • 操作系统
    • 云原生
    • 虚拟化
    • 网络

    【免费下载链接】IncludeOS

    A minimal, resource efficient unikernel for cloud services

    项目地址:
    https://gitcode.com/gh_mirrors/in/IncludeOS

    点击查看 免费下载

    上一篇:
    3分钟掌握DeepSeek-V4-Flash-DSpark编码技巧:从消息格式到工具调用全攻略

    下一篇:
    Sequin开发指南:如何为这个ANSI解析工具贡献代码

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » IncludeOS HTTP 集成测试实战:在本地服务器上验证 HTTP 客户端与服务器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!