Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 073b587962 | |||
| 96785eea21 | |||
| 3093bdd9ab | |||
| 6607a6a592 | |||
| 831b64bdeb | |||
| 32c82492de | |||
| b7e02de4a7 | |||
| a9359df42e | |||
| 9a97e948f0 | |||
| 6fd97aeca0 | |||
| 05540e4d50 | |||
| ceefc14e7d | |||
| a77284a634 | |||
| 315a87520d | |||
| 703abbb53b | |||
| cb8365349f | |||
| 3792ce0da7 | |||
| 7178f451a4 | |||
| c2bdb1c5c1 | |||
| 45820de332 | |||
| dd8071a7d4 | |||
| c59ef98b3b | |||
| 1c6b3ea5a0 | |||
| 4a1e9443ee | |||
| 6f2717e623 | |||
| 257b266190 | |||
| ba0d0b82db | |||
| 5ecba74a99 | |||
| ec1ffbc27d | |||
| 4978f26f86 | |||
| bb7c7ab075 | |||
| 1c3d35f83c | |||
| b1bb2b7ecc | |||
| f6ed5fc60f | |||
| 69d468f4d9 | |||
| 2e61fd3e6e | |||
| 3ad4a4243a | |||
| 511e3ef9e5 | |||
| f787f31b87 | |||
| 43a54a3e3d | |||
| 83e98a28dd | |||
| 4d7c9a788d | |||
| 1cd0347ace | |||
| b3a8af80b9 | |||
| 1e97c28e36 | |||
| d279eff4db | |||
| 188035fb6d | |||
| 125272f34b | |||
| 9ced2f614d | |||
| 68fa9bce0f | |||
| 1f34c541b0 | |||
| e41ec36274 | |||
| 7489fd3a8b | |||
| ab3098f18b | |||
| 0d3a3d805c | |||
| a058e31a3a | |||
| c87d442aac | |||
| de296af3eb | |||
| 41899acece |
@@ -0,0 +1,34 @@
|
||||
name: docs
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs-src/**'
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install docs-gen
|
||||
run: cargo install docs-gen
|
||||
- name: Build
|
||||
run: docs-gen build docs-src docs
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,36 @@
|
||||
name: 32-bit Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-win32:
|
||||
name: Windows 32-bit (MSVC x86)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build (Win32)
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86
|
||||
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
|
||||
|
||||
test-arm32:
|
||||
name: ARM 32-bit (cross-compile)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install cross compiler
|
||||
run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf
|
||||
- name: Build (ARM 32-bit)
|
||||
run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
clang-format --version
|
||||
cd test && make style_check
|
||||
|
||||
build-error-check-on-32bit:
|
||||
build-and-test-on-32bit:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
@@ -64,9 +64,8 @@ jobs:
|
||||
libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \
|
||||
zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \
|
||||
libzstd-dev${{ matrix.config.arch_suffix }}
|
||||
- name: build and run tests (expect failure)
|
||||
- name: build and run tests
|
||||
run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
|
||||
continue-on-error: true
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -229,7 +228,7 @@ jobs:
|
||||
for ($i = 0; $i -lt $shards; $i++) {
|
||||
$log = "shard_${i}.log"
|
||||
$procs += Start-Process -FilePath ./Release/httplib-test.exe `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}-*BenchmarkTest*" `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}" `
|
||||
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
|
||||
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
|
||||
}
|
||||
@@ -249,9 +248,6 @@ jobs:
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
Write-Host "All shards passed."
|
||||
- name: Run benchmark tests with retry ${{ matrix.config.name }}
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5
|
||||
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
name: benchmark
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build and run
|
||||
run: cd test && make test_benchmark && ./test_benchmark
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build and run
|
||||
run: cd test && make test_benchmark && ./test_benchmark
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
steps:
|
||||
- name: Prepare Git for Checkout on Windows
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
- name: Cache vcpkg packages
|
||||
id: vcpkg-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:/vcpkg/installed
|
||||
key: vcpkg-installed-windows-gtest
|
||||
- name: Install vcpkg dependencies
|
||||
if: steps.vcpkg-cache.outputs.cache-hit != 'true'
|
||||
run: vcpkg install gtest
|
||||
- name: Configure and build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$cmake_content = @"
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(httplib-benchmark CXX)
|
||||
find_package(GTest REQUIRED)
|
||||
add_executable(httplib-benchmark test/test_benchmark.cc)
|
||||
target_include_directories(httplib-benchmark PRIVATE .)
|
||||
target_link_libraries(httplib-benchmark PRIVATE GTest::gtest_main)
|
||||
target_compile_options(httplib-benchmark PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
|
||||
"@
|
||||
New-Item -ItemType Directory -Force -Path build_bench/test | Out-Null
|
||||
Set-Content -Path build_bench/CMakeLists.txt -Value $cmake_content
|
||||
Copy-Item -Path httplib.h -Destination build_bench/
|
||||
Copy-Item -Path test/test_benchmark.cc -Destination build_bench/test/
|
||||
cmake -B build_bench/build -S build_bench `
|
||||
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||
cmake --build build_bench/build --config Release
|
||||
- name: Run with retry
|
||||
run: ctest --output-on-failure --test-dir build_bench/build -C Release --repeat until-pass:5
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
@@ -28,6 +28,10 @@ jobs:
|
||||
- name: Run proxy tests (OpenSSL)
|
||||
if: matrix.tls_backend == 'openssl'
|
||||
run: cd test && make proxy
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
|
||||
- name: Run proxy tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make proxy_mbedtls
|
||||
run: cd test && make proxy_mbedtls
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
|
||||
@@ -1,4 +1,8 @@
|
||||
tags
|
||||
AGENTS.md
|
||||
docs-src/pages/AGENTS.md
|
||||
plans/
|
||||
work/
|
||||
|
||||
# Ignore executables (no extension) but not source files
|
||||
example/server
|
||||
@@ -48,6 +52,7 @@ test/test_split_wolfssl
|
||||
test/test_split_no_tls
|
||||
test/test_websocket_heartbeat
|
||||
test/test_thread_pool
|
||||
test/test_benchmark
|
||||
test/test.xcodeproj/xcuser*
|
||||
test/test.xcodeproj/*/xcuser*
|
||||
test/*.o
|
||||
@@ -55,9 +60,7 @@ test/*.pem
|
||||
test/*.srl
|
||||
test/*.log
|
||||
test/_build_*
|
||||
work/
|
||||
benchmark/server*
|
||||
docs-gen/target/
|
||||
|
||||
*.swp
|
||||
|
||||
|
||||
@@ -173,9 +173,6 @@ if(CMAKE_SYSTEM_NAME MATCHES "Windows")
|
||||
message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.")
|
||||
endif()
|
||||
endif()
|
||||
if(CMAKE_SIZEOF_VOID_P LESS 8)
|
||||
message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.")
|
||||
endif()
|
||||
|
||||
# Set some variables that are used in-tree and while building based on our options
|
||||
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
|
||||
@@ -245,27 +242,33 @@ endif()
|
||||
# zstd < 1.5.6 does not provide the CMake imported target `zstd::libzstd`.
|
||||
# Older versions must be consumed via their pkg-config file.
|
||||
if(HTTPLIB_REQUIRE_ZSTD)
|
||||
find_package(zstd 1.5.6 CONFIG)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
|
||||
if (NOT TARGET zstd::libzstd)
|
||||
find_package(zstd 1.5.6 CONFIG)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
|
||||
endif()
|
||||
endif()
|
||||
set(HTTPLIB_IS_USING_ZSTD TRUE)
|
||||
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
|
||||
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(zstd QUIET IMPORTED_TARGET libzstd)
|
||||
if (TARGET zstd::libzstd)
|
||||
set(HTTPLIB_IS_USING_ZSTD TRUE)
|
||||
else()
|
||||
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(zstd QUIET IMPORTED_TARGET libzstd)
|
||||
|
||||
if(TARGET PkgConfig::zstd)
|
||||
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
|
||||
if(TARGET PkgConfig::zstd)
|
||||
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
# Both find_package and PkgConf set a XXX_FOUND var
|
||||
set(HTTPLIB_IS_USING_ZSTD ${zstd_FOUND})
|
||||
endif()
|
||||
# Both find_package and PkgConf set a XXX_FOUND var
|
||||
set(HTTPLIB_IS_USING_ZSTD ${zstd_FOUND})
|
||||
endif()
|
||||
|
||||
# Used for default, common dirs that the end-user can change (if needed)
|
||||
@@ -320,13 +323,13 @@ if(HTTPLIB_COMPILE)
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
|
||||
)
|
||||
|
||||
|
||||
# Add C++20 module support if requested
|
||||
# Include from separate file to prevent parse errors on older CMake versions
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28")
|
||||
include(cmake/modules.cmake)
|
||||
endif()
|
||||
|
||||
|
||||
set_target_properties(${PROJECT_NAME}
|
||||
PROPERTIES
|
||||
VERSION ${${PROJECT_NAME}_VERSION}
|
||||
|
||||
@@ -74,6 +74,9 @@ sse.set_reconnect_interval(5000);
|
||||
|
||||
// Set max reconnect attempts (default: 0 = unlimited)
|
||||
sse.set_max_reconnect_attempts(10);
|
||||
|
||||
// Update headers at any time (thread-safe)
|
||||
sse.set_headers({{"Authorization", "Bearer new_token"}});
|
||||
```
|
||||
|
||||
#### Control
|
||||
@@ -154,6 +157,25 @@ httplib::sse::SSEClient sse(cli, "/events", headers);
|
||||
sse.start();
|
||||
```
|
||||
|
||||
### Refreshing Auth Token on Reconnect
|
||||
|
||||
```cpp
|
||||
httplib::sse::SSEClient sse(cli, "/events",
|
||||
{{"Authorization", "Bearer " + get_token()}});
|
||||
|
||||
// Preemptively refresh token on each successful connection
|
||||
sse.on_open([&sse]() {
|
||||
sse.set_headers({{"Authorization", "Bearer " + get_token()}});
|
||||
});
|
||||
|
||||
// Or reactively refresh on auth failure (401 triggers reconnect)
|
||||
sse.on_error([&sse](httplib::Error) {
|
||||
sse.set_headers({{"Authorization", "Bearer " + refresh_token()}});
|
||||
});
|
||||
|
||||
sse.start();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -45,7 +45,7 @@ cpp-httplib provides multiple API layers for different use cases:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SSEClient (planned) │ ← SSE-specific, parsed events
|
||||
│ SSEClient │ ← SSE-specific, parsed events
|
||||
│ - on_message(), on_event() │
|
||||
│ - Auto-reconnect, Last-Event-ID │
|
||||
├─────────────────────────────────────────────┤
|
||||
@@ -61,7 +61,7 @@ cpp-httplib provides multiple API layers for different use cases:
|
||||
|
||||
| Use Case | Recommended API |
|
||||
|----------|----------------|
|
||||
| SSE with auto-reconnect | SSEClient (planned) or `ssecli-stream.cc` example |
|
||||
| SSE with auto-reconnect | `SSEClient` (see [README-sse.md](README-sse.md)) |
|
||||
| LLM streaming (JSON Lines) | `stream::Get()` |
|
||||
| Large file download | `stream::Get()` or `open_stream()` |
|
||||
| Reverse proxy | `open_stream()` |
|
||||
|
||||
@@ -353,6 +353,26 @@ if (ws.connect()) {
|
||||
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
|
||||
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
||||
|
||||
### Runtime Ping Interval
|
||||
|
||||
You can override the ping interval at runtime instead of changing the compile-time macro. Set it to `0` to disable automatic pings entirely.
|
||||
|
||||
```cpp
|
||||
// Server side
|
||||
httplib::Server svr;
|
||||
svr.set_websocket_ping_interval(10); // 10 seconds
|
||||
|
||||
// Or using std::chrono
|
||||
svr.set_websocket_ping_interval(std::chrono::seconds(10));
|
||||
|
||||
// Client side
|
||||
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
|
||||
ws.set_websocket_ping_interval(10); // 10 seconds
|
||||
|
||||
// Disable automatic pings
|
||||
ws.set_websocket_ping_interval(0);
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
A C++11 single-file header-only cross platform HTTP/HTTPS library.<br>
|
||||
It's extremely easy to set up. Just include the **[httplib.h](https://raw.githubusercontent.com/yhirose/cpp-httplib/refs/heads/master/httplib.h)** file in your code!
|
||||
|
||||
**Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/)**.
|
||||
Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/) (built with [docs-gen](https://github.com/yhirose/docs-gen)).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
|
||||
|
||||
> [!WARNING]
|
||||
> 32-bit platforms are **NOT supported**. Use at your own risk. The library may compile on 32-bit targets, but no security review has been conducted for 32-bit environments. Integer truncation and other 32-bit-specific issues may exist. **Security reports that only affect 32-bit platforms will be closed without action.** The maintainer does not have access to 32-bit environments for testing or fixing issues. CI includes basic compile checks only, not functional or security testing.
|
||||
|
||||
## Main Features
|
||||
|
||||
- HTTP Server/Client
|
||||
@@ -61,26 +64,14 @@ if (auto res = cli.Get("/hi")) {
|
||||
|
||||
cpp-httplib supports multiple TLS backends through an abstraction layer:
|
||||
|
||||
| Backend | Define | Libraries |
|
||||
| :------ | :----- | :-------- |
|
||||
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` |
|
||||
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` |
|
||||
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` |
|
||||
| Backend | Define | Libraries | Notes |
|
||||
| :------ | :----- | :-------- | :---- |
|
||||
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | [3.0 or later](https://www.openssl.org/policies/releasestrat.html) required |
|
||||
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | 2.x and 3.x supported (auto-detected) |
|
||||
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | 5.x supported; must build with `--enable-opensslall` |
|
||||
|
||||
> [!NOTE]
|
||||
> OpenSSL 3.0 or later is required. Please see [this page](https://www.openssl.org/policies/releasestrat.html) for more information.
|
||||
|
||||
> [!NOTE]
|
||||
> Mbed TLS 2.x and 3.x are supported. The library automatically detects the version and uses the appropriate API.
|
||||
|
||||
> [!NOTE]
|
||||
> wolfSSL must be built with OpenSSL compatibility layer enabled (`--enable-opensslall`). wolfSSL 5.x is supported.
|
||||
|
||||
> [!NOTE]
|
||||
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()` or `load_ca_cert_store(pem, size)`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable with these backends.
|
||||
|
||||
> [!TIP]
|
||||
> For macOS: cpp-httplib automatically loads system certs from the Keychain when a TLS backend is enabled. `CoreFoundation` and `Security` must be linked with `-framework`. To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
|
||||
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
|
||||
|
||||
```c++
|
||||
// Use either OpenSSL, Mbed TLS, or wolfSSL
|
||||
@@ -194,27 +185,21 @@ svr.Get("/", [](const httplib::Request &req, httplib::Response &res) {
|
||||
});
|
||||
```
|
||||
|
||||
### Windows Certificate Verification
|
||||
### Platform-specific Certificate Handling
|
||||
|
||||
On Windows, cpp-httplib automatically performs additional certificate verification using the Windows certificate store via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`). This works with all TLS backends (OpenSSL, Mbed TLS, and wolfSSL), providing:
|
||||
cpp-httplib automatically integrates with the OS certificate store on macOS and Windows. This works with all TLS backends.
|
||||
|
||||
- Real-time certificate validation integrated with Windows Update
|
||||
- Certificate revocation checking
|
||||
- SSL/TLS policy verification using the system certificate store (ROOT and CA)
|
||||
| Platform | Behavior | Disable (compile time) |
|
||||
| :------- | :------- | :--------------------- |
|
||||
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`) | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
||||
| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||
|
||||
This feature is enabled by default and can be controlled at runtime:
|
||||
On Windows, verification can also be disabled at runtime:
|
||||
|
||||
```c++
|
||||
// Disable Windows certificate verification (use only OpenSSL/Mbed TLS/wolfSSL verification)
|
||||
cli.enable_windows_certificate_verification(false);
|
||||
```
|
||||
|
||||
To disable this feature at compile time, define:
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.
|
||||
|
||||
@@ -347,6 +332,11 @@ The following are built-in mappings:
|
||||
> [!WARNING]
|
||||
> These static file server methods are not thread-safe.
|
||||
|
||||
<!-- -->
|
||||
|
||||
> [!NOTE]
|
||||
> On POSIX systems, the static file server rejects requests that resolve (via symlinks) to a path outside the mounted base directory. Ensure that the served directory has appropriate permissions, as managing access to the served directory is the application developer's responsibility.
|
||||
|
||||
### File request handler
|
||||
|
||||
```cpp
|
||||
@@ -471,7 +461,7 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
||||
|
||||
### Response user data
|
||||
|
||||
`res.user_data` is a `std::map<std::string, httplib::any>` that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||
|
||||
```cpp
|
||||
struct AuthContext {
|
||||
@@ -481,12 +471,12 @@ struct AuthContext {
|
||||
|
||||
svr.set_pre_routing_handler([](const auto& req, auto& res) {
|
||||
auto token = req.get_header_value("Authorization");
|
||||
res.user_data["auth"] = AuthContext{decode_token(token)};
|
||||
res.user_data.set("auth", AuthContext{decode_token(token)});
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto& /*req*/, auto& res) {
|
||||
auto* ctx = httplib::any_cast<AuthContext>(&res.user_data["auth"]);
|
||||
auto* ctx = res.user_data.get<AuthContext>("auth");
|
||||
if (!ctx) {
|
||||
res.status = StatusCode::Unauthorized_401;
|
||||
return;
|
||||
@@ -495,8 +485,6 @@ svr.Get("/me", [](const auto& /*req*/, auto& res) {
|
||||
});
|
||||
```
|
||||
|
||||
`httplib::any` mirrors the C++17 `std::any` API. On C++17 and later it is an alias for `std::any`; on C++11/14 a compatible implementation is provided.
|
||||
|
||||
### Form data handling
|
||||
|
||||
#### URL-encoded form data ('application/x-www-form-urlencoded')
|
||||
@@ -538,16 +526,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
||||
}
|
||||
|
||||
// IMPORTANT: file.filename is an untrusted value from the client.
|
||||
// Always extract only the basename to prevent path traversal attacks.
|
||||
auto safe_name = std::filesystem::path(file.filename).filename();
|
||||
if (safe_name.empty() || safe_name == "." || safe_name == "..") {
|
||||
// Always sanitize to prevent path traversal attacks.
|
||||
auto safe_name = httplib::sanitize_filename(file.filename);
|
||||
if (safe_name.empty()) {
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
res.set_content("Invalid filename", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
std::ofstream ofs(upload_dir / safe_name, std::ios::binary);
|
||||
std::ofstream ofs(upload_dir + "/" + safe_name, std::ios::binary);
|
||||
ofs << file.content;
|
||||
}
|
||||
|
||||
@@ -583,6 +571,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
||||
});
|
||||
```
|
||||
|
||||
#### Filename Sanitization
|
||||
|
||||
`file.filename` in multipart uploads is an untrusted value from the client. Always sanitize before using it in file paths:
|
||||
|
||||
```cpp
|
||||
auto safe = httplib::sanitize_filename(file.filename);
|
||||
```
|
||||
|
||||
This function strips path separators (`/`, `\`), null bytes, leading/trailing whitespace, and rejects `.` and `..`. Returns an empty string if the filename is unsafe.
|
||||
|
||||
### Receive content with a content receiver
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -10,7 +10,10 @@ if(HTTPLIB_BUILD_MODULES)
|
||||
|
||||
target_sources(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
FILE_SET CXX_MODULES FILES
|
||||
FILE_SET CXX_MODULES
|
||||
BASE_DIRS
|
||||
"${_httplib_build_includedir}"
|
||||
FILES
|
||||
"${_httplib_build_includedir}/httplib.cppm"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "docs-gen"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
pulldown-cmark = "0.12"
|
||||
tera = "1"
|
||||
walkdir = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
toml = "0.8"
|
||||
syntect = "5"
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
notify = "7"
|
||||
tiny_http = "0.12"
|
||||
tungstenite = "0.24"
|
||||
open = "5"
|
||||
tempfile = "3"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
@@ -1,254 +0,0 @@
|
||||
# docs-gen
|
||||
|
||||
A static site generator for multi-language documentation. Markdown content, Tera templates, and syntax highlighting — all in a single binary.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Scaffold a new project
|
||||
docs-gen init my-docs
|
||||
|
||||
# 2. Start the local dev server with live-reload
|
||||
docs-gen serve my-docs --open
|
||||
|
||||
# 3. Build for production
|
||||
docs-gen build my-docs --out docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### `init [DIR]`
|
||||
|
||||
Creates a new project scaffold in `DIR` (default: `.`).
|
||||
|
||||
Generated files:
|
||||
|
||||
```
|
||||
config.toml
|
||||
pages/
|
||||
en/index.md
|
||||
ja/index.md
|
||||
templates/
|
||||
base.html
|
||||
page.html
|
||||
portal.html
|
||||
static/
|
||||
css/main.css
|
||||
js/main.js
|
||||
```
|
||||
|
||||
Existing files are never overwritten.
|
||||
|
||||
---
|
||||
|
||||
### `serve [SRC] [--port PORT] [--open]`
|
||||
|
||||
Builds the site into a temporary directory and serves it locally. Watches for changes and live-reloads the browser automatically.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `SRC` | `.` | Source directory |
|
||||
| `--port` | `8080` | HTTP server port |
|
||||
| `--open` | — | Open browser on startup |
|
||||
|
||||
---
|
||||
|
||||
### `build [SRC] [--out OUT]`
|
||||
|
||||
Generates the static site from source.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `SRC` | `.` | Source directory |
|
||||
| `--out` | `docs` | Output directory |
|
||||
|
||||
---
|
||||
|
||||
## Source Directory Structure
|
||||
|
||||
Only `config.toml` and `pages/` are required. `templates/` and `static/` are optional — when absent, built-in defaults are used automatically.
|
||||
|
||||
```
|
||||
my-docs/
|
||||
├── config.toml # Site configuration (required)
|
||||
├── pages/ # Markdown content (required)
|
||||
│ ├── en/
|
||||
│ │ ├── index.md # Portal page (homepage, no sidebar)
|
||||
│ │ └── guide/
|
||||
│ │ ├── index.md # Section index
|
||||
│ │ ├── 01-intro.md
|
||||
│ │ └── 02-usage.md
|
||||
│ └── ja/
|
||||
│ └── ...
|
||||
├── templates/ # Override built-in HTML templates (optional)
|
||||
│ ├── base.html
|
||||
│ ├── page.html
|
||||
│ └── portal.html
|
||||
└── static/ # Override built-in CSS/JS/assets (optional)
|
||||
├── css/main.css
|
||||
└── js/main.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## config.toml
|
||||
|
||||
```toml
|
||||
[site]
|
||||
title = "My Project"
|
||||
version = "1.0.0" # Optional. Shown in header.
|
||||
hostname = "https://example.github.io" # Optional. Combined with base_path for full URLs.
|
||||
base_path = "/my-project" # URL prefix. Use "" for local-only.
|
||||
|
||||
[[nav]]
|
||||
label = "Guide"
|
||||
path = "guide/" # Internal section path (resolved per language)
|
||||
icon_svg = '<svg ...>...</svg>' # Optional inline SVG icon
|
||||
|
||||
[[nav]]
|
||||
label = "GitHub"
|
||||
url = "https://github.com/your/repo" # External URL
|
||||
icon_svg = '<svg ...>...</svg>'
|
||||
|
||||
[i18n]
|
||||
langs = ["en", "ja"] # First entry is the default language
|
||||
|
||||
[highlight]
|
||||
dark_theme = "base16-eighties.dark" # Dark mode theme
|
||||
light_theme = "InspiredGitHub" # Light mode theme (optional)
|
||||
```
|
||||
|
||||
### `[site]`
|
||||
|
||||
| Key | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `title` | yes | Site title displayed in the header |
|
||||
| `version` | no | Version string displayed in the header |
|
||||
| `hostname` | no | Base hostname (e.g. `"https://user.github.io"`). Combined with `base_path` to form `site.base_url` in templates. |
|
||||
| `base_path` | no | URL path prefix. Use `"/repo-name"` for GitHub Pages, `""` for local development. |
|
||||
|
||||
### `[[nav]]` — Toolbar Buttons
|
||||
|
||||
Defines buttons in the site header. Each entry supports:
|
||||
|
||||
| Key | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `label` | yes | Button label text |
|
||||
| `path` | no | Internal section path relative to `<lang>/` (e.g. `"guide/"`). Resolved using the current language. |
|
||||
| `url` | no | Absolute external URL. Takes precedence over `path` if both are set. |
|
||||
| `icon_svg` | no | Inline SVG markup displayed as an icon |
|
||||
|
||||
### `[i18n]`
|
||||
|
||||
| Key | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `langs` | yes | List of language codes. At least one is required. The first entry is used as the default language. |
|
||||
|
||||
### `[highlight]`
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `dark_theme` | `base16-ocean.dark` | Theme for dark mode |
|
||||
| `light_theme` | _(none)_ | Theme for light mode. When set, both dark and light code blocks are emitted and toggled via CSS. |
|
||||
|
||||
Available themes: `base16-ocean.dark`, `base16-ocean.light`, `base16-eighties.dark`, `base16-mocha.dark`, `InspiredGitHub`, `Solarized (dark)`, `Solarized (light)`.
|
||||
|
||||
---
|
||||
|
||||
## Writing Pages
|
||||
|
||||
### Frontmatter
|
||||
|
||||
Every `.md` file must begin with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Getting Started"
|
||||
order: 1
|
||||
---
|
||||
|
||||
Page content goes here...
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `title` | yes | Page title shown in the heading and browser tab |
|
||||
| `order` | no | Sort order within the section (default: `0`) |
|
||||
| `status` | no | Set to `"draft"` to display a DRAFT banner |
|
||||
|
||||
### URL Routing
|
||||
|
||||
Files are mapped to URLs as follows:
|
||||
|
||||
| File | URL |
|
||||
|------|-----|
|
||||
| `en/index.md` | `<base_path>/en/` |
|
||||
| `en/guide/index.md` | `<base_path>/en/guide/` |
|
||||
| `en/guide/01-intro.md` | `<base_path>/en/guide/01-intro/` |
|
||||
|
||||
The root `index.html` is generated automatically and redirects to the default language, respecting the user's `localStorage` language preference.
|
||||
|
||||
### Sidebar Navigation
|
||||
|
||||
Sidebar navigation is generated automatically:
|
||||
|
||||
- Each subdirectory under a language becomes a **section**
|
||||
- The section's `index.md` title is used as the section heading
|
||||
- Pages within a section are sorted by `order`, then by filename
|
||||
- `index.md` at the language root uses `portal.html` (no sidebar)
|
||||
- All other pages use `page.html` (with sidebar)
|
||||
|
||||
---
|
||||
|
||||
## Customizing Templates and Assets
|
||||
|
||||
When `templates/` or `static/` directories exist in the source, files there override the built-in defaults. Use `docs-gen init` to generate the defaults as a starting point.
|
||||
|
||||
Three templates are available:
|
||||
|
||||
| Template | Used for |
|
||||
|----------|----------|
|
||||
| `base.html` | Shared layout: `<head>`, header, footer, scripts |
|
||||
| `page.html` | Content pages with sidebar |
|
||||
| `portal.html` | Homepage (`index.md` at language root), no sidebar |
|
||||
|
||||
---
|
||||
|
||||
## Template Variables
|
||||
|
||||
Templates use [Tera](https://keats.github.io/tera/) syntax. Available variables:
|
||||
|
||||
### All templates
|
||||
|
||||
| Variable | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `page.title` | string | Page title from frontmatter |
|
||||
| `page.url` | string | Page URL path |
|
||||
| `page.status` | string? | `"draft"` or null |
|
||||
| `content` | string | Rendered HTML (use `{{ content \| safe }}`) |
|
||||
| `lang` | string | Current language code |
|
||||
| `site.title` | string | Site title |
|
||||
| `site.version` | string? | Site version |
|
||||
| `site.base_url` | string | Full base URL (`hostname` + `base_path`) |
|
||||
| `site.base_path` | string | URL path prefix |
|
||||
| `site.langs` | list | All language codes |
|
||||
| `site.nav` | list | Toolbar button entries |
|
||||
| `site.nav[].label` | string | Button label |
|
||||
| `site.nav[].url` | string? | External URL (if set) |
|
||||
| `site.nav[].path` | string? | Internal section path (if set) |
|
||||
| `site.nav[].icon_svg` | string? | Inline SVG icon (if set) |
|
||||
|
||||
### `page.html` only
|
||||
|
||||
| Variable | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `nav` | list | Sidebar sections |
|
||||
| `nav[].title` | string | Section title |
|
||||
| `nav[].url` | string | Section index URL |
|
||||
| `nav[].active` | bool | True if this section contains the current page |
|
||||
| `nav[].children` | list | Pages within this section |
|
||||
| `nav[].children[].title` | string | Page title |
|
||||
| `nav[].children[].url` | string | Page URL |
|
||||
| `nav[].children[].active` | bool | True if this is the current page |
|
||||
@@ -1,20 +0,0 @@
|
||||
[site]
|
||||
title = "My Docs"
|
||||
hostname = "https://example.github.io"
|
||||
base_path = "/my-project"
|
||||
|
||||
# [[nav]]
|
||||
# label = "Guide"
|
||||
# path = "guide/"
|
||||
#
|
||||
# [[nav]]
|
||||
# label = "GitHub"
|
||||
# url = "https://github.com/your/repo"
|
||||
# icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>'
|
||||
|
||||
[i18n]
|
||||
langs = ["en"]
|
||||
|
||||
[highlight]
|
||||
dark_theme = "base16-ocean.dark"
|
||||
light_theme = "InspiredGitHub"
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
title: Welcome
|
||||
order: 0
|
||||
---
|
||||
|
||||
## Welcome
|
||||
|
||||
This is the home page of your documentation site.
|
||||
|
||||
Edit this file at `pages/en/index.md` to get started.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
title: ようこそ
|
||||
order: 0
|
||||
---
|
||||
|
||||
## ようこそ
|
||||
|
||||
ドキュメントサイトのトップページです。
|
||||
|
||||
`pages/ja/index.md` を編集して始めましょう。
|
||||
@@ -1,603 +0,0 @@
|
||||
:root {
|
||||
--bg: #333;
|
||||
--bg-secondary: #3c3c3c;
|
||||
--bg-code: #2a2a2a;
|
||||
--text: #ccc;
|
||||
--text-bright: white;
|
||||
--text-muted: #999;
|
||||
--text-code: #b0b0b0;
|
||||
--text-inline-code: plum;
|
||||
--border: #555;
|
||||
--border-code: #3a3a3a;
|
||||
--link: palegoldenrod;
|
||||
--heading: lightskyblue;
|
||||
--heading-link: #f0c090;
|
||||
--header-nav-link: pink;
|
||||
--emphasis: pink;
|
||||
--nav-section: #bbb;
|
||||
--nav-section-active: #ddd;
|
||||
--content-width: 900px;
|
||||
--sidebar-width: 280px;
|
||||
--header-height: 48px;
|
||||
--line-height: 1.6;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--header-height);
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: var(--text);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-title:hover {
|
||||
text-decoration: none;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--header-nav-link);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.header-nav a svg {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.header-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.lang-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lang-popup {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 4px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
min-width: 60px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.lang-popup.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lang-popup li a {
|
||||
display: block;
|
||||
padding: 6px 12px;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.lang-popup li a:hover {
|
||||
background: var(--bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* Draft banner */
|
||||
.draft-banner {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
right: 0;
|
||||
background: #c44;
|
||||
color: white;
|
||||
padding: 4px 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.layout {
|
||||
margin-top: var(--header-height);
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
}
|
||||
|
||||
.layout.no-sidebar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
padding: 24px 16px;
|
||||
border-right: 1px solid var(--bg-secondary);
|
||||
position: sticky;
|
||||
top: var(--header-height);
|
||||
height: calc(100vh - var(--header-height));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
color: var(--nav-section);
|
||||
font-weight: bold;
|
||||
font-size: 1.0rem;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nav-section-title.active {
|
||||
color: var(--nav-section-active);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-list li a {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.nav-list li a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-list li a.active {
|
||||
color: var(--emphasis);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
min-width: 0;
|
||||
max-width: var(--content-width);
|
||||
padding: 32px 24px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.content.portal {
|
||||
max-width: var(--content-width);
|
||||
padding: 48px 24px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content article h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 24px;
|
||||
color: var(--heading);
|
||||
}
|
||||
|
||||
.content article h2 {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--heading-link);
|
||||
}
|
||||
|
||||
.content article h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.content article p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.content article ul,
|
||||
.content article ol {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.content article li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.content article strong {
|
||||
color: var(--emphasis);
|
||||
}
|
||||
|
||||
.content article code {
|
||||
background: var(--bg-code);
|
||||
color: var(--text-inline-code);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.content article pre {
|
||||
background: var(--bg-code);
|
||||
color: var(--text-code);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border-code);
|
||||
}
|
||||
|
||||
.content article pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content article table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content article th,
|
||||
.content article td {
|
||||
border: 1px solid var(--bg-secondary);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.content article th {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.content article blockquote {
|
||||
border-left: 3px solid var(--text-muted);
|
||||
padding-left: 16px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Code block theme switching: show dark by default, swap on light mode */
|
||||
.code-block-wrapper > [data-code-theme="light"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .code-block-wrapper > [data-code-theme="dark"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .code-block-wrapper > [data-code-theme="light"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: calc(-1 * var(--sidebar-width));
|
||||
width: var(--sidebar-width);
|
||||
top: var(--header-height);
|
||||
height: calc(100vh - var(--header-height));
|
||||
background: var(--bg);
|
||||
z-index: 50;
|
||||
transition: left 0.2s ease;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
:root {
|
||||
--header-height: 44px;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
padding: 0 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-nav a {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.content article h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.content article h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light mode */
|
||||
[data-theme="light"] {
|
||||
--bg: #f5f5f5;
|
||||
--bg-secondary: #e8e8e8;
|
||||
--bg-code: #eee;
|
||||
--text: #333;
|
||||
--text-bright: #000;
|
||||
--text-muted: #666;
|
||||
--text-code: #333;
|
||||
--text-inline-code: #8b5ca0;
|
||||
--border: #ccc;
|
||||
--border-code: #ddd;
|
||||
--link: #b8860b;
|
||||
--heading: #2a6496;
|
||||
--heading-link: #c06020;
|
||||
--header-nav-link: #c04060;
|
||||
--emphasis: #c04060;
|
||||
--nav-section: #666;
|
||||
--nav-section-active: #333;
|
||||
}
|
||||
|
||||
/* Theme toggle */
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Search button */
|
||||
.search-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Search overlay & modal */
|
||||
.search-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 300;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: 12vh;
|
||||
}
|
||||
|
||||
.search-overlay.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 560px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-input-wrap svg {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-bright);
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-esc {
|
||||
background: var(--bg);
|
||||
color: var(--text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
list-style: none;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-results:empty::after {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-results li {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-results li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-results li:hover,
|
||||
.search-results li.active {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.search-results li .search-result-title {
|
||||
color: var(--text-bright);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.search-results li .search-result-snippet {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.search-results li .search-result-snippet mark {
|
||||
background: rgba(255, 215, 0, 0.3);
|
||||
color: var(--text-bright);
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .search-results li .search-result-snippet mark {
|
||||
background: rgba(255, 200, 0, 0.4);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search-no-results {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,297 +0,0 @@
|
||||
// Language selector
|
||||
(function () {
|
||||
var btn = document.querySelector('.lang-btn');
|
||||
var popup = document.querySelector('.lang-popup');
|
||||
if (!btn || !popup) return;
|
||||
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
popup.classList.toggle('open');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function () {
|
||||
popup.classList.remove('open');
|
||||
});
|
||||
|
||||
popup.addEventListener('click', function (e) {
|
||||
var link = e.target.closest('[data-lang]');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
var lang = link.getAttribute('data-lang');
|
||||
localStorage.setItem('preferred-lang', lang);
|
||||
var basePath = document.documentElement.getAttribute('data-base-path') || '';
|
||||
var path = window.location.pathname;
|
||||
// Strip base path prefix, replace lang, then re-add base path
|
||||
var pathWithoutBase = path.slice(basePath.length);
|
||||
var newPath = basePath + pathWithoutBase.replace(/^\/[a-z]{2}\//, '/' + lang + '/');
|
||||
window.location.href = newPath;
|
||||
});
|
||||
})();
|
||||
|
||||
// Theme toggle
|
||||
(function () {
|
||||
var btn = document.querySelector('.theme-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
// Feather Icons: sun (light mode) and moon (dark mode)
|
||||
var sunSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
||||
var moonSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
||||
|
||||
function getTheme() {
|
||||
var stored = localStorage.getItem('preferred-theme');
|
||||
if (stored) return stored;
|
||||
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
btn.innerHTML = theme === 'light' ? sunSVG : moonSVG;
|
||||
}
|
||||
|
||||
applyTheme(getTheme());
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
var current = getTheme();
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem('preferred-theme', next);
|
||||
applyTheme(next);
|
||||
});
|
||||
})();
|
||||
|
||||
// Mobile sidebar toggle
|
||||
(function () {
|
||||
var toggle = document.querySelector('.sidebar-toggle');
|
||||
var sidebar = document.querySelector('.sidebar');
|
||||
if (!toggle || !sidebar) return;
|
||||
|
||||
toggle.addEventListener('click', function () {
|
||||
sidebar.classList.toggle('open');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!sidebar.contains(e.target) && e.target !== toggle) {
|
||||
sidebar.classList.remove('open');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Site search (⌘K / Ctrl+K)
|
||||
(function () {
|
||||
var overlay = document.getElementById('search-overlay');
|
||||
var input = document.getElementById('search-input');
|
||||
var resultsList = document.getElementById('search-results');
|
||||
if (!overlay || !input || !resultsList) return;
|
||||
|
||||
var searchBtn = document.querySelector('.search-btn');
|
||||
var pagesData = null; // cached pages-data.json
|
||||
var activeIndex = -1;
|
||||
|
||||
function getCurrentLang() {
|
||||
return document.documentElement.getAttribute('lang') || 'en';
|
||||
}
|
||||
|
||||
function getBasePath() {
|
||||
return document.documentElement.getAttribute('data-base-path') || '';
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
overlay.classList.add('open');
|
||||
input.value = '';
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
input.focus();
|
||||
loadPagesData();
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
overlay.classList.remove('open');
|
||||
input.value = '';
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
}
|
||||
|
||||
function loadPagesData() {
|
||||
if (pagesData) return;
|
||||
var basePath = getBasePath();
|
||||
fetch(basePath + '/pages-data.json')
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) { pagesData = data; })
|
||||
.catch(function () { pagesData = []; });
|
||||
}
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function highlightText(text, query) {
|
||||
if (!query) return text;
|
||||
var escaped = escapeRegExp(query);
|
||||
var re = new RegExp('(' + escaped + ')', 'gi');
|
||||
return text.replace(re, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
function buildSnippet(body, query) {
|
||||
if (!query || !body) return '';
|
||||
var lower = body.toLowerCase();
|
||||
var idx = lower.indexOf(query.toLowerCase());
|
||||
var start, end, snippet;
|
||||
if (idx === -1) {
|
||||
snippet = body.substring(0, 120);
|
||||
} else {
|
||||
start = Math.max(0, idx - 40);
|
||||
end = Math.min(body.length, idx + query.length + 80);
|
||||
snippet = (start > 0 ? '...' : '') + body.substring(start, end) + (end < body.length ? '...' : '');
|
||||
}
|
||||
return highlightText(snippet, query);
|
||||
}
|
||||
|
||||
function search(query) {
|
||||
if (!pagesData || !query) {
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
var lang = getCurrentLang();
|
||||
var q = query.toLowerCase();
|
||||
|
||||
// Score and filter
|
||||
var scored = [];
|
||||
for (var i = 0; i < pagesData.length; i++) {
|
||||
var page = pagesData[i];
|
||||
if (page.lang !== lang) continue;
|
||||
|
||||
var score = 0;
|
||||
var titleLower = page.title.toLowerCase();
|
||||
var bodyLower = (page.body || '').toLowerCase();
|
||||
|
||||
if (titleLower.indexOf(q) !== -1) {
|
||||
score += 10;
|
||||
// Bonus for exact title match
|
||||
if (titleLower === q) score += 5;
|
||||
}
|
||||
if (bodyLower.indexOf(q) !== -1) {
|
||||
score += 3;
|
||||
}
|
||||
if (page.section.toLowerCase().indexOf(q) !== -1) {
|
||||
score += 1;
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
scored.push({ page: page, score: score });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
scored.sort(function (a, b) { return b.score - a.score; });
|
||||
|
||||
// Limit results
|
||||
var results = scored.slice(0, 20);
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsList.innerHTML = '<li class="search-no-results">No results found.</li>';
|
||||
activeIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
var r = results[j];
|
||||
var snippet = buildSnippet(r.page.body, query);
|
||||
html += '<li data-url="' + r.page.url + '">'
|
||||
+ '<div class="search-result-title">' + highlightText(r.page.title, query) + '</div>'
|
||||
+ (snippet ? '<div class="search-result-snippet">' + snippet + '</div>' : '')
|
||||
+ '</li>';
|
||||
}
|
||||
resultsList.innerHTML = html;
|
||||
activeIndex = -1;
|
||||
}
|
||||
|
||||
function setActive(index) {
|
||||
var items = resultsList.querySelectorAll('li[data-url]');
|
||||
if (items.length === 0) return;
|
||||
// Remove previous active
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
items[i].classList.remove('active');
|
||||
}
|
||||
if (index < 0) index = items.length - 1;
|
||||
if (index >= items.length) index = 0;
|
||||
activeIndex = index;
|
||||
items[activeIndex].classList.add('active');
|
||||
items[activeIndex].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function navigateToActive() {
|
||||
var items = resultsList.querySelectorAll('li[data-url]');
|
||||
if (activeIndex >= 0 && activeIndex < items.length) {
|
||||
var url = items[activeIndex].getAttribute('data-url');
|
||||
if (url) {
|
||||
closeSearch();
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event: search button
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
openSearch();
|
||||
});
|
||||
}
|
||||
|
||||
// Use capture phase to intercept keys before browser default behavior
|
||||
// (e.g. ESC clearing input text in some browsers)
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
overlay.classList.contains('open') ? closeSearch() : openSearch();
|
||||
return;
|
||||
}
|
||||
if (!overlay.classList.contains('open')) return;
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSearch();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
navigateToActive();
|
||||
}
|
||||
}, true); // capture phase
|
||||
|
||||
// Event: click overlay background to close
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) {
|
||||
closeSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: click result item
|
||||
resultsList.addEventListener('click', function (e) {
|
||||
var li = e.target.closest('li[data-url]');
|
||||
if (!li) return;
|
||||
var url = li.getAttribute('data-url');
|
||||
if (url) {
|
||||
closeSearch();
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
// Event: input for live search
|
||||
var debounceTimer = null;
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(function () {
|
||||
search(input.value.trim());
|
||||
}, 150);
|
||||
});
|
||||
})();
|
||||
@@ -1,88 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}" data-base-path="{{ site.base_path }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ page.title }} - {{ site.title }}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ site.base_path }}/favicon.svg">
|
||||
<link rel="stylesheet" href="{{ site.base_path }}/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="{{ site.base_path }}/{{ lang }}/" class="header-title">{{ site.title }}{% if site.version %} <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v{{ site.version }}</span>{% endif %}</a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="{{ site.base_path }}/{{ lang }}/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
{% for link in site.nav %}
|
||||
{% if link.url %}
|
||||
<a href="{{ link.url }}"{% if link.icon_svg %} aria-label="{{ link.label }}"{% endif %}>
|
||||
{% if link.icon_svg %}{{ link.icon_svg | safe }}{% endif %}
|
||||
{{ link.label }}
|
||||
</a>
|
||||
{% elif link.path %}
|
||||
<a href="{{ site.base_path }}/{{ lang }}/{{ link.path }}">
|
||||
{% if link.icon_svg %}{{ link.icon_svg | safe }}{% endif %}
|
||||
{{ link.label }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
{{ lang | upper }}
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
{% for l in site.langs %}
|
||||
<li><a href="#" data-lang="{{ l }}">{{ l | upper }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% block sidebar_toggle %}{% endblock %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if page.status == "draft" %}
|
||||
<div class="draft-banner">DRAFT</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="layout {% block layout_class %}{% endblock %}">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ site.base_path }}/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block layout_class %}has-sidebar{% endblock %}
|
||||
|
||||
{% block sidebar_toggle %}<button class="sidebar-toggle" aria-label="Menu">☰</button>{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
{% for section in nav %}
|
||||
<div class="nav-section">
|
||||
<a href="{{ section.url }}" class="nav-section-title {% if section.active %}active{% endif %}">{{ section.title }}</a>
|
||||
{% if section.children %}
|
||||
<ul class="nav-list">
|
||||
{% for item in section.children %}
|
||||
<li><a href="{{ item.url }}" class="{% if item.active %}active{% endif %}">{{ item.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ content | safe }}
|
||||
</article>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -1,12 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block layout_class %}no-sidebar{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main class="content portal">
|
||||
<article>
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ content | safe }}
|
||||
</article>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -1,468 +0,0 @@
|
||||
use crate::config::{NavLink, SiteConfig};
|
||||
use crate::defaults;
|
||||
use crate::markdown::{Frontmatter, MarkdownRenderer};
|
||||
use crate::utils::copy_dir_recursive;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tera::Tera;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PageContext {
|
||||
title: String,
|
||||
url: String,
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
/// Entry for pages-data.json used by client-side search.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PageDataEntry {
|
||||
title: String,
|
||||
url: String,
|
||||
lang: String,
|
||||
section: String,
|
||||
/// Plain-text body with HTML tags stripped (truncated to ~500 chars).
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct NavItem {
|
||||
title: String,
|
||||
url: String,
|
||||
children: Vec<NavItem>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SiteContext {
|
||||
title: String,
|
||||
version: Option<String>,
|
||||
base_url: String,
|
||||
base_path: String,
|
||||
langs: Vec<String>,
|
||||
nav: Vec<NavLink>,
|
||||
}
|
||||
|
||||
struct Page {
|
||||
frontmatter: Frontmatter,
|
||||
html_content: String,
|
||||
url: String,
|
||||
out_path: PathBuf,
|
||||
rel_path: String,
|
||||
section: String,
|
||||
}
|
||||
|
||||
pub fn build(src: &Path, out: &Path) -> Result<()> {
|
||||
let config = SiteConfig::load(src)?;
|
||||
let renderer = MarkdownRenderer::new(
|
||||
config.highlight_dark_theme(),
|
||||
config.highlight_light_theme(),
|
||||
);
|
||||
|
||||
// Build Tera: start with embedded defaults, then override with user templates
|
||||
let tera = build_tera(src)?;
|
||||
|
||||
// Clean output directory
|
||||
if out.exists() {
|
||||
fs::remove_dir_all(out).context("Failed to clean output directory")?;
|
||||
}
|
||||
fs::create_dir_all(out)?;
|
||||
|
||||
// Copy static files: embedded defaults first, then user overrides on top
|
||||
copy_default_static(out)?;
|
||||
let static_dir = src.join("static");
|
||||
if static_dir.exists() {
|
||||
copy_dir_recursive(&static_dir, out)?;
|
||||
}
|
||||
|
||||
// Collect page data entries for pages-data.json across all languages
|
||||
let mut page_data_entries: Vec<PageDataEntry> = Vec::new();
|
||||
|
||||
// Build each language
|
||||
for lang in &config.i18n.langs {
|
||||
let pages_dir = src.join("pages").join(lang);
|
||||
if !pages_dir.exists() {
|
||||
eprintln!("Warning: pages directory not found for lang '{}', skipping", lang);
|
||||
continue;
|
||||
}
|
||||
|
||||
let pages = collect_pages(&pages_dir, lang, out, &renderer, &config.site.base_path)?;
|
||||
let nav = build_nav(&pages);
|
||||
|
||||
for page in &pages {
|
||||
// Collect search data for pages-data.json
|
||||
let plain_body = strip_html_tags(&remove_light_theme_blocks(&page.html_content));
|
||||
let truncated_body: String = plain_body.chars().take(500).collect();
|
||||
page_data_entries.push(PageDataEntry {
|
||||
title: page.frontmatter.title.clone(),
|
||||
url: page.url.clone(),
|
||||
lang: lang.clone(),
|
||||
section: page.section.clone(),
|
||||
body: truncated_body,
|
||||
});
|
||||
|
||||
let template_name = if page.section.is_empty() {
|
||||
"portal.html"
|
||||
} else {
|
||||
"page.html"
|
||||
};
|
||||
|
||||
// Filter nav to only the current section
|
||||
let section_nav: Vec<&NavItem> = nav
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
let item_section = extract_section(&item.url, &config.site.base_path);
|
||||
item_section == page.section
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("page", &PageContext {
|
||||
title: page.frontmatter.title.clone(),
|
||||
url: page.url.clone(),
|
||||
status: page.frontmatter.status.clone(),
|
||||
});
|
||||
ctx.insert("content", &page.html_content);
|
||||
ctx.insert("lang", lang);
|
||||
ctx.insert("site", &SiteContext {
|
||||
title: config.site.title.clone(),
|
||||
version: config.site.version.clone(),
|
||||
base_url: config.site.base_url(),
|
||||
base_path: config.site.base_path.clone(),
|
||||
langs: config.i18n.langs.clone(),
|
||||
nav: config.nav.clone(),
|
||||
});
|
||||
|
||||
// Set active state and pass nav
|
||||
let mut nav_with_active: Vec<NavItem> = section_nav
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
set_active(&mut item, &page.url);
|
||||
item
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If we're on a section index page, expand its children
|
||||
if let Some(item) = nav_with_active.first_mut() {
|
||||
if item.url == page.url {
|
||||
item.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.insert("nav", &nav_with_active);
|
||||
|
||||
let html = tera
|
||||
.render(template_name, &ctx)
|
||||
.with_context(|| format!("Failed to render template for {}", page.url))?;
|
||||
|
||||
if let Some(parent) = page.out_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(&page.out_path, html)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate pages-data.json for client-side search
|
||||
let pages_json = serde_json::to_string(&page_data_entries)
|
||||
.context("Failed to serialize pages-data.json")?;
|
||||
fs::write(out.join("pages-data.json"), pages_json)?;
|
||||
|
||||
// Generate root redirect
|
||||
generate_root_redirect(out, &config)?;
|
||||
|
||||
println!(
|
||||
"Site generated: {} languages, output at {}",
|
||||
config.i18n.langs.len(),
|
||||
out.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_pages(
|
||||
pages_dir: &Path,
|
||||
lang: &str,
|
||||
out: &Path,
|
||||
renderer: &MarkdownRenderer,
|
||||
base_path: &str,
|
||||
) -> Result<Vec<Page>> {
|
||||
let mut pages = Vec::new();
|
||||
|
||||
for entry in WalkDir::new(pages_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.path().extension().map_or(false, |ext| ext == "md")
|
||||
})
|
||||
{
|
||||
let path = entry.path();
|
||||
let content = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
|
||||
let (frontmatter, body) = MarkdownRenderer::parse_frontmatter(&content)
|
||||
.with_context(|| format!("Failed to parse frontmatter in {}", path.display()))?;
|
||||
|
||||
let html_content = renderer.render(body);
|
||||
|
||||
let rel = path.strip_prefix(pages_dir)?;
|
||||
let rel_str = rel.to_string_lossy().to_string();
|
||||
|
||||
// Compute URL and output path
|
||||
let (url, out_path) = if rel.file_name().map_or(false, |f| f == "index.md") {
|
||||
// index.md -> <base_path>/<lang>/dir/
|
||||
let parent = rel.parent().unwrap_or(Path::new(""));
|
||||
if parent.as_os_str().is_empty() {
|
||||
// Root index.md
|
||||
(
|
||||
format!("{}/{}/", base_path, lang),
|
||||
out.join(lang).join("index.html"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("{}/{}/{}/", base_path, lang, parent.display()),
|
||||
out.join(lang).join(parent).join("index.html"),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// foo.md -> <base_path>/<lang>/foo/
|
||||
let stem = rel.with_extension("");
|
||||
(
|
||||
format!("{}/{}/{}/", base_path, lang, stem.display()),
|
||||
out.join(lang).join(&stem).join("index.html"),
|
||||
)
|
||||
};
|
||||
|
||||
let section = extract_section(&url, base_path);
|
||||
|
||||
pages.push(Page {
|
||||
frontmatter,
|
||||
html_content,
|
||||
url,
|
||||
out_path,
|
||||
rel_path: rel_str,
|
||||
section,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
fn extract_section(url: &str, base_path: &str) -> String {
|
||||
// Strip base_path prefix before parsing
|
||||
let stripped = url.strip_prefix(base_path).unwrap_or(url);
|
||||
// URL format: /<lang>/ or /<lang>/section/...
|
||||
let parts: Vec<&str> = stripped.trim_matches('/').split('/').collect();
|
||||
if parts.len() >= 2 {
|
||||
parts[1].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_nav(pages: &[Page]) -> Vec<NavItem> {
|
||||
// Group pages by section (top-level directory)
|
||||
let mut sections: std::collections::BTreeMap<String, Vec<&Page>> =
|
||||
std::collections::BTreeMap::new();
|
||||
|
||||
for page in pages {
|
||||
if page.section.is_empty() {
|
||||
continue; // Skip root index (portal)
|
||||
}
|
||||
sections
|
||||
.entry(page.section.clone())
|
||||
.or_default()
|
||||
.push(page);
|
||||
}
|
||||
|
||||
let mut nav = Vec::new();
|
||||
|
||||
for (section, mut section_pages) in sections {
|
||||
// Sort by order, then by filename
|
||||
section_pages.sort_by(|a, b| {
|
||||
a.frontmatter
|
||||
.order
|
||||
.cmp(&b.frontmatter.order)
|
||||
.then_with(|| a.rel_path.cmp(&b.rel_path))
|
||||
});
|
||||
|
||||
// Find the section index page
|
||||
let index_page = section_pages
|
||||
.iter()
|
||||
.find(|p| p.rel_path.ends_with("index.md") && p.section == section);
|
||||
|
||||
let section_title = index_page
|
||||
.map(|p| p.frontmatter.title.clone())
|
||||
.unwrap_or_else(|| section.clone());
|
||||
let section_url = index_page
|
||||
.map(|p| p.url.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let children: Vec<NavItem> = section_pages
|
||||
.iter()
|
||||
.filter(|p| !p.rel_path.ends_with("index.md") || p.section != section)
|
||||
.map(|p| NavItem {
|
||||
title: p.frontmatter.title.clone(),
|
||||
url: p.url.clone(),
|
||||
children: Vec::new(),
|
||||
active: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
nav.push(NavItem {
|
||||
title: section_title,
|
||||
url: section_url,
|
||||
children,
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort nav sections by order of their index pages
|
||||
nav
|
||||
}
|
||||
|
||||
fn set_active(item: &mut NavItem, current_url: &str) {
|
||||
if item.url == current_url {
|
||||
item.active = true;
|
||||
}
|
||||
for child in &mut item.children {
|
||||
set_active(child, current_url);
|
||||
if child.active {
|
||||
item.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_root_redirect(out: &Path, config: &SiteConfig) -> Result<()> {
|
||||
let base_path = &config.site.base_path;
|
||||
let html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
(function() {{
|
||||
var lang = localStorage.getItem('preferred-lang') || '{default_lang}';
|
||||
window.location.replace('{base_path}/' + lang + '/');
|
||||
}})();
|
||||
</script>
|
||||
<meta http-equiv="refresh" content="0;url={base_path}/{default_lang}/">
|
||||
<title>Redirecting...</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="{base_path}/{default_lang}/">{base_path}/{default_lang}/</a>...</p>
|
||||
</body>
|
||||
</html>"#,
|
||||
default_lang = config.i18n.default_lang(),
|
||||
base_path = base_path,
|
||||
);
|
||||
|
||||
fs::write(out.join("index.html"), html)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build Tera with embedded default templates, then override with any files
|
||||
/// found in `<src>/templates/`.
|
||||
fn build_tera(src: &Path) -> Result<Tera> {
|
||||
let mut tera = Tera::default();
|
||||
|
||||
// Register embedded defaults
|
||||
for (name, source) in defaults::default_templates() {
|
||||
tera.add_raw_template(name, source)
|
||||
.with_context(|| format!("Failed to add default template '{}'", name))?;
|
||||
}
|
||||
|
||||
// Override with user-provided templates (if any)
|
||||
let templates_dir = src.join("templates");
|
||||
if templates_dir.exists() {
|
||||
for entry in WalkDir::new(&templates_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().map_or(false, |ext| ext == "html"))
|
||||
{
|
||||
let path = entry.path();
|
||||
let rel = path.strip_prefix(&templates_dir)?;
|
||||
let name = rel.to_string_lossy().replace('\\', "/");
|
||||
let source = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read template {}", path.display()))?;
|
||||
tera.add_raw_template(&name, &source)
|
||||
.with_context(|| format!("Failed to register template '{}'", name))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tera)
|
||||
}
|
||||
|
||||
/// Write embedded default static files (css/js) to the output directory.
|
||||
fn copy_default_static(out: &Path) -> Result<()> {
|
||||
for (rel_path, content) in defaults::default_static_files() {
|
||||
let target = out.join(rel_path);
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
// Only write if not already present (user file takes precedence via
|
||||
// the subsequent copy_dir_recursive call, but write defaults first)
|
||||
fs::write(&target, content)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Strip HTML tags from a string and collapse whitespace into a single space,
|
||||
/// producing a plain-text representation suitable for search indexing.
|
||||
fn strip_html_tags(html: &str) -> String {
|
||||
let mut result = String::with_capacity(html.len());
|
||||
let mut in_tag = false;
|
||||
|
||||
for ch in html.chars() {
|
||||
match ch {
|
||||
'<' => in_tag = true,
|
||||
'>' => {
|
||||
in_tag = false;
|
||||
// Insert a space to avoid words being glued across tags
|
||||
result.push(' ');
|
||||
}
|
||||
_ if !in_tag => result.push(ch),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse whitespace
|
||||
let collapsed: String = result.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
collapsed
|
||||
}
|
||||
|
||||
/// Remove `<div data-code-theme="light">...</div>` blocks so that
|
||||
/// dual-theme code snippets are only indexed once.
|
||||
fn remove_light_theme_blocks(html: &str) -> String {
|
||||
const MARKER: &str = "<div data-code-theme=\"light\"";
|
||||
let mut result = String::with_capacity(html.len());
|
||||
let mut remaining = html;
|
||||
|
||||
while let Some(start) = remaining.find(MARKER) {
|
||||
result.push_str(&remaining[..start]);
|
||||
remaining = &remaining[start..];
|
||||
|
||||
let mut depth: usize = 0;
|
||||
let mut i = 0;
|
||||
while i < remaining.len() {
|
||||
if remaining[i..].starts_with("<div") {
|
||||
depth += 1;
|
||||
i += 4;
|
||||
} else if remaining[i..].starts_with("</div>") {
|
||||
depth -= 1;
|
||||
i += 6;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
i += remaining[i..].chars().next().map_or(1, |c| c.len_utf8());
|
||||
}
|
||||
}
|
||||
remaining = &remaining[i..];
|
||||
}
|
||||
|
||||
result.push_str(remaining);
|
||||
result
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SiteConfig {
|
||||
pub site: Site,
|
||||
pub i18n: I18n,
|
||||
pub highlight: Option<Highlight>,
|
||||
#[serde(default)]
|
||||
pub nav: Vec<NavLink>,
|
||||
}
|
||||
|
||||
/// A navigation link entry defined in config.toml under [[nav]].
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct NavLink {
|
||||
pub label: String,
|
||||
/// Absolute or external URL (e.g. GitHub link).
|
||||
pub url: Option<String>,
|
||||
/// Path relative to /<base_path>/<lang>/ (e.g. "tour/").
|
||||
pub path: Option<String>,
|
||||
/// Optional inline SVG string to display as an icon.
|
||||
pub icon_svg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Site {
|
||||
pub title: String,
|
||||
pub version: Option<String>,
|
||||
/// Optional hostname (e.g. "https://example.github.io"). Combined with
|
||||
/// base_path to form the full base URL.
|
||||
pub hostname: Option<String>,
|
||||
#[serde(default)]
|
||||
pub base_path: String,
|
||||
}
|
||||
|
||||
impl Site {
|
||||
/// Returns the full base URL derived from hostname + base_path.
|
||||
/// Falls back to base_path alone if hostname is not set.
|
||||
pub fn base_url(&self) -> String {
|
||||
match &self.hostname {
|
||||
Some(h) => format!("{}{}", h.trim_end_matches('/'), self.base_path),
|
||||
None => self.base_path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct I18n {
|
||||
pub langs: Vec<String>,
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
/// Returns the default language, which is the first entry in langs.
|
||||
pub fn default_lang(&self) -> &str {
|
||||
self.langs.first().map(|s| s.as_str()).unwrap_or("en")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Highlight {
|
||||
pub dark_theme: Option<String>,
|
||||
pub light_theme: Option<String>,
|
||||
}
|
||||
|
||||
impl SiteConfig {
|
||||
pub fn load(src_dir: &Path) -> Result<Self> {
|
||||
let path = src_dir.join("config.toml");
|
||||
let content =
|
||||
std::fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
let mut config: SiteConfig =
|
||||
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
|
||||
// Validate required fields
|
||||
if config.i18n.langs.is_empty() {
|
||||
anyhow::bail!("[i18n] langs must not be empty. Please specify at least one language.");
|
||||
}
|
||||
|
||||
// Normalize base_path: strip trailing slash (but keep empty for root)
|
||||
let bp = config.site.base_path.trim_end_matches('/').to_string();
|
||||
config.site.base_path = bp;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn highlight_dark_theme(&self) -> &str {
|
||||
self.highlight
|
||||
.as_ref()
|
||||
.and_then(|h| h.dark_theme.as_deref())
|
||||
.unwrap_or("base16-ocean.dark")
|
||||
}
|
||||
|
||||
pub fn highlight_light_theme(&self) -> Option<&str> {
|
||||
self.highlight
|
||||
.as_ref()
|
||||
.and_then(|h| h.light_theme.as_deref())
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Default embedded theme files. Users can override any of these by placing
|
||||
// a file with the same name under their <SRC>/templates/ or <SRC>/static/.
|
||||
|
||||
pub const TEMPLATE_BASE: &str = include_str!("../defaults/templates/base.html");
|
||||
pub const TEMPLATE_PAGE: &str = include_str!("../defaults/templates/page.html");
|
||||
pub const TEMPLATE_PORTAL: &str = include_str!("../defaults/templates/portal.html");
|
||||
|
||||
pub const STATIC_CSS_MAIN: &str = include_str!("../defaults/static/css/main.css");
|
||||
pub const STATIC_JS_MAIN: &str = include_str!("../defaults/static/js/main.js");
|
||||
pub const STATIC_FAVICON_SVG: &str = include_str!("../defaults/static/favicon.svg");
|
||||
|
||||
// Init command templates
|
||||
pub const INIT_CONFIG_TOML: &str = include_str!("../defaults/config.toml");
|
||||
pub const INIT_PAGE_EN_INDEX: &str = include_str!("../defaults/pages/en/index.md");
|
||||
pub const INIT_PAGE_JA_INDEX: &str = include_str!("../defaults/pages/ja/index.md");
|
||||
|
||||
/// Returns all default templates as (name, source) pairs for Tera registration.
|
||||
pub fn default_templates() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("base.html", TEMPLATE_BASE),
|
||||
("page.html", TEMPLATE_PAGE),
|
||||
("portal.html", TEMPLATE_PORTAL),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns all default static files as (relative_path, content) pairs.
|
||||
pub fn default_static_files() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("css/main.css", STATIC_CSS_MAIN),
|
||||
("js/main.js", STATIC_JS_MAIN),
|
||||
("favicon.svg", STATIC_FAVICON_SVG),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns all init scaffold files as (relative_path, content) pairs.
|
||||
pub fn init_files() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("config.toml", INIT_CONFIG_TOML),
|
||||
("templates/base.html", TEMPLATE_BASE),
|
||||
("templates/page.html", TEMPLATE_PAGE),
|
||||
("templates/portal.html", TEMPLATE_PORTAL),
|
||||
("static/css/main.css", STATIC_CSS_MAIN),
|
||||
("static/js/main.js", STATIC_JS_MAIN),
|
||||
("pages/en/index.md", INIT_PAGE_EN_INDEX),
|
||||
("pages/ja/index.md", INIT_PAGE_JA_INDEX),
|
||||
]
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
mod builder;
|
||||
mod config;
|
||||
mod defaults;
|
||||
mod markdown;
|
||||
mod serve;
|
||||
mod utils;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand, CommandFactory};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about = "A simple static site generator")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
|
||||
/// Source directory containing config.toml (used when no subcommand given)
|
||||
#[arg(default_value = ".")]
|
||||
src: PathBuf,
|
||||
|
||||
/// Output directory (used when no subcommand given)
|
||||
#[arg(long, default_value = "docs")]
|
||||
out: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Build the documentation site
|
||||
Build {
|
||||
/// Source directory containing config.toml
|
||||
#[arg(default_value = ".")]
|
||||
src: PathBuf,
|
||||
|
||||
/// Output directory
|
||||
#[arg(long, default_value = "docs")]
|
||||
out: PathBuf,
|
||||
},
|
||||
/// Initialize a new docs project with default scaffold files
|
||||
Init {
|
||||
/// Target directory to initialize (default: current directory)
|
||||
#[arg(default_value = ".")]
|
||||
src: PathBuf,
|
||||
},
|
||||
/// Start a local development server with live-reload
|
||||
Serve {
|
||||
/// Source directory containing config.toml
|
||||
#[arg(default_value = ".")]
|
||||
src: PathBuf,
|
||||
|
||||
/// Port number for the HTTP server
|
||||
#[arg(long, default_value = "8080")]
|
||||
port: u16,
|
||||
|
||||
/// Open browser automatically
|
||||
#[arg(long)]
|
||||
open: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Build { src, out }) => builder::build(&src, &out),
|
||||
Some(Command::Init { src }) => cmd_init(&src),
|
||||
Some(Command::Serve { src, port, open }) => serve::serve(&src, port, open),
|
||||
None => {
|
||||
Cli::command().print_help()?;
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_init(target: &Path) -> Result<()> {
|
||||
let mut skipped = 0usize;
|
||||
let mut created = 0usize;
|
||||
|
||||
for (rel_path, content) in defaults::init_files() {
|
||||
let dest = target.join(rel_path);
|
||||
if dest.exists() {
|
||||
eprintln!("Skipping (already exists): {}", dest.display());
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(&dest, content)?;
|
||||
println!("Created: {}", dest.display());
|
||||
created += 1;
|
||||
}
|
||||
|
||||
println!("\nInit complete: {} file(s) created, {} skipped.", created, skipped);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
|
||||
use serde::Deserialize;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::html::highlighted_html_for_string;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Frontmatter {
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub order: i32,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
pub struct MarkdownRenderer {
|
||||
syntax_set: SyntaxSet,
|
||||
theme_set: ThemeSet,
|
||||
dark_theme: String,
|
||||
light_theme: Option<String>,
|
||||
}
|
||||
|
||||
impl MarkdownRenderer {
|
||||
pub fn new(dark_theme: &str, light_theme: Option<&str>) -> Self {
|
||||
Self {
|
||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||
theme_set: ThemeSet::load_defaults(),
|
||||
dark_theme: dark_theme.to_string(),
|
||||
light_theme: light_theme.map(|s| s.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_frontmatter(content: &str) -> Result<(Frontmatter, &str)> {
|
||||
let content = content.trim_start();
|
||||
if !content.starts_with("---") {
|
||||
anyhow::bail!("Missing frontmatter delimiter");
|
||||
}
|
||||
let after_first = &content[3..];
|
||||
let end = after_first
|
||||
.find("\n---")
|
||||
.context("Missing closing frontmatter delimiter")?;
|
||||
let yaml = &after_first[..end];
|
||||
let body = &after_first[end + 4..];
|
||||
let fm: Frontmatter =
|
||||
serde_yaml::from_str(yaml).context("Failed to parse frontmatter YAML")?;
|
||||
Ok((fm, body))
|
||||
}
|
||||
|
||||
pub fn render(&self, markdown: &str) -> String {
|
||||
let options = Options::ENABLE_TABLES
|
||||
| Options::ENABLE_STRIKETHROUGH
|
||||
| Options::ENABLE_TASKLISTS;
|
||||
|
||||
let parser = Parser::new_ext(markdown, options);
|
||||
|
||||
let mut in_code_block = false;
|
||||
let mut code_lang = String::new();
|
||||
let mut code_buf = String::new();
|
||||
let mut events: Vec<Event> = Vec::new();
|
||||
|
||||
for event in parser {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(kind)) => {
|
||||
in_code_block = true;
|
||||
code_buf.clear();
|
||||
code_lang = match kind {
|
||||
CodeBlockKind::Fenced(lang) => lang.to_string(),
|
||||
CodeBlockKind::Indented => String::new(),
|
||||
};
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
in_code_block = false;
|
||||
let html = self.highlight_code(&code_buf, &code_lang);
|
||||
events.push(Event::Html(html.into()));
|
||||
}
|
||||
Event::Text(text) if in_code_block => {
|
||||
code_buf.push_str(&text);
|
||||
}
|
||||
other => events.push(other),
|
||||
}
|
||||
}
|
||||
|
||||
let mut html_output = String::new();
|
||||
pulldown_cmark::html::push_html(&mut html_output, events.into_iter());
|
||||
html_output
|
||||
}
|
||||
|
||||
fn highlight_code(&self, code: &str, lang: &str) -> String {
|
||||
let syntax = if lang.is_empty() {
|
||||
self.syntax_set.find_syntax_plain_text()
|
||||
} else {
|
||||
self.syntax_set
|
||||
.find_syntax_by_token(lang)
|
||||
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text())
|
||||
};
|
||||
|
||||
let dark_html = self.highlight_with_theme(code, syntax, &self.dark_theme);
|
||||
|
||||
match &self.light_theme {
|
||||
Some(light) => {
|
||||
let light_html = self.highlight_with_theme(code, syntax, light);
|
||||
format!(
|
||||
concat!(
|
||||
"<div class=\"code-block-wrapper\">",
|
||||
"<div data-code-theme=\"dark\">{}</div>",
|
||||
"<div data-code-theme=\"light\">{}</div>",
|
||||
"</div>",
|
||||
),
|
||||
dark_html, light_html
|
||||
)
|
||||
}
|
||||
None => dark_html,
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_with_theme(
|
||||
&self,
|
||||
code: &str,
|
||||
syntax: &syntect::parsing::SyntaxReference,
|
||||
theme_name: &str,
|
||||
) -> String {
|
||||
let theme = self
|
||||
.theme_set
|
||||
.themes
|
||||
.get(theme_name)
|
||||
.unwrap_or_else(|| {
|
||||
self.theme_set
|
||||
.themes
|
||||
.values()
|
||||
.next()
|
||||
.expect("No themes available")
|
||||
});
|
||||
|
||||
match highlighted_html_for_string(code, &self.syntax_set, syntax, theme) {
|
||||
Ok(html) => html,
|
||||
Err(_) => format!("<pre><code>{}</code></pre>", escape_html(code)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_html(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
use crate::builder;
|
||||
use crate::config::SiteConfig;
|
||||
use crate::utils::copy_dir_recursive;
|
||||
use anyhow::{Context, Result};
|
||||
use notify::{Event, RecursiveMode, Watcher};
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Live-reload WebSocket script injected into every HTML page during serve.
|
||||
const LIVE_RELOAD_SCRIPT: &str = r#"<script>
|
||||
(function() {
|
||||
var ws = new WebSocket('ws://' + location.hostname + ':{{WS_PORT}}');
|
||||
ws.onmessage = function(e) { if (e.data === 'reload') location.reload(); };
|
||||
ws.onclose = function() {
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
};
|
||||
})();
|
||||
</script>"#;
|
||||
|
||||
/// Run the serve command: build, start HTTP + WebSocket servers, watch for changes.
|
||||
pub fn serve(src: &Path, port: u16, open_browser: bool) -> Result<()> {
|
||||
let config = SiteConfig::load(src)?;
|
||||
let base_path = config.site.base_path.clone();
|
||||
let ws_port = port + 1;
|
||||
|
||||
// Create temp directory for serving
|
||||
let tmp_dir = tempfile::tempdir().context("Failed to create temp directory")?;
|
||||
let serve_root = tmp_dir.path().to_path_buf();
|
||||
|
||||
println!("Serving from temp directory: {}", serve_root.display());
|
||||
|
||||
// Initial build
|
||||
build_and_copy(src, &serve_root, &base_path, ws_port)?;
|
||||
|
||||
// Track connected WebSocket clients
|
||||
let clients: Arc<Mutex<Vec<TcpStream>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Create HTTP and WebSocket listeners upfront with SO_REUSEADDR
|
||||
// so that rapid restarts (after Ctrl+C) don't hit "address in use".
|
||||
let http_listener = create_reuse_listener(port)
|
||||
.with_context(|| format!("Failed to bind HTTP server to port {}", port))?;
|
||||
let ws_listener = create_reuse_listener(ws_port)
|
||||
.with_context(|| format!("Failed to bind WebSocket server to port {}", ws_port))?;
|
||||
|
||||
// Start WebSocket server for live-reload notifications
|
||||
let ws_clients = clients.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = run_ws_server(ws_listener, ws_clients) {
|
||||
eprintln!("WebSocket server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Start HTTP server
|
||||
let http_root = serve_root.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = run_http_server(http_listener, &http_root) {
|
||||
eprintln!("HTTP server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let url = if base_path.is_empty() {
|
||||
format!("http://localhost:{}/", port)
|
||||
} else {
|
||||
format!("http://localhost:{}{}/", port, base_path)
|
||||
};
|
||||
|
||||
println!("\n Local: {}", url);
|
||||
println!(" Press Ctrl+C to stop.\n");
|
||||
|
||||
if open_browser {
|
||||
let _ = open::that(&url);
|
||||
}
|
||||
|
||||
// File watcher
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
|
||||
if let Ok(event) = res {
|
||||
if event.kind.is_modify() || event.kind.is_create() || event.kind.is_remove() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let src_abs = fs::canonicalize(src)?;
|
||||
watcher.watch(&src_abs, RecursiveMode::Recursive)?;
|
||||
|
||||
println!("Watching for changes in {}...", src_abs.display());
|
||||
|
||||
// Debounce: wait for changes, then rebuild
|
||||
loop {
|
||||
// Block until a change notification arrives
|
||||
if rx.recv().is_err() {
|
||||
break;
|
||||
}
|
||||
// Drain any additional events within a short debounce window
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
println!("Change detected, rebuilding...");
|
||||
match build_and_copy(src, &serve_root, &base_path, ws_port) {
|
||||
Ok(()) => {
|
||||
println!("Rebuild complete. Notifying browser...");
|
||||
notify_clients(&clients);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Rebuild failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build site into a temp build dir, then copy to serve_root/<base_path>/
|
||||
/// with live-reload script injected.
|
||||
fn build_and_copy(src: &Path, serve_root: &Path, base_path: &str, ws_port: u16) -> Result<()> {
|
||||
// Build into a temporary output directory
|
||||
let build_tmp = tempfile::tempdir().context("Failed to create build temp dir")?;
|
||||
let build_out = build_tmp.path();
|
||||
|
||||
builder::build(src, build_out)?;
|
||||
|
||||
// Determine the target directory under serve_root
|
||||
let target = if base_path.is_empty() {
|
||||
serve_root.to_path_buf()
|
||||
} else {
|
||||
let bp = base_path.trim_start_matches('/');
|
||||
serve_root.join(bp)
|
||||
};
|
||||
|
||||
// Clean target and copy
|
||||
if target.exists() {
|
||||
fs::remove_dir_all(&target).ok();
|
||||
}
|
||||
copy_dir_recursive(build_out, &target)?;
|
||||
|
||||
// Inject live-reload script into all HTML files
|
||||
inject_live_reload(&target, ws_port)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inject live-reload WebSocket script into all HTML files under dir.
|
||||
fn inject_live_reload(dir: &Path, ws_port: u16) -> Result<()> {
|
||||
let script = LIVE_RELOAD_SCRIPT.replace("{{WS_PORT}}", &ws_port.to_string());
|
||||
|
||||
for entry in WalkDir::new(dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.path()
|
||||
.extension()
|
||||
.map_or(false, |ext| ext == "html")
|
||||
})
|
||||
{
|
||||
let path = entry.path();
|
||||
let content = fs::read_to_string(path)?;
|
||||
if let Some(pos) = content.rfind("</body>") {
|
||||
let injected = format!("{}{}{}", &content[..pos], script, &content[pos..]);
|
||||
fs::write(path, injected)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Simple HTTP static file server using tiny_http.
|
||||
fn run_http_server(listener: TcpListener, root: &Path) -> Result<()> {
|
||||
let server = tiny_http::Server::from_listener(listener, None)
|
||||
.map_err(|e| anyhow::anyhow!("HTTP server: {}", e))?;
|
||||
|
||||
for request in server.incoming_requests() {
|
||||
let url_path = percent_decode(request.url());
|
||||
let rel = url_path.trim_start_matches('/');
|
||||
|
||||
let file_path = if rel.is_empty() {
|
||||
root.join("index.html")
|
||||
} else {
|
||||
let candidate = root.join(rel);
|
||||
if candidate.is_dir() {
|
||||
candidate.join("index.html")
|
||||
} else {
|
||||
candidate
|
||||
}
|
||||
};
|
||||
|
||||
if file_path.exists() && file_path.is_file() {
|
||||
let content = fs::read(&file_path).unwrap_or_default();
|
||||
let mime = guess_mime(&file_path);
|
||||
let response = tiny_http::Response::from_data(content)
|
||||
.with_header(
|
||||
tiny_http::Header::from_bytes(&b"Content-Type"[..], mime.as_bytes()).unwrap(),
|
||||
);
|
||||
let _ = request.respond(response);
|
||||
} else {
|
||||
let response = tiny_http::Response::from_string("404 Not Found")
|
||||
.with_status_code(404);
|
||||
let _ = request.respond(response);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// WebSocket server that accepts connections and stores them for later notification.
|
||||
fn run_ws_server(listener: TcpListener, clients: Arc<Mutex<Vec<TcpStream>>>) -> Result<()> {
|
||||
|
||||
for stream in listener.incoming().flatten() {
|
||||
let clients = clients.clone();
|
||||
thread::spawn(move || {
|
||||
if let Ok(ws) = tungstenite::accept(stream.try_clone().unwrap()) {
|
||||
// Store the underlying TCP stream for later notification
|
||||
if let Ok(mut list) = clients.lock() {
|
||||
list.push(stream);
|
||||
}
|
||||
// Keep the WebSocket connection alive - read until closed
|
||||
let mut ws = ws;
|
||||
loop {
|
||||
match ws.read() {
|
||||
Ok(msg) => {
|
||||
if msg.is_close() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send "reload" to all connected WebSocket clients.
|
||||
fn notify_clients(clients: &Arc<Mutex<Vec<TcpStream>>>) {
|
||||
if let Ok(mut list) = clients.lock() {
|
||||
let mut alive = Vec::new();
|
||||
for stream in list.drain(..) {
|
||||
if stream.try_clone().is_ok() {
|
||||
// Re-wrap as WebSocket and send reload message
|
||||
// Since we can't easily re-wrap existing TCP streams,
|
||||
// we'll use a simpler approach: raw WebSocket frame
|
||||
if send_ws_text_frame(&stream, "reload").is_ok() {
|
||||
alive.push(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
*list = alive;
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a WebSocket text frame directly on a TCP stream.
|
||||
fn send_ws_text_frame(mut stream: &TcpStream, msg: &str) -> Result<()> {
|
||||
let payload = msg.as_bytes();
|
||||
let len = payload.len();
|
||||
|
||||
// WebSocket text frame: opcode 0x81
|
||||
let mut frame = Vec::new();
|
||||
frame.push(0x81);
|
||||
if len < 126 {
|
||||
frame.push(len as u8);
|
||||
} else if len < 65536 {
|
||||
frame.push(126);
|
||||
frame.push((len >> 8) as u8);
|
||||
frame.push((len & 0xFF) as u8);
|
||||
}
|
||||
frame.extend_from_slice(payload);
|
||||
|
||||
stream.write_all(&frame)?;
|
||||
stream.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn guess_mime(path: &Path) -> String {
|
||||
match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("html") => "text/html; charset=utf-8".to_string(),
|
||||
Some("css") => "text/css; charset=utf-8".to_string(),
|
||||
Some("js") => "application/javascript; charset=utf-8".to_string(),
|
||||
Some("json") => "application/json; charset=utf-8".to_string(),
|
||||
Some("svg") => "image/svg+xml".to_string(),
|
||||
Some("png") => "image/png".to_string(),
|
||||
Some("jpg") | Some("jpeg") => "image/jpeg".to_string(),
|
||||
Some("gif") => "image/gif".to_string(),
|
||||
Some("ico") => "image/x-icon".to_string(),
|
||||
Some("wasm") => "application/wasm".to_string(),
|
||||
Some("woff") => "font/woff".to_string(),
|
||||
Some("woff2") => "font/woff2".to_string(),
|
||||
Some("ttf") => "font/ttf".to_string(),
|
||||
_ => "application/octet-stream".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn percent_decode(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
let mut chars = input.bytes();
|
||||
while let Some(b) = chars.next() {
|
||||
if b == b'%' {
|
||||
let hi = chars.next().and_then(|c| hex_val(c));
|
||||
let lo = chars.next().and_then(|c| hex_val(c));
|
||||
if let (Some(h), Some(l)) = (hi, lo) {
|
||||
result.push((h << 4 | l) as char);
|
||||
}
|
||||
} else {
|
||||
result.push(b as char);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn hex_val(b: u8) -> Option<u8> {
|
||||
match b {
|
||||
b'0'..=b'9' => Some(b - b'0'),
|
||||
b'a'..=b'f' => Some(b - b'a' + 10),
|
||||
b'A'..=b'F' => Some(b - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a TCP listener with SO_REUSEADDR (and SO_REUSEPORT on Unix) set,
|
||||
/// so that rapid restarts after Ctrl+C don't fail with "address in use".
|
||||
fn create_reuse_listener(port: u16) -> Result<TcpListener> {
|
||||
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
|
||||
socket.set_reuse_address(true)?;
|
||||
#[cfg(unix)]
|
||||
socket.set_reuse_port(true)?;
|
||||
let addr: std::net::SocketAddr = format!("0.0.0.0:{}", port).parse()?;
|
||||
socket.bind(&addr.into())?;
|
||||
socket.listen(128)?;
|
||||
Ok(socket.into())
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
||||
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
let rel = path.strip_prefix(src)?;
|
||||
let target = dst.join(rel);
|
||||
|
||||
if path.is_dir() {
|
||||
fs::create_dir_all(&target)?;
|
||||
} else {
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(path, &target)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,22 +1,30 @@
|
||||
[system]
|
||||
theme = "monotone"
|
||||
langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.36.0"
|
||||
version = "0.41.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
[[nav]]
|
||||
label = "Tour"
|
||||
path = "tour/"
|
||||
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>'
|
||||
|
||||
[[nav]]
|
||||
label = "Cookbook"
|
||||
path = "cookbook/"
|
||||
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
|
||||
|
||||
[[nav]]
|
||||
label = "LLM App"
|
||||
path = "llm-app/"
|
||||
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
|
||||
|
||||
[[nav]]
|
||||
label = "GitHub"
|
||||
url = "https://github.com/yhirose/cpp-httplib"
|
||||
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>'
|
||||
|
||||
[i18n]
|
||||
langs = ["en", "ja"]
|
||||
|
||||
[highlight]
|
||||
dark_theme = "base16-eighties.dark"
|
||||
light_theme = "InspiredGitHub"
|
||||
|
||||
@@ -1,8 +1,96 @@
|
||||
---
|
||||
title: "Cookbook"
|
||||
order: 1
|
||||
order: 0
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
This section is under construction.
|
||||
A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need.
|
||||
|
||||
Check back soon for a collection of recipes organized by topic.
|
||||
## Client
|
||||
|
||||
### Basics
|
||||
- Get the response body as a string / save to a file
|
||||
- Send and receive JSON
|
||||
- Set default headers (`set_default_headers`)
|
||||
- Follow redirects (`set_follow_location`)
|
||||
|
||||
### Authentication
|
||||
- Use Basic authentication (`set_basic_auth`)
|
||||
- Call an API with a Bearer token
|
||||
|
||||
### File Upload
|
||||
- Upload a file as multipart form data (`make_file_provider`)
|
||||
- POST a file as raw binary (`make_file_body`)
|
||||
- Send the body with chunked transfer (Content Provider)
|
||||
|
||||
### Streaming & Progress
|
||||
- Receive a response as a stream
|
||||
- Use the progress callback (`DownloadProgress` / `UploadProgress`)
|
||||
|
||||
### Connection & Performance
|
||||
- Set timeouts (`set_connection_timeout` / `set_read_timeout`)
|
||||
- Set an overall timeout (`set_max_timeout`)
|
||||
- Understand connection reuse and Keep-Alive behavior
|
||||
- Enable compression (`set_compress` / `set_decompress`)
|
||||
- Send requests through a proxy (`set_proxy`)
|
||||
|
||||
### Error Handling & Debugging
|
||||
- Handle error codes (`Result::error()`)
|
||||
- Handle SSL errors (`ssl_error()` / `ssl_backend_error()`)
|
||||
- Set up client logging (`set_logger` / `set_error_logger`)
|
||||
|
||||
## Server
|
||||
|
||||
### Basics
|
||||
- Register GET / POST / PUT / DELETE handlers
|
||||
- Receive JSON requests and return JSON responses
|
||||
- Use path parameters (`/users/:id`)
|
||||
- Set up a static file server (`set_mount_point`)
|
||||
|
||||
### Streaming & Files
|
||||
- Stream a large file in the response (`ContentProvider`)
|
||||
- Return a file download response (`Content-Disposition`)
|
||||
- Receive multipart data as a stream (`ContentReader`)
|
||||
- Compress responses (gzip)
|
||||
|
||||
### Handler Chain
|
||||
- Add pre-processing to all routes (Pre-routing handler)
|
||||
- Add response headers with a Post-routing handler (CORS, etc.)
|
||||
- Authenticate per route with a Pre-request handler (`matched_route`)
|
||||
- Pass data between handlers with `res.user_data`
|
||||
|
||||
### Error Handling & Debugging
|
||||
- Return custom error pages (`set_error_handler`)
|
||||
- Catch exceptions (`set_exception_handler`)
|
||||
- Log requests (Logger)
|
||||
- Detect client disconnection (`req.is_connection_closed()`)
|
||||
|
||||
### Operations & Tuning
|
||||
- Assign a port dynamically (`bind_to_any_port`)
|
||||
- Control startup order with `listen_after_bind`
|
||||
- Shut down gracefully (`stop()` and signal handling)
|
||||
- Tune Keep-Alive (`set_keep_alive_max_count` / `set_keep_alive_timeout`)
|
||||
- Configure the thread pool (`new_task_queue`)
|
||||
|
||||
## TLS / Security
|
||||
|
||||
- Choosing between OpenSSL, mbedTLS, and wolfSSL (build-time `#define` differences)
|
||||
- Control SSL certificate verification (disable, custom CA, custom callback)
|
||||
- Use a custom certificate verification callback (`set_server_certificate_verifier`)
|
||||
- Set up an SSL/TLS server (certificate and private key)
|
||||
- Configure mTLS (mutual TLS with client certificates)
|
||||
- Access the peer certificate on the server (`req.peer_cert()` / SNI)
|
||||
|
||||
## SSE
|
||||
|
||||
- Implement an SSE server
|
||||
- Use event names to distinguish event types
|
||||
- Handle reconnection (`Last-Event-ID`)
|
||||
- Receive SSE events on the client
|
||||
|
||||
## WebSocket
|
||||
|
||||
- Implement a WebSocket echo server and client
|
||||
- Configure heartbeats (`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`)
|
||||
- Handle connection close
|
||||
- Send and receive binary frames
|
||||
|
||||
@@ -18,4 +18,8 @@ Under the hood, it uses blocking I/O with a thread pool. It's not built for hand
|
||||
## Documentation
|
||||
|
||||
- [A Tour of cpp-httplib](tour/) — A step-by-step tutorial covering the basics. Start here if you're new
|
||||
- [Building a Desktop LLM App](llm-app/) — A hands-on guide to building a desktop app with llama.cpp, step by step
|
||||
|
||||
## Stay Tuned
|
||||
|
||||
- [Cookbook](cookbook/) — A collection of recipes organized by topic. Jump to whatever you need
|
||||
|
||||
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: "1. Setting Up the Project Environment"
|
||||
order: 1
|
||||
|
||||
---
|
||||
|
||||
Let's incrementally build a text translation REST API server using llama.cpp as the inference engine. By the end, a request like this will return a translation result.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "The weather is nice today. Shall we go for a walk?", "target_lang": "ja"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"translation": "今日はいい天気ですね。散歩に行きましょうか?"
|
||||
}
|
||||
```
|
||||
|
||||
The "Translation API" is just one example. By swapping out the prompt, you can adapt this to any LLM application you like, such as summarization, code generation, or a chatbot.
|
||||
|
||||
Here's the full list of APIs the server will provide.
|
||||
|
||||
| Method | Path | Description | Chapter |
|
||||
| -------- | ---- | ---- | -- |
|
||||
| `GET` | `/health` | Returns server status | 1 |
|
||||
| `POST` | `/translate` | Translates text and returns JSON | 2 |
|
||||
| `POST` | `/translate/stream` | SSE streaming on a per-token basis | 3 |
|
||||
| `GET` | `/models` | Model list (available / downloaded / selected) | 4 |
|
||||
| `POST` | `/models/select` | Select a model (automatically downloads if not yet downloaded) | 4 |
|
||||
|
||||
In this chapter, let's set up the project environment. We'll fetch the dependency libraries, create the directory structure, configure the build settings, and grab the model file, so that we're ready to start writing code in the next chapter.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A C++20-compatible compiler (GCC 10+, Clang 10+, MSVC 2019 16.8+)
|
||||
- CMake 3.20 or later
|
||||
- OpenSSL (used for the HTTPS client in Chapter 4. macOS: `brew install openssl`, Ubuntu: `sudo apt install libssl-dev`)
|
||||
- Sufficient disk space (model files can be several GB)
|
||||
|
||||
## 1.1 What We Will Use
|
||||
|
||||
Here are the libraries we'll use.
|
||||
|
||||
| Library | Role |
|
||||
| ----------- | ------ |
|
||||
| [cpp-httplib](https://github.com/yhirose/cpp-httplib) | HTTP server/client |
|
||||
| [nlohmann/json](https://github.com/nlohmann/json) | JSON parser |
|
||||
| [cpp-llamalib](https://github.com/yhirose/cpp-llamalib) | llama.cpp wrapper |
|
||||
| [llama.cpp](https://github.com/ggml-org/llama.cpp) | LLM inference engine |
|
||||
| [webview/webview](https://github.com/webview/webview) | Desktop WebView (used in Chapter 6) |
|
||||
|
||||
cpp-httplib, nlohmann/json, and cpp-llamalib are header-only libraries. You could just download a single header file with `curl` and `#include` it, but in this book we use CMake's `FetchContent` to fetch them automatically. Declare them in `CMakeLists.txt`, and `cmake -B build` downloads and builds everything for you. webview is used in Chapter 6, so you don't need to worry about it for now.
|
||||
|
||||
## 1.2 Directory Structure
|
||||
|
||||
The final structure will look like this.
|
||||
|
||||
```ascii
|
||||
translate-app/
|
||||
├── CMakeLists.txt
|
||||
├── models/
|
||||
│ └── (GGUF files)
|
||||
└── src/
|
||||
└── main.cpp
|
||||
```
|
||||
|
||||
We don't include library source code in the project. CMake's `FetchContent` fetches them automatically at build time, so all you need is your own code.
|
||||
|
||||
Let's create the project directory and initialize a git repository.
|
||||
|
||||
```bash
|
||||
mkdir translate-app && cd translate-app
|
||||
mkdir src models
|
||||
git init
|
||||
```
|
||||
|
||||
## 1.3 Obtaining the GGUF Model File
|
||||
|
||||
You need a model file for LLM inference. GGUF is the model format used by llama.cpp, and you can find many models on Hugging Face.
|
||||
|
||||
Let's start by trying a small model. The quantized version of Google's Gemma 2 2B (~1.6 GB) is a good starting point. It's lightweight but supports multiple languages and works well for translation tasks.
|
||||
|
||||
```bash
|
||||
curl -L -o models/gemma-2-2b-it-Q4_K_M.gguf \
|
||||
https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf
|
||||
```
|
||||
|
||||
In Chapter 4, we'll add the ability to download models from within the app using cpp-httplib's client functionality.
|
||||
|
||||
## 1.4 CMakeLists.txt
|
||||
|
||||
Create a `CMakeLists.txt` in the project root. By declaring dependencies with `FetchContent`, CMake will automatically download and build them for you.
|
||||
|
||||
<!-- data-file="CMakeLists.txt" -->
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(translate-server CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# llama.cpp (LLM inference engine)
|
||||
FetchContent_Declare(llama
|
||||
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(llama)
|
||||
|
||||
# cpp-httplib (HTTP server/client)
|
||||
FetchContent_Declare(httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# nlohmann/json (JSON parser)
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# cpp-llamalib (header-only llama.cpp wrapper)
|
||||
FetchContent_Declare(cpp_llamalib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp_llamalib)
|
||||
|
||||
add_executable(translate-server src/main.cpp)
|
||||
|
||||
target_link_libraries(translate-server PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
cpp-llamalib
|
||||
)
|
||||
```
|
||||
|
||||
`FetchContent_Declare` tells CMake where to find each library, and `FetchContent_MakeAvailable` fetches and builds them. The first `cmake -B build` will take some time because it downloads all libraries and builds llama.cpp, but subsequent runs will use the cache.
|
||||
|
||||
Just link with `target_link_libraries`, and each library's CMake configuration sets up include paths and build settings for you.
|
||||
|
||||
## 1.5 Creating the Skeleton Code
|
||||
|
||||
We'll use this skeleton code as a base and add functionality chapter by chapter.
|
||||
|
||||
<!-- data-file="main.cpp" -->
|
||||
```cpp
|
||||
// src/main.cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
// Graceful shutdown on `Ctrl+C`
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Log requests and responses
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
// Health check
|
||||
svr.Get("/health", [](const auto &, auto &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// Stub implementations for each endpoint (replaced with real ones in later chapters)
|
||||
svr.Post("/translate",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content(json{{"translation", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/translate/stream",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content("data: \"TODO\"\n\ndata: [DONE]\n\n", "text/event-stream");
|
||||
});
|
||||
|
||||
svr.Get("/models",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/models/select",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// Start the server
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
## 1.6 Building and Verifying
|
||||
|
||||
Build the project, start the server, and verify that requests work with curl.
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
From another terminal, try it with curl.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
# => {"status":"ok"}
|
||||
```
|
||||
|
||||
If you see JSON come back, the setup is complete.
|
||||
|
||||
## Next Chapter
|
||||
|
||||
Now that the environment is set up, in the next chapter we'll implement the translation REST API on top of this skeleton. We'll run inference with llama.cpp and expose it as an HTTP endpoint with cpp-httplib.
|
||||
|
||||
**Next:** [Integrating llama.cpp to Build a REST API](../ch02-rest-api)
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: "2. Integrating llama.cpp to Build a REST API"
|
||||
order: 2
|
||||
|
||||
---
|
||||
|
||||
In the skeleton from Chapter 1, `/translate` simply returned `"TODO"`. In this chapter we integrate llama.cpp inference and turn it into an API that actually returns translation results.
|
||||
|
||||
Calling the llama.cpp API directly makes the code quite long, so we use a thin wrapper library called [cpp-llamalib](https://github.com/yhirose/cpp-llamalib). It lets you load a model and run inference in just a few lines, keeping the focus on cpp-httplib.
|
||||
|
||||
## 2.1 Initializing the LLM
|
||||
|
||||
Simply pass the path to a model file to `llamalib::Llama`, and model loading, context creation, and sampler configuration are all taken care of. If you downloaded a different model in Chapter 1, adjust the path accordingly.
|
||||
|
||||
```cpp
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
int main() {
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
|
||||
|
||||
// LLM inference takes time, so set a longer timeout (default is 5 seconds)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
// ... Build and start the HTTP server ...
|
||||
}
|
||||
```
|
||||
|
||||
If you want to change the number of GPU layers, context length, or other settings, you can specify them via `llamalib::Options`.
|
||||
|
||||
```cpp
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf", {
|
||||
.n_gpu_layers = 0, // CPU only
|
||||
.n_ctx = 4096,
|
||||
}};
|
||||
```
|
||||
|
||||
## 2.2 The `/translate` Handler
|
||||
|
||||
We replace the handler that returned dummy JSON in Chapter 1 with actual inference.
|
||||
|
||||
```cpp
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// Parse JSON (3rd arg `false`: don't throw on failure, check with `is_discarded()`)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja"); // Default is Japanese
|
||||
|
||||
// Build the prompt and run inference
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`llm.chat()` can throw exceptions during inference (for example, when the context length is exceeded). By catching them with `try/catch` and returning the error as JSON, we prevent the server from crashing.
|
||||
|
||||
## 2.3 Complete Code
|
||||
|
||||
Here is the finished code with all the changes so far.
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
// Graceful shutdown on `Ctrl+C`
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Load the model downloaded in Chapter 1
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
|
||||
|
||||
// LLM inference takes time, so set a longer timeout (default is 5 seconds)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
// Log requests and responses
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// Parse JSON (3rd arg `false`: don't throw on failure, check with `is_discarded()`)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja"); // Default is Japanese
|
||||
|
||||
// Build the prompt and run inference
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// Dummy implementations to be replaced with real ones in later chapters
|
||||
svr.Get("/models",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/models/select",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// Start the server (blocks until `stop()` is called)
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 2.4 Testing It Out
|
||||
|
||||
Rebuild and start the server, then verify that it now returns actual translation results.
|
||||
|
||||
```bash
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.", "target_lang": "ja"}'
|
||||
# => {"translation":"去年の春に東京を訪れた。桜が綺麗だった。"}
|
||||
```
|
||||
|
||||
In Chapter 1 the response was `"TODO"`, but now you get an actual translation back.
|
||||
|
||||
## Next Chapter
|
||||
|
||||
The REST API we built in this chapter waits for the entire translation to complete before sending the response, so for long texts the user has to wait with no indication of progress.
|
||||
|
||||
In the next chapter, we use SSE (Server-Sent Events) to stream tokens back in real time as they are generated.
|
||||
|
||||
**Next:** [Adding Token Streaming with SSE](../ch03-sse-streaming)
|
||||
@@ -0,0 +1,264 @@
|
||||
---
|
||||
title: "3. Adding Token Streaming with SSE"
|
||||
order: 3
|
||||
|
||||
---
|
||||
|
||||
The `/translate` endpoint from Chapter 2 returned the entire translation at once after completion. This is fine for short sentences, but for longer text the user has to wait several seconds with nothing displayed.
|
||||
|
||||
In this chapter, we add a `/translate/stream` endpoint that uses SSE (Server-Sent Events) to return tokens in real time as they are generated. This is the same approach used by the ChatGPT and Claude APIs.
|
||||
|
||||
## 3.1 What is SSE?
|
||||
|
||||
SSE is a way to send HTTP responses as a stream. When a client sends a request, the server keeps the connection open and gradually returns events. The format is simple text.
|
||||
|
||||
```text
|
||||
data: "去年の"
|
||||
data: "春に"
|
||||
data: "東京を"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
Each line starts with `data:` and events are separated by blank lines. The Content-Type is `text/event-stream`. Tokens are sent as escaped JSON strings, so they appear enclosed in double quotes (we implement this in Section 3.3).
|
||||
|
||||
## 3.2 Streaming with cpp-httplib
|
||||
|
||||
In cpp-httplib, you can use `set_chunked_content_provider` to send responses incrementally. Each time you write to `sink.os` inside the callback, data is sent to the client.
|
||||
|
||||
```cpp
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[](size_t offset, httplib::DataSink &sink) {
|
||||
sink.os << "data: hello\n\n";
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
Calling `sink.done()` ends the stream. If the client disconnects mid-stream, writing to `sink.os` will fail and `sink.os.fail()` will return `true`. You can use this to detect disconnection and abort unnecessary inference.
|
||||
|
||||
## 3.3 The `/translate/stream` Handler
|
||||
|
||||
JSON parsing and validation are the same as the `/translate` endpoint from Chapter 2. The only difference is how the response is returned. We combine the streaming callback of `llm.chat()` with `set_chunked_content_provider`.
|
||||
|
||||
```cpp
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// ... JSON parsing and validation same as /translate ...
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // Abort inference on disconnect
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
A few key points:
|
||||
|
||||
- When you pass a callback to `llm.chat()`, it is called each time a token is generated. If the callback returns `false`, generation is aborted
|
||||
- After writing to `sink.os`, you can check whether the client is still connected with `sink.os.good()`. If the client has disconnected, it returns `false` to stop inference
|
||||
- Each token is escaped as a JSON string using `json(token).dump()` before sending. This is safe even for tokens containing newlines or quotes
|
||||
- The first three arguments of `dump(-1, ' ', false, ...)` are the defaults. What matters is the fourth argument, `json::error_handler_t::replace`. Since the LLM returns tokens at the subword level, multi-byte characters (such as Japanese) can be split mid-character across tokens. Passing an incomplete UTF-8 byte sequence directly to `dump()` would throw an exception, so `replace` safely substitutes them. The browser reassembles the bytes on its end, so everything displays correctly
|
||||
- The entire lambda is wrapped in `try/catch`. `llm.chat()` can throw exceptions for reasons such as exceeding the context window. If an exception goes uncaught inside the lambda, the server will crash, so we return the error as an SSE event instead
|
||||
- `data: [DONE]` follows the OpenAI API convention to signal the end of the stream to the client
|
||||
|
||||
## 3.4 Complete Code
|
||||
|
||||
Here is the complete code with the `/translate/stream` endpoint added to the code from Chapter 2.
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
// Graceful shutdown on `Ctrl+C`
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Load the GGUF model
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
|
||||
|
||||
// LLM inference takes time, so set a longer timeout (default is 5 seconds)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
// Log requests and responses
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// Standard translation endpoint from Chapter 2
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSON parsing and validation (see Chapter 2 for details)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// SSE streaming translation endpoint
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSON parsing and validation (same as /translate)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // Abort inference on disconnect
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// Dummy implementations to be replaced in later chapters
|
||||
svr.Get("/models",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/models/select",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// Start the server (blocks until `stop()` is called)
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 3.5 Testing It Out
|
||||
|
||||
Build and start the server.
|
||||
|
||||
```bash
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
Using curl's `-N` option to disable buffering, you can see tokens displayed in real time as they arrive.
|
||||
|
||||
```bash
|
||||
curl -N -X POST http://localhost:8080/translate/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.", "target_lang": "ja"}'
|
||||
```
|
||||
|
||||
```text
|
||||
data: "去年の"
|
||||
data: "春に"
|
||||
data: "東京を"
|
||||
data: "訪れた"
|
||||
data: "。"
|
||||
data: "桜が"
|
||||
data: "綺麗だった"
|
||||
data: "。"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
You should see tokens streaming in one by one. The `/translate` endpoint from Chapter 2 continues to work as well.
|
||||
|
||||
## Next Chapter
|
||||
|
||||
The server's translation functionality is now complete. In the next chapter, we use cpp-httplib's client functionality to add the ability to fetch and manage models from Hugging Face.
|
||||
|
||||
**Next:** [Adding Model Download and Management](../ch04-model-management)
|
||||
@@ -0,0 +1,788 @@
|
||||
---
|
||||
title: "4. Adding Model Download and Management"
|
||||
order: 4
|
||||
|
||||
---
|
||||
|
||||
By the end of Chapter 3, the server's translation functionality was fully in place. However, the only model file available is the one we manually downloaded in Chapter 1. In this chapter, we'll use cpp-httplib's **client functionality** to enable downloading and switching Hugging Face models from within the app.
|
||||
|
||||
Once complete, you'll be able to manage models with requests like these:
|
||||
|
||||
```bash
|
||||
# Get the list of available models
|
||||
curl http://localhost:8080/models
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"models": [
|
||||
{"name": "gemma-2-2b-it", "params": "2B", "size": "1.6 GB", "downloaded": true, "selected": true},
|
||||
{"name": "gemma-2-9b-it", "params": "9B", "size": "5.8 GB", "downloaded": false, "selected": false},
|
||||
{"name": "Llama-3.1-8B-Instruct", "params": "8B", "size": "4.9 GB", "downloaded": false, "selected": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Select a different model (automatically downloads if not yet available)
|
||||
curl -N -X POST http://localhost:8080/models/select \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gemma-2-9b-it"}'
|
||||
```
|
||||
|
||||
```text
|
||||
data: {"status":"downloading","progress":0}
|
||||
data: {"status":"downloading","progress":12}
|
||||
...
|
||||
data: {"status":"downloading","progress":100}
|
||||
data: {"status":"loading"}
|
||||
data: {"status":"ready"}
|
||||
```
|
||||
|
||||
## 4.1 httplib::Client Basics
|
||||
|
||||
So far we've only used `httplib::Server`, but cpp-httplib also provides client functionality. Since Hugging Face uses HTTPS, we need a TLS-capable client.
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
|
||||
// Including the URL scheme automatically uses SSLClient
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
|
||||
// Automatically follow redirects (Hugging Face redirects to a CDN)
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/api/models");
|
||||
if (res && res->status == 200) {
|
||||
std::cout << res->body << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
To use HTTPS, you need to enable OpenSSL at build time. Add the following to your `CMakeLists.txt`:
|
||||
|
||||
```cmake
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
target_link_libraries(translate-server PRIVATE OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
|
||||
# macOS: required for loading system certificates
|
||||
if(APPLE)
|
||||
target_link_libraries(translate-server PRIVATE "-framework CoreFoundation" "-framework Security")
|
||||
endif()
|
||||
```
|
||||
|
||||
Defining `CPPHTTPLIB_OPENSSL_SUPPORT` enables `httplib::Client("https://...")` to make TLS connections. On macOS, you also need to link the CoreFoundation and Security frameworks to access the system certificate store. See Section 4.8 for the complete `CMakeLists.txt`.
|
||||
|
||||
## 4.2 Defining the Model List
|
||||
|
||||
Let's define the list of models that the app can handle. Here are four models we've verified for translation tasks.
|
||||
|
||||
```cpp
|
||||
struct ModelInfo {
|
||||
std::string name; // Display name
|
||||
std::string params; // Parameter count
|
||||
std::string size; // GGUF Q4 size
|
||||
std::string repo; // Hugging Face repository
|
||||
std::string filename; // GGUF filename
|
||||
};
|
||||
|
||||
const std::vector<ModelInfo> MODELS = {
|
||||
{
|
||||
.name = "gemma-2-2b-it",
|
||||
.params = "2B",
|
||||
.size = "1.6 GB",
|
||||
.repo = "bartowski/gemma-2-2b-it-GGUF",
|
||||
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "gemma-2-9b-it",
|
||||
.params = "9B",
|
||||
.size = "5.8 GB",
|
||||
.repo = "bartowski/gemma-2-9b-it-GGUF",
|
||||
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "Llama-3.1-8B-Instruct",
|
||||
.params = "8B",
|
||||
.size = "4.9 GB",
|
||||
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
|
||||
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 4.3 Model Storage Location
|
||||
|
||||
Up through Chapter 3, we stored models in the `models/` directory within the project. However, when managing multiple models, a dedicated app directory makes more sense. On macOS/Linux we use `~/.translate-app/models/`, and on Windows we use `%APPDATA%\translate-app\models\`.
|
||||
|
||||
```cpp
|
||||
std::filesystem::path get_models_dir() {
|
||||
#ifdef _WIN32
|
||||
auto env = std::getenv("APPDATA");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / "translate-app" / "models";
|
||||
#else
|
||||
auto env = std::getenv("HOME");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / ".translate-app" / "models";
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
If the environment variable isn't set, it falls back to the current directory. The app creates this directory at startup (`create_directories` won't error even if it already exists).
|
||||
|
||||
## 4.4 Rewriting Model Initialization
|
||||
|
||||
We rewrite the model initialization at the beginning of `main()`. In Chapter 1 we hardcoded the path, but from here on we support model switching. We track the currently loaded filename in `selected_model` and load the first entry in `MODELS` at startup. The `GET /models` and `POST /models/select` handlers reference and update this variable.
|
||||
|
||||
Since cpp-httplib runs handlers concurrently on a thread pool, reassigning `llm` while another thread is calling `llm.chat()` would crash. We add a `std::mutex` to protect against this.
|
||||
|
||||
```cpp
|
||||
int main() {
|
||||
auto models_dir = get_models_dir();
|
||||
std::filesystem::create_directories(models_dir);
|
||||
|
||||
std::string selected_model = MODELS[0].filename;
|
||||
auto path = models_dir / selected_model;
|
||||
|
||||
// Automatically download the default model if not yet present
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::cout << "Downloading " << selected_model << "..." << std::endl;
|
||||
if (!download_model(MODELS[0], [](int pct) {
|
||||
std::cout << "\r" << pct << "%" << std::flush;
|
||||
return true;
|
||||
})) {
|
||||
std::cerr << "\nFailed to download model." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
auto llm = llamalib::Llama{path};
|
||||
std::mutex llm_mutex; // Protect access during model switching
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This ensures that users don't need to manually download models with curl on first launch. It uses the `download_model` function from Section 4.6 and displays progress on the console.
|
||||
|
||||
## 4.5 The `GET /models` Handler
|
||||
|
||||
This returns the model list with information about whether each model has been downloaded and whether it's currently selected.
|
||||
|
||||
```cpp
|
||||
svr.Get("/models",
|
||||
[&](const httplib::Request &, httplib::Response &res) {
|
||||
auto arr = json::array();
|
||||
for (const auto &m : MODELS) {
|
||||
auto path = get_models_dir() / m.filename;
|
||||
arr.push_back({
|
||||
{"name", m.name},
|
||||
{"params", m.params},
|
||||
{"size", m.size},
|
||||
{"downloaded", std::filesystem::exists(path)},
|
||||
{"selected", m.filename == selected_model},
|
||||
});
|
||||
}
|
||||
res.set_content(json{{"models", arr}}.dump(), "application/json");
|
||||
});
|
||||
```
|
||||
|
||||
## 4.6 Downloading Large Files
|
||||
|
||||
GGUF models are several gigabytes, so we can't load the entire file into memory. By passing callbacks to `httplib::Client::Get`, we can receive data chunk by chunk.
|
||||
|
||||
```cpp
|
||||
// content_receiver: callback that receives data chunks
|
||||
// progress: download progress callback
|
||||
cli.Get(url,
|
||||
[&](const char *data, size_t len) { // content_receiver
|
||||
ofs.write(data, len);
|
||||
return true; // returning false aborts the download
|
||||
},
|
||||
[&](size_t current, size_t total) { // progress
|
||||
int pct = total ? (int)(current * 100 / total) : 0;
|
||||
std::cout << pct << "%" << std::endl;
|
||||
return true; // returning false aborts the download
|
||||
});
|
||||
```
|
||||
|
||||
Let's use this to create a function that downloads models from Hugging Face.
|
||||
|
||||
```cpp
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
// Download a model and report progress via progress_cb.
|
||||
// If progress_cb returns false, the download is aborted.
|
||||
bool download_model(const ModelInfo &model,
|
||||
std::function<bool(int)> progress_cb) {
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
cli.set_follow_location(true);
|
||||
cli.set_read_timeout(std::chrono::hours(1));
|
||||
|
||||
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
|
||||
auto path = get_models_dir() / model.filename;
|
||||
auto tmp_path = std::filesystem::path(path).concat(".tmp");
|
||||
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
if (!ofs) { return false; }
|
||||
|
||||
auto res = cli.Get(url,
|
||||
[&](const char *data, size_t len) {
|
||||
ofs.write(data, len);
|
||||
return ofs.good();
|
||||
},
|
||||
[&](size_t current, size_t total) {
|
||||
return progress_cb(total ? (int)(current * 100 / total) : 0);
|
||||
});
|
||||
|
||||
ofs.close();
|
||||
|
||||
if (!res || res->status != 200) {
|
||||
std::filesystem::remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write to .tmp first, then rename, so that an incomplete file
|
||||
// is never mistaken for a usable model if the download is interrupted
|
||||
std::filesystem::rename(tmp_path, path);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.7 The `/models/select` Handler
|
||||
|
||||
This handles model selection requests. We always respond with SSE, reporting status in sequence: download progress, loading, and ready.
|
||||
|
||||
```cpp
|
||||
svr.Post("/models/select",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded() || !input.contains("model")) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'model' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = input["model"].get<std::string>();
|
||||
|
||||
// Find the model in the list
|
||||
auto it = std::find_if(MODELS.begin(), MODELS.end(),
|
||||
[&](const ModelInfo &m) { return m.name == name; });
|
||||
|
||||
if (it == MODELS.end()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"error", "Unknown model"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &model = *it;
|
||||
|
||||
// Always respond with SSE (same format whether already downloaded or not)
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, model](size_t, httplib::DataSink &sink) {
|
||||
// SSE event sending helper
|
||||
auto send = [&](const json &event) {
|
||||
sink.os << "data: " << event.dump() << "\n\n";
|
||||
};
|
||||
|
||||
// Download if not yet present (report progress via SSE)
|
||||
auto path = get_models_dir() / model.filename;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
bool ok = download_model(model, [&](int pct) {
|
||||
send({{"status", "downloading"}, {"progress", pct}});
|
||||
return sink.os.good(); // Abort download on client disconnect
|
||||
});
|
||||
if (!ok) {
|
||||
send({{"status", "error"}, {"message", "Download failed"}});
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Load and switch to the model
|
||||
send({{"status", "loading"}});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
llm = llamalib::Llama{path};
|
||||
selected_model = model.filename;
|
||||
}
|
||||
|
||||
send({{"status", "ready"}});
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
A few notes:
|
||||
|
||||
- We send SSE events directly from the `download_model` progress callback. This is an application of `set_chunked_content_provider` + `sink.os` from Chapter 3
|
||||
- Since the callback returns `sink.os.good()`, the download stops if the client disconnects. The cancel button we add in Chapter 5 uses this
|
||||
- When we update `selected_model`, it's reflected in the `selected` flag of `GET /models`
|
||||
- The `llm` reassignment is protected by `llm_mutex`. The `/translate` and `/translate/stream` handlers also lock the same mutex, so inference can't run during a model switch (see the complete code)
|
||||
|
||||
## 4.8 Complete Code
|
||||
|
||||
Here is the complete code with model management added to the Chapter 3 code.
|
||||
|
||||
<details>
|
||||
<summary data-file="CMakeLists.txt">Complete code (CMakeLists.txt)</summary>
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(translate-server CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# llama.cpp
|
||||
FetchContent_Declare(llama
|
||||
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(llama)
|
||||
|
||||
# cpp-httplib
|
||||
FetchContent_Declare(httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# nlohmann/json
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# cpp-llamalib
|
||||
FetchContent_Declare(cpp_llamalib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp_llamalib)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_executable(translate-server src/main.cpp)
|
||||
|
||||
target_link_libraries(translate-server PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
cpp-llamalib
|
||||
OpenSSL::SSL OpenSSL::Crypto
|
||||
)
|
||||
|
||||
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(translate-server PRIVATE
|
||||
"-framework CoreFoundation"
|
||||
"-framework Security"
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Model definitions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
struct ModelInfo {
|
||||
std::string name;
|
||||
std::string params;
|
||||
std::string size;
|
||||
std::string repo;
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
const std::vector<ModelInfo> MODELS = {
|
||||
{
|
||||
.name = "gemma-2-2b-it",
|
||||
.params = "2B",
|
||||
.size = "1.6 GB",
|
||||
.repo = "bartowski/gemma-2-2b-it-GGUF",
|
||||
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "gemma-2-9b-it",
|
||||
.params = "9B",
|
||||
.size = "5.8 GB",
|
||||
.repo = "bartowski/gemma-2-9b-it-GGUF",
|
||||
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "Llama-3.1-8B-Instruct",
|
||||
.params = "8B",
|
||||
.size = "4.9 GB",
|
||||
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
|
||||
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
},
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Model storage directory
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path get_models_dir() {
|
||||
#ifdef _WIN32
|
||||
auto env = std::getenv("APPDATA");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / "translate-app" / "models";
|
||||
#else
|
||||
auto env = std::getenv("HOME");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / ".translate-app" / "models";
|
||||
#endif
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Model download
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// If progress_cb returns false, the download is aborted
|
||||
bool download_model(const ModelInfo &model,
|
||||
std::function<bool(int)> progress_cb) {
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
cli.set_follow_location(true); // Hugging Face redirects to a CDN
|
||||
cli.set_read_timeout(std::chrono::hours(1)); // Set a long timeout for large models
|
||||
|
||||
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
|
||||
auto path = get_models_dir() / model.filename;
|
||||
auto tmp_path = std::filesystem::path(path).concat(".tmp");
|
||||
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
if (!ofs) { return false; }
|
||||
|
||||
auto res = cli.Get(url,
|
||||
// content_receiver: receive data chunk by chunk and write to file
|
||||
[&](const char *data, size_t len) {
|
||||
ofs.write(data, len);
|
||||
return ofs.good();
|
||||
},
|
||||
// progress: report download progress (returning false aborts)
|
||||
[&, last_pct = -1](size_t current, size_t total) mutable {
|
||||
int pct = total ? (int)(current * 100 / total) : 0;
|
||||
if (pct == last_pct) return true; // Skip if same value
|
||||
last_pct = pct;
|
||||
return progress_cb(pct);
|
||||
});
|
||||
|
||||
ofs.close();
|
||||
|
||||
if (!res || res->status != 200) {
|
||||
std::filesystem::remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rename after download completes
|
||||
std::filesystem::rename(tmp_path, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Server
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Create the model storage directory
|
||||
auto models_dir = get_models_dir();
|
||||
std::filesystem::create_directories(models_dir);
|
||||
|
||||
// Automatically download the default model if not yet present
|
||||
std::string selected_model = MODELS[0].filename;
|
||||
auto path = models_dir / selected_model;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::cout << "Downloading " << selected_model << "..." << std::endl;
|
||||
if (!download_model(MODELS[0], [](int pct) {
|
||||
std::cout << "\r" << pct << "%" << std::flush;
|
||||
return true;
|
||||
})) {
|
||||
std::cerr << "\nFailed to download model." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
auto llm = llamalib::Llama{path};
|
||||
std::mutex llm_mutex; // Protect access during model switching
|
||||
|
||||
// Set a long timeout since LLM inference takes time (default is 5 seconds)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- Translation endpoint (Chapter 2) ------------------------------------
|
||||
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSON parsing and validation (see Chapter 2 for details)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// --- SSE streaming translation (Chapter 3) -------------------------------
|
||||
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // Abort inference on disconnect
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// --- Model list (Chapter 4) ----------------------------------------------
|
||||
|
||||
svr.Get("/models",
|
||||
[&](const httplib::Request &, httplib::Response &res) {
|
||||
auto models_dir = get_models_dir();
|
||||
auto arr = json::array();
|
||||
for (const auto &m : MODELS) {
|
||||
auto path = models_dir / m.filename;
|
||||
arr.push_back({
|
||||
{"name", m.name},
|
||||
{"params", m.params},
|
||||
{"size", m.size},
|
||||
{"downloaded", std::filesystem::exists(path)},
|
||||
{"selected", m.filename == selected_model},
|
||||
});
|
||||
}
|
||||
res.set_content(json{{"models", arr}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- Model selection (Chapter 4) -----------------------------------------
|
||||
|
||||
svr.Post("/models/select",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded() || !input.contains("model")) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'model' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = input["model"].get<std::string>();
|
||||
|
||||
auto it = std::find_if(MODELS.begin(), MODELS.end(),
|
||||
[&](const ModelInfo &m) { return m.name == name; });
|
||||
|
||||
if (it == MODELS.end()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"error", "Unknown model"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &model = *it;
|
||||
|
||||
// Always respond with SSE (same format whether already downloaded or not)
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, model](size_t, httplib::DataSink &sink) {
|
||||
// SSE event sending helper
|
||||
auto send = [&](const json &event) {
|
||||
sink.os << "data: " << event.dump() << "\n\n";
|
||||
};
|
||||
|
||||
// Download if not yet present (report progress via SSE)
|
||||
auto path = get_models_dir() / model.filename;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
bool ok = download_model(model, [&](int pct) {
|
||||
send({{"status", "downloading"}, {"progress", pct}});
|
||||
return sink.os.good(); // Abort download on client disconnect
|
||||
});
|
||||
if (!ok) {
|
||||
send({{"status", "error"}, {"message", "Download failed"}});
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Load and switch to the model
|
||||
send({{"status", "loading"}});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
llm = llamalib::Llama{path};
|
||||
selected_model = model.filename;
|
||||
}
|
||||
|
||||
send({{"status", "ready"}});
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 4.9 Testing
|
||||
|
||||
Since we added OpenSSL configuration to CMakeLists.txt, we need to re-run CMake before building.
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
### Checking the Model List
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/models
|
||||
```
|
||||
|
||||
The gemma-2-2b-it model downloaded in Chapter 1 should show `downloaded: true` and `selected: true`.
|
||||
|
||||
### Switching to a Different Model
|
||||
|
||||
```bash
|
||||
curl -N -X POST http://localhost:8080/models/select \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gemma-2-9b-it"}'
|
||||
```
|
||||
|
||||
Download progress streams via SSE, and `"ready"` appears when it's done.
|
||||
|
||||
### Comparing Translations Across Models
|
||||
|
||||
Let's translate the same sentence with different models.
|
||||
|
||||
```bash
|
||||
# Translate with gemma-2-9b-it (the model we just switched to)
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
|
||||
|
||||
# Switch back to gemma-2-2b-it
|
||||
curl -N -X POST http://localhost:8080/models/select \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gemma-2-2b-it"}'
|
||||
|
||||
# Translate the same sentence
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
|
||||
```
|
||||
|
||||
Translation results vary depending on the model, even with the same code and the same prompt. Since cpp-llamalib automatically applies the appropriate chat template for each model, no code changes are needed.
|
||||
|
||||
## Next Chapter
|
||||
|
||||
The server's main features are now complete: REST API, SSE streaming, and model download and switching. In the next chapter, we'll add static file serving and build a Web UI you can use from a browser.
|
||||
|
||||
**Next:** [Adding a Web UI](../ch05-web-ui)
|
||||
@@ -0,0 +1,724 @@
|
||||
---
|
||||
title: "6. Turning It into a Desktop App with WebView"
|
||||
order: 6
|
||||
|
||||
---
|
||||
|
||||
In Chapter 5, we completed a translation app you can use from a browser. But every time, you have to start the server, open the URL in a browser... Wouldn't it be nice to just double-click and start using it, like a normal app?
|
||||
|
||||
In this chapter, we'll do two things:
|
||||
|
||||
1. **WebView integration** — Use [webview/webview](https://github.com/webview/webview) to turn it into a desktop app that runs without a browser
|
||||
2. **Single binary packaging** — Use [cpp-embedlib](https://github.com/yhirose/cpp-embedlib) to embed HTML/CSS/JS into the binary, making the distributable a single file
|
||||
|
||||
When finished, you'll be able to just run `./translate-app` to open a window and start translating.
|
||||
|
||||

|
||||
|
||||
The model downloads automatically on first launch, so the only thing you need to give users is the single binary.
|
||||
|
||||
## 6.1 Introducing webview/webview
|
||||
|
||||
[webview/webview](https://github.com/webview/webview) is a library that lets you use the OS's native WebView component (WKWebView on macOS, WebKitGTK on Linux, WebView2 on Windows) from C/C++. Unlike Electron, it doesn't bundle its own browser, so the impact on binary size is negligible.
|
||||
|
||||
We'll fetch it with CMake. Add the following to your `CMakeLists.txt`:
|
||||
|
||||
```cmake
|
||||
# webview/webview
|
||||
FetchContent_Declare(webview
|
||||
GIT_REPOSITORY https://github.com/webview/webview
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(webview)
|
||||
```
|
||||
|
||||
This makes the `webview::core` CMake target available. When you link it with `target_link_libraries`, it automatically sets up include paths and platform-specific frameworks.
|
||||
|
||||
> **macOS**: No additional dependencies are needed. WKWebView is built into the system.
|
||||
>
|
||||
> **Linux**: WebKitGTK is required. Install it with `sudo apt install libwebkit2gtk-4.1-dev`.
|
||||
>
|
||||
> **Windows**: The WebView2 runtime is required. It comes pre-installed on Windows 11. For Windows 10, download it from the [official Microsoft website](https://developer.microsoft.com/en-us/microsoft-edge/webview2/).
|
||||
|
||||
## 6.2 Running the Server on a Background Thread
|
||||
|
||||
Up through Chapter 5, the server's `listen()` was blocking the main thread. To use WebView, we need to run the server on a separate thread and run the WebView event loop on the main thread.
|
||||
|
||||
```cpp
|
||||
#include "webview/webview.h"
|
||||
#include <thread>
|
||||
|
||||
int main() {
|
||||
// ... (server setup is the same as Chapter 5) ...
|
||||
|
||||
// Start the server on a background thread
|
||||
auto port = svr.bind_to_any_port("127.0.0.1");
|
||||
std::thread server_thread([&]() { svr.listen_after_bind(); });
|
||||
|
||||
std::cout << "Listening on http://127.0.0.1:" << port << std::endl;
|
||||
|
||||
// Display the UI with WebView
|
||||
webview::webview w(false, nullptr);
|
||||
w.set_title("Translate App");
|
||||
w.set_size(1024, 768, WEBVIEW_HINT_NONE);
|
||||
w.navigate("http://127.0.0.1:" + std::to_string(port));
|
||||
w.run(); // Block until the window is closed
|
||||
|
||||
// Stop the server when the window is closed
|
||||
svr.stop();
|
||||
server_thread.join();
|
||||
}
|
||||
```
|
||||
|
||||
Let's look at the key points:
|
||||
|
||||
- **`bind_to_any_port`** — Instead of `listen("127.0.0.1", 8080)`, we let the OS choose an available port. Since desktop apps can be launched multiple times, using a fixed port would cause conflicts
|
||||
- **`listen_after_bind`** — Starts accepting requests on the port reserved by `bind_to_any_port`. While `listen()` does bind and listen in one call, we need to know the port number first, so we split the operations
|
||||
- **Shutdown order** — When the WebView window is closed, we stop the server with `svr.stop()` and wait for the thread to finish with `server_thread.join()`. If we reversed the order, WebView would lose access to the server
|
||||
|
||||
The `signal_handler` from Chapter 5 is no longer needed. In a desktop app, closing the window means terminating the application.
|
||||
|
||||
## 6.3 Embedding Static Files with cpp-embedlib
|
||||
|
||||
In Chapter 5, we served files from the `public/` directory, so you'd need to distribute `public/` alongside the binary. With [cpp-embedlib](https://github.com/yhirose/cpp-embedlib), you can embed HTML, CSS, and JavaScript into the binary, packaging the distributable into a single file.
|
||||
|
||||
### CMakeLists.txt
|
||||
|
||||
Fetch cpp-embedlib and embed `public/`:
|
||||
|
||||
```cmake
|
||||
# cpp-embedlib
|
||||
FetchContent_Declare(cpp-embedlib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-embedlib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp-embedlib)
|
||||
|
||||
# Embed the public/ directory into the binary
|
||||
cpp_embedlib_add(WebAssets
|
||||
FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/public
|
||||
NAMESPACE Web
|
||||
)
|
||||
|
||||
target_link_libraries(translate-app PRIVATE
|
||||
WebAssets # Embedded files
|
||||
cpp-embedlib-httplib # cpp-httplib integration
|
||||
)
|
||||
```
|
||||
|
||||
`cpp_embedlib_add` converts the files under `public/` into binary data at compile time and creates a static library called `WebAssets`. When linked, you can access the embedded files through a `Web::FS` object. `cpp-embedlib-httplib` is a helper library that provides the `httplib::mount()` function.
|
||||
|
||||
### Replacing set_mount_point with httplib::mount
|
||||
|
||||
Simply replace Chapter 5's `set_mount_point` with cpp-embedlib's `httplib::mount`:
|
||||
|
||||
```cpp
|
||||
#include <cpp-embedlib-httplib.h>
|
||||
#include "WebAssets.h"
|
||||
|
||||
// Chapter 5:
|
||||
// svr.set_mount_point("/", "./public");
|
||||
|
||||
// Chapter 6:
|
||||
httplib::mount(svr, Web::FS);
|
||||
```
|
||||
|
||||
`httplib::mount` registers handlers that serve the files embedded in `Web::FS` over HTTP. MIME types are automatically determined from file extensions, so there's no need to manually set `Content-Type`.
|
||||
|
||||
The file contents are directly mapped to the binary's data segment, so no memory copies or heap allocations occur.
|
||||
|
||||
## 6.4 macOS: Adding the Edit Menu
|
||||
|
||||
If you try to paste text into the input field with `Cmd+V`, you'll find it doesn't work. On macOS, keyboard shortcuts like `Cmd+V` (paste) and `Cmd+C` (copy) are routed through the application's menu bar. Since webview/webview doesn't create one, these shortcuts never reach the WebView. We need to add a macOS Edit menu using the Objective-C runtime:
|
||||
|
||||
```cpp
|
||||
#ifdef __APPLE__
|
||||
#include <objc/objc-runtime.h>
|
||||
|
||||
void setup_macos_edit_menu() {
|
||||
auto cls = [](const char *n) { return (id)objc_getClass(n); };
|
||||
auto sel = sel_registerName;
|
||||
auto msg = reinterpret_cast<id (*)(id, SEL)>(objc_msgSend);
|
||||
auto msg_s = reinterpret_cast<id (*)(id, SEL, const char *)>(objc_msgSend);
|
||||
auto msg_id = reinterpret_cast<id (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_v = reinterpret_cast<void (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_mi = reinterpret_cast<id (*)(id, SEL, id, SEL, id)>(objc_msgSend);
|
||||
|
||||
auto str = [&](const char *s) {
|
||||
return msg_s(cls("NSString"), sel("stringWithUTF8String:"), s);
|
||||
};
|
||||
|
||||
id app = msg(cls("NSApplication"), sel("sharedApplication"));
|
||||
id mainMenu = msg(msg(cls("NSMenu"), sel("alloc")), sel("init"));
|
||||
id editItem = msg(msg(cls("NSMenuItem"), sel("alloc")), sel("init"));
|
||||
id editMenu = msg_id(msg(cls("NSMenu"), sel("alloc")),
|
||||
sel("initWithTitle:"), str("Edit"));
|
||||
|
||||
struct { const char *title; const char *action; const char *key; } items[] = {
|
||||
{"Undo", "undo:", "z"},
|
||||
{"Redo", "redo:", "Z"},
|
||||
{"Cut", "cut:", "x"},
|
||||
{"Copy", "copy:", "c"},
|
||||
{"Paste", "paste:", "v"},
|
||||
{"Select All", "selectAll:", "a"},
|
||||
};
|
||||
|
||||
for (auto &[title, action, key] : items) {
|
||||
id mi = msg_mi(msg(cls("NSMenuItem"), sel("alloc")),
|
||||
sel("initWithTitle:action:keyEquivalent:"),
|
||||
str(title), sel(action), str(key));
|
||||
msg_v(editMenu, sel("addItem:"), mi);
|
||||
}
|
||||
|
||||
msg_v(editItem, sel("setSubmenu:"), editMenu);
|
||||
msg_v(mainMenu, sel("addItem:"), editItem);
|
||||
msg_v(app, sel("setMainMenu:"), mainMenu);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
Call this before `w.run()`:
|
||||
|
||||
```cpp
|
||||
#ifdef __APPLE__
|
||||
setup_macos_edit_menu();
|
||||
#endif
|
||||
w.run();
|
||||
```
|
||||
|
||||
On Windows and Linux, keyboard shortcuts are delivered directly to the focused control without going through the menu bar, so this workaround is macOS-specific.
|
||||
|
||||
## 6.5 Complete Code
|
||||
|
||||
<details>
|
||||
<summary data-file="CMakeLists.txt">Complete code (CMakeLists.txt)</summary>
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(translate-app CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# llama.cpp
|
||||
FetchContent_Declare(llama
|
||||
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(llama)
|
||||
|
||||
# cpp-httplib
|
||||
FetchContent_Declare(httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# nlohmann/json
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# cpp-llamalib
|
||||
FetchContent_Declare(cpp_llamalib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp_llamalib)
|
||||
|
||||
# webview/webview
|
||||
FetchContent_Declare(webview
|
||||
GIT_REPOSITORY https://github.com/webview/webview
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(webview)
|
||||
|
||||
# cpp-embedlib
|
||||
FetchContent_Declare(cpp-embedlib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-embedlib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp-embedlib)
|
||||
|
||||
# Embed the public/ directory into the binary
|
||||
cpp_embedlib_add(WebAssets
|
||||
FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/public
|
||||
NAMESPACE Web
|
||||
)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_executable(translate-app src/main.cpp)
|
||||
|
||||
target_link_libraries(translate-app PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
cpp-llamalib
|
||||
OpenSSL::SSL OpenSSL::Crypto
|
||||
WebAssets
|
||||
cpp-embedlib-httplib
|
||||
webview::core
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(translate-app PRIVATE
|
||||
"-framework CoreFoundation"
|
||||
"-framework Security"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(translate-app PRIVATE
|
||||
CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
#include <cpp-embedlib-httplib.h>
|
||||
#include "WebAssets.h"
|
||||
#include "webview/webview.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <objc/objc-runtime.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// macOS Edit menu (Cmd+C/V/X/A require an Edit menu on macOS)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
#ifdef __APPLE__
|
||||
void setup_macos_edit_menu() {
|
||||
auto cls = [](const char *n) { return (id)objc_getClass(n); };
|
||||
auto sel = sel_registerName;
|
||||
auto msg = reinterpret_cast<id (*)(id, SEL)>(objc_msgSend);
|
||||
auto msg_s = reinterpret_cast<id (*)(id, SEL, const char *)>(objc_msgSend);
|
||||
auto msg_id = reinterpret_cast<id (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_v = reinterpret_cast<void (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_mi = reinterpret_cast<id (*)(id, SEL, id, SEL, id)>(objc_msgSend);
|
||||
|
||||
auto str = [&](const char *s) {
|
||||
return msg_s(cls("NSString"), sel("stringWithUTF8String:"), s);
|
||||
};
|
||||
|
||||
id app = msg(cls("NSApplication"), sel("sharedApplication"));
|
||||
id mainMenu = msg(msg(cls("NSMenu"), sel("alloc")), sel("init"));
|
||||
id editItem = msg(msg(cls("NSMenuItem"), sel("alloc")), sel("init"));
|
||||
id editMenu = msg_id(msg(cls("NSMenu"), sel("alloc")),
|
||||
sel("initWithTitle:"), str("Edit"));
|
||||
|
||||
struct { const char *title; const char *action; const char *key; } items[] = {
|
||||
{"Undo", "undo:", "z"},
|
||||
{"Redo", "redo:", "Z"},
|
||||
{"Cut", "cut:", "x"},
|
||||
{"Copy", "copy:", "c"},
|
||||
{"Paste", "paste:", "v"},
|
||||
{"Select All", "selectAll:", "a"},
|
||||
};
|
||||
|
||||
for (auto &[title, action, key] : items) {
|
||||
id mi = msg_mi(msg(cls("NSMenuItem"), sel("alloc")),
|
||||
sel("initWithTitle:action:keyEquivalent:"),
|
||||
str(title), sel(action), str(key));
|
||||
msg_v(editMenu, sel("addItem:"), mi);
|
||||
}
|
||||
|
||||
msg_v(editItem, sel("setSubmenu:"), editMenu);
|
||||
msg_v(mainMenu, sel("addItem:"), editItem);
|
||||
msg_v(app, sel("setMainMenu:"), mainMenu);
|
||||
}
|
||||
#endif
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Model definitions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
struct ModelInfo {
|
||||
std::string name;
|
||||
std::string params;
|
||||
std::string size;
|
||||
std::string repo;
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
const std::vector<ModelInfo> MODELS = {
|
||||
{
|
||||
.name = "gemma-2-2b-it",
|
||||
.params = "2B",
|
||||
.size = "1.6 GB",
|
||||
.repo = "bartowski/gemma-2-2b-it-GGUF",
|
||||
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "gemma-2-9b-it",
|
||||
.params = "9B",
|
||||
.size = "5.8 GB",
|
||||
.repo = "bartowski/gemma-2-9b-it-GGUF",
|
||||
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "Llama-3.1-8B-Instruct",
|
||||
.params = "8B",
|
||||
.size = "4.9 GB",
|
||||
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
|
||||
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
},
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Model storage directory
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path get_models_dir() {
|
||||
#ifdef _WIN32
|
||||
auto env = std::getenv("APPDATA");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / "translate-app" / "models";
|
||||
#else
|
||||
auto env = std::getenv("HOME");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / ".translate-app" / "models";
|
||||
#endif
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Model download
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Abort the download if progress_cb returns false
|
||||
bool download_model(const ModelInfo &model,
|
||||
std::function<bool(int)> progress_cb) {
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
cli.set_follow_location(true); // Hugging Face redirects to a CDN
|
||||
cli.set_read_timeout(std::chrono::hours(1)); // Long timeout for large models
|
||||
|
||||
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
|
||||
auto path = get_models_dir() / model.filename;
|
||||
auto tmp_path = std::filesystem::path(path).concat(".tmp");
|
||||
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
if (!ofs) { return false; }
|
||||
|
||||
auto res = cli.Get(url,
|
||||
// content_receiver: Receive data chunk by chunk and write to file
|
||||
[&](const char *data, size_t len) {
|
||||
ofs.write(data, len);
|
||||
return ofs.good();
|
||||
},
|
||||
// progress: Report download progress (return false to abort)
|
||||
[&, last_pct = -1](size_t current, size_t total) mutable {
|
||||
int pct = total ? (int)(current * 100 / total) : 0;
|
||||
if (pct == last_pct) return true; // Skip if the value hasn't changed
|
||||
last_pct = pct;
|
||||
return progress_cb(pct);
|
||||
});
|
||||
|
||||
ofs.close();
|
||||
|
||||
if (!res || res->status != 200) {
|
||||
std::filesystem::remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rename after download completes
|
||||
std::filesystem::rename(tmp_path, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Server
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
int main() {
|
||||
httplib::Server svr;
|
||||
// Create the model storage directory
|
||||
auto models_dir = get_models_dir();
|
||||
std::filesystem::create_directories(models_dir);
|
||||
|
||||
// Auto-download the default model if not already present
|
||||
std::string selected_model = MODELS[0].filename;
|
||||
auto path = models_dir / selected_model;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::cout << "Downloading " << selected_model << "..." << std::endl;
|
||||
if (!download_model(MODELS[0], [](int pct) {
|
||||
std::cout << "\r" << pct << "%" << std::flush;
|
||||
return true;
|
||||
})) {
|
||||
std::cerr << "\nFailed to download model." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
auto llm = llamalib::Llama{path};
|
||||
std::mutex llm_mutex; // Protect access during model switching
|
||||
|
||||
// Set a long timeout since LLM inference takes time (default is 5 seconds)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- Translation endpoint (Chapter 2) ------------------------------------
|
||||
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// --- SSE streaming translation (Chapter 3) -------------------------------
|
||||
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // Abort inference on disconnect
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// --- Model list (Chapter 4) ----------------------------------------------
|
||||
|
||||
svr.Get("/models",
|
||||
[&](const httplib::Request &, httplib::Response &res) {
|
||||
auto models_dir = get_models_dir();
|
||||
auto arr = json::array();
|
||||
for (const auto &m : MODELS) {
|
||||
auto path = models_dir / m.filename;
|
||||
arr.push_back({
|
||||
{"name", m.name},
|
||||
{"params", m.params},
|
||||
{"size", m.size},
|
||||
{"downloaded", std::filesystem::exists(path)},
|
||||
{"selected", m.filename == selected_model},
|
||||
});
|
||||
}
|
||||
res.set_content(json{{"models", arr}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- Model selection (Chapter 4) -----------------------------------------
|
||||
|
||||
svr.Post("/models/select",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded() || !input.contains("model")) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'model' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = input["model"].get<std::string>();
|
||||
|
||||
auto it = std::find_if(MODELS.begin(), MODELS.end(),
|
||||
[&](const ModelInfo &m) { return m.name == name; });
|
||||
|
||||
if (it == MODELS.end()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"error", "Unknown model"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &model = *it;
|
||||
|
||||
// Always respond with SSE (same format whether downloaded or not)
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, model](size_t, httplib::DataSink &sink) {
|
||||
// SSE event sending helper
|
||||
auto send = [&](const json &event) {
|
||||
sink.os << "data: " << event.dump() << "\n\n";
|
||||
};
|
||||
|
||||
// Download if not yet downloaded (report progress via SSE)
|
||||
auto path = get_models_dir() / model.filename;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
bool ok = download_model(model, [&](int pct) {
|
||||
send({{"status", "downloading"}, {"progress", pct}});
|
||||
return sink.os.good(); // Abort download on client disconnect
|
||||
});
|
||||
if (!ok) {
|
||||
send({{"status", "error"}, {"message", "Download failed"}});
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Load and switch to the model
|
||||
send({{"status", "loading"}});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
llm = llamalib::Llama{path};
|
||||
selected_model = model.filename;
|
||||
}
|
||||
|
||||
send({{"status", "ready"}});
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// --- Embedded file serving (Chapter 6) ------------------------------------
|
||||
// Chapter 5: svr.set_mount_point("/", "./public");
|
||||
httplib::mount(svr, Web::FS);
|
||||
|
||||
// Start the server on a background thread
|
||||
auto port = svr.bind_to_any_port("127.0.0.1");
|
||||
std::thread server_thread([&]() { svr.listen_after_bind(); });
|
||||
|
||||
std::cout << "Listening on http://127.0.0.1:" << port << std::endl;
|
||||
|
||||
// Display the UI with WebView
|
||||
webview::webview w(false, nullptr);
|
||||
w.set_title("Translate App");
|
||||
w.set_size(1024, 768, WEBVIEW_HINT_NONE);
|
||||
w.navigate("http://127.0.0.1:" + std::to_string(port));
|
||||
|
||||
#ifdef __APPLE__
|
||||
setup_macos_edit_menu();
|
||||
#endif
|
||||
w.run(); // Block until the window is closed
|
||||
|
||||
// Stop the server when the window is closed
|
||||
svr.stop();
|
||||
server_thread.join();
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
To summarize the changes from Chapter 5:
|
||||
|
||||
- `#include <csignal>` replaced with `#include <thread>`, `<cpp-embedlib-httplib.h>`, `"WebAssets.h"`, `"webview/webview.h"`
|
||||
- Removed the `signal_handler` function
|
||||
- `svr.set_mount_point("/", "./public")` replaced with `httplib::mount(svr, Web::FS)`
|
||||
- `svr.listen("127.0.0.1", 8080)` replaced with `bind_to_any_port` + `listen_after_bind` + WebView event loop
|
||||
|
||||
Not a single line of handler code has changed. The REST API, SSE streaming, and model management built through Chapter 5 all work as-is.
|
||||
|
||||
## 6.6 Building and Testing
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
Launch the app:
|
||||
|
||||
```bash
|
||||
./build/translate-app
|
||||
```
|
||||
|
||||
No browser is needed. A window opens automatically. The same UI from Chapter 5 appears as-is, and translation and model switching all work just the same.
|
||||
|
||||
When you close the window, the server shuts down automatically. There's no need for `Ctrl+C`.
|
||||
|
||||
### What Needs to Be Distributed
|
||||
|
||||
You only need to distribute:
|
||||
|
||||
- The single `translate-app` binary
|
||||
|
||||
That's it. You don't need the `public/` directory. HTML, CSS, and JavaScript are embedded in the binary. Model files download automatically on first launch, so there's no need to ask users to prepare anything in advance.
|
||||
|
||||
## Next Chapter
|
||||
|
||||
Congratulations! 🎉
|
||||
|
||||
In Chapter 1, `/health` just returned `{"status":"ok"}`. Now we have a desktop app where you type text and translations stream in real time, pick a different model from a dropdown and it downloads automatically, and closing the window cleanly shuts everything down — all in a single distributable binary.
|
||||
|
||||
What we changed in this chapter was just the static file serving and the server startup. Not a single line of handler code changed. The REST API, SSE streaming, and model management we built through Chapter 5 all work as a desktop app, as-is.
|
||||
|
||||
In the next chapter, we'll shift perspective and read through the code of llama.cpp's own `llama-server`. Let's compare our simple server with a production-quality one and see what design decisions differ and why.
|
||||
|
||||
**Next:** [Reading the llama.cpp Server Source Code](../ch07-code-reading)
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
title: "7. Reading the llama.cpp Server Source Code"
|
||||
order: 7
|
||||
|
||||
---
|
||||
|
||||
Over the course of six chapters, we built a translation desktop app from scratch. We have a working product, but it's ultimately a "learning-oriented" implementation. So how does "production-quality" code differ? Let's read the source code of `llama-server`, the official server bundled with llama.cpp, and compare.
|
||||
|
||||
`llama-server` is located at `llama.cpp/tools/server/`. It uses the same cpp-httplib, so you can read the code the same way as in the previous chapters.
|
||||
|
||||
## 7.1 Source Code Location
|
||||
|
||||
```ascii
|
||||
llama.cpp/tools/server/
|
||||
├── server.cpp # Main server implementation
|
||||
├── httplib.h # cpp-httplib (bundled version)
|
||||
└── ...
|
||||
```
|
||||
|
||||
The code is contained in a single `server.cpp`. It runs to several thousand lines, but once you understand the structure, you can narrow down the parts worth reading.
|
||||
|
||||
## 7.2 OpenAI-Compatible API
|
||||
|
||||
The biggest difference between the server we built and `llama-server` is the API design.
|
||||
|
||||
**Our API:**
|
||||
|
||||
```text
|
||||
POST /translate → {"translation": "..."}
|
||||
POST /translate/stream → SSE: data: "token"
|
||||
```
|
||||
|
||||
**llama-server's API:**
|
||||
|
||||
```text
|
||||
POST /v1/chat/completions → OpenAI-compatible JSON
|
||||
POST /v1/completions → OpenAI-compatible JSON
|
||||
POST /v1/embeddings → Text embedding vectors
|
||||
```
|
||||
|
||||
`llama-server` conforms to [OpenAI's API specification](https://platform.openai.com/docs/api-reference). This means OpenAI's official client libraries (such as the Python `openai` package) work out of the box.
|
||||
|
||||
```python
|
||||
# Example of connecting to llama-server with the OpenAI client
|
||||
from openai import OpenAI
|
||||
client = OpenAI(base_url="http://localhost:8080/v1", api_key="dummy")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="local-model",
|
||||
messages=[{"role": "user", "content": "Hello!"}]
|
||||
)
|
||||
```
|
||||
|
||||
Compatibility with existing tools and libraries is a big design decision. We designed a simple translation-specific API, but if you're building a general-purpose server, OpenAI compatibility has become the de facto standard.
|
||||
|
||||
## 7.3 Concurrent Request Handling
|
||||
|
||||
Our server processes requests one at a time. If another request arrives while a translation is in progress, it waits until the previous inference finishes. This is fine for a desktop app used by one person, but it becomes a problem for a server shared by multiple users.
|
||||
|
||||
`llama-server` handles concurrent requests through a mechanism called **slots**.
|
||||
|
||||

|
||||
|
||||
The key point is that tokens from each slot are not inferred **one by one in sequence**, but rather **all at once in a single batch**. GPUs excel at parallel processing, so processing two users simultaneously takes almost the same time as processing one. This is called "continuous batching."
|
||||
|
||||
In our server, cpp-httplib's thread pool assigns one thread per request, but the inference itself runs single-threaded inside `llm.chat()`. `llama-server` consolidates this inference step into a shared batch processing loop.
|
||||
|
||||
## 7.4 Differences in SSE Format
|
||||
|
||||
The streaming mechanism itself is the same (`set_chunked_content_provider` + SSE), but the data format differs.
|
||||
|
||||
**Our format:**
|
||||
|
||||
```text
|
||||
data: "去年の"
|
||||
data: "春に"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
**llama-server (OpenAI-compatible):**
|
||||
|
||||
```text
|
||||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"去年の"}}]}
|
||||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"春に"}}]}
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
Our format simply sends the tokens. Because `llama-server` follows the OpenAI specification, even a single token comes wrapped in JSON. It may look verbose, but it includes useful information for clients, like an `id` to identify the request and a `finish_reason` to indicate why generation stopped.
|
||||
|
||||
## 7.5 KV Cache Reuse
|
||||
|
||||
In our server, we process the entire prompt from scratch on every request. Our translation app's prompt is short ("Translate the following text to ja..." + input text), so this isn't a problem.
|
||||
|
||||
`llama-server` reuses the KV cache for the prefix portion when a request shares a common prompt prefix with a previous request.
|
||||
|
||||

|
||||
|
||||
For chatbots that send a long system prompt and few-shot examples with every request, this alone dramatically reduces response time. The difference is night and day: processing several thousand tokens of system prompt every time versus reading them from cache in an instant.
|
||||
|
||||
For our translation app, where the system prompt is just a single sentence, the benefit is limited. However, it's an optimization worth keeping in mind when applying this to your own applications.
|
||||
|
||||
## 7.6 Structured Output
|
||||
|
||||
Since our translation API returns plain text, there was no need to constrain the output format. But what if you want the LLM to respond in JSON?
|
||||
|
||||
```text
|
||||
Prompt: Analyze the sentiment of the following text and return it as JSON.
|
||||
LLM output (expected): {"sentiment": "positive", "score": 0.8}
|
||||
LLM output (reality): Here are the results of the sentiment analysis. {"sentiment": ...
|
||||
```
|
||||
|
||||
LLMs sometimes ignore instructions and add extraneous text. `llama-server` solves this problem with **grammar constraints**.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v1/chat/completions \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Analyze sentiment..."}],
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
|
||||
"score": {"type": "number"}
|
||||
},
|
||||
"required": ["sentiment", "score"]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
When you specify `json_schema`, tokens that don't conform to the grammar are excluded during token generation. This guarantees that the output is always valid JSON, so there's no need to worry about `json::parse` failing.
|
||||
|
||||
When embedding LLMs into applications, whether you can reliably parse the output directly impacts reliability. Grammar constraints are unnecessary for free-text output like translation, but they're essential for use cases where you need to return structured data as an API response.
|
||||
|
||||
## 7.7 Summary
|
||||
|
||||
Let's organize the differences we've covered.
|
||||
|
||||
| Aspect | Our Server | llama-server |
|
||||
|------|-------------|--------------|
|
||||
| API design | Translation-specific | OpenAI-compatible |
|
||||
| Concurrent requests | Sequential processing | Slots + continuous batching |
|
||||
| SSE format | Tokens only | OpenAI-compatible JSON |
|
||||
| KV cache | Cleared each time | Prefix reuse |
|
||||
| Structured output | None | JSON Schema / grammar constraints |
|
||||
| Code size | ~200 lines | Several thousand lines |
|
||||
|
||||
Our code is simple because of the assumption that "one person uses it as a desktop app." If you're building a server for multiple users or one that integrates with the existing ecosystem, `llama-server`'s design serves as a valuable reference.
|
||||
|
||||
Conversely, even 200 lines of code is enough to make a fully functional translation app. I hope this code reading exercise has also conveyed the value of "building only what you need."
|
||||
|
||||
## Next Chapter
|
||||
|
||||
In the next chapter, we'll cover the key points for swapping in your own library and customizing the app to make it truly yours.
|
||||
|
||||
**Next:** [Making It Your Own](../ch08-customization)
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: "8. Making It Your Own"
|
||||
order: 8
|
||||
|
||||
---
|
||||
|
||||
Through Chapter 7, we've built a translation desktop app and studied how production-quality code differs. In this chapter, let's go over the key points for **turning this app into something entirely your own**.
|
||||
|
||||
The translation app was just a vehicle. Replace llama.cpp with your own library, and the same architecture works for any application.
|
||||
|
||||
## 8.1 Swapping Out the Build Configuration
|
||||
|
||||
First, replace the llama.cpp-related `FetchContent` entries in `CMakeLists.txt` with your own library.
|
||||
|
||||
```cmake
|
||||
# Remove: llama.cpp and cpp-llamalib FetchContent
|
||||
|
||||
# Add: your own library
|
||||
FetchContent_Declare(my_lib
|
||||
GIT_REPOSITORY https://github.com/yourname/my-lib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(my_lib)
|
||||
|
||||
target_link_libraries(my-app PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
my_lib # Your library instead of cpp-llamalib
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
If your library doesn't support CMake, you can place the header and source files directly in `src/` and add them to `add_executable`. Keep cpp-httplib, nlohmann/json, and webview as they are.
|
||||
|
||||
## 8.2 Adapting the API to Your Task
|
||||
|
||||
Change the translation API's endpoints and parameters to match your task.
|
||||
|
||||
| Translation app | Your app (e.g., image processing) |
|
||||
|---|---|
|
||||
| `POST /translate` | `POST /process` |
|
||||
| `{"text": "...", "target_lang": "ja"}` | `{"image": "base64...", "filter": "blur"}` |
|
||||
| `POST /translate/stream` | `POST /process/stream` |
|
||||
| `GET /models` | `GET /filters` or `GET /presets` |
|
||||
|
||||
Then update each handler's implementation. For example, just replace the `llm.chat()` calls with your own library's API.
|
||||
|
||||
```cpp
|
||||
// Before: LLM translation
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(), "application/json");
|
||||
|
||||
// After: e.g., an image processing library
|
||||
auto result = my_lib::process(input_image, options);
|
||||
res.set_content(json{{"result", result}}.dump(), "application/json");
|
||||
```
|
||||
|
||||
The same goes for SSE streaming. If your library has a function that reports progress via a callback, you can use the exact same pattern from Chapter 3 to send incremental responses. SSE isn't limited to LLMs — it's useful for any time-consuming task: image processing progress, data conversion steps, long-running computations.
|
||||
|
||||
## 8.3 Design Considerations
|
||||
|
||||
### Libraries with Expensive Initialization
|
||||
|
||||
In this book, we load the LLM model at the top of `main()` and keep it in a variable. This is intentional. Loading the model on every request would take several seconds, so we load it once at startup and reuse it. If your library has expensive initialization (loading large data files, acquiring GPU resources, etc.), the same approach works well.
|
||||
|
||||
### Thread Safety
|
||||
|
||||
cpp-httplib processes requests concurrently using a thread pool. In Chapter 4 we protected the `llm` object with a `std::mutex` to prevent crashes during model switching. The same pattern applies when integrating your own library. If your library isn't thread-safe or you need to swap objects at runtime, protect access with a `std::mutex`.
|
||||
|
||||
## 8.4 Customizing the UI
|
||||
|
||||
Edit the three files in `public/`.
|
||||
|
||||
- **`index.html`** — Change the input form layout. Swap `<textarea>` for `<input type="file">`, add parameter fields, etc.
|
||||
- **`style.css`** — Adjust the layout and colors. Keep the two-column design or switch to a single column
|
||||
- **`script.js`** — Update the `fetch()` target URLs, request bodies, and how responses are displayed
|
||||
|
||||
Even without changing any server code, just swapping the HTML makes the app look completely different. Since these are static files, you can iterate quickly — just reload the browser without restarting the server.
|
||||
|
||||
This book used plain HTML, CSS, and JavaScript, but combining them with a frontend framework like Vue or React, or a CSS framework, would let you build an even more polished app.
|
||||
|
||||
## 8.5 Distribution Considerations
|
||||
|
||||
### Licenses
|
||||
|
||||
Check the licenses of the libraries you're using. cpp-httplib (MIT), nlohmann/json (MIT), and webview (MIT) all allow commercial use. Don't forget to check the license of your own library and its dependencies too.
|
||||
|
||||
### Models and Data Files
|
||||
|
||||
The download mechanism we built in Chapter 4 isn't limited to LLM models. If your app needs large data files, the same pattern lets you auto-download them on first launch, keeping the binary small while sparing users the manual setup.
|
||||
|
||||
If the data is small, you can embed it directly into the binary with cpp-embedlib.
|
||||
|
||||
### Cross-Platform Builds
|
||||
|
||||
webview supports macOS, Linux, and Windows. When building for each platform:
|
||||
|
||||
- **macOS** — No additional dependencies
|
||||
- **Linux** — Requires `libwebkit2gtk-4.1-dev`
|
||||
- **Windows** — Requires the WebView2 runtime (pre-installed on Windows 11)
|
||||
|
||||
Consider setting up cross-platform builds in CI (e.g., GitHub Actions) too.
|
||||
|
||||
## Closing
|
||||
|
||||
Thank you so much for reading to the end. 🙏
|
||||
|
||||
This book started with `/health` returning `{"status":"ok"}` in Chapter 1. From there we built a REST API, added SSE streaming, downloaded models from Hugging Face, created a browser-based Web UI, and packaged it all into a single-binary desktop app. In Chapter 7 we read through `llama-server`'s code and learned how production-quality servers differ in their design. It's been quite a journey, and I'm truly grateful you stuck with it all the way through.
|
||||
|
||||
Looking back, we used several key cpp-httplib features hands-on:
|
||||
|
||||
- **Server**: routing, JSON responses, SSE streaming with `set_chunked_content_provider`, static file serving with `set_mount_point`
|
||||
- **Client**: HTTPS connections, redirect following, large downloads with content receivers, progress callbacks
|
||||
- **WebView integration**: `bind_to_any_port` + `listen_after_bind` for background threading
|
||||
|
||||
cpp-httplib offers many more features beyond what we covered here, including multipart file uploads, authentication, timeout control, compression, and range requests. See [A Tour of cpp-httplib](../../tour/) for details.
|
||||
|
||||
These patterns aren't limited to a translation app. If you want to add a web API to your C++ library, give it a browser UI, or ship it as an easy-to-distribute desktop app — I hope this book serves as a useful reference.
|
||||
|
||||
Take your own library, build your own app, and have fun with it. Happy hacking! 🚀
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: "Building a Desktop LLM App with cpp-httplib"
|
||||
order: 0
|
||||
|
||||
---
|
||||
|
||||
Have you ever wanted to add a web API to your own C++ library, or quickly build an Electron-like desktop app? In Rust you might reach for "Tauri + axum," but in C++ it always seemed out of reach.
|
||||
|
||||
With [cpp-httplib](https://github.com/yhirose/cpp-httplib), [webview/webview](https://github.com/webview/webview), and [cpp-embedlib](https://github.com/yhirose/cpp-embedlib), you can take the same approach in pure C++ — and produce a small, easy-to-distribute single binary.
|
||||
|
||||
In this tutorial we build an LLM-powered translation app using [llama.cpp](https://github.com/ggml-org/llama.cpp), progressing step by step from "REST API" to "SSE streaming" to "Web UI" to "desktop app." Translation is just the vehicle — replace llama.cpp with your own library and the same architecture works for any application.
|
||||
|
||||

|
||||
|
||||
If you know basic C++17 and understand the basics of HTTP / REST APIs, you're ready to start.
|
||||
|
||||
## Chapters
|
||||
|
||||
1. **[Set up the project](ch01-setup)** — Fetch dependencies, configure the build, write scaffold code
|
||||
2. **[Embed llama.cpp and create a REST API](ch02-rest-api)** — Return translation results as JSON
|
||||
3. **[Add token streaming with SSE](ch03-sse-streaming)** — Stream responses token by token
|
||||
4. **[Add model discovery and management](ch04-model-management)** — Download and switch models from Hugging Face
|
||||
5. **[Add a Web UI](ch05-web-ui)** — A browser-based translation interface
|
||||
6. **[Turn it into a desktop app with WebView](ch06-desktop-app)** — A single-binary desktop application
|
||||
7. **[Reading the llama.cpp server source code](ch07-code-reading)** — Compare with production-quality code
|
||||
8. **[Making it your own](ch08-customization)** — Swap in your own library and customize
|
||||
@@ -0,0 +1,36 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426 160" font-family="system-ui, sans-serif" font-size="14">
|
||||
<rect x="0" y="0" width="426" height="160" rx="8" fill="#f5f3ef"/>
|
||||
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto">
|
||||
<polygon points="0,0 7,2.5 0,5" fill="#198754"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- request 1 (y=16) -->
|
||||
<text x="94" y="35" fill="#333" font-weight="bold" text-anchor="end">Request 1:</text>
|
||||
<rect x="106" y="16" width="138" height="30" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1"/>
|
||||
<text x="175" y="36" fill="#333" text-anchor="middle">System prompt</text>
|
||||
<text x="256" y="36" fill="#333" text-anchor="middle">+</text>
|
||||
<rect x="270" y="16" width="138" height="30" rx="4" fill="#cfe2ff" stroke="#0d6efd" stroke-width="1"/>
|
||||
<text x="339" y="36" fill="#333" text-anchor="middle">User question A</text>
|
||||
|
||||
<!-- annotation: cache save -->
|
||||
<text x="175" y="64" fill="#198754" font-size="11" text-anchor="middle">Saved to KV cache</text>
|
||||
|
||||
<!-- arrow -->
|
||||
<line x1="175" y1="70" x2="175" y2="90" stroke="#198754" stroke-width="1.2" marker-end="url(#arrowhead)"/>
|
||||
<text x="188" y="85" fill="#198754" font-size="11">Reuse</text>
|
||||
|
||||
<!-- request 2 (y=96) -->
|
||||
<text x="94" y="115" fill="#333" font-weight="bold" text-anchor="end">Request 2:</text>
|
||||
<rect x="106" y="96" width="138" height="30" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1" stroke-dasharray="6,3"/>
|
||||
<text x="175" y="116" fill="#333" text-anchor="middle">System prompt</text>
|
||||
<text x="256" y="116" fill="#333" text-anchor="middle">+</text>
|
||||
<rect x="270" y="96" width="138" height="30" rx="4" fill="#cfe2ff" stroke="#0d6efd" stroke-width="1"/>
|
||||
<text x="339" y="116" fill="#333" text-anchor="middle">User question B</text>
|
||||
|
||||
<!-- bottom labels -->
|
||||
<text x="175" y="144" fill="#198754" font-size="11" text-anchor="middle">No recomputation</text>
|
||||
<text x="339" y="144" fill="#0d6efd" font-size="11" text-anchor="middle">Only this is computed</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 440 240" font-family="system-ui, sans-serif" font-size="14">
|
||||
<!-- outer box -->
|
||||
<rect x="0" y="0" width="440" height="240" rx="8" fill="#f5f3ef"/>
|
||||
<text x="20" y="28" font-weight="bold" font-size="16" fill="#333">llama-server</text>
|
||||
|
||||
<!-- slot 0 -->
|
||||
<rect x="20" y="46" width="400" height="32" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1"/>
|
||||
<text x="32" y="67" fill="#333">Slot 0: User A's request</text>
|
||||
|
||||
<!-- slot 1 -->
|
||||
<rect x="20" y="86" width="400" height="32" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1"/>
|
||||
<text x="32" y="107" fill="#333">Slot 1: User B's request</text>
|
||||
|
||||
<!-- slot 2 -->
|
||||
<rect x="20" y="126" width="400" height="32" rx="4" fill="#e9ecef" stroke="#adb5bd" stroke-width="1"/>
|
||||
<text x="32" y="147" fill="#999">Slot 2: (idle)</text>
|
||||
|
||||
<!-- slot 3 -->
|
||||
<rect x="20" y="166" width="400" height="32" rx="4" fill="#e9ecef" stroke="#adb5bd" stroke-width="1"/>
|
||||
<text x="32" y="187" fill="#999">Slot 3: (idle)</text>
|
||||
|
||||
<!-- arrow + label -->
|
||||
<text x="20" y="224" fill="#333" font-size="13">→ Active slots are inferred together in a single batch</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 130 KiB |
@@ -60,7 +60,7 @@ For uploading large files, `make_file_provider()` comes in handy. It streams the
|
||||
```cpp
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
|
||||
auto res = cli.Post("/upload", {}, {
|
||||
auto res = cli.Post("/upload", {}, {}, {
|
||||
httplib::make_file_provider("file", "/path/to/large-file.zip")
|
||||
});
|
||||
```
|
||||
@@ -160,17 +160,17 @@ svr.set_post_routing_handler([](const auto &req, auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
Use `req.user_data` to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens.
|
||||
Use `res.user_data` to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens.
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto &req, auto &res) {
|
||||
req.user_data["auth_user"] = std::string("alice");
|
||||
res.user_data.set("auth_user", std::string("alice"));
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto &req, auto &res) {
|
||||
auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
|
||||
res.set_content("Hello, " + user, "text/plain");
|
||||
auto *user = res.user_data.get<std::string>("auth_user");
|
||||
res.set_content("Hello, " + *user, "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "A Tour of cpp-httplib"
|
||||
order: 1
|
||||
order: 0
|
||||
---
|
||||
|
||||
This is a step-by-step tutorial that walks you through the basics of cpp-httplib. Each chapter builds on the previous one, so please read them in order starting from Chapter 1.
|
||||
|
||||
@@ -1,8 +1,96 @@
|
||||
---
|
||||
title: "Cookbook"
|
||||
order: 1
|
||||
order: 0
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
This section is under construction.
|
||||
「〇〇をするには?」という問いに答えるレシピ集です。各レシピは独立しているので、必要なページだけ読めます。
|
||||
|
||||
Check back soon for a collection of recipes organized by topic.
|
||||
## クライアント
|
||||
|
||||
### 基本
|
||||
- レスポンスボディを文字列で取得する / ファイルに保存する
|
||||
- JSON を送受信する
|
||||
- デフォルトヘッダーを設定する(`set_default_headers`)
|
||||
- リダイレクトを追従する(`set_follow_location`)
|
||||
|
||||
### 認証
|
||||
- Basic 認証を使う(`set_basic_auth`)
|
||||
- Bearer トークンで API を呼ぶ
|
||||
|
||||
### ファイル送信
|
||||
- ファイルをマルチパートフォームとしてアップロードする(`make_file_provider`)
|
||||
- ファイルを生バイナリとして POST する(`make_file_body`)
|
||||
- チャンク転送でボディを送る(Content Provider)
|
||||
|
||||
### ストリーミング・進捗
|
||||
- レスポンスをストリーミングで受信する
|
||||
- 進捗コールバックを使う(`DownloadProgress` / `UploadProgress`)
|
||||
|
||||
### 接続・パフォーマンス
|
||||
- タイムアウトを設定する(`set_connection_timeout` / `set_read_timeout`)
|
||||
- 全体タイムアウトを設定する(`set_max_timeout`)
|
||||
- 接続の再利用と Keep-Alive の挙動を理解する
|
||||
- 圧縮を有効にする(`set_compress` / `set_decompress`)
|
||||
- プロキシを経由してリクエストを送る(`set_proxy`)
|
||||
|
||||
### エラー処理・デバッグ
|
||||
- エラーコードをハンドリングする(`Result::error()`)
|
||||
- SSL エラーをハンドリングする(`ssl_error()` / `ssl_backend_error()`)
|
||||
- クライアントにログを設定する(`set_logger` / `set_error_logger`)
|
||||
|
||||
## サーバー
|
||||
|
||||
### 基本
|
||||
- GET / POST / PUT / DELETE ハンドラを登録する
|
||||
- JSON リクエストを受け取り JSON レスポンスを返す
|
||||
- パスパラメーターを使う(`/users/:id`)
|
||||
- 静的ファイルサーバーを設定する(`set_mount_point`)
|
||||
|
||||
### ストリーミング・ファイル
|
||||
- 大きなファイルをストリーミングで返す(`ContentProvider`)
|
||||
- ファイルダウンロードレスポンスを返す(`Content-Disposition`)
|
||||
- マルチパートデータをストリーミングで受け取る(`ContentReader`)
|
||||
- レスポンスを圧縮して返す(gzip)
|
||||
|
||||
### ハンドラチェーン
|
||||
- 全ルートに共通の前処理をする(Pre-routing handler)
|
||||
- Post-routing handler でレスポンスヘッダーを追加する(CORS など)
|
||||
- Pre-request handler でルート単位の認証を行う(`matched_route`)
|
||||
- `res.user_data` でハンドラ間データを渡す
|
||||
|
||||
### エラー処理・デバッグ
|
||||
- カスタムエラーページを返す(`set_error_handler`)
|
||||
- 例外をキャッチする(`set_exception_handler`)
|
||||
- リクエストをログに記録する(Logger)
|
||||
- クライアントが切断したか検出する(`req.is_connection_closed()`)
|
||||
|
||||
### 運用・チューニング
|
||||
- ポートを動的に割り当てる(`bind_to_any_port`)
|
||||
- `listen_after_bind` で起動順序を制御する
|
||||
- グレースフルシャットダウンする(`stop()` とシグナルハンドリング)
|
||||
- Keep-Alive を調整する(`set_keep_alive_max_count` / `set_keep_alive_timeout`)
|
||||
- マルチスレッド数を設定する(`new_task_queue`)
|
||||
|
||||
## TLS / セキュリティ
|
||||
|
||||
- OpenSSL・mbedTLS・wolfSSL の選択指針(ビルド時の `#define` の違い)
|
||||
- SSL 証明書の検証を制御する(証明書の無効化・カスタム CA・カスタムコールバック)
|
||||
- カスタム証明書検証コールバックを使う(`set_server_certificate_verifier`)
|
||||
- SSL/TLS サーバーを立ち上げる(証明書・秘密鍵の設定)
|
||||
- mTLS(クライアント証明書による相互認証)を設定する
|
||||
- サーバー側でピア証明書を参照する(`req.peer_cert()` / SNI)
|
||||
|
||||
## SSE
|
||||
|
||||
- SSE サーバーを実装する
|
||||
- SSE でイベント名を使い分ける
|
||||
- SSE の再接続を処理する(`Last-Event-ID`)
|
||||
- SSE をクライアントで受信する
|
||||
|
||||
## WebSocket
|
||||
|
||||
- WebSocket エコーサーバー/クライアントを実装する
|
||||
- ハートビートを設定する(`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`)
|
||||
- 接続クローズをハンドリングする
|
||||
- バイナリフレームを送受信する
|
||||
|
||||
@@ -18,4 +18,8 @@ HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバ
|
||||
## ドキュメント
|
||||
|
||||
- [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
|
||||
- [Building a Desktop LLM App](llm-app/) — llama.cppを組み込んだデスクトップアプリを段階的に構築する実践ガイド
|
||||
|
||||
## お楽しみに
|
||||
|
||||
- [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: "1. プロジェクト環境を作る"
|
||||
order: 1
|
||||
|
||||
---
|
||||
|
||||
llama.cppを推論エンジンに使って、テキスト翻訳のREST APIサーバーを段階的に作っていきます。最終的にはこんなリクエストで翻訳結果が返ってくるようになります。
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "The weather is nice today. Shall we go for a walk?", "target_lang": "ja"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"translation": "今日はいい天気ですね。散歩に行きましょうか?"
|
||||
}
|
||||
```
|
||||
|
||||
「翻訳API」はあくまで一例です。プロンプトを差し替えれば、要約・コード生成・チャットボットなど、お好きなLLMアプリに応用できます。
|
||||
|
||||
最終的にサーバーが提供するAPIの一覧です。
|
||||
|
||||
| メソッド | パス | 説明 | 章 |
|
||||
| -------- | ---- | ---- | -- |
|
||||
| `GET` | `/health` | サーバーの状態を返す | 1 |
|
||||
| `POST` | `/translate` | テキストを翻訳してJSONで返す | 2 |
|
||||
| `POST` | `/translate/stream` | トークン単位でSSEストリーミング | 3 |
|
||||
| `GET` | `/models` | モデル一覧(available / downloaded / selected) | 4 |
|
||||
| `POST` | `/models/select` | モデルを選択(未ダウンロードなら自動取得) | 4 |
|
||||
|
||||
この章では、まずプロジェクトの環境を整えます。依存ライブラリの取得、ディレクトリ構成、ビルド設定、モデルファイルの入手まで済ませて、次の章ですぐにコードを書き始められるようにしましょう。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- C++20対応コンパイラ(GCC 10+、Clang 10+、MSVC 2019 16.8+)
|
||||
- CMake 3.20以上
|
||||
- OpenSSL(4章でHTTPSクライアントに使用。macOS: `brew install openssl`、Ubuntu: `sudo apt install libssl-dev`)
|
||||
- 十分なディスク容量(モデルファイルが数GBになります)
|
||||
|
||||
## 1.1 何を使うか
|
||||
|
||||
使うライブラリはこちらです。
|
||||
|
||||
| ライブラリ | 役割 |
|
||||
| ----------- | ------ |
|
||||
| [cpp-httplib](https://github.com/yhirose/cpp-httplib) | HTTPサーバー/クライアント |
|
||||
| [nlohmann/json](https://github.com/nlohmann/json) | JSONパーサー |
|
||||
| [cpp-llamalib](https://github.com/yhirose/cpp-llamalib) | llama.cppラッパー |
|
||||
| [llama.cpp](https://github.com/ggml-org/llama.cpp) | LLM推論エンジン |
|
||||
| [webview/webview](https://github.com/webview/webview) | デスクトップWebView(6章で使用) |
|
||||
|
||||
cpp-httplib、nlohmann/json、cpp-llamalibはヘッダーオンリーライブラリです。`curl`でヘッダーファイルを1枚ダウンロードして`#include`するだけでも使えますが、本書ではCMakeの`FetchContent`で自動取得します。`CMakeLists.txt`に書いておけば、`cmake -B build`の時点で全ライブラリが自動でダウンロード・ビルドされるので、手作業の手順が減ります。`webview`は6章で使うので、今は気にしなくて大丈夫です。
|
||||
|
||||
## 1.2 ディレクトリ構成
|
||||
|
||||
最終的にこんな構成になります。
|
||||
|
||||
```ascii
|
||||
translate-app/
|
||||
├── CMakeLists.txt
|
||||
├── models/
|
||||
│ └── (GGUFファイル)
|
||||
└── src/
|
||||
└── main.cpp
|
||||
```
|
||||
|
||||
ライブラリのソースコードはプロジェクトに含めません。CMakeの`FetchContent`がビルド時に自動で取得するので、必要なのは自分のコードだけです。
|
||||
|
||||
プロジェクトディレクトリを作って、gitリポジトリにしましょう。
|
||||
|
||||
```bash
|
||||
mkdir translate-app && cd translate-app
|
||||
mkdir src models
|
||||
git init
|
||||
```
|
||||
|
||||
## 1.3 GGUFモデルファイルを入手する
|
||||
|
||||
LLMの推論にはモデルファイルが必要です。GGUFはllama.cppが使うモデル形式で、Hugging Faceにたくさんあります。
|
||||
|
||||
まずは小さいモデルで試してみましょう。GoogleのGemma 2 2Bの量子化版(約1.6GB)がおすすめです。軽量ですが多言語に対応していて、翻訳タスクにも向いています。
|
||||
|
||||
```bash
|
||||
curl -L -o models/gemma-2-2b-it-Q4_K_M.gguf \
|
||||
https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf
|
||||
```
|
||||
|
||||
4章で、このダウンロード自体をアプリ内からcpp-httplibのクライアント機能で行えるようにします。
|
||||
|
||||
## 1.4 CMakeLists.txt
|
||||
|
||||
プロジェクトルートに`CMakeLists.txt`を作ります。`FetchContent`で依存ライブラリを宣言しておくと、CMakeが自動でダウンロード・ビルドしてくれます。
|
||||
|
||||
<!-- data-file="CMakeLists.txt" -->
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(translate-server CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# llama.cpp(LLM推論エンジン)
|
||||
FetchContent_Declare(llama
|
||||
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(llama)
|
||||
|
||||
# cpp-httplib(HTTPサーバー/クライアント)
|
||||
FetchContent_Declare(httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# nlohmann/json(JSONパーサー)
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# cpp-llamalib(llama.cppヘッダーオンリーラッパー)
|
||||
FetchContent_Declare(cpp_llamalib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp_llamalib)
|
||||
|
||||
add_executable(translate-server src/main.cpp)
|
||||
|
||||
target_link_libraries(translate-server PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
cpp-llamalib
|
||||
)
|
||||
```
|
||||
|
||||
`FetchContent_Declare`でライブラリのソース取得先を宣言し、`FetchContent_MakeAvailable`で実際に取得・ビルドします。初回の`cmake -B build`は全ライブラリのダウンロードとllama.cppのビルドが走るので時間がかかりますが、2回目以降はキャッシュが効きます。
|
||||
|
||||
`target_link_libraries`でリンクするだけで、インクルードパスやビルド設定は各ライブラリのCMakeが自動で設定してくれます。
|
||||
|
||||
## 1.5 雛形コードの作成
|
||||
|
||||
この雛形コードをベースに、章ごとに機能を追加していきます。
|
||||
|
||||
<!-- data-file="main.cpp" -->
|
||||
```cpp
|
||||
// src/main.cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
// `Ctrl+C`でgraceful shutdown
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// リクエストとレスポンスをログに記録
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
// ヘルスチェック
|
||||
svr.Get("/health", [](const auto &, auto &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// 各エンドポイントのダミー実装(以降の章で本物に差し替えていく)
|
||||
svr.Post("/translate",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content(json{{"translation", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/translate/stream",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content("data: \"TODO\"\n\ndata: [DONE]\n\n", "text/event-stream");
|
||||
});
|
||||
|
||||
svr.Get("/models",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/models/select",
|
||||
[](const auto &req, auto &res) {
|
||||
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// `Ctrl+C` (`SIGINT`)や`kill` (`SIGTERM`)でサーバーを停止できるようにする
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// サーバー起動
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
## 1.6 ビルドと動作確認
|
||||
|
||||
ビルドしてサーバーを起動し、curlでリクエストが通るか確かめます。
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
別のターミナルからcurlで確認してみましょう。
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
# => {"status":"ok"}
|
||||
```
|
||||
|
||||
JSONが返ってくれば環境構築は完了です。
|
||||
|
||||
## 次の章へ
|
||||
|
||||
環境が整ったので、次の章ではこの雛形に翻訳REST APIを実装します。llama.cppで推論を行い、cpp-httplibでそれをHTTPエンドポイントとして公開します。
|
||||
|
||||
**Next:** [llama.cppを組み込んでREST APIを作る](../ch02-rest-api)
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: "2. llama.cppを組み込んでREST APIを作る"
|
||||
order: 2
|
||||
|
||||
---
|
||||
|
||||
1章の雛形では`/translate`が`"TODO"`を返すだけでした。この章ではllama.cppの推論を組み込んで、実際に翻訳結果を返すAPIに仕上げます。
|
||||
|
||||
llama.cppのAPIを直接扱うとコードが長くなるので、薄いラッパーライブラリ[cpp-llamalib](https://github.com/yhirose/cpp-llamalib)を使います。モデルのロードから推論まで数行で書けるので、cpp-httplibの使い方に集中できます。
|
||||
|
||||
## 2.1 LLMの初期化
|
||||
|
||||
`llamalib::Llama`にモデルファイルのパスを渡すだけで、モデルのロード・コンテキスト作成・サンプラー設定がすべて済みます。1章で別のモデルをダウンロードした場合は、パスをそのモデルに合わせてください。
|
||||
|
||||
```cpp
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
int main() {
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
|
||||
|
||||
// LLM推論は時間がかかるのでタイムアウトを長めに設定(デフォルトは5秒)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
// ... HTTPサーバーの構築・起動 ...
|
||||
}
|
||||
```
|
||||
|
||||
GPU層数やコンテキスト長などを変えたい場合は`llamalib::Options`で指定できます。
|
||||
|
||||
```cpp
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf", {
|
||||
.n_gpu_layers = 0, // CPUのみ
|
||||
.n_ctx = 4096,
|
||||
}};
|
||||
```
|
||||
|
||||
## 2.2 `/translate`ハンドラ
|
||||
|
||||
1章ではダミーのJSONを返していたハンドラを、実際の推論に差し替えます。
|
||||
|
||||
```cpp
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSONパース(第3引数`false`: 失敗時に例外を投げず`is_discarded()`で判定)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// 必須フィールドの検証
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja"); // デフォルトは日本語
|
||||
|
||||
// プロンプトを組み立てて推論
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`llm.chat()`は推論中に例外を投げることがあります(コンテキスト長の超過など)。`try/catch`で捕捉してエラーをJSONで返すことで、サーバーがクラッシュするのを防ぎます。
|
||||
|
||||
## 2.3 全体のコード
|
||||
|
||||
ここまでの変更をまとめた完成形です。
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">全体のコード(main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
// `Ctrl+C`でgraceful shutdown
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 1章でダウンロードしたモデルをロード
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
|
||||
|
||||
// LLM推論は時間がかかるのでタイムアウトを長めに設定(デフォルトは5秒)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
// リクエストとレスポンスをログに記録
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSONパース(第3引数`false`: 失敗時に例外を投げず`is_discarded()`で判定)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// 必須フィールドの検証
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja"); // デフォルトは日本語
|
||||
|
||||
// プロンプトを組み立てて推論
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// 以降の章で本物に差し替えるダミー実装
|
||||
svr.Get("/models",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/models/select",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// `Ctrl+C` (`SIGINT`)や`kill` (`SIGTERM`)でサーバーを停止できるようにする
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// サーバー起動(`stop()`が呼ばれるまでブロック)
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 2.4 動作確認
|
||||
|
||||
ビルドし直してサーバーを起動し、今度は実際の翻訳結果が返ってくるか確かめましょう。
|
||||
|
||||
```bash
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.", "target_lang": "ja"}'
|
||||
# => {"translation":"去年の春に東京を訪れた。桜が綺麗だった。"}
|
||||
```
|
||||
|
||||
1章では`"TODO"`が返ってきていましたが、今度は実際の翻訳結果が返ってきます。
|
||||
|
||||
## 次の章へ
|
||||
|
||||
この章で作ったREST APIは、翻訳が完了するまで全文を待つので、長いテキストだとユーザーは進捗がわからないまま待つことになります。
|
||||
|
||||
次の章ではSSE(Server-Sent Events)を使って、トークンが生成されるたびにリアルタイムで返す仕組みにします。
|
||||
|
||||
**Next:** [SSEでトークンストリーミングを追加する](../ch03-sse-streaming)
|
||||
@@ -0,0 +1,264 @@
|
||||
---
|
||||
title: "3. SSEでトークンストリーミングを追加する"
|
||||
order: 3
|
||||
|
||||
---
|
||||
|
||||
2章の`/translate`は、翻訳が完了してから結果をまとめて返していました。短い文なら問題ありませんが、長い文だとユーザーは何も表示されないまま何秒も待つことになります。
|
||||
|
||||
この章ではSSE(Server-Sent Events)を使って、トークンが生成されるたびにリアルタイムで返す`/translate/stream`エンドポイントを追加します。ChatGPTやClaudeのAPIでおなじみの方式です。
|
||||
|
||||
## 3.1 SSEとは
|
||||
|
||||
SSEはHTTPのレスポンスをストリームとして送る仕組みです。クライアントがリクエストを送ると、サーバーは接続を保ったままイベントを少しずつ返します。フォーマットはシンプルなテキストです。
|
||||
|
||||
```text
|
||||
data: "去年の"
|
||||
data: "春に"
|
||||
data: "東京を"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
各行は`data:`で始まり、空行で区切ります。Content-Typeは`text/event-stream`です。トークンはJSON文字列としてエスケープして送るので、ダブルクォートで囲んだ形式になります(3.3節で実装します)。
|
||||
|
||||
## 3.2 cpp-httplibでのストリーミング
|
||||
|
||||
cpp-httplibでは`set_chunked_content_provider`を使うと、レスポンスを少しずつ送れます。コールバックの中で`sink.os`に書き込むたびにデータがクライアントに届きます。
|
||||
|
||||
```cpp
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[](size_t offset, httplib::DataSink &sink) {
|
||||
sink.os << "data: hello\n\n";
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
`sink.done()`を呼ぶとストリームが終了します。クライアントが途中で接続を切った場合、`sink.os`の書き込みが失敗して`sink.os.fail()`が`true`になります。これを使って切断を検知し、不要な推論を中断できます。
|
||||
|
||||
## 3.3 `/translate/stream`ハンドラ
|
||||
|
||||
JSONパースとバリデーションは2章の`/translate`と同じです。違うのはレスポンスの返し方だけ。`llm.chat()`のストリーミングコールバックと`set_chunked_content_provider`を組み合わせます。
|
||||
|
||||
```cpp
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// ... JSONパース・バリデーションは/translateと同じ ...
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // 切断されたらfalse→推論を中断
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
ポイントをいくつか。
|
||||
|
||||
- `llm.chat()`にコールバックを渡すと、トークンが生成されるたびに呼ばれます。コールバックが`false`を返すと生成を中断します
|
||||
- `sink.os`に書き込んだ後、`sink.os.good()`でクライアントがまだ接続しているかを確認できます。切断されていたら`false`を返して推論を止めます
|
||||
- 各トークンは`json(token).dump()`でJSON文字列としてエスケープしてから送ります。改行やクォートを含むトークンでも安全です
|
||||
- `dump(-1, ' ', false, ...)`の最初の3つの引数はデフォルトと同じです。重要なのは第4引数の`json::error_handler_t::replace`です。LLMはトークンをサブワード単位で返すため、マルチバイト文字(日本語など)の途中でトークンが切れることがあります。不完全なUTF-8バイト列をそのまま`dump()`に渡すと例外が飛ぶので、`replace`で安全に置換します。ブラウザ側で結合されるため、表示上の問題はありません
|
||||
- `try/catch`でラムダ全体を囲んでいます。`llm.chat()`はコンテキストウィンドウの超過などで例外を投げることがあります。ラムダ内で例外が未捕捉だとサーバーがクラッシュするので、エラーをSSEイベントとして返します
|
||||
- `data: [DONE]`はOpenAI APIと同じ慣習で、ストリームの終了をクライアントに伝えます
|
||||
|
||||
## 3.4 全体のコード
|
||||
|
||||
2章のコードに`/translate/stream`を追加した完成形です。
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">全体のコード(main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
// `Ctrl+C`でgraceful shutdown
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// GGUFモデルをロード
|
||||
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
|
||||
|
||||
// LLM推論は時間がかかるのでタイムアウトを長めに設定(デフォルトは5秒)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
// リクエストとレスポンスをログに記録
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// 2章で作った通常の翻訳エンドポイント
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSONパース・バリデーション(詳細は2章を参照)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// SSEストリーミング翻訳エンドポイント
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSONパース・バリデーション(/translateと同じ)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // 切断されたら推論を中断
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// 以降の章で本物に差し替えるダミー実装
|
||||
svr.Get("/models",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/models/select",
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// `Ctrl+C` (`SIGINT`)や`kill` (`SIGTERM`)でサーバーを停止できるようにする
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// サーバー起動(`stop()`が呼ばれるまでブロック)
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 3.5 動作確認
|
||||
|
||||
ビルドしてサーバーを起動します。
|
||||
|
||||
```bash
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
curlの`-N`オプションでバッファリングを無効にすると、トークンが届くたびにリアルタイムで表示されます。
|
||||
|
||||
```bash
|
||||
curl -N -X POST http://localhost:8080/translate/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.", "target_lang": "ja"}'
|
||||
```
|
||||
|
||||
```text
|
||||
data: "去年の"
|
||||
data: "春に"
|
||||
data: "東京を"
|
||||
data: "訪れた"
|
||||
data: "。"
|
||||
data: "桜が"
|
||||
data: "綺麗だった"
|
||||
data: "。"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
トークンがひとつずつ流れてくるのが確認できるはずです。2章の`/translate`も引き続き使えます。
|
||||
|
||||
## 次の章へ
|
||||
|
||||
サーバーの翻訳機能はこれで一通り揃いました。次の章では、cpp-httplibのクライアント機能を使ってHugging Faceからモデルを取得・管理する機能を追加します。
|
||||
|
||||
**Next:** [モデルの取得・管理機能を追加する](../ch04-model-management)
|
||||
@@ -0,0 +1,788 @@
|
||||
---
|
||||
title: "4. モデルの取得・管理機能を追加する"
|
||||
order: 4
|
||||
|
||||
---
|
||||
|
||||
3章まででサーバーの翻訳機能は一通り揃いました。しかし、モデルファイルは1章で手動ダウンロードした1つだけです。この章ではcpp-httplibの**クライアント機能**を使い、アプリ内からHugging Faceのモデルをダウンロード・切り替えできるようにします。
|
||||
|
||||
完成すると、こんなリクエストでモデルを管理できるようになります。
|
||||
|
||||
```bash
|
||||
# 利用可能なモデル一覧を取得
|
||||
curl http://localhost:8080/models
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"models": [
|
||||
{"name": "gemma-2-2b-it", "params": "2B", "size": "1.6 GB", "downloaded": true, "selected": true},
|
||||
{"name": "gemma-2-9b-it", "params": "9B", "size": "5.8 GB", "downloaded": false, "selected": false},
|
||||
{"name": "Llama-3.1-8B-Instruct", "params": "8B", "size": "4.9 GB", "downloaded": false, "selected": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 別のモデルを選択(未ダウンロードなら自動で取得)
|
||||
curl -N -X POST http://localhost:8080/models/select \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gemma-2-9b-it"}'
|
||||
```
|
||||
|
||||
```text
|
||||
data: {"status":"downloading","progress":0}
|
||||
data: {"status":"downloading","progress":12}
|
||||
...
|
||||
data: {"status":"downloading","progress":100}
|
||||
data: {"status":"loading"}
|
||||
data: {"status":"ready"}
|
||||
```
|
||||
|
||||
## 4.1 httplib::Clientの基本
|
||||
|
||||
これまでは`httplib::Server`だけを使ってきましたが、cpp-httplibはクライアント機能も備えています。Hugging FaceはHTTPSなので、TLS対応のクライアントが必要です。
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
|
||||
// URLスキームを含めると自動でSSLClientが使われる
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
|
||||
// リダイレクト先を自動で追従(Hugging FaceはCDNにリダイレクトする)
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/api/models");
|
||||
if (res && res->status == 200) {
|
||||
std::cout << res->body << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
HTTPSを使うには、ビルド時にOpenSSLを有効にする必要があります。`CMakeLists.txt`に以下を追加しましょう。
|
||||
|
||||
```cmake
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
target_link_libraries(translate-server PRIVATE OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
|
||||
# macOS: システム証明書の読み込みに必要
|
||||
if(APPLE)
|
||||
target_link_libraries(translate-server PRIVATE "-framework CoreFoundation" "-framework Security")
|
||||
endif()
|
||||
```
|
||||
|
||||
`CPPHTTPLIB_OPENSSL_SUPPORT`を定義すると、`httplib::Client("https://...")`がTLS接続を行います。macOSではシステム証明書ストアにアクセスするため、CoreFoundationとSecurityフレームワークのリンクも必要です。完全な`CMakeLists.txt`は4.8節にあります。
|
||||
|
||||
## 4.2 モデル一覧を定義する
|
||||
|
||||
アプリが扱えるモデルの一覧を定義します。翻訳タスクで検証済みの4モデルを用意しました。
|
||||
|
||||
```cpp
|
||||
struct ModelInfo {
|
||||
std::string name; // 表示名
|
||||
std::string params; // パラメータ数
|
||||
std::string size; // GGUF Q4サイズ
|
||||
std::string repo; // Hugging Faceリポジトリ
|
||||
std::string filename; // GGUFファイル名
|
||||
};
|
||||
|
||||
const std::vector<ModelInfo> MODELS = {
|
||||
{
|
||||
.name = "gemma-2-2b-it",
|
||||
.params = "2B",
|
||||
.size = "1.6 GB",
|
||||
.repo = "bartowski/gemma-2-2b-it-GGUF",
|
||||
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "gemma-2-9b-it",
|
||||
.params = "9B",
|
||||
.size = "5.8 GB",
|
||||
.repo = "bartowski/gemma-2-9b-it-GGUF",
|
||||
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "Llama-3.1-8B-Instruct",
|
||||
.params = "8B",
|
||||
.size = "4.9 GB",
|
||||
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
|
||||
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 4.3 モデルの保存場所
|
||||
|
||||
3章まではプロジェクトディレクトリ内の`models/`にモデルを置いていました。しかし複数モデルを管理するなら、アプリ専用のディレクトリに保存する方が適切です。macOS/Linuxでは`~/.translate-app/models/`、Windowsでは`%APPDATA%\translate-app\models\`を使います。
|
||||
|
||||
```cpp
|
||||
std::filesystem::path get_models_dir() {
|
||||
#ifdef _WIN32
|
||||
auto env = std::getenv("APPDATA");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / "translate-app" / "models";
|
||||
#else
|
||||
auto env = std::getenv("HOME");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / ".translate-app" / "models";
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
環境変数が未設定の場合はカレントディレクトリにフォールバックします。このディレクトリはアプリ起動時に自動作成します(`create_directories`は既に存在していてもエラーになりません)。
|
||||
|
||||
## 4.4 モデルの初期化を書き換える
|
||||
|
||||
モデルの初期化を`main()`の先頭で書き換えます。1章ではパスをハードコードしていましたが、ここからはモデルの切り替えに対応します。現在ロード中のファイル名は`selected_model`変数で管理します。起動時は`MODELS`の先頭エントリーをロードします。この変数は`GET /models`や`POST /models/select`のハンドラから参照・更新します。
|
||||
|
||||
cpp-httplibはスレッドプールでハンドラを並行実行します。そのため、モデル切り替え中(`llm`の上書き中)に別スレッドで`llm.chat()`が走るとクラッシュします。`std::mutex`で排他制御を入れておきます。
|
||||
|
||||
```cpp
|
||||
int main() {
|
||||
auto models_dir = get_models_dir();
|
||||
std::filesystem::create_directories(models_dir);
|
||||
|
||||
std::string selected_model = MODELS[0].filename;
|
||||
auto path = models_dir / selected_model;
|
||||
|
||||
// デフォルトモデルが未ダウンロードなら起動時に自動取得
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::cout << "Downloading " << selected_model << "..." << std::endl;
|
||||
if (!download_model(MODELS[0], [](int pct) {
|
||||
std::cout << "\r" << pct << "%" << std::flush;
|
||||
return true;
|
||||
})) {
|
||||
std::cerr << "\nFailed to download model." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
auto llm = llamalib::Llama{path};
|
||||
std::mutex llm_mutex; // モデル切り替え中のアクセスを保護する
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
初回起動時にユーザーが`curl`で手動ダウンロードしなくても済むようにしています。4.6節の`download_model`関数を使い、進捗をコンソールに表示します。
|
||||
|
||||
## 4.5 `GET /models`ハンドラ
|
||||
|
||||
モデル一覧に「ダウンロード済みか」「選択中か」の情報を付けて返します。
|
||||
|
||||
```cpp
|
||||
svr.Get("/models",
|
||||
[&](const httplib::Request &, httplib::Response &res) {
|
||||
auto arr = json::array();
|
||||
for (const auto &m : MODELS) {
|
||||
auto path = get_models_dir() / m.filename;
|
||||
arr.push_back({
|
||||
{"name", m.name},
|
||||
{"params", m.params},
|
||||
{"size", m.size},
|
||||
{"downloaded", std::filesystem::exists(path)},
|
||||
{"selected", m.filename == selected_model},
|
||||
});
|
||||
}
|
||||
res.set_content(json{{"models", arr}}.dump(), "application/json");
|
||||
});
|
||||
```
|
||||
|
||||
## 4.6 大きなファイルをダウンロードする
|
||||
|
||||
GGUFモデルは数GBあるため、全体をメモリに載せるわけにはいきません。`httplib::Client::Get`にコールバックを渡すと、チャンクごとにデータを受け取れます。
|
||||
|
||||
```cpp
|
||||
// content_receiver: データチャンクを受け取るコールバック
|
||||
// progress: ダウンロード進捗コールバック
|
||||
cli.Get(url,
|
||||
[&](const char *data, size_t len) { // content_receiver
|
||||
ofs.write(data, len);
|
||||
return true; // falseを返すと中断
|
||||
},
|
||||
[&](size_t current, size_t total) { // progress
|
||||
int pct = total ? (int)(current * 100 / total) : 0;
|
||||
std::cout << pct << "%" << std::endl;
|
||||
return true; // falseを返すと中断
|
||||
});
|
||||
```
|
||||
|
||||
これを使ってHugging Faceからモデルをダウンロードする関数を作ります。
|
||||
|
||||
```cpp
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
// モデルをダウンロードし、進捗をprogress_cbで通知する
|
||||
// progress_cbがfalseを返すとダウンロードを中断する
|
||||
bool download_model(const ModelInfo &model,
|
||||
std::function<bool(int)> progress_cb) {
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
cli.set_follow_location(true);
|
||||
cli.set_read_timeout(std::chrono::hours(1));
|
||||
|
||||
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
|
||||
auto path = get_models_dir() / model.filename;
|
||||
auto tmp_path = std::filesystem::path(path).concat(".tmp");
|
||||
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
if (!ofs) { return false; }
|
||||
|
||||
auto res = cli.Get(url,
|
||||
[&](const char *data, size_t len) {
|
||||
ofs.write(data, len);
|
||||
return ofs.good();
|
||||
},
|
||||
[&](size_t current, size_t total) {
|
||||
return progress_cb(total ? (int)(current * 100 / total) : 0);
|
||||
});
|
||||
|
||||
ofs.close();
|
||||
|
||||
if (!res || res->status != 200) {
|
||||
std::filesystem::remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// .tmpに書いてからリネームすることで、DLが途中で止まっても
|
||||
// 不完全なファイルがモデルとして使われるのを防ぐ
|
||||
std::filesystem::rename(tmp_path, path);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.7 `/models/select`ハンドラ
|
||||
|
||||
モデルの選択リクエストを処理します。レスポンスは常にSSEで返し、ダウンロード進捗 → ロード → 完了のステータスを順に通知します。
|
||||
|
||||
```cpp
|
||||
svr.Post("/models/select",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded() || !input.contains("model")) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'model' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = input["model"].get<std::string>();
|
||||
|
||||
// モデル一覧から探す
|
||||
auto it = std::find_if(MODELS.begin(), MODELS.end(),
|
||||
[&](const ModelInfo &m) { return m.name == name; });
|
||||
|
||||
if (it == MODELS.end()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"error", "Unknown model"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &model = *it;
|
||||
|
||||
// 常にSSEで応答する(DL済みでも未DLでも同じ形式)
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, model](size_t, httplib::DataSink &sink) {
|
||||
// SSEイベント送信ヘルパー
|
||||
auto send = [&](const json &event) {
|
||||
sink.os << "data: " << event.dump() << "\n\n";
|
||||
};
|
||||
|
||||
// 未ダウンロードならダウンロード(進捗をSSEで通知)
|
||||
auto path = get_models_dir() / model.filename;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
bool ok = download_model(model, [&](int pct) {
|
||||
send({{"status", "downloading"}, {"progress", pct}});
|
||||
return sink.os.good(); // クライアント切断時にダウンロードを中断
|
||||
});
|
||||
if (!ok) {
|
||||
send({{"status", "error"}, {"message", "Download failed"}});
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// モデルをロードして切り替え
|
||||
send({{"status", "loading"}});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
llm = llamalib::Llama{path};
|
||||
selected_model = model.filename;
|
||||
}
|
||||
|
||||
send({{"status", "ready"}});
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
いくつか補足します。
|
||||
|
||||
- `download_model`の進捗コールバックから直接SSEイベントを送っています。3章の`set_chunked_content_provider` + `sink.os`の応用です
|
||||
- コールバックが`sink.os.good()`を返すので、クライアントが接続を切るとダウンロードも中断します。5章で追加するキャンセルボタンで使います
|
||||
- `selected_model`を更新すると、`GET /models`の`selected`フラグに反映されます
|
||||
- `llm`の上書きを`llm_mutex`で保護しています。`/translate`や`/translate/stream`のハンドラも同じ`mutex`でロックするので、モデル切り替え中に推論が走ることはありません(全体コードを参照)
|
||||
|
||||
## 4.8 全体のコード
|
||||
|
||||
3章のコードにモデル管理機能を追加した完成形です。
|
||||
|
||||
<details>
|
||||
<summary data-file="CMakeLists.txt">全体のコード(CMakeLists.txt)</summary>
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(translate-server CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# llama.cpp
|
||||
FetchContent_Declare(llama
|
||||
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(llama)
|
||||
|
||||
# cpp-httplib
|
||||
FetchContent_Declare(httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# nlohmann/json
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# cpp-llamalib
|
||||
FetchContent_Declare(cpp_llamalib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp_llamalib)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_executable(translate-server src/main.cpp)
|
||||
|
||||
target_link_libraries(translate-server PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
cpp-llamalib
|
||||
OpenSSL::SSL OpenSSL::Crypto
|
||||
)
|
||||
|
||||
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(translate-server PRIVATE
|
||||
"-framework CoreFoundation"
|
||||
"-framework Security"
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">全体のコード(main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// モデル定義
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
struct ModelInfo {
|
||||
std::string name;
|
||||
std::string params;
|
||||
std::string size;
|
||||
std::string repo;
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
const std::vector<ModelInfo> MODELS = {
|
||||
{
|
||||
.name = "gemma-2-2b-it",
|
||||
.params = "2B",
|
||||
.size = "1.6 GB",
|
||||
.repo = "bartowski/gemma-2-2b-it-GGUF",
|
||||
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "gemma-2-9b-it",
|
||||
.params = "9B",
|
||||
.size = "5.8 GB",
|
||||
.repo = "bartowski/gemma-2-9b-it-GGUF",
|
||||
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "Llama-3.1-8B-Instruct",
|
||||
.params = "8B",
|
||||
.size = "4.9 GB",
|
||||
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
|
||||
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
},
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// モデル保存ディレクトリ
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path get_models_dir() {
|
||||
#ifdef _WIN32
|
||||
auto env = std::getenv("APPDATA");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / "translate-app" / "models";
|
||||
#else
|
||||
auto env = std::getenv("HOME");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / ".translate-app" / "models";
|
||||
#endif
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// モデルダウンロード
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// progress_cbがfalseを返したらダウンロードを中断する
|
||||
bool download_model(const ModelInfo &model,
|
||||
std::function<bool(int)> progress_cb) {
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
cli.set_follow_location(true); // Hugging FaceはCDNにリダイレクトする
|
||||
cli.set_read_timeout(std::chrono::hours(1)); // 大きなモデルに備えて長めに
|
||||
|
||||
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
|
||||
auto path = get_models_dir() / model.filename;
|
||||
auto tmp_path = std::filesystem::path(path).concat(".tmp");
|
||||
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
if (!ofs) { return false; }
|
||||
|
||||
auto res = cli.Get(url,
|
||||
// content_receiver: チャンクごとにデータを受け取ってファイルに書き込む
|
||||
[&](const char *data, size_t len) {
|
||||
ofs.write(data, len);
|
||||
return ofs.good();
|
||||
},
|
||||
// progress: ダウンロード進捗を通知(falseを返すと中断)
|
||||
[&, last_pct = -1](size_t current, size_t total) mutable {
|
||||
int pct = total ? (int)(current * 100 / total) : 0;
|
||||
if (pct == last_pct) return true; // 同じ値なら通知をスキップ
|
||||
last_pct = pct;
|
||||
return progress_cb(pct);
|
||||
});
|
||||
|
||||
ofs.close();
|
||||
|
||||
if (!res || res->status != 200) {
|
||||
std::filesystem::remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ダウンロード完了後にリネーム
|
||||
std::filesystem::rename(tmp_path, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// サーバー
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
void signal_handler(int sig) {
|
||||
if (sig == SIGINT || sig == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// モデル保存ディレクトリを作成
|
||||
auto models_dir = get_models_dir();
|
||||
std::filesystem::create_directories(models_dir);
|
||||
|
||||
// デフォルトモデルが未ダウンロードなら自動取得
|
||||
std::string selected_model = MODELS[0].filename;
|
||||
auto path = models_dir / selected_model;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::cout << "Downloading " << selected_model << "..." << std::endl;
|
||||
if (!download_model(MODELS[0], [](int pct) {
|
||||
std::cout << "\r" << pct << "%" << std::flush;
|
||||
return true;
|
||||
})) {
|
||||
std::cerr << "\nFailed to download model." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
auto llm = llamalib::Llama{path};
|
||||
std::mutex llm_mutex; // モデル切り替え中のアクセスを保護する
|
||||
|
||||
// LLM推論は時間がかかるのでタイムアウトを長めに設定(デフォルトは5秒)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- 翻訳エンドポイント(2章) -----------------------------------------
|
||||
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
// JSONパース・バリデーション(詳細は2章を参照)
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// --- SSEストリーミング翻訳(3章)--------------------------------------
|
||||
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // 切断されたら推論を中断
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// --- モデル一覧(4章) -------------------------------------------------
|
||||
|
||||
svr.Get("/models",
|
||||
[&](const httplib::Request &, httplib::Response &res) {
|
||||
auto models_dir = get_models_dir();
|
||||
auto arr = json::array();
|
||||
for (const auto &m : MODELS) {
|
||||
auto path = models_dir / m.filename;
|
||||
arr.push_back({
|
||||
{"name", m.name},
|
||||
{"params", m.params},
|
||||
{"size", m.size},
|
||||
{"downloaded", std::filesystem::exists(path)},
|
||||
{"selected", m.filename == selected_model},
|
||||
});
|
||||
}
|
||||
res.set_content(json{{"models", arr}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- モデル選択(4章) -------------------------------------------------
|
||||
|
||||
svr.Post("/models/select",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded() || !input.contains("model")) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'model' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = input["model"].get<std::string>();
|
||||
|
||||
auto it = std::find_if(MODELS.begin(), MODELS.end(),
|
||||
[&](const ModelInfo &m) { return m.name == name; });
|
||||
|
||||
if (it == MODELS.end()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"error", "Unknown model"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &model = *it;
|
||||
|
||||
// 常にSSEで応答する(DL済みでも未DLでも同じ形式)
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, model](size_t, httplib::DataSink &sink) {
|
||||
// SSEイベント送信ヘルパー
|
||||
auto send = [&](const json &event) {
|
||||
sink.os << "data: " << event.dump() << "\n\n";
|
||||
};
|
||||
|
||||
// 未ダウンロードならダウンロード(進捗をSSEで通知)
|
||||
auto path = get_models_dir() / model.filename;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
bool ok = download_model(model, [&](int pct) {
|
||||
send({{"status", "downloading"}, {"progress", pct}});
|
||||
return sink.os.good(); // クライアント切断時にダウンロードを中断
|
||||
});
|
||||
if (!ok) {
|
||||
send({{"status", "error"}, {"message", "Download failed"}});
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// モデルをロードして切り替え
|
||||
send({{"status", "loading"}});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
llm = llamalib::Llama{path};
|
||||
selected_model = model.filename;
|
||||
}
|
||||
|
||||
send({{"status", "ready"}});
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// `Ctrl+C` (`SIGINT`)や`kill` (`SIGTERM`)でサーバーを停止できるようにする
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 4.9 動作確認
|
||||
|
||||
CMakeLists.txtにOpenSSLの設定を追加したので、CMakeを再実行してからビルドします。
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build -j
|
||||
./build/translate-server
|
||||
```
|
||||
|
||||
### モデル一覧の確認
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/models
|
||||
```
|
||||
|
||||
1章でダウンロードした`gemma-2-2b-it`が`downloaded: true`、`selected: true`になっているはずです。
|
||||
|
||||
### 別のモデルに切り替える
|
||||
|
||||
```bash
|
||||
curl -N -X POST http://localhost:8080/models/select \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gemma-2-9b-it"}'
|
||||
```
|
||||
|
||||
SSEでダウンロード進捗が流れ、完了すると`"ready"`が返ります。
|
||||
|
||||
### 複数モデルで翻訳を比較する
|
||||
|
||||
同じ例文を異なるモデルで翻訳してみましょう。
|
||||
|
||||
```bash
|
||||
# gemma-2-9b-itで翻訳(先ほど切り替えたモデル)
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
|
||||
|
||||
# gemma-2-2b-itに戻す
|
||||
curl -N -X POST http://localhost:8080/models/select \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gemma-2-2b-it"}'
|
||||
|
||||
# 同じ文を翻訳
|
||||
curl -X POST http://localhost:8080/translate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
|
||||
```
|
||||
|
||||
同じコード・同じプロンプトでもモデルによって翻訳結果が変わることがわかります。cpp-llamalibがモデルごとのチャットテンプレートを自動適用するので、コード側の変更は不要です。
|
||||
|
||||
## 次の章へ
|
||||
|
||||
これでサーバーの主要な機能が揃いました。REST API、SSEストリーミング、モデルのダウンロードと切り替え。次の章では静的ファイル配信を追加して、ブラウザから操作できるWeb UIを作ります。
|
||||
|
||||
**Next:** [Web UIを追加する](../ch05-web-ui)
|
||||
@@ -0,0 +1,724 @@
|
||||
---
|
||||
title: "6. WebViewでデスクトップアプリ化する"
|
||||
order: 6
|
||||
|
||||
---
|
||||
|
||||
5章で、ブラウザから操作できる翻訳アプリが完成しました。でも使うたびに「サーバーを起動して、ブラウザでURLを開いて…」という手順が必要です。普通のアプリのように、ダブルクリックで起動してすぐ使えるようにしたいですよね。
|
||||
|
||||
この章では2つのことをやります。
|
||||
|
||||
1. **WebView化** — [webview/webview](https://github.com/webview/webview)でブラウザなしで動くデスクトップアプリにする
|
||||
2. **シングルバイナリ化** — [cpp-embedlib](https://github.com/yhirose/cpp-embedlib)でHTML/CSS/JSをバイナリに埋め込み、配布物を1ファイルにする
|
||||
|
||||
完成すると、`./translate-app`を実行するだけでウインドウが開き、翻訳が使えるようになります。
|
||||
|
||||

|
||||
|
||||
モデルは初回起動時に自動ダウンロードされるので、ユーザーに渡すのはバイナリ1つだけです。
|
||||
|
||||
## 6.1 webview/webview を導入する
|
||||
|
||||
[webview/webview](https://github.com/webview/webview)は、OS標準のWebViewコンポーネント(macOSならWKWebView、LinuxならWebKitGTK、WindowsならWebView2)をC/C++から使えるようにするライブラリです。Electronのように独自ブラウザを同梱するわけではないので、バイナリサイズへの影響はほぼありません。
|
||||
|
||||
CMakeで取得します。`CMakeLists.txt`に以下を追加してください。
|
||||
|
||||
```cmake
|
||||
# webview/webview
|
||||
FetchContent_Declare(webview
|
||||
GIT_REPOSITORY https://github.com/webview/webview
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(webview)
|
||||
```
|
||||
|
||||
これで`webview::core`というCMakeターゲットが使えるようになります。`target_link_libraries`でリンクすると、インクルードパスやプラットフォーム固有のフレームワークを自動で設定してくれます。
|
||||
|
||||
> **macOS**: 追加の依存は不要です。WKWebViewはシステムに組み込まれています。
|
||||
>
|
||||
> **Linux**: WebKitGTKが必要です。`sudo apt install libwebkit2gtk-4.1-dev`でインストールしてください。
|
||||
>
|
||||
> **Windows**: WebView2ランタイムが必要です。Windows 11には標準搭載されています。Windows 10の場合は[Microsoft公式サイト](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)から入手してください。
|
||||
|
||||
## 6.2 サーバーをバックグラウンドスレッドで動かす
|
||||
|
||||
5章まではサーバーの`listen()`がメインスレッドをブロックしていました。WebViewを使うには、サーバーを別スレッドで動かし、メインスレッドでWebViewのイベントループを回す必要があります。
|
||||
|
||||
```cpp
|
||||
#include "webview/webview.h"
|
||||
#include <thread>
|
||||
|
||||
int main() {
|
||||
// ... (サーバーのセットアップは5章と同じ) ...
|
||||
|
||||
// サーバーをバックグラウンドスレッドで起動
|
||||
auto port = svr.bind_to_any_port("127.0.0.1");
|
||||
std::thread server_thread([&]() { svr.listen_after_bind(); });
|
||||
|
||||
std::cout << "Listening on http://127.0.0.1:" << port << std::endl;
|
||||
|
||||
// WebViewでUIを表示
|
||||
webview::webview w(false, nullptr);
|
||||
w.set_title("Translate App");
|
||||
w.set_size(1024, 768, WEBVIEW_HINT_NONE);
|
||||
w.navigate("http://127.0.0.1:" + std::to_string(port));
|
||||
w.run(); // ウインドウが閉じるまでブロック
|
||||
|
||||
// ウインドウが閉じたらサーバーも停止
|
||||
svr.stop();
|
||||
server_thread.join();
|
||||
}
|
||||
```
|
||||
|
||||
ポイントを見ていきましょう。
|
||||
|
||||
- **`bind_to_any_port`** — `listen("127.0.0.1", 8080)`の代わりに、OSに空いているポートを選んでもらいます。デスクトップアプリは複数起動されることがあるので、ポートを固定するとぶつかります
|
||||
- **`listen_after_bind`** — `bind_to_any_port`で確保したポートでリクエストの受付を開始します。`listen()`はbindとlistenを一度にやりますが、ポート番号を先に知る必要があるので分けています
|
||||
- **シャットダウン順序** — WebViewのウインドウが閉じたら`svr.stop()`でサーバーを止め、`server_thread.join()`でスレッドの終了を待ちます。逆順だとWebViewがサーバーにアクセスできなくなります
|
||||
|
||||
5章の`signal_handler`は不要になります。デスクトップアプリではウインドウを閉じることがアプリの終了を意味するからです。
|
||||
|
||||
## 6.3 cpp-embedlib で静的ファイルを埋め込む
|
||||
|
||||
5章では`public/`ディレクトリからファイルを配信していました。これだと配布時に`public/`も一緒に渡す必要があります。[cpp-embedlib](https://github.com/yhirose/cpp-embedlib)を使うと、HTML・CSS・JavaScriptをバイナリに埋め込んで、配布物をバイナリ1つにまとめられます。
|
||||
|
||||
### CMakeLists.txt
|
||||
|
||||
cpp-embedlibを取得し、`public/`を埋め込みます。
|
||||
|
||||
```cmake
|
||||
# cpp-embedlib
|
||||
FetchContent_Declare(cpp-embedlib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-embedlib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp-embedlib)
|
||||
|
||||
# public/ ディレクトリをバイナリに埋め込む
|
||||
cpp_embedlib_add(WebAssets
|
||||
FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/public
|
||||
NAMESPACE Web
|
||||
)
|
||||
|
||||
target_link_libraries(translate-app PRIVATE
|
||||
WebAssets # 埋め込みファイル
|
||||
cpp-embedlib-httplib # cpp-httplib連携
|
||||
)
|
||||
```
|
||||
|
||||
`cpp_embedlib_add`は、`public/`配下のファイルをコンパイル時にバイナリに変換し、`WebAssets`という静的ライブラリを作ります。リンクすると`Web::FS`というオブジェクトから埋め込みファイルにアクセスできます。`cpp-embedlib-httplib`は`httplib::mount()`関数を提供するヘルパーライブラリです。
|
||||
|
||||
### set_mount_point を httplib::mount に置き換える
|
||||
|
||||
5章の`set_mount_point`をcpp-embedlibの`httplib::mount`に置き換えるだけです。
|
||||
|
||||
```cpp
|
||||
#include <cpp-embedlib-httplib.h>
|
||||
#include "WebAssets.h"
|
||||
|
||||
// 5章:
|
||||
// svr.set_mount_point("/", "./public");
|
||||
|
||||
// 6章:
|
||||
httplib::mount(svr, Web::FS);
|
||||
```
|
||||
|
||||
`httplib::mount`は、`Web::FS`に埋め込まれたファイルをHTTPで配信するハンドラを登録します。MIMEタイプはファイルの拡張子から自動判定するので、`Content-Type`を手動で設定する必要はありません。
|
||||
|
||||
ファイルの中身はバイナリのデータセグメントに直接マップしているので、メモリコピーもヒープ割り当ても発生しません。
|
||||
|
||||
## 6.4 macOS: Editメニューの追加
|
||||
|
||||
入力欄に`Cmd+V`でテキストをペーストしようとすると、動かないことに気づくはずです。macOSでは、`Cmd+V`(ペースト)や`Cmd+C`(コピー)などのキーボードショートカットは、アプリケーションのメニューバーを経由してWebViewに届きます。webview/webviewはメニューバーを作らないので、これらのショートカットが効きません。Objective-CランタイムAPIを使ってEditメニューを追加する必要があります。
|
||||
|
||||
```cpp
|
||||
#ifdef __APPLE__
|
||||
#include <objc/objc-runtime.h>
|
||||
|
||||
void setup_macos_edit_menu() {
|
||||
auto cls = [](const char *n) { return (id)objc_getClass(n); };
|
||||
auto sel = sel_registerName;
|
||||
auto msg = reinterpret_cast<id (*)(id, SEL)>(objc_msgSend);
|
||||
auto msg_s = reinterpret_cast<id (*)(id, SEL, const char *)>(objc_msgSend);
|
||||
auto msg_id = reinterpret_cast<id (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_v = reinterpret_cast<void (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_mi = reinterpret_cast<id (*)(id, SEL, id, SEL, id)>(objc_msgSend);
|
||||
|
||||
auto str = [&](const char *s) {
|
||||
return msg_s(cls("NSString"), sel("stringWithUTF8String:"), s);
|
||||
};
|
||||
|
||||
id app = msg(cls("NSApplication"), sel("sharedApplication"));
|
||||
id mainMenu = msg(msg(cls("NSMenu"), sel("alloc")), sel("init"));
|
||||
id editItem = msg(msg(cls("NSMenuItem"), sel("alloc")), sel("init"));
|
||||
id editMenu = msg_id(msg(cls("NSMenu"), sel("alloc")),
|
||||
sel("initWithTitle:"), str("Edit"));
|
||||
|
||||
struct { const char *title; const char *action; const char *key; } items[] = {
|
||||
{"Undo", "undo:", "z"},
|
||||
{"Redo", "redo:", "Z"},
|
||||
{"Cut", "cut:", "x"},
|
||||
{"Copy", "copy:", "c"},
|
||||
{"Paste", "paste:", "v"},
|
||||
{"Select All", "selectAll:", "a"},
|
||||
};
|
||||
|
||||
for (auto &[title, action, key] : items) {
|
||||
id mi = msg_mi(msg(cls("NSMenuItem"), sel("alloc")),
|
||||
sel("initWithTitle:action:keyEquivalent:"),
|
||||
str(title), sel(action), str(key));
|
||||
msg_v(editMenu, sel("addItem:"), mi);
|
||||
}
|
||||
|
||||
msg_v(editItem, sel("setSubmenu:"), editMenu);
|
||||
msg_v(mainMenu, sel("addItem:"), editItem);
|
||||
msg_v(app, sel("setMainMenu:"), mainMenu);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
`w.run()`の前に呼び出します。
|
||||
|
||||
```cpp
|
||||
#ifdef __APPLE__
|
||||
setup_macos_edit_menu();
|
||||
#endif
|
||||
w.run();
|
||||
```
|
||||
|
||||
WindowsとLinuxでは、キーボードショートカットはメニューバーを介さずフォーカスのあるコントロールに直接届くので、この対処はmacOS固有です。
|
||||
|
||||
## 6.5 全体のコード
|
||||
|
||||
<details>
|
||||
<summary data-file="CMakeLists.txt">全体のコード(CMakeLists.txt)</summary>
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(translate-app CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# llama.cpp
|
||||
FetchContent_Declare(llama
|
||||
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(llama)
|
||||
|
||||
# cpp-httplib
|
||||
FetchContent_Declare(httplib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# nlohmann/json
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# cpp-llamalib
|
||||
FetchContent_Declare(cpp_llamalib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp_llamalib)
|
||||
|
||||
# webview/webview
|
||||
FetchContent_Declare(webview
|
||||
GIT_REPOSITORY https://github.com/webview/webview
|
||||
GIT_TAG master
|
||||
)
|
||||
FetchContent_MakeAvailable(webview)
|
||||
|
||||
# cpp-embedlib
|
||||
FetchContent_Declare(cpp-embedlib
|
||||
GIT_REPOSITORY https://github.com/yhirose/cpp-embedlib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(cpp-embedlib)
|
||||
|
||||
# public/ ディレクトリをバイナリに埋め込む
|
||||
cpp_embedlib_add(WebAssets
|
||||
FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/public
|
||||
NAMESPACE Web
|
||||
)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_executable(translate-app src/main.cpp)
|
||||
|
||||
target_link_libraries(translate-app PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
cpp-llamalib
|
||||
OpenSSL::SSL OpenSSL::Crypto
|
||||
WebAssets
|
||||
cpp-embedlib-httplib
|
||||
webview::core
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(translate-app PRIVATE
|
||||
"-framework CoreFoundation"
|
||||
"-framework Security"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(translate-app PRIVATE
|
||||
CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary data-file="main.cpp">全体のコード(main.cpp)</summary>
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cpp-llamalib.h>
|
||||
#include <cpp-embedlib-httplib.h>
|
||||
#include "WebAssets.h"
|
||||
#include "webview/webview.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <objc/objc-runtime.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// macOS Editメニュー(Cmd+C/V/X/AにはEditメニューが必要)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
#ifdef __APPLE__
|
||||
void setup_macos_edit_menu() {
|
||||
auto cls = [](const char *n) { return (id)objc_getClass(n); };
|
||||
auto sel = sel_registerName;
|
||||
auto msg = reinterpret_cast<id (*)(id, SEL)>(objc_msgSend);
|
||||
auto msg_s = reinterpret_cast<id (*)(id, SEL, const char *)>(objc_msgSend);
|
||||
auto msg_id = reinterpret_cast<id (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_v = reinterpret_cast<void (*)(id, SEL, id)>(objc_msgSend);
|
||||
auto msg_mi = reinterpret_cast<id (*)(id, SEL, id, SEL, id)>(objc_msgSend);
|
||||
|
||||
auto str = [&](const char *s) {
|
||||
return msg_s(cls("NSString"), sel("stringWithUTF8String:"), s);
|
||||
};
|
||||
|
||||
id app = msg(cls("NSApplication"), sel("sharedApplication"));
|
||||
id mainMenu = msg(msg(cls("NSMenu"), sel("alloc")), sel("init"));
|
||||
id editItem = msg(msg(cls("NSMenuItem"), sel("alloc")), sel("init"));
|
||||
id editMenu = msg_id(msg(cls("NSMenu"), sel("alloc")),
|
||||
sel("initWithTitle:"), str("Edit"));
|
||||
|
||||
struct { const char *title; const char *action; const char *key; } items[] = {
|
||||
{"Undo", "undo:", "z"},
|
||||
{"Redo", "redo:", "Z"},
|
||||
{"Cut", "cut:", "x"},
|
||||
{"Copy", "copy:", "c"},
|
||||
{"Paste", "paste:", "v"},
|
||||
{"Select All", "selectAll:", "a"},
|
||||
};
|
||||
|
||||
for (auto &[title, action, key] : items) {
|
||||
id mi = msg_mi(msg(cls("NSMenuItem"), sel("alloc")),
|
||||
sel("initWithTitle:action:keyEquivalent:"),
|
||||
str(title), sel(action), str(key));
|
||||
msg_v(editMenu, sel("addItem:"), mi);
|
||||
}
|
||||
|
||||
msg_v(editItem, sel("setSubmenu:"), editMenu);
|
||||
msg_v(mainMenu, sel("addItem:"), editItem);
|
||||
msg_v(app, sel("setMainMenu:"), mainMenu);
|
||||
}
|
||||
#endif
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// モデル定義
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
struct ModelInfo {
|
||||
std::string name;
|
||||
std::string params;
|
||||
std::string size;
|
||||
std::string repo;
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
const std::vector<ModelInfo> MODELS = {
|
||||
{
|
||||
.name = "gemma-2-2b-it",
|
||||
.params = "2B",
|
||||
.size = "1.6 GB",
|
||||
.repo = "bartowski/gemma-2-2b-it-GGUF",
|
||||
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "gemma-2-9b-it",
|
||||
.params = "9B",
|
||||
.size = "5.8 GB",
|
||||
.repo = "bartowski/gemma-2-9b-it-GGUF",
|
||||
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
|
||||
},
|
||||
{
|
||||
.name = "Llama-3.1-8B-Instruct",
|
||||
.params = "8B",
|
||||
.size = "4.9 GB",
|
||||
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
|
||||
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
},
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// モデル保存ディレクトリ
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
std::filesystem::path get_models_dir() {
|
||||
#ifdef _WIN32
|
||||
auto env = std::getenv("APPDATA");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / "translate-app" / "models";
|
||||
#else
|
||||
auto env = std::getenv("HOME");
|
||||
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
|
||||
return base / ".translate-app" / "models";
|
||||
#endif
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// モデルダウンロード
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// progress_cbがfalseを返したらダウンロードを中断する
|
||||
bool download_model(const ModelInfo &model,
|
||||
std::function<bool(int)> progress_cb) {
|
||||
httplib::Client cli("https://huggingface.co");
|
||||
cli.set_follow_location(true); // Hugging FaceはCDNにリダイレクトする
|
||||
cli.set_read_timeout(std::chrono::hours(1)); // 大きなモデルに備えて長めに
|
||||
|
||||
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
|
||||
auto path = get_models_dir() / model.filename;
|
||||
auto tmp_path = std::filesystem::path(path).concat(".tmp");
|
||||
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
if (!ofs) { return false; }
|
||||
|
||||
auto res = cli.Get(url,
|
||||
// content_receiver: チャンクごとにデータを受け取ってファイルに書き込む
|
||||
[&](const char *data, size_t len) {
|
||||
ofs.write(data, len);
|
||||
return ofs.good();
|
||||
},
|
||||
// progress: ダウンロード進捗を通知(falseを返すと中断)
|
||||
[&, last_pct = -1](size_t current, size_t total) mutable {
|
||||
int pct = total ? (int)(current * 100 / total) : 0;
|
||||
if (pct == last_pct) return true; // 同じ値なら通知をスキップ
|
||||
last_pct = pct;
|
||||
return progress_cb(pct);
|
||||
});
|
||||
|
||||
ofs.close();
|
||||
|
||||
if (!res || res->status != 200) {
|
||||
std::filesystem::remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ダウンロード完了後にリネーム
|
||||
std::filesystem::rename(tmp_path, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// サーバー
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
int main() {
|
||||
httplib::Server svr;
|
||||
// モデル保存ディレクトリを作成
|
||||
auto models_dir = get_models_dir();
|
||||
std::filesystem::create_directories(models_dir);
|
||||
|
||||
// デフォルトモデルが未ダウンロードなら自動取得
|
||||
std::string selected_model = MODELS[0].filename;
|
||||
auto path = models_dir / selected_model;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::cout << "Downloading " << selected_model << "..." << std::endl;
|
||||
if (!download_model(MODELS[0], [](int pct) {
|
||||
std::cout << "\r" << pct << "%" << std::flush;
|
||||
return true;
|
||||
})) {
|
||||
std::cerr << "\nFailed to download model." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
auto llm = llamalib::Llama{path};
|
||||
std::mutex llm_mutex; // モデル切り替え中のアクセスを保護する
|
||||
|
||||
// LLM推論は時間がかかるのでタイムアウトを長めに設定(デフォルトは5秒)
|
||||
svr.set_read_timeout(300);
|
||||
svr.set_write_timeout(300);
|
||||
|
||||
svr.set_logger([](const auto &req, const auto &res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status
|
||||
<< std::endl;
|
||||
});
|
||||
|
||||
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- 翻訳エンドポイント(2章) -----------------------------------------
|
||||
|
||||
svr.Post("/translate",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(),
|
||||
"application/json");
|
||||
} catch (const std::exception &e) {
|
||||
res.status = 500;
|
||||
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
// --- SSEストリーミング翻訳(3章)--------------------------------------
|
||||
|
||||
svr.Post("/translate/stream",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input.contains("text") || !input["text"].is_string() ||
|
||||
input["text"].get<std::string>().empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'text' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = input["text"].get<std::string>();
|
||||
auto target_lang = input.value("target_lang", "ja");
|
||||
|
||||
auto prompt = "Translate the following text to " + target_lang +
|
||||
". Output only the translation, nothing else.\n\n" + text;
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, prompt](size_t, httplib::DataSink &sink) {
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
try {
|
||||
llm.chat(prompt, [&](std::string_view token) {
|
||||
sink.os << "data: "
|
||||
<< json(std::string(token)).dump(
|
||||
-1, ' ', false, json::error_handler_t::replace)
|
||||
<< "\n\n";
|
||||
return sink.os.good(); // 切断されたら推論を中断
|
||||
});
|
||||
sink.os << "data: [DONE]\n\n";
|
||||
} catch (const std::exception &e) {
|
||||
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// --- モデル一覧(4章) -------------------------------------------------
|
||||
|
||||
svr.Get("/models",
|
||||
[&](const httplib::Request &, httplib::Response &res) {
|
||||
auto models_dir = get_models_dir();
|
||||
auto arr = json::array();
|
||||
for (const auto &m : MODELS) {
|
||||
auto path = models_dir / m.filename;
|
||||
arr.push_back({
|
||||
{"name", m.name},
|
||||
{"params", m.params},
|
||||
{"size", m.size},
|
||||
{"downloaded", std::filesystem::exists(path)},
|
||||
{"selected", m.filename == selected_model},
|
||||
});
|
||||
}
|
||||
res.set_content(json{{"models", arr}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
// --- モデル選択(4章) -------------------------------------------------
|
||||
|
||||
svr.Post("/models/select",
|
||||
[&](const httplib::Request &req, httplib::Response &res) {
|
||||
auto input = json::parse(req.body, nullptr, false);
|
||||
if (input.is_discarded() || !input.contains("model")) {
|
||||
res.status = 400;
|
||||
res.set_content(json{{"error", "'model' is required"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = input["model"].get<std::string>();
|
||||
|
||||
auto it = std::find_if(MODELS.begin(), MODELS.end(),
|
||||
[&](const ModelInfo &m) { return m.name == name; });
|
||||
|
||||
if (it == MODELS.end()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"error", "Unknown model"}}.dump(),
|
||||
"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &model = *it;
|
||||
|
||||
// 常にSSEで応答する(DL済みでも未DLでも同じ形式)
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream",
|
||||
[&, model](size_t, httplib::DataSink &sink) {
|
||||
// SSEイベント送信ヘルパー
|
||||
auto send = [&](const json &event) {
|
||||
sink.os << "data: " << event.dump() << "\n\n";
|
||||
};
|
||||
|
||||
// 未ダウンロードならダウンロード(進捗をSSEで通知)
|
||||
auto path = get_models_dir() / model.filename;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
bool ok = download_model(model, [&](int pct) {
|
||||
send({{"status", "downloading"}, {"progress", pct}});
|
||||
return sink.os.good(); // クライアント切断時にダウンロードを中断
|
||||
});
|
||||
if (!ok) {
|
||||
send({{"status", "error"}, {"message", "Download failed"}});
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// モデルをロードして切り替え
|
||||
send({{"status", "loading"}});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(llm_mutex);
|
||||
llm = llamalib::Llama{path};
|
||||
selected_model = model.filename;
|
||||
}
|
||||
|
||||
send({{"status", "ready"}});
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// --- 埋め込みファイル配信(6章) ---------------------------------------
|
||||
// 5章: svr.set_mount_point("/", "./public");
|
||||
httplib::mount(svr, Web::FS);
|
||||
|
||||
// サーバーをバックグラウンドスレッドで起動
|
||||
auto port = svr.bind_to_any_port("127.0.0.1");
|
||||
std::thread server_thread([&]() { svr.listen_after_bind(); });
|
||||
|
||||
std::cout << "Listening on http://127.0.0.1:" << port << std::endl;
|
||||
|
||||
// WebViewでUIを表示
|
||||
webview::webview w(false, nullptr);
|
||||
w.set_title("Translate App");
|
||||
w.set_size(1024, 768, WEBVIEW_HINT_NONE);
|
||||
w.navigate("http://127.0.0.1:" + std::to_string(port));
|
||||
|
||||
#ifdef __APPLE__
|
||||
setup_macos_edit_menu();
|
||||
#endif
|
||||
w.run(); // ウインドウが閉じるまでブロック
|
||||
|
||||
// ウインドウが閉じたらサーバーも停止
|
||||
svr.stop();
|
||||
server_thread.join();
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
5章からの変更点をまとめると:
|
||||
|
||||
- `#include <csignal>` → `#include <thread>`, `<cpp-embedlib-httplib.h>`, `"WebAssets.h"`, `"webview/webview.h"`
|
||||
- `signal_handler`関数を削除
|
||||
- `svr.set_mount_point("/", "./public")` → `httplib::mount(svr, Web::FS)`
|
||||
- `svr.listen("127.0.0.1", 8080)` → `bind_to_any_port` + `listen_after_bind` + WebViewのイベントループ
|
||||
|
||||
ハンドラのコードは1行も変わっていません。5章まで作ってきたREST API・SSEストリーミング・モデル管理がそのまま動きます。
|
||||
|
||||
## 6.6 ビルドと動作確認
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
起動します。
|
||||
|
||||
```bash
|
||||
./build/translate-app
|
||||
```
|
||||
|
||||
ブラウザは不要です。ウインドウが自動で開きます。5章と同じUIがそのまま表示され、翻訳やモデル切り替えがすべてそのまま動きます。
|
||||
|
||||
ウインドウを閉じるとサーバーも自動で終了します。`Ctrl+C`は不要です。
|
||||
|
||||
### 何が配布に必要か
|
||||
|
||||
配布に必要なのは:
|
||||
|
||||
- `translate-app`バイナリ1つ
|
||||
|
||||
これだけです。`public/`ディレクトリは不要です。HTML・CSS・JavaScriptはバイナリに埋め込まれています。モデルファイルは初回起動時に自動ダウンロードするので、ユーザーに事前準備を求める必要もありません。
|
||||
|
||||
## 次の章へ
|
||||
|
||||
お疲れさまでした!🎉
|
||||
|
||||
1章では`/health`が`{"status":"ok"}`を返すだけでした。それが今、テキストを入力すればリアルタイムで翻訳が流れ、ドロップダウンからモデルを切り替えれば自動でダウンロードが始まり、ウインドウを閉じればサーバーも一緒に終了する―そんなデスクトップアプリになりました。しかもバイナリ1つで配れます。
|
||||
|
||||
6章で変えたのは、静的ファイルの配信方法とサーバーの起動方法だけです。ハンドラのコードは1行も変わっていません。5章までに積み上げてきたREST API・SSEストリーミング・モデル管理が、そのままデスクトップアプリとして動いています。
|
||||
|
||||
次の章では視点を変えて、llama.cpp本家の`llama-server`のコードを読みます。本書のシンプルなサーバーと、プロダクション品質のサーバーを比較して、設計判断の違いとその理由を学びましょう。
|
||||
|
||||
**Next:** [llama.cpp本家のサーバー実装をコードリーディング](../ch07-code-reading)
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
title: "7. llama.cpp本家のサーバー実装をコードリーディング"
|
||||
order: 7
|
||||
|
||||
---
|
||||
|
||||
6章かけてゼロから翻訳デスクトップアプリを作りました。動くものは完成しましたが、あくまで「学習用」の実装です。では「プロダクション品質」のコードはどう違うのか? llama.cppに同梱されている公式サーバー`llama-server`のソースコードを読んで、比較してみましょう。
|
||||
|
||||
`llama-server`は`llama.cpp/tools/server/`にあります。同じcpp-httplibを使っているので、コードの読み方はこれまでの章と同じです。
|
||||
|
||||
## 7.1 ソースコードの場所
|
||||
|
||||
```ascii
|
||||
llama.cpp/tools/server/
|
||||
├── server.cpp # メインのサーバー実装
|
||||
├── httplib.h # cpp-httplib(同梱版)
|
||||
└── ...
|
||||
```
|
||||
|
||||
ファイルは1つの`server.cpp`にまとまっています。数千行ありますが、構造を知っていれば読むべき箇所は絞れます。
|
||||
|
||||
## 7.2 OpenAI互換API
|
||||
|
||||
ここまで作ってきたサーバーと`llama-server`の最も大きな違いはAPIの設計です。
|
||||
|
||||
**私たちのAPI:**
|
||||
|
||||
```text
|
||||
POST /translate → {"translation": "..."}
|
||||
POST /translate/stream → SSE: data: "token"
|
||||
```
|
||||
|
||||
**llama-serverのAPI:**
|
||||
|
||||
```text
|
||||
POST /v1/chat/completions → OpenAI互換のJSON
|
||||
POST /v1/completions → OpenAI互換のJSON
|
||||
POST /v1/embeddings → テキスト埋め込みベクトル
|
||||
```
|
||||
|
||||
`llama-server`は[OpenAIのAPI仕様](https://platform.openai.com/docs/api-reference)に合わせています。つまり、OpenAIの公式クライアントライブラリ(Pythonの`openai`パッケージなど)がそのまま動きます。
|
||||
|
||||
```python
|
||||
# OpenAIクライアントでllama-serverに接続する例
|
||||
from openai import OpenAI
|
||||
client = OpenAI(base_url="http://localhost:8080/v1", api_key="dummy")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="local-model",
|
||||
messages=[{"role": "user", "content": "Hello!"}]
|
||||
)
|
||||
```
|
||||
|
||||
既存のツールやライブラリとの互換性を持たせるかどうかは、大きな設計判断です。私たちは翻訳専用のAPIをシンプルに設計しましたが、汎用のサーバーを作るならOpenAI互換が事実上の標準になっています。
|
||||
|
||||
## 7.3 同時リクエスト処理
|
||||
|
||||
私たちのサーバーはリクエストを1つずつ処理します。翻訳中に別のリクエストが来ると、前の推論が終わるまで待ちます。1人で使うデスクトップアプリなら問題ありませんが、複数人で共有するサーバーでは困ります。
|
||||
|
||||
`llama-server`は**スロット**という仕組みで同時リクエストを処理します。
|
||||
|
||||

|
||||
|
||||
ポイントは、各スロットのトークンを**1つずつ順番に**ではなく、**まとめて1回のバッチ**で推論することです。GPUは並列処理が得意なので、2人分を同時に処理しても1人分とほとんど変わらない時間で済みます。これを「連続バッチ処理(continuous batching)」と呼びます。
|
||||
|
||||
私たちのサーバーではcpp-httplibのスレッドプールが各リクエストに1スレッドを割り当てますが、推論自体は`llm.chat()`の中でシングルスレッドです。`llama-server`はこの推論部分を共有のバッチ処理ループに集約しています。
|
||||
|
||||
## 7.4 SSEフォーマットの違い
|
||||
|
||||
ストリーミングの仕組み自体は同じ(`set_chunked_content_provider` + SSE)ですが、送るデータのフォーマットが違います。
|
||||
|
||||
**私たちの形式:**
|
||||
|
||||
```text
|
||||
data: "去年の"
|
||||
data: "春に"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
**llama-server(OpenAI互換):**
|
||||
|
||||
```text
|
||||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"去年の"}}]}
|
||||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"春に"}}]}
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
私たちの形式はトークンだけを送るシンプルなものです。`llama-server`はOpenAI互換のため、1つのトークンにもJSONのラッパーが付きます。冗長に見えますが、`id`でリクエストを識別したり、`finish_reason`で停止理由を返せたりと、クライアントにとって便利な情報が含まれています。
|
||||
|
||||
## 7.5 KVキャッシュの再利用
|
||||
|
||||
私たちのサーバーでは、リクエストのたびにプロンプト全体をゼロから処理しています。翻訳アプリのプロンプトは短い("Translate the following text to ja..." + 入力テキスト)ので、これで問題ありません。
|
||||
|
||||
`llama-server`は、前のリクエストと共通するプロンプトのprefixがある場合、その部分のKVキャッシュを再利用します。
|
||||
|
||||

|
||||
|
||||
長いシステムプロンプトやfew-shot例を毎回送るチャットボットでは、これだけで応答時間が大幅に短縮されます。数千トークンのシステムプロンプトを毎回処理するのと、キャッシュから一瞬で読むのとでは、体感が全く違います。
|
||||
|
||||
翻訳アプリではシステムプロンプトが1文だけなので効果は限定的ですが、自分のアプリに応用するときは意識したい最適化です。
|
||||
|
||||
## 7.6 構造化出力
|
||||
|
||||
翻訳APIはプレーンテキストを返すので、出力形式を制約する必要がありませんでした。でも、LLMにJSONで返させたい場合はどうでしょう?
|
||||
|
||||
```text
|
||||
プロンプト: 以下の文の感情を分析してJSONで返してください。
|
||||
LLMの出力(期待): {"sentiment": "positive", "score": 0.8}
|
||||
LLMの出力(現実): 感情分析の結果は以下の通りです。{"sentiment": ...
|
||||
```
|
||||
|
||||
LLMは指示を無視して余計なテキストを付けることがあります。`llama-server`はこの問題を**文法制約(grammar)**で解決しています。
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v1/chat/completions \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Analyze sentiment..."}],
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
|
||||
"score": {"type": "number"}
|
||||
},
|
||||
"required": ["sentiment", "score"]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
`json_schema`を指定すると、LLMのトークン生成時に文法に合わないトークンを除外します。出力が必ず有効なJSONになるので、`json::parse`が失敗する心配がありません。
|
||||
|
||||
LLMをアプリに組み込むとき、出力を確実にパースできるかどうかは信頼性に直結します。翻訳のようなフリーテキスト出力では不要ですが、APIのレスポンスとして構造化データを返す用途では必須の機能です。
|
||||
|
||||
## 7.7 まとめ
|
||||
|
||||
ここまでの違いを整理します。
|
||||
|
||||
| 観点 | 私たちのサーバー | llama-server |
|
||||
|------|-------------|--------------|
|
||||
| API設計 | 翻訳専用 | OpenAI互換 |
|
||||
| 同時リクエスト | 1つずつ処理 | スロット+連続バッチ |
|
||||
| SSEフォーマット | トークンのみ | OpenAI互換JSON |
|
||||
| KVキャッシュ | 毎回クリア | prefixを再利用 |
|
||||
| 構造化出力 | なし | JSON Schema/文法制約 |
|
||||
| コード量 | 約200行 | 数千行 |
|
||||
|
||||
私たちのコードがシンプルなのは、「デスクトップアプリで1人が使う」という前提があるからです。複数人に提供するサーバーや、既存のエコシステムと連携するサーバーを作るなら、`llama-server`の設計が参考になります。
|
||||
|
||||
逆に言えば、200行のコードでも翻訳アプリとしては十分に動きます。「必要な分だけ作る」ことの価値も、このコードリーディングから感じてもらえたら嬉しいです。
|
||||
|
||||
## 次の章へ
|
||||
|
||||
次の章では、ここまで作ったアプリを自分のライブラリに差し替えてカスタマイズするためのポイントをまとめます。
|
||||
|
||||
**Next:** [自分だけのアプリにカスタマイズする](../ch08-customization)
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: "8. 自分だけのアプリにカスタマイズする"
|
||||
order: 8
|
||||
|
||||
---
|
||||
|
||||
7章までで翻訳デスクトップアプリが完成し、プロダクション品質のコードとの違いも学びました。この章では、ここまで作ったアプリを**自分だけのアプリに作り変える**ためのポイントをまとめます。
|
||||
|
||||
翻訳アプリはあくまで題材です。llama.cppを自分のライブラリに差し替えれば、同じ構成でどんなアプリでも作れます。
|
||||
|
||||
## 8.1 ビルド設定を差し替える
|
||||
|
||||
まず`CMakeLists.txt`で、llama.cpp関連の`FetchContent`を自分のライブラリに置き換えます。
|
||||
|
||||
```cmake
|
||||
# 削除: llama.cpp と cpp-llamalib の FetchContent
|
||||
|
||||
# 追加: 自分のライブラリ
|
||||
FetchContent_Declare(my_lib
|
||||
GIT_REPOSITORY https://github.com/yourname/my-lib
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(my_lib)
|
||||
|
||||
target_link_libraries(my-app PRIVATE
|
||||
httplib::httplib
|
||||
nlohmann_json::nlohmann_json
|
||||
my_lib # cpp-llamalib の代わりに自分のライブラリ
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
ライブラリがCMakeに対応していない場合は、ヘッダーファイルとソースファイルを直接`src/`に置いて`add_executable`に追加すればOKです。cpp-httplibやnlohmann/json、webviewはそのまま残します。
|
||||
|
||||
## 8.2 APIを自分のタスクに合わせる
|
||||
|
||||
翻訳APIのエンドポイントとパラメータを、自分のタスクに合わせて変更します。
|
||||
|
||||
| 翻訳アプリ | 自分のアプリ(例: 画像処理) |
|
||||
|---|---|
|
||||
| `POST /translate` | `POST /process` |
|
||||
| `{"text": "...", "target_lang": "ja"}` | `{"image": "base64...", "filter": "blur"}` |
|
||||
| `POST /translate/stream` | `POST /process/stream` |
|
||||
| `GET /models` | `GET /filters`や`GET /presets` |
|
||||
|
||||
個々のハンドラの中身も書き換えます。例えば`llm.chat()`を呼んでいた箇所を、自分のライブラリのAPIに差し替えるだけです。
|
||||
|
||||
```cpp
|
||||
// Before: LLM翻訳
|
||||
auto translation = llm.chat(prompt);
|
||||
res.set_content(json{{"translation", translation}}.dump(), "application/json");
|
||||
|
||||
// After: 例えば画像処理ライブラリの場合
|
||||
auto result = my_lib::process(input_image, options);
|
||||
res.set_content(json{{"result", result}}.dump(), "application/json");
|
||||
```
|
||||
|
||||
SSEストリーミングも同じです。コールバックで進捗を返す関数があれば、3章と同じパターンで逐次レスポンスを返せます。LLMに限らず、処理に時間がかかるタスクならどれでも使えます。画像処理の進捗、データ変換のステップ、長時間の計算結果など、用途は様々です。
|
||||
|
||||
## 8.3 設計上の注意点
|
||||
|
||||
### 初期化コストが高いライブラリ
|
||||
|
||||
本書ではLLMモデルを`main()`の先頭でロードし、変数に保持しています。これは意図的な設計です。リクエストのたびにモデルをロードすると数秒かかるので、起動時に1回だけロードして使い回しています。大きなデータファイルの読み込みやGPUリソースの確保など、初期化が重いライブラリでも同じアプローチが使えます。
|
||||
|
||||
### スレッド安全性
|
||||
|
||||
cpp-httplibはスレッドプールでリクエストを並行処理します。4章ではモデル切り替え時に`llm`オブジェクトが上書きされる問題を`std::mutex`で保護しました。自分のライブラリを組み込む場合も同じパターンが使えます。ライブラリがスレッドセーフでない場合や、オブジェクトの差し替えが発生する場合は`std::mutex`で保護してください。
|
||||
|
||||
## 8.4 UIをカスタマイズする
|
||||
|
||||
`public/`の3ファイルを編集します。
|
||||
|
||||
- **`index.html`** — 入力フォームの構成を変えます。`<textarea>`を`<input type="file">`にしたり、パラメータの入力欄を追加したり
|
||||
- **`style.css`** — レイアウトやカラーを変更します。2カラムのままでも、1カラムに変えても
|
||||
- **`script.js`** — `fetch()`の送信先URLとリクエストボディ、レスポンスの表示方法を書き換えます
|
||||
|
||||
サーバー側のコードは変えなくても、HTMLを差し替えるだけで全く別のアプリに見えます。静的ファイルなのでサーバーの再起動なしにブラウザをリロードするだけで確認でき、試行錯誤しやすいです。
|
||||
|
||||
本書では素のHTML・CSS・JavaScriptで書きましたが、VueやReactなどのフロントエンドフレームワークやCSSフレームワークを組み合わせれば、さらに使い勝手の良いアプリに仕上げることができます。
|
||||
|
||||
## 8.5 配布するときの注意点
|
||||
|
||||
### ライセンス
|
||||
|
||||
使っているライブラリのライセンスを確認してください。cpp-httplib(MIT)、nlohmann/json(MIT)、webview(MIT)はいずれも商用利用可能です。自分のライブラリや、それが依存するライブラリのライセンスも忘れずに確認しましょう。
|
||||
|
||||
### モデルやデータファイル
|
||||
|
||||
4章で作ったダウンロード機能は、LLMモデルに限らず使えます。大きなデータファイルが必要なアプリなら、同じパターンで初回起動時に自動ダウンロードさせると、バイナリサイズを抑えつつユーザーの手間を省けます。
|
||||
|
||||
データが小さければ、cpp-embedlibでバイナリに埋め込んでしまうのも手です。
|
||||
|
||||
### クロスプラットフォームビルド
|
||||
|
||||
webviewはmacOS・Linux・Windowsに対応しています。各プラットフォーム向けにビルドする場合:
|
||||
|
||||
- **macOS** — 追加の依存なし
|
||||
- **Linux** — `libwebkit2gtk-4.1-dev`が必要
|
||||
- **Windows** — WebView2ランタイムが必要(Windows 11は標準搭載)
|
||||
|
||||
CI(GitHub Actionsなど)でクロスプラットフォームビルドを自動化するのもおすすめです。
|
||||
|
||||
## おわりに
|
||||
|
||||
最後まで読んでくださり、ありがとうございます。🙏
|
||||
|
||||
この本は、1章の`/health`が`{"status":"ok"}`を返すところから始まりました。そこからREST API、SSEストリーミング、Hugging Faceからのモデルダウンロード、ブラウザで動くWeb UI、そしてシングルバイナリのデスクトップアプリへ。7章では`llama-server`のコードを読んで、プロダクション品質のサーバーとの設計の違いを学びました。長い道のりでしたが、ここまで付き合ってくださったことに心から感謝します。
|
||||
|
||||
振り返ると、cpp-httplibのいくつかの主要な機能を実際に使いました。
|
||||
|
||||
- **サーバー**: ルーティング、JSONレスポンス、`set_chunked_content_provider`によるSSEストリーミング、`set_mount_point`による静的ファイル配信
|
||||
- **クライアント**: HTTPS接続、リダイレクト追従、コンテンツレシーバーによる大容量ダウンロード、進捗コールバック
|
||||
- **WebView連携**: `bind_to_any_port` + `listen_after_bind`でバックグラウンドスレッド化
|
||||
|
||||
cpp-httplibにはこの他にも、マルチパートによるファイルアップロード、認証、タイムアウト制御、圧縮、レンジリクエストなど便利な機能があります。詳しくは [A Tour of cpp-httplib](../../tour/) をご覧ください。
|
||||
|
||||
これらのパターンは翻訳アプリに限りません。自分のC++ライブラリにWeb APIを付けたい、ブラウザUIで操作できるようにしたい、配布しやすいデスクトップアプリにしたい―そんなときに、この本がリファレンスになれば嬉しいです。
|
||||
|
||||
あなたのライブラリで、あなただけのアプリを作ってみてください。Happy hacking! 🚀
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: "Building a Desktop LLM App with cpp-httplib"
|
||||
order: 0
|
||||
|
||||
---
|
||||
|
||||
自分のC++ライブラリにWeb APIを追加したい、Electronライクなデスクトップアプリをサクッと作りたい―そう思ったことはありませんか? Rustなら「Tauri + axum」という選択肢がありますが、C++では難しいと諦めていませんか?
|
||||
|
||||
[cpp-httplib](https://github.com/yhirose/cpp-httplib)と[webview/webview](https://github.com/webview/webview)、そして[cpp-embedlib](https://github.com/yhirose/cpp-embedlib)を組み合わせれば、C++だけで同じアプローチが取れます。しかも配布しやすい、小さなシングルバイナリーのアプリケーションを作れます。
|
||||
|
||||
今回は、[llama.cpp](https://github.com/ggml-org/llama.cpp)を組み込んだLLM翻訳アプリを題材に、「REST API → SSEストリーミング → Web UI→デスクトップアプリ」と段階的に構築しながら、そのやり方を学んでいきましょう。もちろん、翻訳はあくまで題材です。llama.cppを自分のライブラリに置き換えれば、同じ構成で自分だけのアプリが作れます。
|
||||
|
||||

|
||||
|
||||
C++17の基本文法とHTTP(REST API)の基本がわかれば、すぐに始められます。🚀
|
||||
|
||||
## 目次
|
||||
|
||||
1. **[プロジェクト環境を作る](ch01-setup)** — 依存ライブラリの取得、ビルド設定、雛形コード
|
||||
2. **[llama.cppを組み込んでREST APIを作る](ch02-rest-api)** — JSONで翻訳結果を返すAPIの実装
|
||||
3. **[SSEでトークンストリーミングを追加する](ch03-sse-streaming)** — トークン単位の逐次レスポンス
|
||||
4. **[モデルの取得・管理機能を追加する](ch04-model-management)** — Hugging Faceからのダウンロードと切り替え
|
||||
5. **[Web UIを追加する](ch05-web-ui)** — ブラウザから操作できる翻訳画面
|
||||
6. **[WebViewでデスクトップアプリ化する](ch06-desktop-app)** — シングルバイナリのデスクトップアプリ
|
||||
7. **[llama.cpp本家のサーバー実装をコードリーディング](ch07-code-reading)** — プロダクション品質のコードとの比較
|
||||
8. **[自分だけのアプリにカスタマイズする](ch08-customization)** — 自分のライブラリへの差し替えと応用
|
||||
@@ -0,0 +1,36 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426 160" font-family="system-ui, sans-serif" font-size="14">
|
||||
<rect x="0" y="0" width="426" height="160" rx="8" fill="#f5f3ef"/>
|
||||
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto">
|
||||
<polygon points="0,0 7,2.5 0,5" fill="#198754"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- request 1 (y=16) -->
|
||||
<text x="94" y="35" fill="#333" font-weight="bold" text-anchor="end">リクエスト1:</text>
|
||||
<rect x="106" y="16" width="138" height="30" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1"/>
|
||||
<text x="175" y="36" fill="#333" text-anchor="middle">システムプロンプト</text>
|
||||
<text x="256" y="36" fill="#333" text-anchor="middle">+</text>
|
||||
<rect x="270" y="16" width="138" height="30" rx="4" fill="#cfe2ff" stroke="#0d6efd" stroke-width="1"/>
|
||||
<text x="339" y="36" fill="#333" text-anchor="middle">ユーザーの質問A</text>
|
||||
|
||||
<!-- annotation: cache save -->
|
||||
<text x="175" y="64" fill="#198754" font-size="11" text-anchor="middle">KV キャッシュに保存</text>
|
||||
|
||||
<!-- arrow -->
|
||||
<line x1="175" y1="70" x2="175" y2="90" stroke="#198754" stroke-width="1.2" marker-end="url(#arrowhead)"/>
|
||||
<text x="188" y="85" fill="#198754" font-size="11">再利用</text>
|
||||
|
||||
<!-- request 2 (y=96) -->
|
||||
<text x="94" y="115" fill="#333" font-weight="bold" text-anchor="end">リクエスト2:</text>
|
||||
<rect x="106" y="96" width="138" height="30" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1" stroke-dasharray="6,3"/>
|
||||
<text x="175" y="116" fill="#333" text-anchor="middle">システムプロンプト</text>
|
||||
<text x="256" y="116" fill="#333" text-anchor="middle">+</text>
|
||||
<rect x="270" y="96" width="138" height="30" rx="4" fill="#cfe2ff" stroke="#0d6efd" stroke-width="1"/>
|
||||
<text x="339" y="116" fill="#333" text-anchor="middle">ユーザーの質問B</text>
|
||||
|
||||
<!-- bottom labels -->
|
||||
<text x="175" y="144" fill="#198754" font-size="11" text-anchor="middle">再計算しない</text>
|
||||
<text x="339" y="144" fill="#0d6efd" font-size="11" text-anchor="middle">ここだけ計算</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 440 240" font-family="system-ui, sans-serif" font-size="14">
|
||||
<!-- outer box -->
|
||||
<rect x="0" y="0" width="440" height="240" rx="8" fill="#f5f3ef"/>
|
||||
<text x="20" y="28" font-weight="bold" font-size="16" fill="#333">llama-server</text>
|
||||
|
||||
<!-- slot 0 -->
|
||||
<rect x="20" y="46" width="400" height="32" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1"/>
|
||||
<text x="32" y="67" fill="#333">スロット0: ユーザーAのリクエスト</text>
|
||||
|
||||
<!-- slot 1 -->
|
||||
<rect x="20" y="86" width="400" height="32" rx="4" fill="#d1e7dd" stroke="#198754" stroke-width="1"/>
|
||||
<text x="32" y="107" fill="#333">スロット1: ユーザーBのリクエスト</text>
|
||||
|
||||
<!-- slot 2 -->
|
||||
<rect x="20" y="126" width="400" height="32" rx="4" fill="#e9ecef" stroke="#adb5bd" stroke-width="1"/>
|
||||
<text x="32" y="147" fill="#999">スロット2: (空き)</text>
|
||||
|
||||
<!-- slot 3 -->
|
||||
<rect x="20" y="166" width="400" height="32" rx="4" fill="#e9ecef" stroke="#adb5bd" stroke-width="1"/>
|
||||
<text x="32" y="187" fill="#999">スロット3: (空き)</text>
|
||||
|
||||
<!-- arrow + label -->
|
||||
<text x="20" y="224" fill="#333" font-size="13">→ アクティブなスロットをまとめて1回のバッチで推論</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -60,7 +60,7 @@ svr.Get("/stream", [](const auto &, auto &res) {
|
||||
```cpp
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
|
||||
auto res = cli.Post("/upload", {}, {
|
||||
auto res = cli.Post("/upload", {}, {}, {
|
||||
httplib::make_file_provider("file", "/path/to/large-file.zip")
|
||||
});
|
||||
```
|
||||
@@ -160,17 +160,17 @@ svr.set_post_routing_handler([](const auto &req, auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
`req.user_data` を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。
|
||||
`res.user_data` を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto &req, auto &res) {
|
||||
req.user_data["auth_user"] = std::string("alice");
|
||||
res.user_data.set("auth_user", std::string("alice"));
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto &req, auto &res) {
|
||||
auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
|
||||
res.set_content("Hello, " + user, "text/plain");
|
||||
auto *user = res.user_data.get<std::string>("auth_user");
|
||||
res.set_content("Hello, " + *user, "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "A Tour of cpp-httplib"
|
||||
order: 1
|
||||
order: 0
|
||||
---
|
||||
|
||||
cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT_DIR="$SCRIPT_DIR/build/desktop-app"
|
||||
DOCS_DIR="$SCRIPT_DIR/../../docs-src/pages/ja/llm-app"
|
||||
|
||||
source "$SCRIPT_DIR/extract_code.sh"
|
||||
|
||||
echo "=== Setting up Desktop App (Chapter 6) ==="
|
||||
|
||||
mkdir -p "$OUT_DIR"/{src,public}
|
||||
cd "$OUT_DIR"
|
||||
|
||||
# --- Extract source files from book ---
|
||||
echo "Extracting source from book..."
|
||||
CH05="$DOCS_DIR/ch05-web-ui.md"
|
||||
CH06="$DOCS_DIR/ch06-desktop-app.md"
|
||||
|
||||
extract_code "$CH06" "CMakeLists.txt" > CMakeLists.txt
|
||||
extract_code "$CH06" "main.cpp" > src/main.cpp
|
||||
extract_code "$CH05" "index.html" > public/index.html
|
||||
extract_code "$CH05" "style.css" > public/style.css
|
||||
extract_code "$CH05" "script.js" > public/script.js
|
||||
|
||||
# --- Build ---
|
||||
echo "Building..."
|
||||
cmake -B build 2>&1 | tail -1
|
||||
cmake --build build -j 2>&1 | tail -1
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Run: cd $OUT_DIR && ./build/translate-app"
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT_DIR="$SCRIPT_DIR/build/web-app"
|
||||
DOCS_DIR="$SCRIPT_DIR/../../docs-src/pages/ja/llm-app"
|
||||
|
||||
source "$SCRIPT_DIR/extract_code.sh"
|
||||
|
||||
echo "=== Setting up Web App (Chapter 5) ==="
|
||||
|
||||
mkdir -p "$OUT_DIR"/{src,public}
|
||||
cd "$OUT_DIR"
|
||||
|
||||
# --- Extract source files from book ---
|
||||
echo "Extracting source from book..."
|
||||
CH04="$DOCS_DIR/ch04-model-management.md"
|
||||
CH05="$DOCS_DIR/ch05-web-ui.md"
|
||||
|
||||
extract_code "$CH04" "CMakeLists.txt" > CMakeLists.txt
|
||||
extract_code "$CH05" "main.cpp" > src/main.cpp
|
||||
extract_code "$CH05" "index.html" > public/index.html
|
||||
extract_code "$CH05" "style.css" > public/style.css
|
||||
extract_code "$CH05" "script.js" > public/script.js
|
||||
|
||||
# --- Build ---
|
||||
echo "Building..."
|
||||
cmake -B build 2>&1 | tail -1
|
||||
cmake --build build -j 2>&1 | tail -1
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Run: cd $OUT_DIR && ./build/translate-server"
|
||||
@@ -0,0 +1,18 @@
|
||||
# Extract code block from a <details> section identified by data-file attribute.
|
||||
# Usage: extract_code <file> <data-file>
|
||||
# Example: extract_code ch01.md "main.cpp"
|
||||
extract_code() {
|
||||
local file="$1" name="$2"
|
||||
local output
|
||||
output=$(awk -v name="$name" '
|
||||
$0 ~ "data-file=\"" name "\"" { found=1; next }
|
||||
found && /^```/ && !inside { inside=1; next }
|
||||
inside && /^```/ { exit }
|
||||
inside { print }
|
||||
' "$file")
|
||||
if [ -z "$output" ]; then
|
||||
echo "ERROR: extract_code: no match for data-file=\"$name\" in $file" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Generate the desktop app project by extracting source from the cpp-httplib book.
|
||||
# Usage: generate_desktop_app_project.sh <output-dir>
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT_DIR="${1:?Usage: $0 <output-dir>}"
|
||||
|
||||
BASE_URL="https://raw.githubusercontent.com/yhirose/cpp-httplib/master/docs-src/pages/ja/llm-app"
|
||||
CACHE_DIR="$SCRIPT_DIR/.cache"
|
||||
|
||||
source "$SCRIPT_DIR/extract_code.sh"
|
||||
|
||||
# --- Helper: download markdown files (always fetch latest) ---
|
||||
fetch_md() {
|
||||
local name="$1"
|
||||
local path="$CACHE_DIR/$name"
|
||||
curl -sfL "$BASE_URL/$name" -o "$path" || { echo "ERROR: Failed to download $name" >&2; return 1; }
|
||||
echo "$path"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
echo "=== Generating desktop app project ==="
|
||||
|
||||
mkdir -p "$CACHE_DIR" "$OUT_DIR/src" "$OUT_DIR/public"
|
||||
|
||||
CH05=$(fetch_md "ch05-web-ui.md")
|
||||
CH06=$(fetch_md "ch06-desktop-app.md")
|
||||
|
||||
echo "Extracting source files..."
|
||||
extract_code "$CH06" "CMakeLists.txt" > "$OUT_DIR/CMakeLists.txt"
|
||||
extract_code "$CH06" "main.cpp" > "$OUT_DIR/src/main.cpp"
|
||||
extract_code "$CH05" "index.html" > "$OUT_DIR/public/index.html"
|
||||
extract_code "$CH05" "style.css" > "$OUT_DIR/public/style.css"
|
||||
extract_code "$CH05" "script.js" > "$OUT_DIR/public/script.js"
|
||||
|
||||
echo "=== Done ==="
|
||||
echo "Generated files in: $OUT_DIR"
|
||||
@@ -0,0 +1,28 @@
|
||||
# List available targets
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Remove build artifacts
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
# Test the Book
|
||||
test-book:
|
||||
bash test_book.sh
|
||||
|
||||
# Build Web App (Chapter 5)
|
||||
build-web-app:
|
||||
bash build_web_app.sh
|
||||
|
||||
# Stop any running server, then run Web App
|
||||
run-web-app: build-web-app
|
||||
-lsof -ti :8080 | xargs kill 2>/dev/null
|
||||
cd build/web-app && ./build/translate-server
|
||||
|
||||
# Build Desktop App (Chapter 6)
|
||||
build-desktop-app:
|
||||
bash build_desktop_app.sh
|
||||
|
||||
# Run Desktop App
|
||||
run-desktop-app: build-desktop-app
|
||||
cd build/desktop-app && ./build/translate-app
|
||||
@@ -0,0 +1,561 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# test_book.sh — LLM App Tutorial (Ch1–Ch5) E2E Test
|
||||
#
|
||||
# Code is extracted from the doc markdown files (<!-- test:full-code --> and
|
||||
# <!-- test:cmake --> markers), so tests always stay in sync with the docs.
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
DOCS_DIR="$PROJECT_ROOT/docs-src/pages/ja/llm-app"
|
||||
WORKDIR=$(mktemp -d)
|
||||
MODEL_NAME="gemma-2-2b-it-Q4_K_M.gguf"
|
||||
MODEL_URL="https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/${MODEL_NAME}"
|
||||
PORT=18080
|
||||
GECKODRIVER_PORT=4444
|
||||
SERVER_PID=""
|
||||
GECKODRIVER_PID=""
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
cleanup() {
|
||||
if [[ -n "$SERVER_PID" ]]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "$GECKODRIVER_PID" ]]; then
|
||||
kill "$GECKODRIVER_PID" 2>/dev/null || true
|
||||
wait "$GECKODRIVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$WORKDIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
log() { echo "=== $*"; }
|
||||
pass() { echo " PASS: $*"; PASS_COUNT=$((PASS_COUNT + 1)); }
|
||||
fail() { echo " FAIL: $*"; FAIL_COUNT=$((FAIL_COUNT + 1)); }
|
||||
|
||||
source "$SCRIPT_DIR/extract_code.sh"
|
||||
|
||||
wait_for_server() {
|
||||
local max_wait=30
|
||||
local i=0
|
||||
while ! curl -s -o /dev/null "http://127.0.0.1:${PORT}/" 2>/dev/null; do
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
if [[ $i -ge $max_wait ]]; then
|
||||
fail "Server did not start within ${max_wait}s"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
stop_server() {
|
||||
if [[ -n "$SERVER_PID" ]]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
SERVER_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Make an HTTP request and capture status + body
|
||||
# Usage: http_request METHOD PATH [DATA]
|
||||
# Sets: HTTP_STATUS, HTTP_BODY
|
||||
http_request() {
|
||||
local method="$1" path="$2" data="${3:-}"
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
if [[ -n "$data" ]]; then
|
||||
HTTP_STATUS=$(curl -s -o "$tmp" -w '%{http_code}' \
|
||||
-X "$method" "http://127.0.0.1:${PORT}${path}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data")
|
||||
else
|
||||
HTTP_STATUS=$(curl -s -o "$tmp" -w '%{http_code}' \
|
||||
-X "$method" "http://127.0.0.1:${PORT}${path}")
|
||||
fi
|
||||
HTTP_BODY=$(cat "$tmp")
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# Make an SSE request and capture the raw stream
|
||||
# Usage: http_sse PATH DATA
|
||||
# Sets: HTTP_STATUS, HTTP_BODY
|
||||
http_sse() {
|
||||
local path="$1" data="$2"
|
||||
HTTP_SSE_FILE=$(mktemp)
|
||||
HTTP_STATUS=$(curl -s -N -o "$HTTP_SSE_FILE" -w '%{http_code}' \
|
||||
-X POST "http://127.0.0.1:${PORT}${path}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data")
|
||||
HTTP_BODY=$(cat "$HTTP_SSE_FILE")
|
||||
rm -f "$HTTP_SSE_FILE"
|
||||
}
|
||||
|
||||
assert_status() {
|
||||
local expected="$1" label="$2"
|
||||
if [[ "$HTTP_STATUS" == "$expected" ]]; then
|
||||
pass "$label (status=$HTTP_STATUS)"
|
||||
else
|
||||
fail "$label (expected=$expected, got=$HTTP_STATUS)"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_json_field() {
|
||||
local field="$1" label="$2"
|
||||
if echo "$HTTP_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); assert '$field' in d" 2>/dev/null; then
|
||||
pass "$label (field '$field' exists)"
|
||||
else
|
||||
fail "$label (field '$field' missing in response)"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_json_value() {
|
||||
local field="$1" expected="$2" label="$3"
|
||||
local actual
|
||||
actual=$(echo "$HTTP_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['$field'])" 2>/dev/null || echo "")
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
pass "$label ($field='$actual')"
|
||||
else
|
||||
fail "$label (expected $field='$expected', got='$actual')"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_json_nonempty() {
|
||||
local field="$1" label="$2"
|
||||
local val
|
||||
val=$(echo "$HTTP_BODY" | python3 -c "import sys,json; v=json.load(sys.stdin)['$field']; assert len(str(v))>0; print(v)" 2>/dev/null || echo "")
|
||||
if [[ -n "$val" ]]; then
|
||||
pass "$label ($field is non-empty)"
|
||||
else
|
||||
fail "$label ($field is empty or missing)"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
}
|
||||
|
||||
# Patch port number in extracted source code (8080 -> test port)
|
||||
patch_port() {
|
||||
sed "s/127\.0\.0\.1\", 8080/127.0.0.1\", ${PORT}/g; s/127\.0\.0\.1:8080/127.0.0.1:${PORT}/g"
|
||||
}
|
||||
|
||||
# Patch model path in extracted source code
|
||||
patch_model() {
|
||||
sed "s|models/gemma-2-2b-it-Q4_K_M.gguf|models/${MODEL_NAME}|g"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Ch1: Skeleton Server
|
||||
# =============================================================================
|
||||
test_ch1() {
|
||||
log "Ch1: Project Setup & Skeleton Server"
|
||||
|
||||
local APP_DIR="$WORKDIR/translate-app"
|
||||
mkdir -p "$APP_DIR/src" "$APP_DIR/models"
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Copy httplib.h from project root (test current version)
|
||||
cp "$PROJECT_ROOT/httplib.h" .
|
||||
|
||||
# Download json.hpp into nlohmann/ directory to match #include <nlohmann/json.hpp>
|
||||
mkdir -p nlohmann
|
||||
curl -sL -o nlohmann/json.hpp \
|
||||
https://github.com/nlohmann/json/releases/latest/download/json.hpp
|
||||
|
||||
# CMakeLists.txt — ch1 doesn't need llama.cpp, so use a minimal version
|
||||
# (the doc's cmake includes llama.cpp which isn't cloned yet in ch1)
|
||||
cat > CMakeLists.txt << 'CMAKE_EOF'
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(translate-server LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
add_executable(translate-server src/main.cpp)
|
||||
target_include_directories(translate-server PRIVATE ${CMAKE_SOURCE_DIR})
|
||||
CMAKE_EOF
|
||||
|
||||
# Extract main.cpp from ch1 doc and patch port
|
||||
extract_code "$DOCS_DIR/ch01-setup.md" "main.cpp" | patch_port > src/main.cpp
|
||||
|
||||
# Build
|
||||
log "Ch1: Building..."
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 | tail -1
|
||||
cmake --build build -j 2>&1 | tail -3
|
||||
|
||||
# Start server
|
||||
./build/translate-server &
|
||||
SERVER_PID=$!
|
||||
wait_for_server
|
||||
|
||||
# Tests
|
||||
http_request POST /translate '{"text":"hello","target_lang":"ja"}'
|
||||
assert_status 200 "Ch1 POST /translate"
|
||||
assert_json_value translation "TODO" "Ch1 POST /translate returns TODO"
|
||||
|
||||
http_request GET /models
|
||||
assert_status 200 "Ch1 GET /models"
|
||||
assert_json_field models "Ch1 GET /models"
|
||||
|
||||
http_request POST /models/select '{"model":"test"}'
|
||||
assert_status 200 "Ch1 POST /models/select"
|
||||
assert_json_value status "TODO" "Ch1 POST /models/select returns TODO"
|
||||
|
||||
stop_server
|
||||
log "Ch1: Done"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Ch2: REST API with llama.cpp
|
||||
# =============================================================================
|
||||
test_ch2() {
|
||||
log "Ch2: REST API with llama.cpp"
|
||||
|
||||
local APP_DIR="$WORKDIR/translate-app"
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Clone llama.cpp
|
||||
if [[ ! -d llama.cpp ]]; then
|
||||
log "Ch2: Cloning llama.cpp..."
|
||||
git clone --depth 1 https://github.com/ggml-org/llama.cpp.git 2>&1 | tail -1
|
||||
fi
|
||||
|
||||
# Download cpp-llamalib.h
|
||||
if [[ ! -f cpp-llamalib.h ]]; then
|
||||
curl -sL -o cpp-llamalib.h \
|
||||
https://raw.githubusercontent.com/yhirose/cpp-llamalib/main/cpp-llamalib.h
|
||||
fi
|
||||
|
||||
# Download model
|
||||
if [[ ! -f "models/$MODEL_NAME" ]]; then
|
||||
log "Ch2: Downloading model ${MODEL_NAME} (~1.6GB)..."
|
||||
curl -L -o "models/$MODEL_NAME" "$MODEL_URL"
|
||||
fi
|
||||
|
||||
# CMakeLists.txt from ch1 doc (includes llama.cpp)
|
||||
extract_code "$DOCS_DIR/ch01-setup.md" "CMakeLists.txt" > CMakeLists.txt
|
||||
|
||||
# Extract main.cpp from ch2 doc and patch port + model path
|
||||
extract_code "$DOCS_DIR/ch02-rest-api.md" "main.cpp" | patch_port | patch_model > src/main.cpp
|
||||
|
||||
# Build (clean rebuild needed — cmake config changed)
|
||||
log "Ch2: Building (this may take a while for llama.cpp)..."
|
||||
rm -rf build
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 | tail -1
|
||||
cmake --build build -j 2>&1 | tail -3
|
||||
|
||||
# Start server
|
||||
./build/translate-server &
|
||||
SERVER_PID=$!
|
||||
wait_for_server
|
||||
|
||||
# Tests — normal request
|
||||
http_request POST /translate \
|
||||
'{"text":"I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.","target_lang":"ja"}'
|
||||
assert_status 200 "Ch2 POST /translate normal"
|
||||
assert_json_nonempty translation "Ch2 POST /translate has translation"
|
||||
|
||||
# Tests — invalid JSON
|
||||
http_request POST /translate 'not json'
|
||||
assert_status 400 "Ch2 POST /translate invalid JSON"
|
||||
|
||||
# Tests — missing text
|
||||
http_request POST /translate '{"target_lang":"ja"}'
|
||||
assert_status 400 "Ch2 POST /translate missing text"
|
||||
|
||||
# Tests — empty text
|
||||
http_request POST /translate '{"text":""}'
|
||||
assert_status 400 "Ch2 POST /translate empty text"
|
||||
|
||||
stop_server
|
||||
log "Ch2: Done"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Ch3: SSE Streaming
|
||||
# =============================================================================
|
||||
test_ch3() {
|
||||
log "Ch3: SSE Streaming"
|
||||
|
||||
local APP_DIR="$WORKDIR/translate-app"
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Extract main.cpp from ch3 doc and patch port + model path
|
||||
extract_code "$DOCS_DIR/ch03-sse-streaming.md" "main.cpp" | patch_port | patch_model > src/main.cpp
|
||||
|
||||
# Build (incremental — only main.cpp changed)
|
||||
log "Ch3: Building..."
|
||||
cmake --build build -j 2>&1 | tail -3
|
||||
|
||||
# Start server
|
||||
./build/translate-server &
|
||||
SERVER_PID=$!
|
||||
wait_for_server
|
||||
|
||||
# Tests — /translate still works
|
||||
http_request POST /translate \
|
||||
'{"text":"Hello world","target_lang":"ja"}'
|
||||
assert_status 200 "Ch3 POST /translate still works"
|
||||
|
||||
# Tests — SSE streaming
|
||||
http_sse /translate/stream \
|
||||
'{"text":"I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.","target_lang":"ja"}'
|
||||
assert_status 200 "Ch3 POST /translate/stream status"
|
||||
|
||||
# Check SSE format: has data: lines and ends with [DONE]
|
||||
local data_lines
|
||||
data_lines=$(echo "$HTTP_BODY" | grep -c '^data: ' || true)
|
||||
if [[ $data_lines -ge 2 ]]; then
|
||||
pass "Ch3 SSE has multiple data: lines ($data_lines)"
|
||||
else
|
||||
fail "Ch3 SSE expected multiple data: lines, got $data_lines"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
|
||||
if echo "$HTTP_BODY" | grep -q 'data: \[DONE\]'; then
|
||||
pass "Ch3 SSE ends with data: [DONE]"
|
||||
else
|
||||
fail "Ch3 SSE missing data: [DONE]"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
|
||||
# Tests — SSE invalid JSON
|
||||
http_sse /translate/stream 'not json'
|
||||
assert_status 400 "Ch3 POST /translate/stream invalid JSON"
|
||||
|
||||
stop_server
|
||||
log "Ch3: Done"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Ch4: Model Management
|
||||
# =============================================================================
|
||||
test_ch4() {
|
||||
log "Ch4: Model Management"
|
||||
|
||||
local APP_DIR="$WORKDIR/translate-app"
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Ch4+ uses ~/.translate-app/models/ — symlink model there
|
||||
local MODELS_HOME="$HOME/.translate-app/models"
|
||||
mkdir -p "$MODELS_HOME"
|
||||
ln -sf "$APP_DIR/models/$MODEL_NAME" "$MODELS_HOME/$MODEL_NAME"
|
||||
|
||||
# CMakeLists.txt from ch4 (adds OpenSSL)
|
||||
extract_code "$DOCS_DIR/ch04-model-management.md" "CMakeLists.txt" > CMakeLists.txt
|
||||
|
||||
# Extract main.cpp from ch4 doc
|
||||
extract_code "$DOCS_DIR/ch04-model-management.md" "main.cpp" | patch_port > src/main.cpp
|
||||
|
||||
# Build (reconfigure for OpenSSL, incremental — reuses llama.cpp objects)
|
||||
log "Ch4: Building..."
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 | tail -1
|
||||
cmake --build build -j 2>&1 | tail -3
|
||||
|
||||
# Start server
|
||||
./build/translate-server &
|
||||
SERVER_PID=$!
|
||||
wait_for_server
|
||||
|
||||
# Tests — GET /models
|
||||
http_request GET /models
|
||||
assert_status 200 "Ch4 GET /models"
|
||||
assert_json_field models "Ch4 GET /models has models array"
|
||||
|
||||
# デフォルトモデルがdownloaded+selectedであること
|
||||
local selected
|
||||
selected=$(echo "$HTTP_BODY" | python3 -c "
|
||||
import sys, json
|
||||
models = json.load(sys.stdin)['models']
|
||||
sel = [m for m in models if m['selected']]
|
||||
print(sel[0]['downloaded'] if sel else '')
|
||||
" 2>/dev/null || echo "")
|
||||
if [[ "$selected" == "True" ]]; then
|
||||
pass "Ch4 GET /models default model is downloaded and selected"
|
||||
else
|
||||
fail "Ch4 GET /models default model state unexpected"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
|
||||
# Tests — POST /models/select with already-downloaded model (SSE)
|
||||
http_sse /models/select '{"model": "gemma-2-2b-it"}'
|
||||
assert_status 200 "Ch4 POST /models/select already downloaded"
|
||||
|
||||
if echo "$HTTP_BODY" | grep -q '"ready"'; then
|
||||
pass "Ch4 POST /models/select returns ready"
|
||||
else
|
||||
fail "Ch4 POST /models/select missing ready status"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
|
||||
# Tests — POST /models/select unknown model
|
||||
http_request POST /models/select '{"model": "nonexistent"}'
|
||||
assert_status 404 "Ch4 POST /models/select unknown model"
|
||||
|
||||
# Tests — POST /models/select missing model field
|
||||
http_request POST /models/select '{"foo": "bar"}'
|
||||
assert_status 400 "Ch4 POST /models/select missing model field"
|
||||
|
||||
# Tests — /translate still works after model select
|
||||
http_request POST /translate '{"text": "Hello", "target_lang": "ja"}'
|
||||
assert_status 200 "Ch4 POST /translate still works"
|
||||
assert_json_nonempty translation "Ch4 POST /translate has translation"
|
||||
|
||||
# Tests — switch model via symlink (avoids downloading a second model)
|
||||
# Place a symlink so the server sees Llama-3.1-8B-Instruct as "downloaded"
|
||||
ln -sf "$MODELS_HOME/$MODEL_NAME" "$MODELS_HOME/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf"
|
||||
|
||||
http_sse /models/select '{"model": "Llama-3.1-8B-Instruct"}'
|
||||
assert_status 200 "Ch4 POST /models/select switch model"
|
||||
|
||||
if echo "$HTTP_BODY" | grep -q '"ready"'; then
|
||||
pass "Ch4 model switch returns ready"
|
||||
else
|
||||
fail "Ch4 model switch missing ready"
|
||||
echo " body: $HTTP_BODY"
|
||||
fi
|
||||
|
||||
# Translate with the switched model
|
||||
http_request POST /translate \
|
||||
'{"text": "The weather is nice today.", "target_lang": "ja"}'
|
||||
assert_status 200 "Ch4 POST /translate after model switch"
|
||||
assert_json_nonempty translation "Ch4 POST /translate switched model has translation"
|
||||
|
||||
# Verify model list reflects the switch
|
||||
http_request GET /models
|
||||
local new_selected
|
||||
new_selected=$(echo "$HTTP_BODY" | python3 -c "
|
||||
import sys, json
|
||||
models = json.load(sys.stdin)['models']
|
||||
sel = [m for m in models if m['selected']]
|
||||
print(sel[0]['name'] if sel else '')
|
||||
" 2>/dev/null || echo "")
|
||||
if [[ "$new_selected" == "Llama-3.1-8B-Instruct" ]]; then
|
||||
pass "Ch4 GET /models reflects model switch"
|
||||
else
|
||||
fail "Ch4 GET /models expected Llama-3.1-8B-Instruct selected, got '$new_selected'"
|
||||
fi
|
||||
|
||||
stop_server
|
||||
log "Ch4: Done"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Ch5: Web UI (browser tests via geckodriver + webdriver.h)
|
||||
# =============================================================================
|
||||
|
||||
start_geckodriver() {
|
||||
geckodriver --port "$GECKODRIVER_PORT" &>/dev/null &
|
||||
GECKODRIVER_PID=$!
|
||||
# Wait for geckodriver to be ready
|
||||
local i=0
|
||||
while ! curl -s -o /dev/null "http://127.0.0.1:${GECKODRIVER_PORT}/status" 2>/dev/null; do
|
||||
sleep 0.5
|
||||
i=$((i + 1))
|
||||
if [[ $i -ge 20 ]]; then
|
||||
fail "geckodriver did not start within 10s"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
stop_geckodriver() {
|
||||
if [[ -n "$GECKODRIVER_PID" ]]; then
|
||||
kill "$GECKODRIVER_PID" 2>/dev/null || true
|
||||
wait "$GECKODRIVER_PID" 2>/dev/null || true
|
||||
GECKODRIVER_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
test_ch5() {
|
||||
log "Ch5: Web UI (browser tests)"
|
||||
|
||||
# Check for geckodriver
|
||||
if ! command -v geckodriver &>/dev/null; then
|
||||
log "Ch5: Skipping browser tests (geckodriver not found)"
|
||||
log "Ch5: Install with: brew install geckodriver"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local APP_DIR="$WORKDIR/translate-app"
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Extract source files from ch05
|
||||
extract_code "$DOCS_DIR/ch05-web-ui.md" "main.cpp" \
|
||||
| patch_port > src/main.cpp
|
||||
|
||||
mkdir -p public
|
||||
extract_code "$DOCS_DIR/ch05-web-ui.md" "index.html" > public/index.html
|
||||
extract_code "$DOCS_DIR/ch05-web-ui.md" "style.css" > public/style.css
|
||||
extract_code "$DOCS_DIR/ch05-web-ui.md" "script.js" > public/script.js
|
||||
|
||||
# Build (incremental — only main.cpp changed)
|
||||
log "Ch5: Building server..."
|
||||
cmake --build build -j 2>&1 | tail -3
|
||||
|
||||
# Build browser test program
|
||||
log "Ch5: Building browser test..."
|
||||
g++ -std=c++17 \
|
||||
-I"$APP_DIR" \
|
||||
-I"$SCRIPT_DIR" \
|
||||
-o "$APP_DIR/build/test_webui" \
|
||||
"$SCRIPT_DIR/test_webui.cpp" \
|
||||
-pthread
|
||||
|
||||
# Start server
|
||||
./build/translate-server &
|
||||
SERVER_PID=$!
|
||||
wait_for_server
|
||||
|
||||
# Start geckodriver
|
||||
start_geckodriver
|
||||
|
||||
# Run browser tests
|
||||
log "Ch5: Running browser tests..."
|
||||
local test_exit=0
|
||||
"$APP_DIR/build/test_webui" "$PORT" || test_exit=$?
|
||||
|
||||
# Parse pass/fail from test output and add to totals
|
||||
# (test_webui prints its own pass/fail, but we track via exit code)
|
||||
if [[ $test_exit -ne 0 ]]; then
|
||||
fail "Ch5 browser tests had failures"
|
||||
else
|
||||
pass "Ch5 browser tests all passed"
|
||||
fi
|
||||
|
||||
stop_geckodriver
|
||||
stop_server
|
||||
log "Ch5: Done"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
log "LLM App Tutorial E2E Test"
|
||||
log "Working directory: $WORKDIR"
|
||||
echo ""
|
||||
|
||||
test_ch1
|
||||
echo ""
|
||||
test_ch2
|
||||
echo ""
|
||||
test_ch3
|
||||
echo ""
|
||||
test_ch4
|
||||
echo ""
|
||||
test_ch5
|
||||
|
||||
log "Results: $PASS_COUNT passed, $FAIL_COUNT failed"
|
||||
|
||||
if [[ $FAIL_COUNT -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,300 @@
|
||||
// test_webui.cpp — Browser-based E2E tests for Ch5 Web UI.
|
||||
// Uses webdriver.h (cpp-httplib + json.hpp) to control headless Firefox.
|
||||
//
|
||||
// Usage: test_webui <port>
|
||||
// port: the translate-server port (e.g. 18080)
|
||||
|
||||
#include "webdriver.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// ─── Test framework (minimal) ────────────────────────────────
|
||||
|
||||
static int pass_count = 0;
|
||||
static int fail_count = 0;
|
||||
|
||||
#define PASS(label) \
|
||||
do { \
|
||||
std::cout << " PASS: " << (label) << "\n"; \
|
||||
++pass_count; \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(label, detail) \
|
||||
do { \
|
||||
std::cout << " FAIL: " << (label) << "\n"; \
|
||||
std::cout << " " << (detail) << "\n"; \
|
||||
++fail_count; \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_TRUE(cond, label) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
PASS(label); \
|
||||
} else { \
|
||||
FAIL(label, "condition was false"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_CONTAINS(haystack, needle, label) \
|
||||
do { \
|
||||
if (std::string(haystack).find(needle) != std::string::npos) { \
|
||||
PASS(label); \
|
||||
} else { \
|
||||
FAIL(label, "'" + std::string(haystack) + "' does not contain '" + \
|
||||
std::string(needle) + "'"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_ELEMENT_EXISTS(session, selector) \
|
||||
do { \
|
||||
try { \
|
||||
(session).css(selector); \
|
||||
PASS("Element " selector " exists"); \
|
||||
} catch (...) { FAIL("Element " selector " exists", "not found"); } \
|
||||
} while (0)
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
static std::string base_url;
|
||||
|
||||
void navigate_and_wait_for_models(webdriver::Session &session) {
|
||||
session.navigate(base_url);
|
||||
session.wait_until(
|
||||
"return document.querySelectorAll('#model-select option').length > 0",
|
||||
5000);
|
||||
}
|
||||
|
||||
void test_page_loads(webdriver::Session &session) {
|
||||
std::cout << "=== TC1: Page loads with correct structure\n";
|
||||
|
||||
session.navigate(base_url);
|
||||
|
||||
auto title = session.title();
|
||||
ASSERT_CONTAINS(title, "Translate", "Page title contains 'Translate'");
|
||||
|
||||
// Verify main DOM elements exist
|
||||
ASSERT_ELEMENT_EXISTS(session, "#model-select");
|
||||
ASSERT_ELEMENT_EXISTS(session, "#input-text");
|
||||
ASSERT_ELEMENT_EXISTS(session, "#output-text");
|
||||
ASSERT_ELEMENT_EXISTS(session, "#target-lang");
|
||||
}
|
||||
|
||||
void test_model_dropdown(webdriver::Session &session) {
|
||||
std::cout << "=== TC2: Model dropdown is populated\n";
|
||||
|
||||
navigate_and_wait_for_models(session);
|
||||
|
||||
// Note: WebDriver findElements cannot find <option> elements directly
|
||||
// in geckodriver/Firefox, so we use JS to count them.
|
||||
auto option_count = session.execute_script(
|
||||
"return document.querySelectorAll('#model-select option').length");
|
||||
ASSERT_TRUE(option_count != "0" && option_count != "null",
|
||||
"Model dropdown has options (count=" + option_count + ")");
|
||||
|
||||
// Check that at least one option has a selected attribute
|
||||
auto selected_val = session.execute_script(
|
||||
"return document.querySelector('#model-select').value");
|
||||
ASSERT_TRUE(selected_val != "null" && !selected_val.empty(),
|
||||
"A model is selected (value='" + selected_val + "')");
|
||||
}
|
||||
|
||||
void test_translation_sse(webdriver::Session &session) {
|
||||
std::cout << "=== TC3: Translation with SSE streaming\n";
|
||||
|
||||
navigate_and_wait_for_models(session);
|
||||
|
||||
// Clear and type input — debounce auto-translate triggers after 300ms
|
||||
auto input = session.css("#input-text");
|
||||
input.clear();
|
||||
input.send_keys("Hello world");
|
||||
|
||||
// Wait for output to appear (debounce 300ms + LLM inference)
|
||||
bool has_output = session.wait_until(
|
||||
"return document.querySelector('#output-text').textContent.length > 0",
|
||||
120000);
|
||||
ASSERT_TRUE(has_output, "Translation output appeared");
|
||||
|
||||
auto output_text = session.execute_script(
|
||||
"return document.querySelector('#output-text').textContent");
|
||||
ASSERT_TRUE(!output_text.empty() && output_text != "null",
|
||||
"Output text is non-empty ('" + output_text.substr(0, 50) +
|
||||
"...')");
|
||||
|
||||
// Wait for busy state to be cleared after completion
|
||||
bool busy_cleared = session.wait_until(
|
||||
"return !document.body.classList.contains('busy')", 120000);
|
||||
ASSERT_TRUE(busy_cleared, "Busy state cleared after translation");
|
||||
}
|
||||
|
||||
void test_busy_state(webdriver::Session &session) {
|
||||
std::cout << "=== TC4: Busy state during translation\n";
|
||||
|
||||
navigate_and_wait_for_models(session);
|
||||
|
||||
auto input = session.css("#input-text");
|
||||
input.clear();
|
||||
|
||||
// Clear previous output
|
||||
session.execute_script(
|
||||
"document.querySelector('#output-text').textContent = ''");
|
||||
|
||||
input.send_keys(
|
||||
"I had a great time visiting Tokyo last spring. "
|
||||
"The cherry blossoms were beautiful and the food was amazing.");
|
||||
|
||||
// Check busy state (debounce 300ms then translation starts)
|
||||
bool went_busy = session.wait_until(
|
||||
"return document.body.classList.contains('busy')", 5000);
|
||||
ASSERT_TRUE(went_busy, "Body gets 'busy' class during translation");
|
||||
|
||||
// Wait for completion
|
||||
session.wait_until("return !document.body.classList.contains('busy')",
|
||||
120000);
|
||||
PASS("Busy class removed after completion");
|
||||
}
|
||||
|
||||
void test_empty_input(webdriver::Session &session) {
|
||||
std::cout << "=== TC5: Empty input does nothing\n";
|
||||
|
||||
navigate_and_wait_for_models(session);
|
||||
|
||||
// Clear input and output
|
||||
auto input = session.css("#input-text");
|
||||
input.clear();
|
||||
session.execute_script(
|
||||
"document.querySelector('#output-text').textContent = ''");
|
||||
|
||||
// Trigger input event on empty textarea
|
||||
session.execute_script("document.querySelector('#input-text').dispatchEvent("
|
||||
" new Event('input'));");
|
||||
|
||||
// Wait longer than debounce (300ms) — nothing should happen
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
|
||||
auto output_text = session.execute_script(
|
||||
"return document.querySelector('#output-text').textContent");
|
||||
ASSERT_TRUE(output_text.empty() || output_text == "null" || output_text == "",
|
||||
"No output for empty input");
|
||||
}
|
||||
|
||||
void test_target_lang_selector(webdriver::Session &session) {
|
||||
std::cout << "=== TC6: Target language selector\n";
|
||||
|
||||
navigate_and_wait_for_models(session);
|
||||
|
||||
// Check available language options (use JS — WebDriver can't find <option>)
|
||||
auto lang_count = session.execute_script(
|
||||
"return document.querySelectorAll('#target-lang option').length");
|
||||
ASSERT_TRUE(lang_count != "0" && lang_count != "null",
|
||||
"Language selector has multiple options (count=" + lang_count +
|
||||
")");
|
||||
|
||||
// Switch to English and translate
|
||||
session.execute_script("document.querySelector('#target-lang').value = 'en';"
|
||||
"document.querySelector('#target-lang').dispatchEvent("
|
||||
" new Event('change'));");
|
||||
|
||||
// Clear output, then type — debounce auto-translate triggers
|
||||
session.execute_script(
|
||||
"document.querySelector('#output-text').textContent = ''");
|
||||
|
||||
auto input = session.css("#input-text");
|
||||
input.clear();
|
||||
input.send_keys("こんにちは");
|
||||
|
||||
bool has_output = session.wait_until(
|
||||
"return document.querySelector('#output-text').textContent.length > 0",
|
||||
120000);
|
||||
ASSERT_TRUE(has_output, "Translation with target_lang=en produced output");
|
||||
}
|
||||
|
||||
void test_model_switch(webdriver::Session &session) {
|
||||
std::cout << "=== TC7: Model switching\n";
|
||||
|
||||
navigate_and_wait_for_models(session);
|
||||
|
||||
auto options = session.css_all("#model-select option");
|
||||
if (options.size() < 2) {
|
||||
PASS("Model switch skipped (only 1 model available)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current model
|
||||
auto current = session.execute_script(
|
||||
"return document.querySelector('#model-select').value");
|
||||
|
||||
// Switch to a different model (pick the second option's value)
|
||||
auto other_value = options[1].attribute("value");
|
||||
if (other_value == current && options.size() > 2) {
|
||||
other_value = options[2].attribute("value");
|
||||
}
|
||||
|
||||
session.execute_script(
|
||||
"document.querySelector('#model-select').value = '" + other_value +
|
||||
"';"
|
||||
"document.querySelector('#model-select').dispatchEvent("
|
||||
" new Event('change'));");
|
||||
|
||||
// Wait for model switch to complete (SSE: downloading → loading → ready)
|
||||
bool ready = session.wait_until(
|
||||
"return !document.body.classList.contains('busy')", 120000);
|
||||
ASSERT_TRUE(ready, "Model switch completed");
|
||||
|
||||
auto new_value = session.execute_script(
|
||||
"return document.querySelector('#model-select').value");
|
||||
ASSERT_TRUE(new_value == other_value,
|
||||
"Model changed to '" + other_value + "'");
|
||||
}
|
||||
|
||||
void test_download_dialog_structure(webdriver::Session &session) {
|
||||
std::cout << "=== TC8: Download dialog DOM structure\n";
|
||||
|
||||
session.navigate(base_url);
|
||||
|
||||
ASSERT_ELEMENT_EXISTS(session, "#download-dialog");
|
||||
ASSERT_ELEMENT_EXISTS(session, "#download-progress");
|
||||
ASSERT_ELEMENT_EXISTS(session, "#download-status");
|
||||
ASSERT_ELEMENT_EXISTS(session, "#download-cancel");
|
||||
}
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────────
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: test_webui <server-port>\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
int port = std::atoi(argv[1]);
|
||||
base_url = "http://127.0.0.1:" + std::to_string(port);
|
||||
|
||||
std::cout << "=== Ch5 Web UI Browser Tests\n";
|
||||
std::cout << "=== Server: " << base_url << "\n\n";
|
||||
|
||||
try {
|
||||
webdriver::Session session;
|
||||
|
||||
test_page_loads(session);
|
||||
test_model_dropdown(session);
|
||||
test_translation_sse(session);
|
||||
test_busy_state(session);
|
||||
test_empty_input(session);
|
||||
test_target_lang_selector(session);
|
||||
test_model_switch(session);
|
||||
test_download_dialog_structure(session);
|
||||
|
||||
} catch (const webdriver::Error &e) {
|
||||
std::cerr << "WebDriver error: " << e.what() << "\n";
|
||||
++fail_count;
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Error: " << e.what() << "\n";
|
||||
++fail_count;
|
||||
}
|
||||
|
||||
std::cout << "\n=== Results: " << pass_count << " passed, " << fail_count
|
||||
<< " failed\n";
|
||||
|
||||
return fail_count > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// webdriver.h — Thin W3C WebDriver client using cpp-httplib + nlohmann/json.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Usage:
|
||||
// webdriver::Session session; // starts headless Firefox via geckodriver
|
||||
// session.navigate("http://localhost:8080");
|
||||
// auto el = session.css("h1");
|
||||
// assert(el.text() == "Hello!");
|
||||
// // session destructor closes the browser
|
||||
#pragma once
|
||||
|
||||
#include "httplib.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace webdriver {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ─── Errors ──────────────────────────────────────────────────
|
||||
|
||||
class Error : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// ─── Forward declarations ────────────────────────────────────
|
||||
|
||||
class Session;
|
||||
|
||||
// ─── Element ─────────────────────────────────────────────────
|
||||
|
||||
class Element {
|
||||
friend class Session;
|
||||
|
||||
httplib::Client *cli_;
|
||||
std::string session_id_;
|
||||
std::string element_id_;
|
||||
|
||||
public:
|
||||
Element(httplib::Client *cli, const std::string &session_id,
|
||||
const std::string &element_id)
|
||||
: cli_(cli), session_id_(session_id), element_id_(element_id) {}
|
||||
|
||||
std::string url(const std::string &suffix = "") const {
|
||||
return "/session/" + session_id_ + "/element/" + element_id_ + suffix;
|
||||
}
|
||||
|
||||
public:
|
||||
std::string text() const {
|
||||
auto res = cli_->Get(url("/text"));
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Failed to get element text");
|
||||
}
|
||||
return json::parse(res->body)["value"].get<std::string>();
|
||||
}
|
||||
|
||||
std::string attribute(const std::string &name) const {
|
||||
auto res = cli_->Get(url("/attribute/" + name));
|
||||
if (!res || res->status != 200) { return ""; }
|
||||
auto val = json::parse(res->body)["value"];
|
||||
return val.is_null() ? "" : val.get<std::string>();
|
||||
}
|
||||
|
||||
std::string property(const std::string &name) const {
|
||||
auto res = cli_->Get(url("/property/" + name));
|
||||
if (!res || res->status != 200) { return ""; }
|
||||
auto val = json::parse(res->body)["value"];
|
||||
return val.is_null() ? "" : val.get<std::string>();
|
||||
}
|
||||
|
||||
void click() const {
|
||||
auto res = cli_->Post(url("/click"), "{}", "application/json");
|
||||
if (!res || res->status != 200) { throw Error("Failed to click element"); }
|
||||
}
|
||||
|
||||
void send_keys(const std::string &keys) const {
|
||||
json body = {{"text", keys}};
|
||||
auto res = cli_->Post(url("/value"), body.dump(), "application/json");
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Failed to send keys to element");
|
||||
}
|
||||
}
|
||||
|
||||
void clear() const {
|
||||
auto res = cli_->Post(url("/clear"), "{}", "application/json");
|
||||
if (!res || res->status != 200) { throw Error("Failed to clear element"); }
|
||||
}
|
||||
|
||||
std::string tag_name() const {
|
||||
auto res = cli_->Get(url("/name"));
|
||||
if (!res || res->status != 200) { throw Error("Failed to get tag name"); }
|
||||
return json::parse(res->body)["value"].get<std::string>();
|
||||
}
|
||||
|
||||
bool is_displayed() const {
|
||||
auto res = cli_->Get(url("/displayed"));
|
||||
if (!res || res->status != 200) { return false; }
|
||||
return json::parse(res->body)["value"].get<bool>();
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Session ─────────────────────────────────────────────────
|
||||
|
||||
class Session {
|
||||
httplib::Client cli_;
|
||||
std::string session_id_;
|
||||
|
||||
// W3C WebDriver uses this key for element references
|
||||
static constexpr const char *ELEMENT_KEY =
|
||||
"element-6066-11e4-a52e-4f735466cecf";
|
||||
|
||||
std::string extract_element_id(const json &value) const {
|
||||
if (value.contains(ELEMENT_KEY)) {
|
||||
return value[ELEMENT_KEY].get<std::string>();
|
||||
}
|
||||
// Fallback: try "ELEMENT" (older protocol)
|
||||
if (value.contains("ELEMENT")) {
|
||||
return value["ELEMENT"].get<std::string>();
|
||||
}
|
||||
throw Error("No element identifier in response: " + value.dump());
|
||||
}
|
||||
|
||||
std::string url(const std::string &suffix) const {
|
||||
return "/session/" + session_id_ + suffix;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit Session(const std::string &host = "127.0.0.1", int port = 4444)
|
||||
: cli_(host, port) {
|
||||
cli_.set_read_timeout(std::chrono::seconds(30));
|
||||
cli_.set_connection_timeout(std::chrono::seconds(5));
|
||||
|
||||
json caps = {
|
||||
{"capabilities",
|
||||
{{"alwaysMatch",
|
||||
{{"moz:firefoxOptions", {{"args", json::array({"-headless"})}}}}}}}};
|
||||
|
||||
auto res = cli_.Post("/session", caps.dump(), "application/json");
|
||||
if (!res) { throw Error("Cannot connect to geckodriver"); }
|
||||
if (res->status != 200) {
|
||||
throw Error("Failed to create session: " + res->body);
|
||||
}
|
||||
|
||||
auto body = json::parse(res->body);
|
||||
session_id_ = body["value"]["sessionId"].get<std::string>();
|
||||
}
|
||||
|
||||
~Session() {
|
||||
try {
|
||||
cli_.Delete(url(""));
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
// Non-copyable, non-movable (owns a session)
|
||||
Session(const Session &) = delete;
|
||||
Session &operator=(const Session &) = delete;
|
||||
|
||||
// ─── Navigation ──────────────────────────────────────────
|
||||
|
||||
void navigate(const std::string &nav_url) {
|
||||
json body = {{"url", nav_url}};
|
||||
auto res = cli_.Post(url("/url"), body.dump(), "application/json");
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Failed to navigate to: " + nav_url);
|
||||
}
|
||||
}
|
||||
|
||||
std::string title() {
|
||||
auto res = cli_.Get(url("/title"));
|
||||
if (!res || res->status != 200) { throw Error("Failed to get title"); }
|
||||
return json::parse(res->body)["value"].get<std::string>();
|
||||
}
|
||||
|
||||
std::string current_url() {
|
||||
auto res = cli_.Get(url("/url"));
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Failed to get current URL");
|
||||
}
|
||||
return json::parse(res->body)["value"].get<std::string>();
|
||||
}
|
||||
|
||||
// ─── Find elements ──────────────────────────────────────
|
||||
|
||||
Element find(const std::string &using_, const std::string &value) {
|
||||
json body = {{"using", using_}, {"value", value}};
|
||||
auto res = cli_.Post(url("/element"), body.dump(), "application/json");
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Element not found: " + using_ + "=" + value);
|
||||
}
|
||||
auto eid = extract_element_id(json::parse(res->body)["value"]);
|
||||
return Element(&cli_, session_id_, eid);
|
||||
}
|
||||
|
||||
std::vector<Element> find_all(const std::string &using_,
|
||||
const std::string &value) {
|
||||
json body = {{"using", using_}, {"value", value}};
|
||||
auto res = cli_.Post(url("/elements"), body.dump(), "application/json");
|
||||
if (!res || res->status != 200) { return {}; }
|
||||
|
||||
std::vector<Element> elements;
|
||||
for (auto &v : json::parse(res->body)["value"]) {
|
||||
elements.emplace_back(&cli_, session_id_, extract_element_id(v));
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
// Convenience: find by CSS selector
|
||||
Element css(const std::string &selector) {
|
||||
return find("css selector", selector);
|
||||
}
|
||||
|
||||
std::vector<Element> css_all(const std::string &selector) {
|
||||
return find_all("css selector", selector);
|
||||
}
|
||||
|
||||
// ─── Wait ────────────────────────────────────────────────
|
||||
|
||||
// Poll for an element until it appears or timeout
|
||||
Element wait_for(const std::string &selector, int timeout_ms = 5000) {
|
||||
auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::milliseconds(timeout_ms);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
try {
|
||||
return css(selector);
|
||||
} catch (...) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
}
|
||||
throw Error("Timeout waiting for element: " + selector);
|
||||
}
|
||||
|
||||
// Wait until a JS expression returns truthy
|
||||
bool wait_until(const std::string &script, int timeout_ms = 5000) {
|
||||
auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::milliseconds(timeout_ms);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
auto result = execute_script(script);
|
||||
if (result != "null" && result != "false" && result != "" &&
|
||||
result != "0" && result != "undefined") {
|
||||
return true;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Execute script ─────────────────────────────────────
|
||||
|
||||
std::string execute_script(const std::string &script,
|
||||
const json &args = json::array()) {
|
||||
json body = {{"script", script}, {"args", args}};
|
||||
auto res = cli_.Post(url("/execute/sync"), body.dump(), "application/json");
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Failed to execute script: " + script);
|
||||
}
|
||||
auto val = json::parse(res->body)["value"];
|
||||
if (val.is_null()) { return "null"; }
|
||||
if (val.is_string()) { return val.get<std::string>(); }
|
||||
return val.dump();
|
||||
}
|
||||
|
||||
// ─── Page source ────────────────────────────────────────
|
||||
|
||||
std::string page_source() {
|
||||
auto res = cli_.Get(url("/source"));
|
||||
if (!res || res->status != 200) {
|
||||
throw Error("Failed to get page source");
|
||||
}
|
||||
return json::parse(res->body)["value"].get<std::string>();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace webdriver
|
||||
@@ -1,603 +0,0 @@
|
||||
:root {
|
||||
--bg: #333;
|
||||
--bg-secondary: #3c3c3c;
|
||||
--bg-code: #2a2a2a;
|
||||
--text: #ccc;
|
||||
--text-bright: white;
|
||||
--text-muted: #999;
|
||||
--text-code: #b0b0b0;
|
||||
--text-inline-code: plum;
|
||||
--border: #555;
|
||||
--border-code: #3a3a3a;
|
||||
--link: palegoldenrod;
|
||||
--heading: lightskyblue;
|
||||
--heading-link: #f0c090;
|
||||
--header-nav-link: pink;
|
||||
--emphasis: pink;
|
||||
--nav-section: #bbb;
|
||||
--nav-section-active: #ddd;
|
||||
--content-width: 900px;
|
||||
--sidebar-width: 280px;
|
||||
--header-height: 48px;
|
||||
--line-height: 1.6;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--header-height);
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: var(--text);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-title:hover {
|
||||
text-decoration: none;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--header-nav-link);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.header-nav a svg {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.header-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.lang-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lang-popup {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 4px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
min-width: 60px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.lang-popup.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lang-popup li a {
|
||||
display: block;
|
||||
padding: 6px 12px;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.lang-popup li a:hover {
|
||||
background: var(--bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* Draft banner */
|
||||
.draft-banner {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
right: 0;
|
||||
background: #c44;
|
||||
color: white;
|
||||
padding: 4px 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.layout {
|
||||
margin-top: var(--header-height);
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
}
|
||||
|
||||
.layout.no-sidebar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
padding: 24px 16px;
|
||||
border-right: 1px solid var(--bg-secondary);
|
||||
position: sticky;
|
||||
top: var(--header-height);
|
||||
height: calc(100vh - var(--header-height));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
color: var(--nav-section);
|
||||
font-weight: bold;
|
||||
font-size: 1.0rem;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nav-section-title.active {
|
||||
color: var(--nav-section-active);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-list li a {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.nav-list li a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-list li a.active {
|
||||
color: var(--emphasis);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
min-width: 0;
|
||||
max-width: var(--content-width);
|
||||
padding: 32px 24px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.content.portal {
|
||||
max-width: var(--content-width);
|
||||
padding: 48px 24px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content article h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 24px;
|
||||
color: var(--heading);
|
||||
}
|
||||
|
||||
.content article h2 {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--heading-link);
|
||||
}
|
||||
|
||||
.content article h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.content article p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.content article ul,
|
||||
.content article ol {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.content article li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.content article strong {
|
||||
color: var(--emphasis);
|
||||
}
|
||||
|
||||
.content article code {
|
||||
background: var(--bg-code);
|
||||
color: var(--text-inline-code);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.content article pre {
|
||||
background: var(--bg-code);
|
||||
color: var(--text-code);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border-code);
|
||||
}
|
||||
|
||||
.content article pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content article table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content article th,
|
||||
.content article td {
|
||||
border: 1px solid var(--bg-secondary);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.content article th {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.content article blockquote {
|
||||
border-left: 3px solid var(--text-muted);
|
||||
padding-left: 16px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Code block theme switching: show dark by default, swap on light mode */
|
||||
.code-block-wrapper > [data-code-theme="light"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .code-block-wrapper > [data-code-theme="dark"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .code-block-wrapper > [data-code-theme="light"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: calc(-1 * var(--sidebar-width));
|
||||
width: var(--sidebar-width);
|
||||
top: var(--header-height);
|
||||
height: calc(100vh - var(--header-height));
|
||||
background: var(--bg);
|
||||
z-index: 50;
|
||||
transition: left 0.2s ease;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
:root {
|
||||
--header-height: 44px;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
padding: 0 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-nav a {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.content article h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.content article h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light mode */
|
||||
[data-theme="light"] {
|
||||
--bg: #f5f5f5;
|
||||
--bg-secondary: #e8e8e8;
|
||||
--bg-code: #eee;
|
||||
--text: #333;
|
||||
--text-bright: #000;
|
||||
--text-muted: #666;
|
||||
--text-code: #333;
|
||||
--text-inline-code: #8b5ca0;
|
||||
--border: #ccc;
|
||||
--border-code: #ddd;
|
||||
--link: #b8860b;
|
||||
--heading: #2a6496;
|
||||
--heading-link: #c06020;
|
||||
--header-nav-link: #c04060;
|
||||
--emphasis: #c04060;
|
||||
--nav-section: #666;
|
||||
--nav-section-active: #333;
|
||||
}
|
||||
|
||||
/* Theme toggle */
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Search button */
|
||||
.search-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Search overlay & modal */
|
||||
.search-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 300;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: 12vh;
|
||||
}
|
||||
|
||||
.search-overlay.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 560px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-input-wrap svg {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-bright);
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-esc {
|
||||
background: var(--bg);
|
||||
color: var(--text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
list-style: none;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-results:empty::after {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-results li {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-results li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-results li:hover,
|
||||
.search-results li.active {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.search-results li .search-result-title {
|
||||
color: var(--text-bright);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.search-results li .search-result-snippet {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.search-results li .search-result-snippet mark {
|
||||
background: rgba(255, 215, 0, 0.3);
|
||||
color: var(--text-bright);
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .search-results li .search-result-snippet mark {
|
||||
background: rgba(255, 200, 0, 0.4);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search-no-results {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cookbook - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/cookbook/" class="nav-section-title active">Cookbook</a>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Cookbook</h1>
|
||||
<p>This section is under construction.</p>
|
||||
<p>Check back soon for a collection of recipes organized by topic.</p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,108 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>cpp-httplib - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout no-sidebar">
|
||||
|
||||
<main class="content portal">
|
||||
<article>
|
||||
<h1>cpp-httplib</h1>
|
||||
<p><a href="https://github.com/yhirose/cpp-httplib">cpp-httplib</a> is an HTTP/HTTPS library for C++. Just copy a single header file, <a href="https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h"><code>httplib.h</code></a>, and you're ready to go.</p>
|
||||
<p>When you need a quick HTTP server or client in C++, you want something that just works. That's exactly why I built cpp-httplib. You can start writing both servers and clients in just a few lines of code.</p>
|
||||
<p>The API uses a lambda-based design that feels natural. It runs anywhere you have a C++11 or later compiler. Windows, macOS, Linux — use whatever environment you already have.</p>
|
||||
<p>HTTPS works too. Just link OpenSSL or mbedTLS, and both server and client gain TLS support. Content-Encoding (gzip, Brotli, etc.), file uploads, and other features you actually need in real-world development are all included. WebSocket is also supported.</p>
|
||||
<p>Under the hood, it uses blocking I/O with a thread pool. It's not built for handling massive numbers of simultaneous connections. But for API servers, embedded HTTP in tools, mock servers for testing, and many other use cases, it delivers solid performance.</p>
|
||||
<p>"Solve today's problem, today." That's the kind of simplicity cpp-httplib aims for.</p>
|
||||
<h2>Documentation</h2>
|
||||
<ul>
|
||||
<li><a href="tour/">A Tour of cpp-httplib</a> — A step-by-step tutorial covering the basics. Start here if you're new</li>
|
||||
<li><a href="cookbook/">Cookbook</a> — A collection of recipes organized by topic. Jump to whatever you need</li>
|
||||
</ul>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,234 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Getting Started - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="active">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Getting Started</h1>
|
||||
<p>All you need to get started with cpp-httplib is <code>httplib.h</code> and a C++ compiler. Let's download the file and get a Hello World server running.</p>
|
||||
<h2>Getting httplib.h</h2>
|
||||
<p>You can download it directly from GitHub. Always use the latest version.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -LO</span><span style="color:#d3d0c8;"> https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Place the downloaded <code>httplib.h</code> in your project directory and you're good to go.</p>
|
||||
<h2>Setting Up Your Compiler</h2>
|
||||
<table><thead><tr><th>OS</th><th>Development Environment</th><th>Setup</th></tr></thead><tbody>
|
||||
<tr><td>macOS</td><td>Apple Clang</td><td>Xcode Command Line Tools (<code>xcode-select --install</code>)</td></tr>
|
||||
<tr><td>Ubuntu</td><td>clang++ or g++</td><td><code>apt install clang</code> or <code>apt install g++</code></td></tr>
|
||||
<tr><td>Windows</td><td>MSVC</td><td>Visual Studio 2022 or later (install with C++ components)</td></tr>
|
||||
</tbody></table>
|
||||
<h2>Hello World Server</h2>
|
||||
<p>Save the following code as <code>server.cpp</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request&, httplib::Response& res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, World!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request</span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::Response</span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;"> res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello, World!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>In just a few lines, you have a server that responds to HTTP requests.</p>
|
||||
<h2>Compiling and Running</h2>
|
||||
<p>The sample code in this tutorial is written in C++17 for cleaner, more concise code. cpp-httplib itself can compile with C++11 as well.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;"># macOS
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Linux
|
||||
</span><span style="color:#747369;"># `-pthread`: cpp-httplib uses threads internally
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -pthread -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Windows (Developer Command Prompt)
|
||||
</span><span style="color:#747369;"># `/EHsc`: Enable C++ exception handling
|
||||
</span><span style="color:#6699cc;">cl</span><span style="color:#d3d0c8;"> /EHsc /std:c++17 server.cpp
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;"># macOS
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Linux
|
||||
</span><span style="font-style:italic;color:#969896;"># `-pthread`: cpp-httplib uses threads internally
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -pthread -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Windows (Developer Command Prompt)
|
||||
</span><span style="font-style:italic;color:#969896;"># `/EHsc`: Enable C++ exception handling
|
||||
</span><span style="color:#323232;">cl /EHsc /std:c++17 server.cpp
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Once it compiles, run it.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;"># macOS / Linux
|
||||
</span><span style="color:#6699cc;">./server
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Windows
|
||||
</span><span style="color:#6699cc;">server.exe
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;"># macOS / Linux
|
||||
</span><span style="color:#323232;">./server
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Windows
|
||||
</span><span style="color:#323232;">server.exe
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Open <code>http://localhost:8080</code> in your browser. If you see "Hello, World!", you're all set.</p>
|
||||
<p>You can also verify with <code>curl</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/
|
||||
</span><span style="color:#747369;"># Hello, World!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello, World!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>To stop the server, press <code>Ctrl+C</code> in your terminal.</p>
|
||||
<h2>Next Steps</h2>
|
||||
<p>Now you know the basics of running a server. Next, let's look at the client side. cpp-httplib also comes with HTTP client functionality.</p>
|
||||
<p><strong>Next:</strong> <a href="../02-basic-client">Basic Client</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,527 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Basic Client - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="active">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Basic Client</h1>
|
||||
<p>cpp-httplib isn't just for servers -- it also comes with a full HTTP client. Let's use <code>httplib::Client</code> to send GET and POST requests.</p>
|
||||
<h2>Preparing a Test Server</h2>
|
||||
<p>To try out the client, you need a server that accepts requests. Save the following code, then compile and run it the same way you did in the previous chapter. We'll cover the server details in the next chapter.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> q = req.</span><span style="color:#6699cc;">get_param_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Query: </span><span style="color:#d3d0c8;">" + q, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(req.</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::string result;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&[key, val] : req.</span><span style="color:#f2777a;">params</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> result += key + "</span><span style="color:#99cc99;"> = </span><span style="color:#d3d0c8;">" + val + "</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(result, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> f = req.</span><span style="color:#f2777a;">form</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">get_file</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> content = f.</span><span style="color:#f2777a;">filename </span><span style="color:#d3d0c8;">+ "</span><span style="color:#99cc99;"> (</span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::to_string</span><span style="color:#d3d0c8;">(f.</span><span style="color:#f2777a;">content</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()) + "</span><span style="color:#99cc99;"> bytes)</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(content, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User ID: </span><span style="color:#d3d0c8;">" + id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">/files/(\d+)</span><span style="color:#d3d0c8;">)", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">matches</span><span style="color:#d3d0c8;">[</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">];
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">File ID: </span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">(id), "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on port 8080...</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> q </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_param_value(</span><span style="color:#183691;">"q"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Query: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> q, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(req.body, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::string result;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">for </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">[key, val] </span><span style="font-weight:bold;color:#a71d5d;">:</span><span style="color:#323232;"> req.params) {
|
||||
</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">+=</span><span style="color:#323232;"> key </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" = " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> val </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">"</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> res.set_content(result, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> f </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.form.get_file(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> content </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> f.filename </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" (" </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::to_string(f.content.size()) </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" bytes)"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(content, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/users/:id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User ID: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"(/files/(\d+))"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.matches[</span><span style="color:#0086b3;">1</span><span style="color:#323232;">];
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"File ID: " </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::string(id), </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on port 8080..." </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>GET Request</h2>
|
||||
<p>Once the server is running, open a separate terminal and give it a try. Let's start with the simplest GET request.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Pass the server address to the <code>httplib::Client</code> constructor, then call <code>Get()</code> to send a request. You can retrieve the status code and body from the returned <code>res</code>.</p>
|
||||
<p>Here's the equivalent <code>curl</code> command.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/hi
|
||||
</span><span style="color:#747369;"># Hello!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/hi
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Checking the Response</h2>
|
||||
<p>A response contains header information in addition to the status code and body.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Status code
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Body
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello!
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Headers
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Content-Type</span><span style="color:#d3d0c8;">") << std::endl; </span><span style="color:#747369;">// text/plain
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Status code
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Body
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello!
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Headers
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->get_header_value(</span><span style="color:#183691;">"Content-Type"</span><span style="color:#323232;">) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// text/plain
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>res->body</code> is a <code>std::string</code>, so if you want to parse a JSON response, you can pass it directly to a JSON library like <a href="https://github.com/nlohmann/json">nlohmann/json</a>.</p>
|
||||
<h2>Query Parameters</h2>
|
||||
<p>To add query parameters to a GET request, you can either write them directly in the URL or use <code>httplib::Params</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", httplib::Params{{"</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">cpp-httplib</span><span style="color:#d3d0c8;">"}});
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Query: cpp-httplib
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, httplib::Params{{</span><span style="color:#183691;">"q"</span><span style="color:#323232;">, </span><span style="color:#183691;">"cpp-httplib"</span><span style="color:#323232;">}});
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Query: cpp-httplib
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>httplib::Params</code> automatically URL-encodes special characters for you.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">http://localhost:8080/search?q=cpp-httplib</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#747369;"># Query: cpp-httplib
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl </span><span style="color:#183691;">"http://localhost:8080/search?q=cpp-httplib"
|
||||
</span><span style="font-style:italic;color:#969896;"># Query: cpp-httplib
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Path Parameters</h2>
|
||||
<p>When values are embedded directly in the URL path, no special client API is needed. Just pass the path to <code>Get()</code> as-is.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/42</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// User ID: 42
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/users/42"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// User ID: 42
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/users/42
|
||||
</span><span style="color:#747369;"># User ID: 42
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/users/42
|
||||
</span><span style="font-style:italic;color:#969896;"># User ID: 42
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The test server also has a <code>/files/(\d+)</code> route that uses a regex to accept numeric IDs only.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/files/42</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// File ID: 42
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/files/42"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// File ID: 42
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/files/42
|
||||
</span><span style="color:#747369;"># File ID: 42
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/files/42
|
||||
</span><span style="font-style:italic;color:#969896;"># File ID: 42
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Pass a non-numeric ID like <code>/files/abc</code> and you'll get a 404. We'll cover how that works in the next chapter.</p>
|
||||
<h2>Request Headers</h2>
|
||||
<p>To add custom HTTP headers, pass an <code>httplib::Headers</code> object. This works with both <code>Get()</code> and <code>Post()</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", httplib::Headers{
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">Authorization</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Bearer my-token</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, httplib::Headers{
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"Authorization"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Bearer my-token"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -H </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">Authorization: Bearer my-token</span><span style="color:#d3d0c8;">" http://localhost:8080/hi
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -H </span><span style="color:#183691;">"Authorization: Bearer my-token"</span><span style="color:#323232;"> http://localhost:8080/hi
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>POST Request</h2>
|
||||
<p>Let's POST some text data. Pass the body as the second argument to <code>Post()</code> and the Content-Type as the third.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Hello, Server!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello, Server!
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Hello, Server!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, Server!
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The test server's <code>/post</code> endpoint echoes the body back, so you get the same string you sent.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -X</span><span style="color:#d3d0c8;"> POST</span><span style="color:#f2777a;"> -H </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">Content-Type: text/plain</span><span style="color:#d3d0c8;">"</span><span style="color:#f2777a;"> -d </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">Hello, Server!</span><span style="color:#d3d0c8;">" http://localhost:8080/post
|
||||
</span><span style="color:#747369;"># Hello, Server!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -X POST -H </span><span style="color:#183691;">"Content-Type: text/plain"</span><span style="color:#323232;"> -d </span><span style="color:#183691;">"Hello, Server!"</span><span style="color:#323232;"> http://localhost:8080/post
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello, Server!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Sending Form Data</h2>
|
||||
<p>You can send key-value pairs just like an HTML form. Use <code>httplib::Params</code> for this.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", httplib::Params{
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">name</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Alice</span><span style="color:#d3d0c8;">"},
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">age</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">30</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// age = 30
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// name = Alice
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, httplib::Params{
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"name"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Alice"</span><span style="color:#323232;">},
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"age"</span><span style="color:#323232;">, </span><span style="color:#183691;">"30"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// age = 30
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// name = Alice
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>This sends the data in <code>application/x-www-form-urlencoded</code> format.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -X</span><span style="color:#d3d0c8;"> POST</span><span style="color:#f2777a;"> -d </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">name=Alice&age=30</span><span style="color:#d3d0c8;">" http://localhost:8080/submit
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -X POST -d </span><span style="color:#183691;">"name=Alice&age=30"</span><span style="color:#323232;"> http://localhost:8080/submit
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>POSTing a File</h2>
|
||||
<p>To upload a file, use <code>httplib::UploadFormDataItems</code> to send it as multipart form data.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", httplib::UploadFormDataItems{
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Hello, File!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">hello.txt</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// hello.txt (12 bytes)
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, httplib::UploadFormDataItems{
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"file"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Hello, File!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"hello.txt"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// hello.txt (12 bytes)
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Each element in <code>UploadFormDataItems</code> has four fields: <code>{name, content, filename, content_type}</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -F </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">file=Hello, File!;filename=hello.txt;type=text/plain</span><span style="color:#d3d0c8;">" http://localhost:8080/upload
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -F </span><span style="color:#183691;">"file=Hello, File!;filename=hello.txt;type=text/plain"</span><span style="color:#323232;"> http://localhost:8080/upload
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Error Handling</h2>
|
||||
<p>Network communication can fail -- the server might not be reachable. Always check whether <code>res</code> is valid.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:9999</span><span style="color:#d3d0c8;">"); </span><span style="color:#747369;">// Non-existent port
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(!res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Connection error
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Error: </span><span style="color:#d3d0c8;">" << </span><span style="color:#6699cc;">httplib::to_string</span><span style="color:#d3d0c8;">(res.</span><span style="color:#6699cc;">error</span><span style="color:#d3d0c8;">()) << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Error: Connection
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// If we reach here, we have a response
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;">!= </span><span style="color:#f99157;">200</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">HTTP Error: </span><span style="color:#d3d0c8;">" << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:9999"</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// Non-existent port
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">!</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Connection error
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Error: " </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#323232;">httplib::to_string(res.error()) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Error: Connection
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">1</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// If we reach here, we have a response
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res->status </span><span style="font-weight:bold;color:#a71d5d;">!= </span><span style="color:#0086b3;">200</span><span style="color:#323232;">) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"HTTP Error: " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">1</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>There are two levels of errors.</p>
|
||||
<ul>
|
||||
<li><strong>Connection error</strong>: The client couldn't reach the server. <code>res</code> evaluates to false, and you can call <code>res.error()</code> to find out what went wrong.</li>
|
||||
<li><strong>HTTP error</strong>: The server returned an error status (404, 500, etc.). <code>res</code> evaluates to true, but you need to check <code>res->status</code>.</li>
|
||||
</ul>
|
||||
<h2>Next Steps</h2>
|
||||
<p>Now you know how to send requests from a client. Next, let's take a closer look at the server side. We'll dig into routing, path parameters, and more.</p>
|
||||
<p><strong>Next:</strong> <a href="../03-basic-server">Basic Server</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,482 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Basic Server - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="active">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Basic Server</h1>
|
||||
<p>In the previous chapter, you sent requests from a client to a test server. Now let's walk through how that server actually works.</p>
|
||||
<h2>Starting the Server</h2>
|
||||
<p>Once you've registered your routes, call <code>svr.listen()</code> to start the server.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The first argument is the host, and the second is the port. <code>"0.0.0.0"</code> listens on all network interfaces. Use <code>"127.0.0.1"</code> if you want to accept connections from your own machine only.</p>
|
||||
<p><code>listen()</code> is a blocking call. It won't return until the server stops. The server keeps running until you press <code>Ctrl+C</code> in your terminal or call <code>svr.stop()</code> from another thread.</p>
|
||||
<h2>Routing</h2>
|
||||
<p>Routing is the heart of any server. It's how you tell cpp-httplib: when a request comes in for this URL with this HTTP method, run this code.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &req, httplib::Response &res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">req, httplib::Response </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>svr.Get()</code> registers a handler for GET requests. The first argument is the path, the second is the handler function. When a GET request arrives at <code>/hi</code>, your lambda runs.</p>
|
||||
<p>There's a method for each HTTP verb.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// GET
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// POST
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Put</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// PUT
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Delete</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// DELETE
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// GET
|
||||
</span><span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// POST
|
||||
</span><span style="color:#323232;">svr.Put(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// PUT
|
||||
</span><span style="color:#323232;">svr.Delete(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// DELETE
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The handler signature is <code>(const httplib::Request &req, httplib::Response &res)</code>. You can use <code>auto</code> to keep it short.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The handler only runs when the path matches. Requests to unregistered paths automatically return 404.</p>
|
||||
<h2>The Request Object</h2>
|
||||
<p>The first parameter <code>req</code> gives you everything the client sent.</p>
|
||||
<h3>Body</h3>
|
||||
<p><code>req.body</code> holds the request body as a <code>std::string</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Echo the body back to the client
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(req.</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Echo the body back to the client
|
||||
</span><span style="color:#323232;"> res.set_content(req.body, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Headers</h3>
|
||||
<p>Use <code>req.get_header_value()</code> to read a request header.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/check</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> auth = req.</span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Authorization</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Auth: </span><span style="color:#d3d0c8;">" + auth, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/check"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> auth </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_header_value(</span><span style="color:#183691;">"Authorization"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Auth: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> auth, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Query Parameters and Form Data</h3>
|
||||
<p><code>req.get_param_value()</code> retrieves a parameter by name. It works for both GET query parameters and POST form data.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> q = req.</span><span style="color:#6699cc;">get_param_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Query: </span><span style="color:#d3d0c8;">" + q, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> q </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_param_value(</span><span style="color:#183691;">"q"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Query: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> q, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>A request to <code>/search?q=cpp-httplib</code> gives you <code>"cpp-httplib"</code> for <code>q</code>.</p>
|
||||
<p>To loop over all parameters, use <code>req.params</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::string result;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&[key, val] : req.</span><span style="color:#f2777a;">params</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> result += key + "</span><span style="color:#99cc99;"> = </span><span style="color:#d3d0c8;">" + val + "</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(result, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::string result;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">for </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">[key, val] </span><span style="font-weight:bold;color:#a71d5d;">:</span><span style="color:#323232;"> req.params) {
|
||||
</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">+=</span><span style="color:#323232;"> key </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" = " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> val </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">"</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> res.set_content(result, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>File Uploads</h3>
|
||||
<p>Files uploaded via multipart form data are available through <code>req.form.get_file()</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> f = req.</span><span style="color:#f2777a;">form</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">get_file</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> content = f.</span><span style="color:#f2777a;">filename </span><span style="color:#d3d0c8;">+ "</span><span style="color:#99cc99;"> (</span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::to_string</span><span style="color:#d3d0c8;">(f.</span><span style="color:#f2777a;">content</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()) + "</span><span style="color:#99cc99;"> bytes)</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(content, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> f </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.form.get_file(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> content </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> f.filename </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" (" </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::to_string(f.content.size()) </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" bytes)"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(content, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>f.filename</code> gives you the filename, and <code>f.content</code> gives you the file data.</p>
|
||||
<h2>Path Parameters</h2>
|
||||
<p>Sometimes you want to capture part of the URL as a variable -- for example, the <code>42</code> in <code>/users/42</code>. Use the <code>:param</code> syntax to do that.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User ID: </span><span style="color:#d3d0c8;">" + id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/users/:id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User ID: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>A request to <code>/users/42</code> gives you <code>"42"</code> from <code>req.path_params.at("id")</code>. <code>/users/100</code> gives you <code>"100"</code>.</p>
|
||||
<p>You can capture multiple segments at once.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:user_id/posts/:post_id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> user_id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">user_id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> post_id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">post_id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User: </span><span style="color:#d3d0c8;">" + user_id + "</span><span style="color:#99cc99;">, Post: </span><span style="color:#d3d0c8;">" + post_id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/users/:user_id/posts/:post_id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> user_id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"user_id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> post_id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"post_id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> user_id </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">", Post: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> post_id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Regex Patterns</h3>
|
||||
<p>You can also write a regular expression directly in the path instead of <code>:param</code>. Capture group values are available via <code>req.matches</code>, which is a <code>std::smatch</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// Only accept numeric IDs
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">/files/(\d+)</span><span style="color:#d3d0c8;">)", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">matches</span><span style="color:#d3d0c8;">[</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">]; </span><span style="color:#747369;">// First capture group
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">File ID: </span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">(id), "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// Only accept numeric IDs
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"(/files/(\d+))"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.matches[</span><span style="color:#0086b3;">1</span><span style="color:#323232;">]; </span><span style="font-style:italic;color:#969896;">// First capture group
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"File ID: " </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::string(id), </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>/files/42</code> matches, but <code>/files/abc</code> doesn't. This is handy when you want to constrain what values are accepted.</p>
|
||||
<h2>Building a Response</h2>
|
||||
<p>The second parameter <code>res</code> is how you send data back to the client.</p>
|
||||
<h3>Body and Content-Type</h3>
|
||||
<p><code>res.set_content()</code> sets the body and Content-Type. That's all you need for a 200 response.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Status Code</h3>
|
||||
<p>To return a different status code, assign to <code>res.status</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/not-found</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;">= </span><span style="color:#f99157;">404</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Not found</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/not-found"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.status </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#0086b3;">404</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Not found"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Response Headers</h3>
|
||||
<p>Add response headers with <code>res.set_header()</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/with-header</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_header</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">X-Custom</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">my-value</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/with-header"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_header(</span><span style="color:#183691;">"X-Custom"</span><span style="color:#323232;">, </span><span style="color:#183691;">"my-value"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Walking Through the Test Server</h2>
|
||||
<p>Now let's use what we've learned to read through the test server from the previous chapter.</p>
|
||||
<h3>GET /hi</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The simplest possible handler. We don't need any information from the request, so the <code>req</code> parameter is left unnamed. It just returns <code>"Hello!"</code>.</p>
|
||||
<h3>GET /search</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> q = req.</span><span style="color:#6699cc;">get_param_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Query: </span><span style="color:#d3d0c8;">" + q, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> q </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_param_value(</span><span style="color:#183691;">"q"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Query: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> q, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>req.get_param_value("q")</code> pulls out the query parameter <code>q</code>. A request to <code>/search?q=cpp-httplib</code> returns <code>"Query: cpp-httplib"</code>.</p>
|
||||
<h3>POST /post</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(req.</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(req.body, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>An echo server. Whatever body the client sends, <code>req.body</code> holds it, and we send it straight back.</p>
|
||||
<h3>POST /submit</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::string result;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&[key, val] : req.</span><span style="color:#f2777a;">params</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> result += key + "</span><span style="color:#99cc99;"> = </span><span style="color:#d3d0c8;">" + val + "</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(result, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::string result;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">for </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">[key, val] </span><span style="font-weight:bold;color:#a71d5d;">:</span><span style="color:#323232;"> req.params) {
|
||||
</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">+=</span><span style="color:#323232;"> key </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" = " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> val </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">"</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> res.set_content(result, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Loops over the form data in <code>req.params</code> using structured bindings (<code>auto &[key, val]</code>) to unpack each key-value pair.</p>
|
||||
<h3>POST /upload</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> f = req.</span><span style="color:#f2777a;">form</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">get_file</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> content = f.</span><span style="color:#f2777a;">filename </span><span style="color:#d3d0c8;">+ "</span><span style="color:#99cc99;"> (</span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::to_string</span><span style="color:#d3d0c8;">(f.</span><span style="color:#f2777a;">content</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()) + "</span><span style="color:#99cc99;"> bytes)</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(content, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> f </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.form.get_file(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> content </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> f.filename </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" (" </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::to_string(f.content.size()) </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" bytes)"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(content, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Receives a file uploaded via multipart form data. <code>req.form.get_file("file")</code> fetches the field named <code>"file"</code>, and we respond with the filename and size.</p>
|
||||
<h3>GET /users/:id</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User ID: </span><span style="color:#d3d0c8;">" + id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/users/:id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User ID: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>:id</code> is the path parameter. <code>req.path_params.at("id")</code> retrieves its value. <code>/users/42</code> gives you <code>"42"</code>, <code>/users/alice</code> gives you <code>"alice"</code>.</p>
|
||||
<h3>GET /files/(\d+)</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">/files/(\d+)</span><span style="color:#d3d0c8;">)", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">matches</span><span style="color:#d3d0c8;">[</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">];
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">File ID: </span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">(id), "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"(/files/(\d+))"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.matches[</span><span style="color:#0086b3;">1</span><span style="color:#323232;">];
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"File ID: " </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::string(id), </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The regex <code>(\d+)</code> matches numeric IDs only. <code>/files/42</code> hits this handler, but <code>/files/abc</code> returns 404. <code>req.matches[1]</code> retrieves the first capture group.</p>
|
||||
<h2>Next Steps</h2>
|
||||
<p>You now have the full picture of how a server works. Routing, reading requests, building responses -- that's enough to build a real API server.</p>
|
||||
<p>Next, let's look at serving static files. We'll build a server that delivers HTML and CSS.</p>
|
||||
<p><strong>Next:</strong> <a href="../04-static-file-server">Static File Server</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,305 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Static File Server - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="active">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Static File Server</h1>
|
||||
<p>cpp-httplib can serve static files too — HTML, CSS, images, you name it. No complicated configuration required. One call to <code>set_mount_point()</code> is all it takes.</p>
|
||||
<h2>The basics of set_mount_point</h2>
|
||||
<p>Let's jump right in. <code>set_mount_point()</code> maps a URL path to a local directory.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./html</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on port 8080...</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./html"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on port 8080..." </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>The first argument is the URL mount point. The second is the local directory path. In this example, requests to <code>/</code> are served from the <code>./html</code> directory.</p>
|
||||
<p>Let's try it out. First, create an <code>html</code> directory and add an <code>index.html</code> file.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">mkdir</span><span style="color:#d3d0c8;"> html
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">mkdir html
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;"><!</span><span style="color:#f2777a;">DOCTYPE</span><span style="color:#d3d0c8;"> html>
|
||||
</span><span style="color:#d3d0c8;"><</span><span style="color:#f2777a;">html</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"><</span><span style="color:#f2777a;">head</span><span style="color:#d3d0c8;">><</span><span style="color:#f2777a;">title</span><span style="color:#d3d0c8;">>My Page</</span><span style="color:#f2777a;">title</span><span style="color:#d3d0c8;">></</span><span style="color:#f2777a;">head</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"><</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"> <</span><span style="color:#f2777a;">h1</span><span style="color:#d3d0c8;">>Hello from cpp-httplib!</</span><span style="color:#f2777a;">h1</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"> <</span><span style="color:#f2777a;">p</span><span style="color:#d3d0c8;">>This is a static file.</</span><span style="color:#f2777a;">p</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"></</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"></</span><span style="color:#f2777a;">html</span><span style="color:#d3d0c8;">>
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;"><!</span><span style="color:#63a35c;">DOCTYPE</span><span style="color:#323232;"> html>
|
||||
</span><span style="color:#323232;"><</span><span style="color:#63a35c;">html</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"><</span><span style="color:#63a35c;">head</span><span style="color:#323232;">><</span><span style="color:#63a35c;">title</span><span style="color:#323232;">>My Page</</span><span style="color:#63a35c;">title</span><span style="color:#323232;">></</span><span style="color:#63a35c;">head</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"><</span><span style="color:#63a35c;">body</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"> <</span><span style="color:#63a35c;">h1</span><span style="color:#323232;">>Hello from cpp-httplib!</</span><span style="color:#63a35c;">h1</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"> <</span><span style="color:#63a35c;">p</span><span style="color:#323232;">>This is a static file.</</span><span style="color:#63a35c;">p</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"></</span><span style="color:#63a35c;">body</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"></</span><span style="color:#63a35c;">html</span><span style="color:#323232;">>
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Compile and start the server.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">g++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp</span><span style="color:#f2777a;"> -pthread
|
||||
</span><span style="color:#6699cc;">./server
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">g++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -o server server.cpp -pthread
|
||||
</span><span style="color:#323232;">./server
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Open <code>http://localhost:8080</code> in your browser. You should see the contents of <code>html/index.html</code>. Visiting <code>http://localhost:8080/index.html</code> returns the same page.</p>
|
||||
<p>You can also access it with the client code from the previous chapter, or with <code>curl</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// HTML is displayed
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// HTML is displayed
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Multiple mount points</h2>
|
||||
<p>You can call <code>set_mount_point()</code> as many times as you like. Each URL path gets its own directory.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./public</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/assets</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./static/assets</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/docs</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./documentation</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./public"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/assets"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./static/assets"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/docs"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./documentation"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>A request to <code>/assets/style.css</code> serves <code>./static/assets/style.css</code>. A request to <code>/docs/guide.html</code> serves <code>./documentation/guide.html</code>.</p>
|
||||
<h2>Combining with handlers</h2>
|
||||
<p>Static file serving and routing handlers — the kind you learned about in the previous chapter — work side by side.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// API endpoint
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/api/hello</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">{"message":"Hello!"}</span><span style="color:#d3d0c8;">)", "</span><span style="color:#99cc99;">application/json</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// Static file serving
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./public</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// API endpoint
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/api/hello"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"({"message":"Hello!"})"</span><span style="color:#323232;">, </span><span style="color:#183691;">"application/json"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// Static file serving
|
||||
</span><span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./public"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Handlers take priority. The handler responds to <code>/api/hello</code>. For every other path, the server looks for a file in <code>./public</code>.</p>
|
||||
<h2>Adding response headers</h2>
|
||||
<p>Pass headers as the third argument to <code>set_mount_point()</code> and they get attached to every static file response. This is great for cache control.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./public</span><span style="color:#d3d0c8;">", {
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">Cache-Control</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">max-age=3600</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./public"</span><span style="color:#323232;">, {
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"Cache-Control"</span><span style="color:#323232;">, </span><span style="color:#183691;">"max-age=3600"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>With this in place, the browser caches served files for one hour.</p>
|
||||
<h2>A Dockerfile for your static file server</h2>
|
||||
<p>The cpp-httplib repository includes a <code>Dockerfile</code> built for static file serving. We also publish a pre-built image on Docker Hub, so you can get up and running with a single command.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">> docker run</span><span style="color:#f2777a;"> -p</span><span style="color:#d3d0c8;"> 8080:80</span><span style="color:#f2777a;"> -v</span><span style="color:#d3d0c8;"> ./my-site:/html yhirose4dockerhub/cpp-httplib-server
|
||||
</span><span style="color:#6699cc;">Serving</span><span style="color:#d3d0c8;"> HTTP on 0.0.0.0:80
|
||||
</span><span style="color:#6699cc;">Mount</span><span style="color:#d3d0c8;"> point: / -> ./html
|
||||
</span><span style="color:#6699cc;">Press</span><span style="color:#d3d0c8;"> Ctrl+C to shutdown gracefully...
|
||||
</span><span style="color:#6699cc;">192.168.65.1</span><span style="color:#d3d0c8;"> - - </span><span style="color:#cc99cc;">[</span><span style="color:#d3d0c8;">22/Feb/2026:12:00:00 +0000</span><span style="color:#cc99cc;">] </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">GET / HTTP/1.1</span><span style="color:#d3d0c8;">" 200 256 "</span><span style="color:#99cc99;">-</span><span style="color:#d3d0c8;">" "</span><span style="color:#99cc99;">Mozilla/5.0 ...</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#6699cc;">192.168.65.1</span><span style="color:#d3d0c8;"> - - </span><span style="color:#cc99cc;">[</span><span style="color:#d3d0c8;">22/Feb/2026:12:00:00 +0000</span><span style="color:#cc99cc;">] </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">GET /style.css HTTP/1.1</span><span style="color:#d3d0c8;">" 200 1024 "</span><span style="color:#99cc99;">-</span><span style="color:#d3d0c8;">" "</span><span style="color:#99cc99;">Mozilla/5.0 ...</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#6699cc;">192.168.65.1</span><span style="color:#d3d0c8;"> - - </span><span style="color:#cc99cc;">[</span><span style="color:#d3d0c8;">22/Feb/2026:12:00:01 +0000</span><span style="color:#cc99cc;">] </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">GET /favicon.ico HTTP/1.1</span><span style="color:#d3d0c8;">" 404 152 "</span><span style="color:#99cc99;">-</span><span style="color:#d3d0c8;">" "</span><span style="color:#99cc99;">Mozilla/5.0 ...</span><span style="color:#d3d0c8;">"
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">></span><span style="color:#323232;"> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server
|
||||
</span><span style="color:#323232;">Serving HTTP on 0.0.0.0:80
|
||||
</span><span style="color:#323232;">Mount point: / -</span><span style="font-weight:bold;color:#a71d5d;">></span><span style="color:#323232;"> ./html
|
||||
</span><span style="color:#323232;">Press Ctrl+C to shutdown gracefully...
|
||||
</span><span style="color:#323232;">192.168.65.1 - - </span><span style="font-weight:bold;color:#a71d5d;">[</span><span style="color:#323232;">22/Feb/2026:12:00:00 +0000</span><span style="font-weight:bold;color:#a71d5d;">] </span><span style="color:#183691;">"GET / HTTP/1.1"</span><span style="color:#323232;"> 200 256 </span><span style="color:#183691;">"-" "Mozilla/5.0 ..."
|
||||
</span><span style="color:#323232;">192.168.65.1 - - </span><span style="font-weight:bold;color:#a71d5d;">[</span><span style="color:#323232;">22/Feb/2026:12:00:00 +0000</span><span style="font-weight:bold;color:#a71d5d;">] </span><span style="color:#183691;">"GET /style.css HTTP/1.1"</span><span style="color:#323232;"> 200 1024 </span><span style="color:#183691;">"-" "Mozilla/5.0 ..."
|
||||
</span><span style="color:#323232;">192.168.65.1 - - </span><span style="font-weight:bold;color:#a71d5d;">[</span><span style="color:#323232;">22/Feb/2026:12:00:01 +0000</span><span style="font-weight:bold;color:#a71d5d;">] </span><span style="color:#183691;">"GET /favicon.ico HTTP/1.1"</span><span style="color:#323232;"> 404 152 </span><span style="color:#183691;">"-" "Mozilla/5.0 ..."
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Everything in your <code>./my-site</code> directory gets served on port 8080. The access log follows the same format as NGINX, so you can see exactly what's happening.</p>
|
||||
<h2>What's next</h2>
|
||||
<p>You can now serve static files. A web server that delivers HTML, CSS, and JavaScript — built with this little code.</p>
|
||||
<p>Next, let's encrypt your connections with HTTPS. We'll start by setting up a TLS library.</p>
|
||||
<p><strong>Next:</strong> <a href="../05-tls-setup">TLS Setup</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,229 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TLS Setup - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="active">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>TLS Setup</h1>
|
||||
<p>So far we've been using plain HTTP, but in the real world, HTTPS is the norm. To use HTTPS with cpp-httplib, you need a TLS library.</p>
|
||||
<p>In this tour, we'll use OpenSSL. It's the most widely used option, and you'll find plenty of resources online.</p>
|
||||
<h2>Installing OpenSSL</h2>
|
||||
<p>Install it for your OS.</p>
|
||||
<table><thead><tr><th>OS</th><th>How to install</th></tr></thead><tbody>
|
||||
<tr><td>macOS</td><td><a href="https://brew.sh/">Homebrew</a> (<code>brew install openssl</code>)</td></tr>
|
||||
<tr><td>Ubuntu / Debian</td><td><code>sudo apt install libssl-dev</code></td></tr>
|
||||
<tr><td>Windows</td><td><a href="https://vcpkg.io/">vcpkg</a> (<code>vcpkg install openssl</code>)</td></tr>
|
||||
</tbody></table>
|
||||
<h2>Compile Options</h2>
|
||||
<p>To enable TLS, define the <code>CPPHTTPLIB_OPENSSL_SUPPORT</code> macro when compiling. You'll need a few extra options compared to the previous chapters.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;"># macOS (Homebrew)
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -DCPPHTTPLIB_OPENSSL_SUPPORT </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -I</span><span style="color:#d3d0c8;">$</span><span style="color:#f2777a;">(</span><span style="color:#6699cc;">brew</span><span style="color:#f2777a;"> --prefix openssl)</span><span style="color:#d3d0c8;">/include \
|
||||
</span><span style="color:#f2777a;"> -L</span><span style="color:#d3d0c8;">$</span><span style="color:#f2777a;">(</span><span style="color:#6699cc;">brew</span><span style="color:#f2777a;"> --prefix openssl)</span><span style="color:#d3d0c8;">/lib \
|
||||
</span><span style="color:#f2777a;"> -lssl -lcrypto </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -framework</span><span style="color:#d3d0c8;"> CoreFoundation</span><span style="color:#f2777a;"> -framework</span><span style="color:#d3d0c8;"> Security \
|
||||
</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Linux
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -lssl -lcrypto </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Windows (Developer Command Prompt)
|
||||
</span><span style="color:#6699cc;">cl</span><span style="color:#d3d0c8;"> /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;"># macOS (Homebrew)
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \
|
||||
</span><span style="color:#323232;"> -I$(brew --prefix openssl)/include \
|
||||
</span><span style="color:#323232;"> -L$(brew --prefix openssl)/lib \
|
||||
</span><span style="color:#323232;"> -lssl -lcrypto \
|
||||
</span><span style="color:#323232;"> -framework CoreFoundation -framework Security \
|
||||
</span><span style="color:#323232;"> -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Linux
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \
|
||||
</span><span style="color:#323232;"> -lssl -lcrypto \
|
||||
</span><span style="color:#323232;"> -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Windows (Developer Command Prompt)
|
||||
</span><span style="color:#323232;">cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Let's look at what each option does.</p>
|
||||
<ul>
|
||||
<li><strong><code>-DCPPHTTPLIB_OPENSSL_SUPPORT</code></strong> — Defines the macro that enables TLS support</li>
|
||||
<li><strong><code>-lssl -lcrypto</code></strong> — Links the OpenSSL libraries</li>
|
||||
<li><strong><code>-I</code> / <code>-L</code></strong> (macOS only) — Points to the Homebrew OpenSSL paths</li>
|
||||
<li><strong><code>-framework CoreFoundation -framework Security</code></strong> (macOS only) — Needed to automatically load system certificates from the Keychain</li>
|
||||
</ul>
|
||||
<h2>Verifying the Setup</h2>
|
||||
<p>Let's make sure everything works. Here's a simple program that passes an HTTPS URL to <code>httplib::Client</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://www.google.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Status: </span><span style="color:#d3d0c8;">" << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Error: </span><span style="color:#d3d0c8;">" << </span><span style="color:#6699cc;">httplib::to_string</span><span style="color:#d3d0c8;">(res.</span><span style="color:#6699cc;">error</span><span style="color:#d3d0c8;">()) << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://www.google.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Status: " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> } </span><span style="font-weight:bold;color:#a71d5d;">else </span><span style="color:#323232;">{
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Error: " </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#323232;">httplib::to_string(res.error()) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Compile and run it. If you see <code>Status: 200</code>, your setup is complete.</p>
|
||||
<h2>Other TLS Backends</h2>
|
||||
<p>cpp-httplib also supports Mbed TLS and wolfSSL in addition to OpenSSL. You can switch between them just by changing the macro definition and linked libraries.</p>
|
||||
<table><thead><tr><th style="text-align: left">Backend</th><th style="text-align: left">Macro</th><th style="text-align: left">Libraries to link</th></tr></thead><tbody>
|
||||
<tr><td style="text-align: left">OpenSSL</td><td style="text-align: left"><code>CPPHTTPLIB_OPENSSL_SUPPORT</code></td><td style="text-align: left"><code>libssl</code>, <code>libcrypto</code></td></tr>
|
||||
<tr><td style="text-align: left">Mbed TLS</td><td style="text-align: left"><code>CPPHTTPLIB_MBEDTLS_SUPPORT</code></td><td style="text-align: left"><code>libmbedtls</code>, <code>libmbedx509</code>, <code>libmbedcrypto</code></td></tr>
|
||||
<tr><td style="text-align: left">wolfSSL</td><td style="text-align: left"><code>CPPHTTPLIB_WOLFSSL_SUPPORT</code></td><td style="text-align: left"><code>libwolfssl</code></td></tr>
|
||||
</tbody></table>
|
||||
<p>This tour assumes OpenSSL, but the API is the same regardless of which backend you choose.</p>
|
||||
<h2>Next Step</h2>
|
||||
<p>You're all set with TLS. Next, let's send a request to an HTTPS site.</p>
|
||||
<p><strong>Next:</strong> <a href="../06-https-client">HTTPS Client</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,287 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HTTPS Client - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="active">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>HTTPS Client</h1>
|
||||
<p>In the previous chapter, you set up OpenSSL. Now let's put it to use with an HTTPS client. You can use the same <code>httplib::Client</code> from Chapter 2. Just pass a URL with the <code>https://</code> scheme to the constructor.</p>
|
||||
<h2>GET Request</h2>
|
||||
<p>Let's try accessing a real HTTPS site.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">substr</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">100</span><span style="color:#d3d0c8;">) << std::endl; </span><span style="color:#747369;">// First 100 chars of the HTML
|
||||
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Error: </span><span style="color:#d3d0c8;">" << </span><span style="color:#6699cc;">httplib::to_string</span><span style="color:#d3d0c8;">(res.</span><span style="color:#6699cc;">error</span><span style="color:#d3d0c8;">()) << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body.substr(</span><span style="color:#0086b3;">0</span><span style="color:#323232;">, </span><span style="color:#0086b3;">100</span><span style="color:#323232;">) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// First 100 chars of the HTML
|
||||
</span><span style="color:#323232;"> } </span><span style="font-weight:bold;color:#a71d5d;">else </span><span style="color:#323232;">{
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Error: " </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#323232;">httplib::to_string(res.error()) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>In Chapter 2, you wrote <code>httplib::Client cli("http://localhost:8080")</code>. All you need to change is the scheme to <code>https://</code>. Every API you learned in Chapter 2 -- <code>Get()</code>, <code>Post()</code>, and so on -- works exactly the same way.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Specifying a Port</h2>
|
||||
<p>The default port for HTTPS is 443. If you need a different port, include it in the URL.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>CA Certificate Verification</h2>
|
||||
<p>When connecting over HTTPS, <code>httplib::Client</code> verifies the server certificate by default. It only connects to servers whose certificate was issued by a trusted CA (Certificate Authority).</p>
|
||||
<p>CA certificates are loaded automatically from the Keychain on macOS, the system CA certificate store on Linux, and the Windows certificate store on Windows. In most cases, no extra configuration is needed.</p>
|
||||
<h3>Specifying a CA Certificate File</h3>
|
||||
<p>On some environments, the system CA certificates may not be found. In that case, use <code>set_ca_cert_path()</code> to specify the path directly.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_ca_cert_path</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/etc/ssl/certs/ca-certificates.crt</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_ca_cert_path(</span><span style="color:#183691;">"/etc/ssl/certs/ca-certificates.crt"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> --cacert</span><span style="color:#d3d0c8;"> /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Disabling Certificate Verification</h3>
|
||||
<p>During development, you might want to connect to a server with a self-signed certificate. You can disable verification for that.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">enable_server_certificate_verification</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">false</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.enable_server_certificate_verification(</span><span style="color:#0086b3;">false</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -k</span><span style="color:#d3d0c8;"> https://localhost:8443/
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -k https://localhost:8443/
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Never disable this in production. It opens you up to man-in-the-middle attacks.</p>
|
||||
<h2>Following Redirects</h2>
|
||||
<p>When accessing HTTPS sites, you'll often encounter redirects. For example, <code>http://</code> to <code>https://</code>, or a bare domain to <code>www</code>.</p>
|
||||
<p>By default, redirects are not followed. You can check the redirect target in the <code>Location</code> header.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/httpbin/redirect/3</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 302
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Location</span><span style="color:#d3d0c8;">") << std::endl;
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/httpbin/redirect/3"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 302
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->get_header_value(</span><span style="color:#183691;">"Location"</span><span style="color:#323232;">) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Call <code>set_follow_location(true)</code> to automatically follow redirects and get the final response.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_follow_location</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/httpbin/redirect/3</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200 (the final response)
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_follow_location(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/httpbin/redirect/3"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200 (the final response)
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -L</span><span style="color:#d3d0c8;"> https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -L https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Next Steps</h2>
|
||||
<p>Now you know how to use the HTTPS client. Next, let's set up your own HTTPS server. We'll start with creating a self-signed certificate.</p>
|
||||
<p><strong>Next:</strong> <a href="../07-https-server">HTTPS Server</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,276 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HTTPS Server - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="active">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>HTTPS Server</h1>
|
||||
<p>In the previous chapter, you used an HTTPS client. Now let's set up your own HTTPS server. Just swap <code>httplib::Server</code> from Chapter 3 with <code>httplib::SSLServer</code>.</p>
|
||||
<p>A TLS server needs a server certificate and a private key, though. Let's get those ready first.</p>
|
||||
<h2>Creating a Self-Signed Certificate</h2>
|
||||
<p>For development and testing, a self-signed certificate works just fine. You can generate one quickly with an OpenSSL command.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">openssl</span><span style="color:#d3d0c8;"> req</span><span style="color:#f2777a;"> -x509 -noenc -keyout</span><span style="color:#d3d0c8;"> key.pem</span><span style="color:#f2777a;"> -out</span><span style="color:#d3d0c8;"> cert.pem</span><span style="color:#f2777a;"> -subj</span><span style="color:#d3d0c8;"> /CN=localhost
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>This creates two files:</p>
|
||||
<ul>
|
||||
<li><strong><code>cert.pem</code></strong> — Server certificate</li>
|
||||
<li><strong><code>key.pem</code></strong> — Private key</li>
|
||||
</ul>
|
||||
<h2>A Minimal HTTPS Server</h2>
|
||||
<p>Once you have your certificate, let's write the server.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::SSLServer </span><span style="color:#6699cc;">svr</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">cert.pem</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">key.pem</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, HTTPS!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on https://localhost:8443</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8443</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::SSLServer svr(</span><span style="color:#183691;">"cert.pem"</span><span style="color:#323232;">, </span><span style="color:#183691;">"key.pem"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello, HTTPS!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on https://localhost:8443" </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8443</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Just pass the certificate and private key paths to the <code>httplib::SSLServer</code> constructor. The routing API is exactly the same as <code>httplib::Server</code> from Chapter 3.</p>
|
||||
<p>Compile and start it up.</p>
|
||||
<h2>Testing It Out</h2>
|
||||
<p>With the server running, try accessing it with <code>curl</code>. Since we're using a self-signed certificate, add the <code>-k</code> option to skip certificate verification.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -k</span><span style="color:#d3d0c8;"> https://localhost:8443/
|
||||
</span><span style="color:#747369;"># Hello, HTTPS!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -k https://localhost:8443/
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello, HTTPS!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>If you open <code>https://localhost:8443</code> in a browser, you'll see a "This connection is not secure" warning. That's expected with a self-signed certificate. Just proceed past it.</p>
|
||||
<h2>Connecting from a Client</h2>
|
||||
<p>Let's connect using <code>httplib::Client</code> from the previous chapter. There are two ways to connect to a server with a self-signed certificate.</p>
|
||||
<h3>Option 1: Disable Certificate Verification</h3>
|
||||
<p>This is the quick and easy approach for development.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> cli.</span><span style="color:#6699cc;">enable_server_certificate_verification</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">false</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello, HTTPS!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> cli.enable_server_certificate_verification(</span><span style="color:#0086b3;">false</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, HTTPS!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>Option 2: Specify the Self-Signed Certificate as a CA Certificate</h3>
|
||||
<p>This is the safer approach. You tell the client to trust <code>cert.pem</code> as a CA certificate.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> cli.</span><span style="color:#6699cc;">set_ca_cert_path</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">cert.pem</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello, HTTPS!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> cli.set_ca_cert_path(</span><span style="color:#183691;">"cert.pem"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, HTTPS!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>This way, only connections to the server with that specific certificate are allowed, preventing impersonation. Use this approach whenever possible, even in test environments.</p>
|
||||
<h2>Comparing Server and SSLServer</h2>
|
||||
<p>The <code>httplib::Server</code> API you learned in Chapter 3 works exactly the same with <code>httplib::SSLServer</code>. The only difference is the constructor.</p>
|
||||
<table><thead><tr><th></th><th><code>httplib::Server</code></th><th><code>httplib::SSLServer</code></th></tr></thead><tbody>
|
||||
<tr><td>Constructor</td><td>No arguments</td><td>Certificate and private key paths</td></tr>
|
||||
<tr><td>Protocol</td><td>HTTP</td><td>HTTPS</td></tr>
|
||||
<tr><td>Port (convention)</td><td>8080</td><td>8443</td></tr>
|
||||
<tr><td>Routing</td><td>Same</td><td>Same</td></tr>
|
||||
</tbody></table>
|
||||
<p>To switch an HTTP server to HTTPS, just change the constructor.</p>
|
||||
<h2>Next Steps</h2>
|
||||
<p>Your HTTPS server is up and running. You now have the basics of both HTTP/HTTPS clients and servers covered.</p>
|
||||
<p>Next, let's look at the WebSocket support that was recently added to cpp-httplib.</p>
|
||||
<p><strong>Next:</strong> <a href="../08-websocket">WebSocket</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,328 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebSocket - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="active">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>WebSocket</h1>
|
||||
<p>cpp-httplib supports WebSocket as well. Unlike HTTP request/response, WebSocket lets the server and client exchange messages in both directions. It's great for chat apps and real-time notifications.</p>
|
||||
<p>Let's build an echo server and client right away.</p>
|
||||
<h2>Echo Server</h2>
|
||||
<p>Here's an echo server that sends back whatever message it receives.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg); </span><span style="color:#747369;">// Send back the received message as-is
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on port 8080...</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(ws.read(msg)) {
|
||||
</span><span style="color:#323232;"> ws.send(msg); </span><span style="font-style:italic;color:#969896;">// Send back the received message as-is
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on port 8080..." </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>You register a WebSocket handler with <code>svr.WebSocket()</code>. It works just like <code>svr.Get()</code> and <code>svr.Post()</code> from Chapter 3.</p>
|
||||
<p>Inside the handler, <code>ws.read(msg)</code> waits for a message. When the connection closes, <code>read()</code> returns <code>false</code>, so the loop exits. <code>ws.send(msg)</code> sends a message back.</p>
|
||||
<h2>Connecting from a Client</h2>
|
||||
<p>Let's connect to the server using <code>httplib::ws::WebSocketClient</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::ws::WebSocketClient </span><span style="color:#6699cc;">client</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">ws://localhost:8080/ws</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(!client.</span><span style="color:#6699cc;">connect</span><span style="color:#d3d0c8;">()) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Connection failed</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Send a message
|
||||
</span><span style="color:#d3d0c8;"> client.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, WebSocket!</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Receive a response from the server
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(client.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << msg << std::endl; </span><span style="color:#747369;">// Hello, WebSocket!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> client.</span><span style="color:#6699cc;">close</span><span style="color:#d3d0c8;">();
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::ws::WebSocketClient client(</span><span style="color:#183691;">"ws://localhost:8080/ws"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">!</span><span style="color:#323232;">client.connect()) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Connection failed" </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">1</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Send a message
|
||||
</span><span style="color:#323232;"> client.send(</span><span style="color:#183691;">"Hello, WebSocket!"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Receive a response from the server
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(client.read(msg)) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> msg </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, WebSocket!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> client.close();
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Pass a URL in <code>ws://host:port/path</code> format to the constructor. Call <code>connect()</code> to start the connection, then use <code>send()</code> and <code>read()</code> to exchange messages.</p>
|
||||
<h2>Text and Binary</h2>
|
||||
<p>WebSocket has two types of messages: text and binary. You can tell them apart by the return value of <code>read()</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> httplib::ws::ReadResult ret;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">((ret = ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg))) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(ret == httplib::ws::Binary) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg.</span><span style="color:#6699cc;">data</span><span style="color:#d3d0c8;">(), msg.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()); </span><span style="color:#747369;">// Send as binary
|
||||
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg); </span><span style="color:#747369;">// Send as text
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> httplib::ws::ReadResult ret;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">((ret </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> ws.read(msg))) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(ret </span><span style="font-weight:bold;color:#a71d5d;">==</span><span style="color:#323232;"> httplib::ws::Binary) {
|
||||
</span><span style="color:#323232;"> ws.send(msg.data(), msg.size()); </span><span style="font-style:italic;color:#969896;">// Send as binary
|
||||
</span><span style="color:#323232;"> } </span><span style="font-weight:bold;color:#a71d5d;">else </span><span style="color:#323232;">{
|
||||
</span><span style="color:#323232;"> ws.send(msg); </span><span style="font-style:italic;color:#969896;">// Send as text
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<ul>
|
||||
<li><code>ws.send(const std::string &)</code> — sends as a text message</li>
|
||||
<li><code>ws.send(const char *, size_t)</code> — sends as a binary message</li>
|
||||
</ul>
|
||||
<p>The client-side API is the same.</p>
|
||||
<h2>Accessing Request Information</h2>
|
||||
<p>You can read HTTP request information from the handshake through the first argument <code>req</code> in the handler. This is handy for checking authentication tokens.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &req, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> token = req.</span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Authorization</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(token.</span><span style="color:#6699cc;">empty</span><span style="color:#d3d0c8;">()) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">close</span><span style="color:#d3d0c8;">(httplib::ws::CloseStatus::PolicyViolation, "</span><span style="color:#99cc99;">unauthorized</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg);
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">req, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> token </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_header_value(</span><span style="color:#183691;">"Authorization"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(token.empty()) {
|
||||
</span><span style="color:#323232;"> ws.close(httplib::ws::CloseStatus::PolicyViolation, </span><span style="color:#183691;">"unauthorized"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(ws.read(msg)) {
|
||||
</span><span style="color:#323232;"> ws.send(msg);
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Using WSS</h2>
|
||||
<p>WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on <code>httplib::SSLServer</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::SSLServer </span><span style="color:#6699cc;">svr</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">cert.pem</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">key.pem</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg);
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8443</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::SSLServer </span><span style="font-weight:bold;color:#795da3;">svr</span><span style="color:#323232;">(</span><span style="color:#183691;">"cert.pem"</span><span style="color:#323232;">, </span><span style="color:#183691;">"key.pem"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(ws.read(msg)) {
|
||||
</span><span style="color:#323232;"> ws.send(msg);
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8443</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>On the client side, use the <code>wss://</code> scheme.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::ws::WebSocketClient </span><span style="color:#6699cc;">client</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">wss://localhost:8443/ws</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::ws::WebSocketClient </span><span style="font-weight:bold;color:#795da3;">client</span><span style="color:#323232;">(</span><span style="color:#183691;">"wss://localhost:8443/ws"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Next Steps</h2>
|
||||
<p>Now you know the basics of WebSocket. This wraps up the Tour.</p>
|
||||
<p>The next page gives you a summary of features we didn't cover in the Tour.</p>
|
||||
<p><strong>Next:</strong> <a href="../09-whats-next">What's Next</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,460 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>What's Next - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="active">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>What's Next</h1>
|
||||
<p>Great job finishing the Tour! You now have a solid grasp of the cpp-httplib basics. But there's a lot more to explore. Here's a quick overview of features we didn't cover in the Tour, organized by category.</p>
|
||||
<h2>Streaming API</h2>
|
||||
<p>When you're working with LLM streaming responses or downloading large files, you don't want to load the entire response into memory. Use <code>stream::Get()</code> to process data chunk by chunk.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:11434</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> result = </span><span style="color:#6699cc;">httplib::stream::Get</span><span style="color:#d3d0c8;">(cli, "</span><span style="color:#99cc99;">/api/generate</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(result) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(result.</span><span style="color:#6699cc;">next</span><span style="color:#d3d0c8;">()) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout.</span><span style="color:#6699cc;">write</span><span style="color:#d3d0c8;">(result.</span><span style="color:#6699cc;">data</span><span style="color:#d3d0c8;">(), result.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">());
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:11434"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">httplib::stream::Get(cli, </span><span style="color:#183691;">"/api/generate"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(result) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(result.next()) {
|
||||
</span><span style="color:#323232;"> std::cout.write(result.data(), result.size());
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>You can also pass a <code>content_receiver</code> callback to <code>Get()</code>. This approach works with Keep-Alive.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/stream</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const char </span><span style="color:#d3d0c8;">*data, size_t len) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout.</span><span style="color:#6699cc;">write</span><span style="color:#d3d0c8;">(data, len);
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">cli.Get(</span><span style="color:#183691;">"/stream"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const char *</span><span style="color:#323232;">data, </span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> len) {
|
||||
</span><span style="color:#323232;"> std::cout.write(data, len);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">true</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>On the server side, you have <code>set_content_provider()</code> and <code>set_chunked_content_provider()</code>. Use the former when you know the size, and the latter when you don't.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// With known size (sets Content-Length)
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/file</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> size = </span><span style="color:#6699cc;">get_file_size</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">large.bin</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content_provider</span><span style="color:#d3d0c8;">(size, "</span><span style="color:#99cc99;">application/octet-stream</span><span style="color:#d3d0c8;">",
|
||||
</span><span style="color:#d3d0c8;"> [](size_t offset, size_t length, httplib::DataSink &sink) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Send 'length' bytes starting from 'offset'
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// Unknown size (Chunked Transfer Encoding)
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/stream</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_chunked_content_provider</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">",
|
||||
</span><span style="color:#d3d0c8;"> [](size_t offset, httplib::DataSink &sink) {
|
||||
</span><span style="color:#d3d0c8;"> sink.</span><span style="color:#6699cc;">write</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">chunk</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">6</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">; </span><span style="color:#747369;">// Return false to finish
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// With known size (sets Content-Length)
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/file"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> size </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">get_file_size(</span><span style="color:#183691;">"large.bin"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content_provider(size, </span><span style="color:#183691;">"application/octet-stream"</span><span style="color:#323232;">,
|
||||
</span><span style="color:#323232;"> [](</span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> offset, </span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> length, httplib::DataSink </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">sink) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Send 'length' bytes starting from 'offset'
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">true</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// Unknown size (Chunked Transfer Encoding)
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/stream"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_chunked_content_provider(</span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">,
|
||||
</span><span style="color:#323232;"> [](</span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> offset, httplib::DataSink </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">sink) {
|
||||
</span><span style="color:#323232;"> sink.write(</span><span style="color:#183691;">"chunk</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">6</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">true</span><span style="color:#323232;">; </span><span style="font-style:italic;color:#969896;">// Return false to finish
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>For uploading large files, <code>make_file_provider()</code> comes in handy. It streams the file instead of loading it all into memory.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", {}, {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#6699cc;">httplib::make_file_provider</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">/path/to/large-file.zip</span><span style="color:#d3d0c8;">")
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, {}, {
|
||||
</span><span style="color:#323232;"> httplib::make_file_provider(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">, </span><span style="color:#183691;">"/path/to/large-file.zip"</span><span style="color:#323232;">)
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Server-Sent Events (SSE)</h2>
|
||||
<p>We provide an SSE client as well. It supports automatic reconnection and resuming via <code>Last-Event-ID</code>.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">httplib::sse::SSEClient </span><span style="color:#6699cc;">sse</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">cli</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">/events</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">sse.</span><span style="color:#6699cc;">on_message</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::sse::SSEMessage &msg) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << msg.</span><span style="color:#f2777a;">event </span><span style="color:#d3d0c8;"><< "</span><span style="color:#99cc99;">: </span><span style="color:#d3d0c8;">" << msg.</span><span style="color:#f2777a;">data </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">sse.</span><span style="color:#6699cc;">start</span><span style="color:#d3d0c8;">(); </span><span style="color:#747369;">// Blocking, with auto-reconnection
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">httplib::sse::SSEClient </span><span style="font-weight:bold;color:#795da3;">sse</span><span style="color:#323232;">(cli, </span><span style="color:#183691;">"/events"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">sse.on_message([](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::sse::SSEMessage </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">msg) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> msg.event </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">": " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> msg.data </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">sse.start(); </span><span style="font-style:italic;color:#969896;">// Blocking, with auto-reconnection
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>You can also set separate handlers for each event type.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">sse.</span><span style="color:#6699cc;">on_event</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">update</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::sse::SSEMessage &msg) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Only handles "update" events
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">sse.on_event(</span><span style="color:#183691;">"update"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::sse::SSEMessage </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">msg) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Only handles "update" events
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Authentication</h2>
|
||||
<p>The client has helpers for Basic auth, Bearer Token auth, and Digest auth.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://api.example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_basic_auth</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">user</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">password</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_bearer_token_auth</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">my-token</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://api.example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_basic_auth(</span><span style="color:#183691;">"user"</span><span style="color:#323232;">, </span><span style="color:#183691;">"password"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_bearer_token_auth(</span><span style="color:#183691;">"my-token"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Compression</h2>
|
||||
<p>We support compression and decompression with gzip, Brotli, and Zstandard. Define the corresponding macro when you compile.</p>
|
||||
<table><thead><tr><th>Method</th><th>Macro</th></tr></thead><tbody>
|
||||
<tr><td>gzip</td><td><code>CPPHTTPLIB_ZLIB_SUPPORT</code></td></tr>
|
||||
<tr><td>Brotli</td><td><code>CPPHTTPLIB_BROTLI_SUPPORT</code></td></tr>
|
||||
<tr><td>Zstandard</td><td><code>CPPHTTPLIB_ZSTD_SUPPORT</code></td></tr>
|
||||
</tbody></table>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_compress</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// Compress request body
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_decompress</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// Decompress response body
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_compress(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// Compress request body
|
||||
</span><span style="color:#323232;">cli.set_decompress(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// Decompress response body
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Proxy</h2>
|
||||
<p>You can connect through an HTTP proxy.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_proxy</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">proxy.example.com</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_proxy_basic_auth</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">user</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">password</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_proxy(</span><span style="color:#183691;">"proxy.example.com"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_proxy_basic_auth(</span><span style="color:#183691;">"user"</span><span style="color:#323232;">, </span><span style="color:#183691;">"password"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Timeouts</h2>
|
||||
<p>You can set connection, read, and write timeouts individually.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_connection_timeout</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">5</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// 5 seconds
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_read_timeout</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">10</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// 10 seconds
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_write_timeout</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">10</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// 10 seconds
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_connection_timeout(</span><span style="color:#0086b3;">5</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 5 seconds
|
||||
</span><span style="color:#323232;">cli.set_read_timeout(</span><span style="color:#0086b3;">10</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 10 seconds
|
||||
</span><span style="color:#323232;">cli.set_write_timeout(</span><span style="color:#0086b3;">10</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 10 seconds
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Keep-Alive</h2>
|
||||
<p>If you're making multiple requests to the same server, enable Keep-Alive. It reuses the TCP connection, which is much more efficient.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_keep_alive</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_keep_alive(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Server Middleware</h2>
|
||||
<p>You can hook into request processing before and after handlers run.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_pre_routing_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Runs before every request
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> httplib::Server::HandlerResponse::Unhandled; </span><span style="color:#747369;">// Continue to normal routing
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_post_routing_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Runs after the response is sent
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_header</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">X-Server</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">cpp-httplib</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_pre_routing_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Runs before every request
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return</span><span style="color:#323232;"> httplib::Server::HandlerResponse::Unhandled; </span><span style="font-style:italic;color:#969896;">// Continue to normal routing
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.set_post_routing_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Runs after the response is sent
|
||||
</span><span style="color:#323232;"> res.set_header(</span><span style="color:#183691;">"X-Server"</span><span style="color:#323232;">, </span><span style="color:#183691;">"cpp-httplib"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>Use <code>req.user_data</code> to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_pre_routing_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> req.</span><span style="color:#f2777a;">user_data</span><span style="color:#d3d0c8;">["</span><span style="color:#99cc99;">auth_user</span><span style="color:#d3d0c8;">"] = </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">alice</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> httplib::Server::HandlerResponse::Unhandled;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/me</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> user = std::</span><span style="color:#6699cc;">any_cast</span><span style="color:#d3d0c8;"><std::string>(req.</span><span style="color:#f2777a;">user_data</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">auth_user</span><span style="color:#d3d0c8;">"));
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, </span><span style="color:#d3d0c8;">" + user, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_pre_routing_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> req.user_data[</span><span style="color:#183691;">"auth_user"</span><span style="color:#323232;">] </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">std::string(</span><span style="color:#183691;">"alice"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return</span><span style="color:#323232;"> httplib::Server::HandlerResponse::Unhandled;
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/me"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> user </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">std::any_cast<std::string>(req.user_data.at(</span><span style="color:#183691;">"auth_user"</span><span style="color:#323232;">));
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello, " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> user, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>You can also customize error and exception handlers.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_error_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Custom Error Page</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/html</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_exception_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res, std::exception_ptr ep) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;">= </span><span style="color:#f99157;">500</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Internal Server Error</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_error_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Custom Error Page"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/html"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.set_exception_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res, std::exception_ptr ep) {
|
||||
</span><span style="color:#323232;"> res.status </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#0086b3;">500</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Internal Server Error"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Logging</h2>
|
||||
<p>You can set a logger on both the server and the client.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_logger</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << req.</span><span style="color:#f2777a;">method </span><span style="color:#d3d0c8;"><< " " << req.</span><span style="color:#f2777a;">path </span><span style="color:#d3d0c8;"><< " " << res.</span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_logger([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> req.method </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">" " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> req.path </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">" " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res.status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Unix Domain Socket</h2>
|
||||
<p>In addition to TCP, we support Unix Domain Sockets. You can use them for inter-process communication on the same machine.</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// Server
|
||||
</span><span style="color:#d3d0c8;">httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_address_family</span><span style="color:#d3d0c8;">(AF_UNIX);
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/tmp/httplib.sock</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// Server
|
||||
</span><span style="color:#323232;">httplib::Server svr;
|
||||
</span><span style="color:#323232;">svr.set_address_family(AF_UNIX);
|
||||
</span><span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"/tmp/httplib.sock"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// Client
|
||||
</span><span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_address_family</span><span style="color:#d3d0c8;">(AF_UNIX);
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_hostname_addr_map</span><span style="color:#d3d0c8;">({{"</span><span style="color:#99cc99;">localhost</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">/tmp/httplib.sock</span><span style="color:#d3d0c8;">"}});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// Client
|
||||
</span><span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_address_family(AF_UNIX);
|
||||
</span><span style="color:#323232;">cli.set_hostname_addr_map({{</span><span style="color:#183691;">"localhost"</span><span style="color:#323232;">, </span><span style="color:#183691;">"/tmp/httplib.sock"</span><span style="color:#323232;">}});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Learn More</h2>
|
||||
<p>Want to dig deeper? Check out these resources.</p>
|
||||
<ul>
|
||||
<li>Cookbook — A collection of recipes for common use cases</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README.md">README</a> — Full API reference</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README-sse.md">README-sse</a> — How to use Server-Sent Events</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README-stream.md">README-stream</a> — How to use the Streaming API</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README-websocket.md">README-websocket</a> — How to use the WebSocket server</li>
|
||||
</ul>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,141 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>A Tour of cpp-httplib - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/en/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/en/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/en/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
EN
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/en/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/en/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>A Tour of cpp-httplib</h1>
|
||||
<p>This is a step-by-step tutorial that walks you through the basics of cpp-httplib. Each chapter builds on the previous one, so please read them in order starting from Chapter 1.</p>
|
||||
<ol>
|
||||
<li><a href="01-getting-started">Getting Started</a> — Get httplib.h and build a Hello World server</li>
|
||||
<li><a href="02-basic-client">Basic Client</a> — Send GET/POST requests and use path parameters</li>
|
||||
<li><a href="03-basic-server">Basic Server</a> — Routing, path parameters, and building responses</li>
|
||||
<li><a href="04-static-file-server">Static File Server</a> — Serve static files</li>
|
||||
<li><a href="05-tls-setup">TLS Setup</a> — Set up OpenSSL / mbedTLS</li>
|
||||
<li><a href="06-https-client">HTTPS Client</a> — Make requests to HTTPS sites</li>
|
||||
<li><a href="07-https-server">HTTPS Server</a> — Build an HTTPS server</li>
|
||||
<li><a href="08-websocket">WebSocket</a> — Learn the basics of WebSocket communication</li>
|
||||
<li><a href="09-whats-next">What's Next</a> — Explore more features</li>
|
||||
</ol>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
(function() {
|
||||
var lang = localStorage.getItem('preferred-lang') || 'en';
|
||||
window.location.replace('/cpp-httplib/' + lang + '/');
|
||||
})();
|
||||
</script>
|
||||
<meta http-equiv="refresh" content="0;url=/cpp-httplib/en/">
|
||||
<title>Redirecting...</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="/cpp-httplib/en/">/cpp-httplib/en/</a>...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,109 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cookbook - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/cookbook/" class="nav-section-title active">Cookbook</a>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Cookbook</h1>
|
||||
<p>This section is under construction.</p>
|
||||
<p>Check back soon for a collection of recipes organized by topic.</p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,108 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>cpp-httplib - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout no-sidebar">
|
||||
|
||||
<main class="content portal">
|
||||
<article>
|
||||
<h1>cpp-httplib</h1>
|
||||
<p><a href="https://github.com/yhirose/cpp-httplib">cpp-httplib</a>は、C++用のHTTP/HTTPSライブラリです。<a href="https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h"><code>httplib.h</code></a> というヘッダーファイルを1枚コピーするだけで使えます。</p>
|
||||
<p>C++でちょっとしたHTTPサーバーやクライアントが必要になったとき、すぐに動くものが欲しいですよね。cpp-httplibはまさにそのために作られました。サーバーもクライアントも、数行のコードで書き始められます。</p>
|
||||
<p>APIはラムダ式をベースにした直感的な設計で、C++11以降のコンパイラーがあればどこでも動きます。Windows、macOS、Linux — お使いの環境をそのまま使えます。</p>
|
||||
<p>HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバー・クライアントの両方がTLSに対応します。Content-Encoding(gzip, brotli等)、ファイルアップロードなど、実際の開発で必要になる機能もひと通り揃っています。WebSocketもサポートしています。</p>
|
||||
<p>内部的にはブロッキングI/Oとスレッドプールを使っています。大量の同時接続を捌くような用途には向きませんが、APIサーバーやツールの組み込みHTTP、テスト用のモックサーバーなど、多くのユースケースで十分な性能を発揮します。</p>
|
||||
<p>「今日の課題を、今日中に解決する」— cpp-httplibが目指しているのは、そういうシンプルさです。</p>
|
||||
<h2>ドキュメント</h2>
|
||||
<ul>
|
||||
<li><a href="tour/">A Tour of cpp-httplib</a> — 基本を順を追って学べるチュートリアル。初めての方はここから</li>
|
||||
<li><a href="cookbook/">Cookbook</a> — 目的別のレシピ集。必要なトピックから読めます</li>
|
||||
</ul>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,234 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Getting Started - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="active">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Getting Started</h1>
|
||||
<p>cpp-httplibを始めるのに必要なのは、<code>httplib.h</code>とC++コンパイラーだけです。ファイルをダウンロードして、Hello Worldサーバーを動かすところまでやってみましょう。</p>
|
||||
<h2>httplib.h の入手</h2>
|
||||
<p>GitHubから直接ダウンロードできます。常に最新版を使ってください。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -LO</span><span style="color:#d3d0c8;"> https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>ダウンロードした <code>httplib.h</code> をプロジェクトのディレクトリに置けば、準備完了です。</p>
|
||||
<h2>コンパイラーの準備</h2>
|
||||
<table><thead><tr><th>OS</th><th>開発環境</th><th>セットアップ</th></tr></thead><tbody>
|
||||
<tr><td>macOS</td><td>Apple Clang</td><td>Xcode Command Line Tools (<code>xcode-select --install</code>)</td></tr>
|
||||
<tr><td>Ubuntu</td><td>clang++ または g++</td><td><code>apt install clang</code> または <code>apt install g++</code></td></tr>
|
||||
<tr><td>Windows</td><td>MSVC</td><td>Visual Studio 2022 以降(C++ コンポーネントを含めてインストール)</td></tr>
|
||||
</tbody></table>
|
||||
<h2>Hello World サーバー</h2>
|
||||
<p>次のコードを <code>server.cpp</code> として保存しましょう。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request&, httplib::Response& res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, World!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request</span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::Response</span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;"> res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello, World!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>たった数行で、HTTPリクエストに応答するサーバーが書けます。</p>
|
||||
<h2>コンパイルと実行</h2>
|
||||
<p>このチュートリアルのサンプルコードは、コードを簡潔に書けるC++17で書いています。cpp-httplib自体はC++11でもコンパイルできます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;"># macOS
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Linux
|
||||
</span><span style="color:#747369;"># `-pthread`: cpp-httplibは内部でスレッドを使用
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -pthread -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Windows (Developer Command Prompt)
|
||||
</span><span style="color:#747369;"># `/EHsc`: C++例外処理を有効化
|
||||
</span><span style="color:#6699cc;">cl</span><span style="color:#d3d0c8;"> /EHsc /std:c++17 server.cpp
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;"># macOS
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Linux
|
||||
</span><span style="font-style:italic;color:#969896;"># `-pthread`: cpp-httplibは内部でスレッドを使用
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -pthread -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Windows (Developer Command Prompt)
|
||||
</span><span style="font-style:italic;color:#969896;"># `/EHsc`: C++例外処理を有効化
|
||||
</span><span style="color:#323232;">cl /EHsc /std:c++17 server.cpp
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>コンパイルできたら実行します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;"># macOS / Linux
|
||||
</span><span style="color:#6699cc;">./server
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Windows
|
||||
</span><span style="color:#6699cc;">server.exe
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;"># macOS / Linux
|
||||
</span><span style="color:#323232;">./server
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Windows
|
||||
</span><span style="color:#323232;">server.exe
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>ブラウザで <code>http://localhost:8080</code> を開いてください。"Hello, World!" と表示されれば成功です。</p>
|
||||
<p><code>curl</code> でも確認できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/
|
||||
</span><span style="color:#747369;"># Hello, World!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello, World!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>サーバーを停止するには、ターミナルで <code>Ctrl+C</code> を押します。</p>
|
||||
<h2>次のステップ</h2>
|
||||
<p>サーバーの基本がわかりましたね。次は、クライアント側を見てみましょう。cpp-httplibはHTTPクライアント機能も備えています。</p>
|
||||
<p><strong>次:</strong> <a href="../02-basic-client">Basic Client</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,527 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Basic Client - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="active">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Basic Client</h1>
|
||||
<p>cpp-httplibはサーバーだけでなく、HTTPクライアント機能も備えています。<code>httplib::Client</code> を使って、GETやPOSTリクエストを送ってみましょう。</p>
|
||||
<h2>テスト用サーバーの準備</h2>
|
||||
<p>クライアントの動作を確認するために、リクエストを受け付けるサーバーを用意します。次のコードを保存し、前章と同じ手順でコンパイル・実行してください。サーバーの詳しい解説は次章で行います。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> q = req.</span><span style="color:#6699cc;">get_param_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Query: </span><span style="color:#d3d0c8;">" + q, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(req.</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::string result;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&[key, val] : req.</span><span style="color:#f2777a;">params</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> result += key + "</span><span style="color:#99cc99;"> = </span><span style="color:#d3d0c8;">" + val + "</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(result, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> f = req.</span><span style="color:#f2777a;">form</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">get_file</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> content = f.</span><span style="color:#f2777a;">filename </span><span style="color:#d3d0c8;">+ "</span><span style="color:#99cc99;"> (</span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::to_string</span><span style="color:#d3d0c8;">(f.</span><span style="color:#f2777a;">content</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()) + "</span><span style="color:#99cc99;"> bytes)</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(content, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User ID: </span><span style="color:#d3d0c8;">" + id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">/files/(\d+)</span><span style="color:#d3d0c8;">)", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">matches</span><span style="color:#d3d0c8;">[</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">];
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">File ID: </span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">(id), "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on port 8080...</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> q </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_param_value(</span><span style="color:#183691;">"q"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Query: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> q, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(req.body, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::string result;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">for </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">[key, val] </span><span style="font-weight:bold;color:#a71d5d;">:</span><span style="color:#323232;"> req.params) {
|
||||
</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">+=</span><span style="color:#323232;"> key </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" = " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> val </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">"</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> res.set_content(result, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> f </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.form.get_file(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> content </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> f.filename </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" (" </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::to_string(f.content.size()) </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" bytes)"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(content, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/users/:id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User ID: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"(/files/(\d+))"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.matches[</span><span style="color:#0086b3;">1</span><span style="color:#323232;">];
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"File ID: " </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::string(id), </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on port 8080..." </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>GETリクエスト</h2>
|
||||
<p>サーバーが起動したら、別のターミナルを開いて試してみましょう。まず、最もシンプルなGETリクエストです。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>httplib::Client</code> のコンストラクターにサーバーのアドレスを渡し、<code>Get()</code> でリクエストを送ります。戻り値の <code>res</code> からステータスコードやボディを取得できます。</p>
|
||||
<p>対応する <code>curl</code> コマンドはこうなります。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/hi
|
||||
</span><span style="color:#747369;"># Hello!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/hi
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>レスポンスの確認</h2>
|
||||
<p>レスポンスには、ステータスコードとボディ以外にもヘッダー情報が含まれています。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// ステータスコード
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// ボディ
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello!
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// ヘッダー
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Content-Type</span><span style="color:#d3d0c8;">") << std::endl; </span><span style="color:#747369;">// text/plain
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// ステータスコード
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// ボディ
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello!
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// ヘッダー
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->get_header_value(</span><span style="color:#183691;">"Content-Type"</span><span style="color:#323232;">) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// text/plain
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>res->body</code> は <code>std::string</code> なので、JSON レスポンスをパースしたい場合は <a href="https://github.com/nlohmann/json">nlohmann/json</a> などの JSON ライブラリにそのまま渡せます。</p>
|
||||
<h2>クエリパラメーター</h2>
|
||||
<p>GETリクエストにクエリパラメーターを付けるには、URLに直接書くか、<code>httplib::Params</code> を使います。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", httplib::Params{{"</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">cpp-httplib</span><span style="color:#d3d0c8;">"}});
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Query: cpp-httplib
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, httplib::Params{{</span><span style="color:#183691;">"q"</span><span style="color:#323232;">, </span><span style="color:#183691;">"cpp-httplib"</span><span style="color:#323232;">}});
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Query: cpp-httplib
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>httplib::Params</code> を使うと、特殊文字のURLエンコードを自動で行ってくれます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">http://localhost:8080/search?q=cpp-httplib</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#747369;"># Query: cpp-httplib
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl </span><span style="color:#183691;">"http://localhost:8080/search?q=cpp-httplib"
|
||||
</span><span style="font-style:italic;color:#969896;"># Query: cpp-httplib
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>パスパラメーター</h2>
|
||||
<p>URLのパスに値を直接埋め込む場合も、クライアント側は特別なAPIは不要です。パスをそのまま <code>Get()</code> に渡すだけです。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/42</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// User ID: 42
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/users/42"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// User ID: 42
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/users/42
|
||||
</span><span style="color:#747369;"># User ID: 42
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/users/42
|
||||
</span><span style="font-style:italic;color:#969896;"># User ID: 42
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>テスト用サーバーには、正規表現でIDを数字のみに絞った <code>/files/(\d+)</code> もあります。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/files/42</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// File ID: 42
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/files/42"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// File ID: 42
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080/files/42
|
||||
</span><span style="color:#747369;"># File ID: 42
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080/files/42
|
||||
</span><span style="font-style:italic;color:#969896;"># File ID: 42
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>/files/abc</code> のように数字以外を渡すと404が返ります。仕組みは次章で解説します。</p>
|
||||
<h2>リクエストヘッダー</h2>
|
||||
<p>カスタムHTTPヘッダーを付けるには、<code>httplib::Headers</code> を渡します。<code>Get()</code> や <code>Post()</code> のどちらでも使えます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", httplib::Headers{
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">Authorization</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Bearer my-token</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, httplib::Headers{
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"Authorization"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Bearer my-token"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -H </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">Authorization: Bearer my-token</span><span style="color:#d3d0c8;">" http://localhost:8080/hi
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -H </span><span style="color:#183691;">"Authorization: Bearer my-token"</span><span style="color:#323232;"> http://localhost:8080/hi
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>POSTリクエスト</h2>
|
||||
<p>テキストデータをPOSTしてみましょう。<code>Post()</code> の第2引数にボディ、第3引数にContent-Typeを指定します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Hello, Server!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello, Server!
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Hello, Server!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, Server!
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>テスト用サーバーの <code>/post</code> はボディをそのまま返すので、送った文字列がそのまま返ってきます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -X</span><span style="color:#d3d0c8;"> POST</span><span style="color:#f2777a;"> -H </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">Content-Type: text/plain</span><span style="color:#d3d0c8;">"</span><span style="color:#f2777a;"> -d </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">Hello, Server!</span><span style="color:#d3d0c8;">" http://localhost:8080/post
|
||||
</span><span style="color:#747369;"># Hello, Server!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -X POST -H </span><span style="color:#183691;">"Content-Type: text/plain"</span><span style="color:#323232;"> -d </span><span style="color:#183691;">"Hello, Server!"</span><span style="color:#323232;"> http://localhost:8080/post
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello, Server!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>フォームデータの送信</h2>
|
||||
<p>HTMLフォームのように、キーと値のペアを送ることもできます。<code>httplib::Params</code> を使います。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", httplib::Params{
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">name</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Alice</span><span style="color:#d3d0c8;">"},
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">age</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">30</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// age = 30
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// name = Alice
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, httplib::Params{
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"name"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Alice"</span><span style="color:#323232;">},
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"age"</span><span style="color:#323232;">, </span><span style="color:#183691;">"30"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// age = 30
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// name = Alice
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>これは <code>application/x-www-form-urlencoded</code> 形式で送信されます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -X</span><span style="color:#d3d0c8;"> POST</span><span style="color:#f2777a;"> -d </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">name=Alice&age=30</span><span style="color:#d3d0c8;">" http://localhost:8080/submit
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -X POST -d </span><span style="color:#183691;">"name=Alice&age=30"</span><span style="color:#323232;"> http://localhost:8080/submit
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>ファイルのPOST</h2>
|
||||
<p>ファイルをアップロードするには、<code>httplib::UploadFormDataItems</code> を使ってマルチパートフォームデータとして送信します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", httplib::UploadFormDataItems{
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">Hello, File!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">hello.txt</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// hello.txt (12 bytes)
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, httplib::UploadFormDataItems{
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"file"</span><span style="color:#323232;">, </span><span style="color:#183691;">"Hello, File!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"hello.txt"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// hello.txt (12 bytes)
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>UploadFormDataItems</code> の各要素は <code>{name, content, filename, content_type}</code> の4つのフィールドで構成されます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -F </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">file=Hello, File!;filename=hello.txt;type=text/plain</span><span style="color:#d3d0c8;">" http://localhost:8080/upload
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -F </span><span style="color:#183691;">"file=Hello, File!;filename=hello.txt;type=text/plain"</span><span style="color:#323232;"> http://localhost:8080/upload
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>エラーハンドリング</h2>
|
||||
<p>ネットワーク通信では、サーバーに接続できない場合があります。<code>res</code> が有効かどうかを必ず確認しましょう。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:9999</span><span style="color:#d3d0c8;">"); </span><span style="color:#747369;">// 存在しないポート
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(!res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// 接続エラー
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Error: </span><span style="color:#d3d0c8;">" << </span><span style="color:#6699cc;">httplib::to_string</span><span style="color:#d3d0c8;">(res.</span><span style="color:#6699cc;">error</span><span style="color:#d3d0c8;">()) << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// Error: Connection
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// ここに到達すればレスポンスを受信できている
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;">!= </span><span style="color:#f99157;">200</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">HTTP Error: </span><span style="color:#d3d0c8;">" << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:9999"</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 存在しないポート
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">!</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// 接続エラー
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Error: " </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#323232;">httplib::to_string(res.error()) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// Error: Connection
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">1</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// ここに到達すればレスポンスを受信できている
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res->status </span><span style="font-weight:bold;color:#a71d5d;">!= </span><span style="color:#0086b3;">200</span><span style="color:#323232;">) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"HTTP Error: " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">1</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>エラーには2つのレベルがあります。</p>
|
||||
<ul>
|
||||
<li><strong>接続エラー</strong>: サーバーに到達できなかった場合。<code>res</code> が偽になり、<code>res.error()</code> でエラーの種類を取得できます</li>
|
||||
<li><strong>HTTPエラー</strong>: サーバーからエラーステータス(404、500など)が返ってきた場合。<code>res</code> は真ですが、<code>res->status</code> を確認する必要があります</li>
|
||||
</ul>
|
||||
<h2>次のステップ</h2>
|
||||
<p>クライアントからリクエストを送る方法がわかりました。次は、サーバー側をもっと詳しく見てみましょう。ルーティングやパスパラメータなど、サーバーの機能を掘り下げます。</p>
|
||||
<p><strong>次:</strong> <a href="../03-basic-server">Basic Server</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,482 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Basic Server - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="active">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Basic Server</h1>
|
||||
<p>前章ではクライアントからリクエストを送りました。そのとき、テスト用サーバーを用意しましたね。この章では、あのサーバーの仕組みをひとつずつ紐解いていきます。</p>
|
||||
<h2>サーバーの起動</h2>
|
||||
<p>ルーティングを登録したら、最後に <code>svr.listen()</code> を呼んでサーバーを起動します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>第1引数はホスト、第2引数はポート番号です。<code>"0.0.0.0"</code> を指定すると、すべてのネットワークインターフェースでリクエストを受け付けます。自分のマシンからのアクセスだけに限定したいときは <code>"127.0.0.1"</code> を使います。</p>
|
||||
<p><code>listen()</code> はブロッキング呼び出しです。サーバーが停止するまで、この行から先には進みません。ターミナルで <code>Ctrl+C</code> を押すか、別スレッドから <code>svr.stop()</code> を呼ぶまでサーバーは動き続けます。</p>
|
||||
<h2>ルーティング</h2>
|
||||
<p>サーバーの核になるのは「ルーティング」です。どのURLに、どのHTTPメソッドでアクセスされたら、何をするか。それを登録する仕組みです。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &req, httplib::Response &res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">req, httplib::Response </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>svr.Get()</code> は、GETリクエストに対するハンドラーを登録します。第1引数がパス、第2引数がハンドラー関数です。<code>/hi</code> にGETリクエストが来たら、このラムダが呼ばれます。</p>
|
||||
<p>HTTPメソッドごとにメソッドが用意されています。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// GET
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// POST
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Put</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// PUT
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Delete</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/path</span><span style="color:#d3d0c8;">", handler); </span><span style="color:#747369;">// DELETE
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// GET
|
||||
</span><span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// POST
|
||||
</span><span style="color:#323232;">svr.Put(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// PUT
|
||||
</span><span style="color:#323232;">svr.Delete(</span><span style="color:#183691;">"/path"</span><span style="color:#323232;">, handler); </span><span style="font-style:italic;color:#969896;">// DELETE
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>ハンドラーのシグネチャは <code>(const httplib::Request &req, httplib::Response &res)</code> です。<code>auto</code> を使って短く書くこともできます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>パスが一致したときだけハンドラーが呼ばれます。登録されていないパスにアクセスすると、自動的に404が返ります。</p>
|
||||
<h2>リクエストオブジェクト</h2>
|
||||
<p>ハンドラーの第1引数 <code>req</code> から、クライアントが送ってきた情報を読み取れます。</p>
|
||||
<h3>ボディ</h3>
|
||||
<p><code>req.body</code> でリクエストボディを取得できます。型は <code>std::string</code> です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// クライアントが送ったボディをそのまま返す
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(req.</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// クライアントが送ったボディをそのまま返す
|
||||
</span><span style="color:#323232;"> res.set_content(req.body, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>ヘッダー</h3>
|
||||
<p><code>req.get_header_value()</code> でリクエストヘッダーの値を取得できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/check</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> auth = req.</span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Authorization</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Auth: </span><span style="color:#d3d0c8;">" + auth, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/check"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> auth </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_header_value(</span><span style="color:#183691;">"Authorization"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Auth: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> auth, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>クエリパラメーターとフォームデータ</h3>
|
||||
<p><code>req.get_param_value()</code> でパラメーターを取得できます。GETのクエリパラメーターと、POSTのフォームデータの両方に使えます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> q = req.</span><span style="color:#6699cc;">get_param_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Query: </span><span style="color:#d3d0c8;">" + q, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> q </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_param_value(</span><span style="color:#183691;">"q"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Query: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> q, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>/search?q=cpp-httplib</code> にアクセスすると、<code>q</code> の値は <code>"cpp-httplib"</code> になります。</p>
|
||||
<p>すべてのパラメーターをループで処理したいときは、<code>req.params</code> を使います。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::string result;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&[key, val] : req.</span><span style="color:#f2777a;">params</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> result += key + "</span><span style="color:#99cc99;"> = </span><span style="color:#d3d0c8;">" + val + "</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(result, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::string result;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">for </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">[key, val] </span><span style="font-weight:bold;color:#a71d5d;">:</span><span style="color:#323232;"> req.params) {
|
||||
</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">+=</span><span style="color:#323232;"> key </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" = " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> val </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">"</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> res.set_content(result, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>ファイルアップロード</h3>
|
||||
<p>マルチパートフォームでアップロードされたファイルは、<code>req.form.get_file()</code> で取得します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> f = req.</span><span style="color:#f2777a;">form</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">get_file</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> content = f.</span><span style="color:#f2777a;">filename </span><span style="color:#d3d0c8;">+ "</span><span style="color:#99cc99;"> (</span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::to_string</span><span style="color:#d3d0c8;">(f.</span><span style="color:#f2777a;">content</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()) + "</span><span style="color:#99cc99;"> bytes)</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(content, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> f </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.form.get_file(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> content </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> f.filename </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" (" </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::to_string(f.content.size()) </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" bytes)"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(content, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>f.filename</code> でファイル名、<code>f.content</code> でファイルの中身にアクセスできます。</p>
|
||||
<h2>パスパラメーター</h2>
|
||||
<p>URLの一部を変数として受け取りたいことがあります。たとえば <code>/users/42</code> の <code>42</code> を取得したい場合です。<code>:param</code> 記法を使うと、URLの一部をキャプチャできます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User ID: </span><span style="color:#d3d0c8;">" + id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/users/:id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User ID: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>/users/42</code> にアクセスすると、<code>req.path_params.at("id")</code> は <code>"42"</code> を返します。<code>/users/100</code> なら <code>"100"</code> です。</p>
|
||||
<p>複数のパスパラメーターも使えます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:user_id/posts/:post_id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> user_id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">user_id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> post_id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">post_id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User: </span><span style="color:#d3d0c8;">" + user_id + "</span><span style="color:#99cc99;">, Post: </span><span style="color:#d3d0c8;">" + post_id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/users/:user_id/posts/:post_id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> user_id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"user_id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> post_id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"post_id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> user_id </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">", Post: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> post_id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>正規表現パターン</h3>
|
||||
<p><code>:param</code> の代わりに正規表現をパスに書くこともできます。キャプチャグループの値は <code>req.matches</code> で取得します。型は <code>std::smatch</code> です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// 数字のみのIDを受け付ける
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">/files/(\d+)</span><span style="color:#d3d0c8;">)", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">matches</span><span style="color:#d3d0c8;">[</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">]; </span><span style="color:#747369;">// 最初のキャプチャグループ
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">File ID: </span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">(id), "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// 数字のみのIDを受け付ける
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"(/files/(\d+))"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.matches[</span><span style="color:#0086b3;">1</span><span style="color:#323232;">]; </span><span style="font-style:italic;color:#969896;">// 最初のキャプチャグループ
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"File ID: " </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::string(id), </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>/files/42</code> にはマッチしますが、<code>/files/abc</code> にはマッチしません。入力値を絞り込みたいときに便利です。</p>
|
||||
<h2>レスポンスの組み立て</h2>
|
||||
<p>ハンドラーの第2引数 <code>res</code> を使って、クライアントに返すレスポンスを組み立てます。</p>
|
||||
<h3>ボディとContent-Type</h3>
|
||||
<p><code>res.set_content()</code> でボディとContent-Typeを設定します。これだけでステータスコード200のレスポンスが返ります。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>ステータスコード</h3>
|
||||
<p>ステータスコードを変えたいときは、<code>res.status</code> に代入します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/not-found</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;">= </span><span style="color:#f99157;">404</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Not found</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/not-found"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.status </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#0086b3;">404</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Not found"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>レスポンスヘッダー</h3>
|
||||
<p><code>res.set_header()</code> でレスポンスヘッダーを追加できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/with-header</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_header</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">X-Custom</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">my-value</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/with-header"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_header(</span><span style="color:#183691;">"X-Custom"</span><span style="color:#323232;">, </span><span style="color:#183691;">"my-value"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>前章のサーバーを読み解く</h2>
|
||||
<p>ここまでの知識を使って、前章で用意したテスト用サーバーを改めて見てみましょう。</p>
|
||||
<h3>GET /hi</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/hi</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/hi"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>最もシンプルなハンドラーです。リクエストの情報は使わないので、<code>req</code> の変数名を省略しています。<code>"Hello!"</code> というテキストをそのまま返します。</p>
|
||||
<h3>GET /search</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/search</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> q = req.</span><span style="color:#6699cc;">get_param_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">q</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Query: </span><span style="color:#d3d0c8;">" + q, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/search"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> q </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_param_value(</span><span style="color:#183691;">"q"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Query: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> q, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>req.get_param_value("q")</code> でクエリパラメーター <code>q</code> の値を取り出します。<code>/search?q=cpp-httplib</code> なら、レスポンスは <code>"Query: cpp-httplib"</code> になります。</p>
|
||||
<h3>POST /post</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/post</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(req.</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/post"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(req.body, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>クライアントが送ったリクエストボディを、そのままレスポンスとして返すエコーサーバーです。<code>req.body</code> にボディが丸ごと入っています。</p>
|
||||
<h3>POST /submit</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/submit</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::string result;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&[key, val] : req.</span><span style="color:#f2777a;">params</span><span style="color:#d3d0c8;">) {
|
||||
</span><span style="color:#d3d0c8;"> result += key + "</span><span style="color:#99cc99;"> = </span><span style="color:#d3d0c8;">" + val + "</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(result, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/submit"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::string result;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">for </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">[key, val] </span><span style="font-weight:bold;color:#a71d5d;">:</span><span style="color:#323232;"> req.params) {
|
||||
</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">+=</span><span style="color:#323232;"> key </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" = " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> val </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">"</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> res.set_content(result, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>フォームデータとして送られたキーと値のペアを、<code>req.params</code> でループ処理しています。構造化束縛 <code>auto &[key, val]</code> を使って、各ペアを取り出しています。</p>
|
||||
<h3>POST /upload</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> f = req.</span><span style="color:#f2777a;">form</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">get_file</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> content = f.</span><span style="color:#f2777a;">filename </span><span style="color:#d3d0c8;">+ "</span><span style="color:#99cc99;"> (</span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::to_string</span><span style="color:#d3d0c8;">(f.</span><span style="color:#f2777a;">content</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()) + "</span><span style="color:#99cc99;"> bytes)</span><span style="color:#d3d0c8;">";
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(content, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> f </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.form.get_file(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> content </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> f.filename </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" (" </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::to_string(f.content.size()) </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#183691;">" bytes)"</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(content, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>マルチパートフォームで送られたファイルを受け取ります。<code>req.form.get_file("file")</code> で <code>"file"</code> という名前のフィールドを取得し、<code>f.filename</code> と <code>f.content.size()</code> でファイル名とサイズを返しています。</p>
|
||||
<h3>GET /users/:id</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/users/:id</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">path_params</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">id</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">User ID: </span><span style="color:#d3d0c8;">" + id, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/users/:id"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.path_params.at(</span><span style="color:#183691;">"id"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"User ID: " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> id, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>:id</code> の部分がパスパラメーターです。<code>req.path_params.at("id")</code> で値を取り出しています。<code>/users/42</code> なら <code>"42"</code>、<code>/users/alice</code> なら <code>"alice"</code> が得られます。</p>
|
||||
<h3>GET /files/(\d+)</h3>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">/files/(\d+)</span><span style="color:#d3d0c8;">)", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> id = req.</span><span style="color:#f2777a;">matches</span><span style="color:#d3d0c8;">[</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">];
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">File ID: </span><span style="color:#d3d0c8;">" + </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">(id), "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.Get(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"(/files/(\d+))"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> id </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.matches[</span><span style="color:#0086b3;">1</span><span style="color:#323232;">];
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"File ID: " </span><span style="font-weight:bold;color:#a71d5d;">+ </span><span style="color:#323232;">std::string(id), </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>正規表現 <code>(\d+)</code> で数字だけのIDにマッチします。<code>/files/42</code> にはマッチしますが、<code>/files/abc</code> は404になります。<code>req.matches[1]</code> で最初のキャプチャグループの値を取得しています。</p>
|
||||
<h2>次のステップ</h2>
|
||||
<p>サーバーの基本がわかりましたね。ルーティング、リクエストの読み取り、レスポンスの組み立て。これだけで、十分に実用的なAPIサーバーが作れます。</p>
|
||||
<p>次は、静的ファイルの配信を見てみましょう。HTMLやCSSを配信するサーバーを作ります。</p>
|
||||
<p><strong>次:</strong> <a href="../04-static-file-server">Static File Server</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,305 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Static File Server - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="active">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>Static File Server</h1>
|
||||
<p>cpp-httplibは、HTMLやCSS、画像ファイルなどの静的ファイルも配信できます。面倒な設定は要りません。<code>set_mount_point()</code> を1行呼ぶだけです。</p>
|
||||
<h2>set_mount_point の基本</h2>
|
||||
<p>さっそくやってみましょう。<code>set_mount_point()</code> は、URLのパスとローカルディレクトリを紐づけます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./html</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on port 8080...</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./html"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on port 8080..." </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>第1引数がURLのマウントポイント、第2引数がローカルのディレクトリパスです。この例だと、<code>/</code> へのリクエストを <code>./html</code> ディレクトリから配信します。</p>
|
||||
<p>試してみましょう。まず <code>html</code> ディレクトリを作って、<code>index.html</code> を置きます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">mkdir</span><span style="color:#d3d0c8;"> html
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">mkdir html
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;"><!</span><span style="color:#f2777a;">DOCTYPE</span><span style="color:#d3d0c8;"> html>
|
||||
</span><span style="color:#d3d0c8;"><</span><span style="color:#f2777a;">html</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"><</span><span style="color:#f2777a;">head</span><span style="color:#d3d0c8;">><</span><span style="color:#f2777a;">title</span><span style="color:#d3d0c8;">>My Page</</span><span style="color:#f2777a;">title</span><span style="color:#d3d0c8;">></</span><span style="color:#f2777a;">head</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"><</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"> <</span><span style="color:#f2777a;">h1</span><span style="color:#d3d0c8;">>Hello from cpp-httplib!</</span><span style="color:#f2777a;">h1</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"> <</span><span style="color:#f2777a;">p</span><span style="color:#d3d0c8;">>This is a static file.</</span><span style="color:#f2777a;">p</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"></</span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;"></</span><span style="color:#f2777a;">html</span><span style="color:#d3d0c8;">>
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;"><!</span><span style="color:#63a35c;">DOCTYPE</span><span style="color:#323232;"> html>
|
||||
</span><span style="color:#323232;"><</span><span style="color:#63a35c;">html</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"><</span><span style="color:#63a35c;">head</span><span style="color:#323232;">><</span><span style="color:#63a35c;">title</span><span style="color:#323232;">>My Page</</span><span style="color:#63a35c;">title</span><span style="color:#323232;">></</span><span style="color:#63a35c;">head</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"><</span><span style="color:#63a35c;">body</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"> <</span><span style="color:#63a35c;">h1</span><span style="color:#323232;">>Hello from cpp-httplib!</</span><span style="color:#63a35c;">h1</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"> <</span><span style="color:#63a35c;">p</span><span style="color:#323232;">>This is a static file.</</span><span style="color:#63a35c;">p</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"></</span><span style="color:#63a35c;">body</span><span style="color:#323232;">>
|
||||
</span><span style="color:#323232;"></</span><span style="color:#63a35c;">html</span><span style="color:#323232;">>
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>コンパイルして起動します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">g++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp</span><span style="color:#f2777a;"> -pthread
|
||||
</span><span style="color:#6699cc;">./server
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">g++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -o server server.cpp -pthread
|
||||
</span><span style="color:#323232;">./server
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>ブラウザで <code>http://localhost:8080</code> を開いてみてください。<code>html/index.html</code> の内容が表示されるはずです。<code>http://localhost:8080/index.html</code> でも同じページが返ります。</p>
|
||||
<p>もちろん、前章のクライアントコードや <code>curl</code> でもアクセスできますよ。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// HTMLが表示される
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// HTMLが表示される
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> http://localhost:8080
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl http://localhost:8080
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>複数のマウントポイント</h2>
|
||||
<p><code>set_mount_point()</code> は何回でも呼べます。URLのパスごとに、別々のディレクトリを割り当てられます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./public</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/assets</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./static/assets</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/docs</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./documentation</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./public"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/assets"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./static/assets"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/docs"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./documentation"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>/assets/style.css</code> なら <code>./static/assets/style.css</code> を、<code>/docs/guide.html</code> なら <code>./documentation/guide.html</code> を配信します。</p>
|
||||
<h2>ハンドラーとの組み合わせ</h2>
|
||||
<p>静的ファイルの配信と、前章で学んだルーティングハンドラーは共存できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// APIエンドポイント
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/api/hello</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">R</span><span style="color:#d3d0c8;">"(</span><span style="color:#99cc99;">{"message":"Hello!"}</span><span style="color:#d3d0c8;">)", "</span><span style="color:#99cc99;">application/json</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// 静的ファイル配信
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./public</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// APIエンドポイント
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/api/hello"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="font-weight:bold;color:#a71d5d;">R</span><span style="color:#183691;">"({"message":"Hello!"})"</span><span style="color:#323232;">, </span><span style="color:#183691;">"application/json"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// 静的ファイル配信
|
||||
</span><span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./public"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>ハンドラーが先に評価されます。<code>/api/hello</code> にはハンドラーが応答し、それ以外のパスは <code>./public</code> ディレクトリからファイルを探します。</p>
|
||||
<h2>レスポンスヘッダーの追加</h2>
|
||||
<p><code>set_mount_point()</code> の第3引数にヘッダーを渡すと、静的ファイルのレスポンスにカスタムヘッダーを付けられます。キャッシュ制御に便利です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_mount_point</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">./public</span><span style="color:#d3d0c8;">", {
|
||||
</span><span style="color:#d3d0c8;"> {"</span><span style="color:#99cc99;">Cache-Control</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">max-age=3600</span><span style="color:#d3d0c8;">"}
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_mount_point(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, </span><span style="color:#183691;">"./public"</span><span style="color:#323232;">, {
|
||||
</span><span style="color:#323232;"> {</span><span style="color:#183691;">"Cache-Control"</span><span style="color:#323232;">, </span><span style="color:#183691;">"max-age=3600"</span><span style="color:#323232;">}
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>こうすると、ブラウザは配信されたファイルを1時間キャッシュします。</p>
|
||||
<h2>静的ファイルサーバー用のDockerファイル</h2>
|
||||
<p>cpp-httplibのリポジトリには、静的ファイルサーバー用の <code>Dockerfile</code> が含まれています。Docker Hubにビルド済みイメージも公開しているので、1コマンドで起動できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">> docker run</span><span style="color:#f2777a;"> -p</span><span style="color:#d3d0c8;"> 8080:80</span><span style="color:#f2777a;"> -v</span><span style="color:#d3d0c8;"> ./my-site:/html yhirose4dockerhub/cpp-httplib-server
|
||||
</span><span style="color:#6699cc;">Serving</span><span style="color:#d3d0c8;"> HTTP on 0.0.0.0:80
|
||||
</span><span style="color:#6699cc;">Mount</span><span style="color:#d3d0c8;"> point: / -> ./html
|
||||
</span><span style="color:#6699cc;">Press</span><span style="color:#d3d0c8;"> Ctrl+C to shutdown gracefully...
|
||||
</span><span style="color:#6699cc;">192.168.65.1</span><span style="color:#d3d0c8;"> - - </span><span style="color:#cc99cc;">[</span><span style="color:#d3d0c8;">22/Feb/2026:12:00:00 +0000</span><span style="color:#cc99cc;">] </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">GET / HTTP/1.1</span><span style="color:#d3d0c8;">" 200 256 "</span><span style="color:#99cc99;">-</span><span style="color:#d3d0c8;">" "</span><span style="color:#99cc99;">Mozilla/5.0 ...</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#6699cc;">192.168.65.1</span><span style="color:#d3d0c8;"> - - </span><span style="color:#cc99cc;">[</span><span style="color:#d3d0c8;">22/Feb/2026:12:00:00 +0000</span><span style="color:#cc99cc;">] </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">GET /style.css HTTP/1.1</span><span style="color:#d3d0c8;">" 200 1024 "</span><span style="color:#99cc99;">-</span><span style="color:#d3d0c8;">" "</span><span style="color:#99cc99;">Mozilla/5.0 ...</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#6699cc;">192.168.65.1</span><span style="color:#d3d0c8;"> - - </span><span style="color:#cc99cc;">[</span><span style="color:#d3d0c8;">22/Feb/2026:12:00:01 +0000</span><span style="color:#cc99cc;">] </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">GET /favicon.ico HTTP/1.1</span><span style="color:#d3d0c8;">" 404 152 "</span><span style="color:#99cc99;">-</span><span style="color:#d3d0c8;">" "</span><span style="color:#99cc99;">Mozilla/5.0 ...</span><span style="color:#d3d0c8;">"
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">></span><span style="color:#323232;"> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server
|
||||
</span><span style="color:#323232;">Serving HTTP on 0.0.0.0:80
|
||||
</span><span style="color:#323232;">Mount point: / -</span><span style="font-weight:bold;color:#a71d5d;">></span><span style="color:#323232;"> ./html
|
||||
</span><span style="color:#323232;">Press Ctrl+C to shutdown gracefully...
|
||||
</span><span style="color:#323232;">192.168.65.1 - - </span><span style="font-weight:bold;color:#a71d5d;">[</span><span style="color:#323232;">22/Feb/2026:12:00:00 +0000</span><span style="font-weight:bold;color:#a71d5d;">] </span><span style="color:#183691;">"GET / HTTP/1.1"</span><span style="color:#323232;"> 200 256 </span><span style="color:#183691;">"-" "Mozilla/5.0 ..."
|
||||
</span><span style="color:#323232;">192.168.65.1 - - </span><span style="font-weight:bold;color:#a71d5d;">[</span><span style="color:#323232;">22/Feb/2026:12:00:00 +0000</span><span style="font-weight:bold;color:#a71d5d;">] </span><span style="color:#183691;">"GET /style.css HTTP/1.1"</span><span style="color:#323232;"> 200 1024 </span><span style="color:#183691;">"-" "Mozilla/5.0 ..."
|
||||
</span><span style="color:#323232;">192.168.65.1 - - </span><span style="font-weight:bold;color:#a71d5d;">[</span><span style="color:#323232;">22/Feb/2026:12:00:01 +0000</span><span style="font-weight:bold;color:#a71d5d;">] </span><span style="color:#183691;">"GET /favicon.ico HTTP/1.1"</span><span style="color:#323232;"> 404 152 </span><span style="color:#183691;">"-" "Mozilla/5.0 ..."
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>./my-site</code> ディレクトリの中身が、そのままポート8080で配信されます。NGINXと同じログ形式で、アクセスの様子を確認できますよ。</p>
|
||||
<h2>次のステップ</h2>
|
||||
<p>静的ファイルの配信ができるようになりましたね。HTMLやCSS、JavaScriptを配信するWebサーバーが、これだけのコードで作れます。</p>
|
||||
<p>次は、HTTPSで暗号化通信をしてみましょう。まずはTLSライブラリのセットアップからです。</p>
|
||||
<p><strong>次:</strong> <a href="../05-tls-setup">TLS Setup</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,229 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TLS Setup - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="active">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>TLS Setup</h1>
|
||||
<p>ここまではHTTP(平文)でやってきましたが、実際のWebではHTTPS(暗号化通信)が当たり前ですよね。cpp-httplibでHTTPSを使うには、TLSライブラリが必要です。</p>
|
||||
<p>このTourではOpenSSLを使います。最も広く使われていて、情報も豊富です。</p>
|
||||
<h2>OpenSSLのインストール</h2>
|
||||
<p>お使いのOSに合わせてインストールしましょう。</p>
|
||||
<table><thead><tr><th>OS</th><th>インストール方法</th></tr></thead><tbody>
|
||||
<tr><td>macOS</td><td><a href="https://brew.sh/">Homebrew</a> (<code>brew install openssl</code>)</td></tr>
|
||||
<tr><td>Ubuntu / Debian</td><td><code>sudo apt install libssl-dev</code></td></tr>
|
||||
<tr><td>Windows</td><td><a href="https://vcpkg.io/">vcpkg</a> (<code>vcpkg install openssl</code>)</td></tr>
|
||||
</tbody></table>
|
||||
<h2>コンパイルオプション</h2>
|
||||
<p>TLS機能を有効にするには、<code>CPPHTTPLIB_OPENSSL_SUPPORT</code> マクロを定義してコンパイルします。前章までのコンパイルコマンドに、いくつかオプションが増えます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;"># macOS (Homebrew)
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -DCPPHTTPLIB_OPENSSL_SUPPORT </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -I</span><span style="color:#d3d0c8;">$</span><span style="color:#f2777a;">(</span><span style="color:#6699cc;">brew</span><span style="color:#f2777a;"> --prefix openssl)</span><span style="color:#d3d0c8;">/include \
|
||||
</span><span style="color:#f2777a;"> -L</span><span style="color:#d3d0c8;">$</span><span style="color:#f2777a;">(</span><span style="color:#6699cc;">brew</span><span style="color:#f2777a;"> --prefix openssl)</span><span style="color:#d3d0c8;">/lib \
|
||||
</span><span style="color:#f2777a;"> -lssl -lcrypto </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -framework</span><span style="color:#d3d0c8;"> CoreFoundation</span><span style="color:#f2777a;"> -framework</span><span style="color:#d3d0c8;"> Security \
|
||||
</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Linux
|
||||
</span><span style="color:#6699cc;">clang++</span><span style="color:#f2777a;"> -std</span><span style="color:#d3d0c8;">=c++17</span><span style="color:#f2777a;"> -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -lssl -lcrypto </span><span style="color:#d3d0c8;">\
|
||||
</span><span style="color:#f2777a;"> -o</span><span style="color:#d3d0c8;"> server server.cpp
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;"># Windows (Developer Command Prompt)
|
||||
</span><span style="color:#6699cc;">cl</span><span style="color:#d3d0c8;"> /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;"># macOS (Homebrew)
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \
|
||||
</span><span style="color:#323232;"> -I$(brew --prefix openssl)/include \
|
||||
</span><span style="color:#323232;"> -L$(brew --prefix openssl)/lib \
|
||||
</span><span style="color:#323232;"> -lssl -lcrypto \
|
||||
</span><span style="color:#323232;"> -framework CoreFoundation -framework Security \
|
||||
</span><span style="color:#323232;"> -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Linux
|
||||
</span><span style="color:#323232;">clang++ -std</span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;">c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \
|
||||
</span><span style="color:#323232;"> -lssl -lcrypto \
|
||||
</span><span style="color:#323232;"> -o server server.cpp
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;"># Windows (Developer Command Prompt)
|
||||
</span><span style="color:#323232;">cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>それぞれのオプションの役割を見てみましょう。</p>
|
||||
<ul>
|
||||
<li><strong><code>-DCPPHTTPLIB_OPENSSL_SUPPORT</code></strong> — TLS機能を有効にするマクロ定義</li>
|
||||
<li><strong><code>-lssl -lcrypto</code></strong> — OpenSSLのライブラリをリンク</li>
|
||||
<li><strong><code>-I</code> / <code>-L</code></strong>(macOSのみ)— Homebrew版OpenSSLのパスを指定</li>
|
||||
<li><strong><code>-framework CoreFoundation -framework Security</code></strong>(macOSのみ)— Keychainからシステム証明書を自動で読み込むために必要です</li>
|
||||
</ul>
|
||||
<h2>動作確認</h2>
|
||||
<p>ちゃんと動くか確認してみましょう。<code>httplib::Client</code> にHTTPSのURLを渡してアクセスするだけのプログラムです。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://www.google.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Status: </span><span style="color:#d3d0c8;">" << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Error: </span><span style="color:#d3d0c8;">" << </span><span style="color:#6699cc;">httplib::to_string</span><span style="color:#d3d0c8;">(res.</span><span style="color:#6699cc;">error</span><span style="color:#d3d0c8;">()) << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://www.google.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Status: " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> } </span><span style="font-weight:bold;color:#a71d5d;">else </span><span style="color:#323232;">{
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Error: " </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#323232;">httplib::to_string(res.error()) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>コンパイルして実行してみてください。<code>Status: 200</code> と表示されれば、セットアップ完了です。</p>
|
||||
<h2>他のTLSバックエンド</h2>
|
||||
<p>cpp-httplibはOpenSSL以外にも、Mbed TLSとwolfSSLに対応しています。マクロ定義とリンクするライブラリを変えるだけで切り替えられます。</p>
|
||||
<table><thead><tr><th style="text-align: left">バックエンド</th><th style="text-align: left">マクロ定義</th><th style="text-align: left">リンクするライブラリ</th></tr></thead><tbody>
|
||||
<tr><td style="text-align: left">OpenSSL</td><td style="text-align: left"><code>CPPHTTPLIB_OPENSSL_SUPPORT</code></td><td style="text-align: left"><code>libssl</code>, <code>libcrypto</code></td></tr>
|
||||
<tr><td style="text-align: left">Mbed TLS</td><td style="text-align: left"><code>CPPHTTPLIB_MBEDTLS_SUPPORT</code></td><td style="text-align: left"><code>libmbedtls</code>, <code>libmbedx509</code>, <code>libmbedcrypto</code></td></tr>
|
||||
<tr><td style="text-align: left">wolfSSL</td><td style="text-align: left"><code>CPPHTTPLIB_WOLFSSL_SUPPORT</code></td><td style="text-align: left"><code>libwolfssl</code></td></tr>
|
||||
</tbody></table>
|
||||
<p>このTourではOpenSSLを前提に進めますが、APIはどのバックエンドでも共通です。</p>
|
||||
<h2>次のステップ</h2>
|
||||
<p>TLSの準備ができましたね。次は、HTTPSサイトにリクエストを送ってみましょう。</p>
|
||||
<p><strong>次:</strong> <a href="../06-https-client">HTTPS Client</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,287 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HTTPS Client - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="active">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>HTTPS Client</h1>
|
||||
<p>前章でOpenSSLのセットアップが済んだので、さっそくHTTPSクライアントを使ってみましょう。2章で使った <code>httplib::Client</code> がそのまま使えます。コンストラクタに <code>https://</code> 付きのURLを渡すだけです。</p>
|
||||
<h2>GETリクエスト</h2>
|
||||
<p>実在するHTTPSサイトにアクセスしてみましょう。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">substr</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">100</span><span style="color:#d3d0c8;">) << std::endl; </span><span style="color:#747369;">// HTMLの先頭部分
|
||||
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Error: </span><span style="color:#d3d0c8;">" << </span><span style="color:#6699cc;">httplib::to_string</span><span style="color:#d3d0c8;">(res.</span><span style="color:#6699cc;">error</span><span style="color:#d3d0c8;">()) << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body.substr(</span><span style="color:#0086b3;">0</span><span style="color:#323232;">, </span><span style="color:#0086b3;">100</span><span style="color:#323232;">) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// HTMLの先頭部分
|
||||
</span><span style="color:#323232;"> } </span><span style="font-weight:bold;color:#a71d5d;">else </span><span style="color:#323232;">{
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Error: " </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#323232;">httplib::to_string(res.error()) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>2章では <code>httplib::Client cli("http://localhost:8080")</code> と書きましたよね。スキームを <code>https://</code> に変えるだけです。<code>Get()</code> や <code>Post()</code> など、2章で学んだAPIはすべてそのまま使えます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>ポートの指定</h2>
|
||||
<p>HTTPSのデフォルトポートは443です。別のポートを使いたい場合は、URLにポートを含めます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>CA証明書の検証</h2>
|
||||
<p><code>httplib::Client</code> はHTTPS接続時、デフォルトでサーバー証明書を検証します。信頼できるCA(認証局)が発行した証明書を持つサーバーにしか接続しません。</p>
|
||||
<p>CA証明書は、macOSならKeychain、LinuxならシステムのCA証明書ストア、WindowsならWindowsの証明書ストアから自動で読み込みます。ほとんどの場合、追加の設定は要りません。</p>
|
||||
<h3>CA証明書ファイルの指定</h3>
|
||||
<p>環境によってはシステムのCA証明書が見つからないこともあります。そのときは <code>set_ca_cert_path()</code> でパスを直接指定してください。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_ca_cert_path</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/etc/ssl/certs/ca-certificates.crt</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_ca_cert_path(</span><span style="color:#183691;">"/etc/ssl/certs/ca-certificates.crt"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> --cacert</span><span style="color:#d3d0c8;"> /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>証明書検証の無効化</h3>
|
||||
<p>開発中、自己署名証明書のサーバーに接続したいときは、検証を無効にできます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">enable_server_certificate_verification</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">false</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.enable_server_certificate_verification(</span><span style="color:#0086b3;">false</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -k</span><span style="color:#d3d0c8;"> https://localhost:8443/
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -k https://localhost:8443/
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>本番では絶対に無効にしないでください。中間者攻撃のリスクがあります。</p>
|
||||
<h2>リダイレクトの追跡</h2>
|
||||
<p>HTTPSサイトへのアクセスでは、リダイレクトに遭遇することがよくあります。たとえば <code>http://</code> から <code>https://</code> へ、あるいは <code>www</code> なしから <code>www</code> ありへ転送されるケースです。</p>
|
||||
<p>デフォルトではリダイレクトを追跡しません。リダイレクト先は <code>Location</code> ヘッダーで確認できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/httpbin/redirect/3</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 302
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Location</span><span style="color:#d3d0c8;">") << std::endl;
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/httpbin/redirect/3"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 302
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->get_header_value(</span><span style="color:#183691;">"Location"</span><span style="color:#323232;">) </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#d3d0c8;"> https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>set_follow_location(true)</code> を設定すると、リダイレクトを自動で追跡して、最終的なレスポンスを返してくれます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://nghttp2.org</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_follow_location</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/httpbin/redirect/3</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// 200(最終的なレスポンス)
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://nghttp2.org"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_follow_location(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/httpbin/redirect/3"</span><span style="color:#323232;">);
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// 200(最終的なレスポンス)
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -L</span><span style="color:#d3d0c8;"> https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -L https://nghttp2.org/httpbin/redirect/3
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>次のステップ</h2>
|
||||
<p>HTTPSクライアントの使い方がわかりましたね。次は自分でHTTPSサーバーを立ててみましょう。自己署名証明書の作り方から始めます。</p>
|
||||
<p><strong>次:</strong> <a href="../07-https-server">HTTPS Server</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,276 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HTTPS Server - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="active">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>HTTPS Server</h1>
|
||||
<p>前章ではHTTPSクライアントを使いました。今度は自分でHTTPSサーバーを立ててみましょう。3章の <code>httplib::Server</code> を <code>httplib::SSLServer</code> に置き換えるだけです。</p>
|
||||
<p>ただし、TLSサーバーにはサーバー証明書と秘密鍵が必要です。まずはそこから準備しましょう。</p>
|
||||
<h2>自己署名証明書の作成</h2>
|
||||
<p>開発やテスト用なら、自己署名証明書(いわゆるオレオレ証明書)で十分です。OpenSSLのコマンドでサクッと作れます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">openssl</span><span style="color:#d3d0c8;"> req</span><span style="color:#f2777a;"> -x509 -noenc -keyout</span><span style="color:#d3d0c8;"> key.pem</span><span style="color:#f2777a;"> -out</span><span style="color:#d3d0c8;"> cert.pem</span><span style="color:#f2777a;"> -subj</span><span style="color:#d3d0c8;"> /CN=localhost
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>これで2つのファイルができます。</p>
|
||||
<ul>
|
||||
<li><strong><code>cert.pem</code></strong> — サーバー証明書</li>
|
||||
<li><strong><code>key.pem</code></strong> — 秘密鍵</li>
|
||||
</ul>
|
||||
<h2>最小のHTTPSサーバー</h2>
|
||||
<p>証明書ができたら、さっそくサーバーを書いてみましょう。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::SSLServer </span><span style="color:#6699cc;">svr</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">cert.pem</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">key.pem</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, HTTPS!</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on https://localhost:8443</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8443</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::SSLServer svr(</span><span style="color:#183691;">"cert.pem"</span><span style="color:#323232;">, </span><span style="color:#183691;">"key.pem"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello, HTTPS!"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on https://localhost:8443" </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8443</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>httplib::SSLServer</code> のコンストラクタに証明書と秘密鍵のパスを渡すだけです。ルーティングの書き方は3章の <code>httplib::Server</code> とまったく同じですよ。</p>
|
||||
<p>コンパイルして起動しましょう。</p>
|
||||
<h2>動作確認</h2>
|
||||
<p>サーバーが起動したら、<code>curl</code> でアクセスしてみましょう。自己署名証明書なので、<code>-k</code> オプションで証明書検証をスキップします。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#6699cc;">curl</span><span style="color:#f2777a;"> -k</span><span style="color:#d3d0c8;"> https://localhost:8443/
|
||||
</span><span style="color:#747369;"># Hello, HTTPS!
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">curl -k https://localhost:8443/
|
||||
</span><span style="font-style:italic;color:#969896;"># Hello, HTTPS!
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>ブラウザで <code>https://localhost:8443</code> を開くと、「この接続は安全ではありません」と警告が出ます。自己署名証明書なので正常です。気にせず進めてください。</p>
|
||||
<h2>クライアントからの接続</h2>
|
||||
<p>前章の <code>httplib::Client</code> で接続してみましょう。自己署名証明書のサーバーに接続するには、2つの方法があります。</p>
|
||||
<h3>方法1: 証明書検証を無効にする</h3>
|
||||
<p>開発時の手軽な方法です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> cli.</span><span style="color:#6699cc;">enable_server_certificate_verification</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">false</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello, HTTPS!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> cli.enable_server_certificate_verification(</span><span style="color:#0086b3;">false</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, HTTPS!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h3>方法2: 自己署名証明書をCA証明書として指定する</h3>
|
||||
<p>こちらのほうが安全です。<code>cert.pem</code> をCA証明書として信頼するよう指定します。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#define </span><span style="color:#d3d0c8;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://localhost:8443</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> cli.</span><span style="color:#6699cc;">set_ca_cert_path</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">cert.pem</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << res-></span><span style="color:#f2777a;">body </span><span style="color:#d3d0c8;"><< std::endl; </span><span style="color:#747369;">// Hello, HTTPS!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#define </span><span style="color:#323232;">CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Client cli(</span><span style="color:#183691;">"https://localhost:8443"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> cli.set_ca_cert_path(</span><span style="color:#183691;">"cert.pem"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res->body </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, HTTPS!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>この方法なら、指定した証明書のサーバーにだけ接続を許可して、なりすましを防げます。テスト環境でもなるべくこちらを使いましょう。</p>
|
||||
<h2>Server と SSLServer の比較</h2>
|
||||
<p>3章で学んだ <code>httplib::Server</code> のAPIは、<code>httplib::SSLServer</code> でもそのまま使えます。違いはコンストラクタだけです。</p>
|
||||
<table><thead><tr><th></th><th><code>httplib::Server</code></th><th><code>httplib::SSLServer</code></th></tr></thead><tbody>
|
||||
<tr><td>コンストラクタ</td><td>引数なし</td><td>証明書と秘密鍵のパス</td></tr>
|
||||
<tr><td>プロトコル</td><td>HTTP</td><td>HTTPS</td></tr>
|
||||
<tr><td>ポート(慣例)</td><td>8080</td><td>8443</td></tr>
|
||||
<tr><td>ルーティング</td><td>共通</td><td>共通</td></tr>
|
||||
</tbody></table>
|
||||
<p>HTTPサーバーをHTTPSに切り替えるには、コンストラクタを変えるだけです。</p>
|
||||
<h2>次のステップ</h2>
|
||||
<p>HTTPSサーバーが動きましたね。これでHTTP/HTTPSのクライアントとサーバー、両方の基本がそろいました。</p>
|
||||
<p>次は、cpp-httplibに新しく加わったWebSocket機能を見てみましょう。</p>
|
||||
<p><strong>次:</strong> <a href="../08-websocket">WebSocket</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,328 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebSocket - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="active">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>WebSocket</h1>
|
||||
<p>cpp-httplibはWebSocketにも対応しています。HTTPのリクエスト/レスポンスと違い、WebSocketはサーバーとクライアントが双方向にメッセージをやり取りできます。チャットやリアルタイム通知に便利です。</p>
|
||||
<p>さっそく、エコーサーバーとクライアントを作ってみましょう。</p>
|
||||
<h2>エコーサーバー</h2>
|
||||
<p>受け取ったメッセージをそのまま返すエコーサーバーです。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg); </span><span style="color:#747369;">// 受け取ったメッセージをそのまま返す
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Listening on port 8080...</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::Server svr;
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(ws.read(msg)) {
|
||||
</span><span style="color:#323232;"> ws.send(msg); </span><span style="font-style:italic;color:#969896;">// 受け取ったメッセージをそのまま返す
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Listening on port 8080..." </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>svr.WebSocket()</code> でWebSocketハンドラーを登録します。3章の <code>svr.Get()</code> や <code>svr.Post()</code> と同じ感覚ですね。</p>
|
||||
<p>ハンドラーの中では、<code>ws.read(msg)</code> でメッセージを待ちます。接続が閉じられると <code>read()</code> が <code>false</code> を返すので、ループを抜けます。<code>ws.send(msg)</code> でメッセージを送り返します。</p>
|
||||
<h2>クライアントからの接続</h2>
|
||||
<p><code>httplib::ws::WebSocketClient</code> を使ってサーバーに接続してみましょう。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;">"</span><span style="color:#99cc99;">httplib.h</span><span style="color:#d3d0c8;">"
|
||||
</span><span style="color:#cc99cc;">#include </span><span style="color:#d3d0c8;"><</span><span style="color:#99cc99;">iostream</span><span style="color:#d3d0c8;">>
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">int </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
|
||||
</span><span style="color:#d3d0c8;"> httplib::ws::WebSocketClient </span><span style="color:#6699cc;">client</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">ws://localhost:8080/ws</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(!client.</span><span style="color:#6699cc;">connect</span><span style="color:#d3d0c8;">()) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << "</span><span style="color:#99cc99;">Connection failed</span><span style="color:#d3d0c8;">" << std::endl;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// メッセージを送信
|
||||
</span><span style="color:#d3d0c8;"> client.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, WebSocket!</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// サーバーからの応答を受信
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(client.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << msg << std::endl; </span><span style="color:#747369;">// Hello, WebSocket!
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> client.</span><span style="color:#6699cc;">close</span><span style="color:#d3d0c8;">();
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;">"httplib.h"
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">#include </span><span style="color:#183691;"><iostream>
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">int </span><span style="font-weight:bold;color:#795da3;">main</span><span style="color:#323232;">() {
|
||||
</span><span style="color:#323232;"> httplib::ws::WebSocketClient client(</span><span style="color:#183691;">"ws://localhost:8080/ws"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(</span><span style="font-weight:bold;color:#a71d5d;">!</span><span style="color:#323232;">client.connect()) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">"Connection failed" </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">1</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// メッセージを送信
|
||||
</span><span style="color:#323232;"> client.send(</span><span style="color:#183691;">"Hello, WebSocket!"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// サーバーからの応答を受信
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(client.read(msg)) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> msg </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl; </span><span style="font-style:italic;color:#969896;">// Hello, WebSocket!
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> client.close();
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>コンストラクタには <code>ws://host:port/path</code> 形式のURLを渡します。<code>connect()</code> で接続を開始し、<code>send()</code> と <code>read()</code> でメッセージをやり取りします。</p>
|
||||
<h2>テキストとバイナリ</h2>
|
||||
<p>WebSocketにはテキストとバイナリの2種類のメッセージがあります。<code>read()</code> の戻り値で区別できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> httplib::ws::ReadResult ret;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">((ret = ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg))) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(ret == httplib::ws::Binary) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg.</span><span style="color:#6699cc;">data</span><span style="color:#d3d0c8;">(), msg.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">()); </span><span style="color:#747369;">// バイナリとして送信
|
||||
</span><span style="color:#d3d0c8;"> } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg); </span><span style="color:#747369;">// テキストとして送信
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> httplib::ws::ReadResult ret;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">((ret </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> ws.read(msg))) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(ret </span><span style="font-weight:bold;color:#a71d5d;">==</span><span style="color:#323232;"> httplib::ws::Binary) {
|
||||
</span><span style="color:#323232;"> ws.send(msg.data(), msg.size()); </span><span style="font-style:italic;color:#969896;">// バイナリとして送信
|
||||
</span><span style="color:#323232;"> } </span><span style="font-weight:bold;color:#a71d5d;">else </span><span style="color:#323232;">{
|
||||
</span><span style="color:#323232;"> ws.send(msg); </span><span style="font-style:italic;color:#969896;">// テキストとして送信
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<ul>
|
||||
<li><code>ws.send(const std::string &)</code> — テキストメッセージとして送信</li>
|
||||
<li><code>ws.send(const char *, size_t)</code> — バイナリメッセージとして送信</li>
|
||||
</ul>
|
||||
<p>クライアント側も同じAPIです。</p>
|
||||
<h2>リクエスト情報へのアクセス</h2>
|
||||
<p>ハンドラーの第1引数 <code>req</code> から、ハンドシェイク時のHTTPリクエスト情報を読み取れます。認証トークンの確認などに便利です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &req, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> token = req.</span><span style="color:#6699cc;">get_header_value</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Authorization</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(token.</span><span style="color:#6699cc;">empty</span><span style="color:#d3d0c8;">()) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">close</span><span style="color:#d3d0c8;">(httplib::ws::CloseStatus::PolicyViolation, "</span><span style="color:#99cc99;">unauthorized</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg);
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">req, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> token </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> req.get_header_value(</span><span style="color:#183691;">"Authorization"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(token.empty()) {
|
||||
</span><span style="color:#323232;"> ws.close(httplib::ws::CloseStatus::PolicyViolation, </span><span style="color:#183691;">"unauthorized"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(ws.read(msg)) {
|
||||
</span><span style="color:#323232;"> ws.send(msg);
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>WSSで使う</h2>
|
||||
<p>HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は <code>httplib::SSLServer</code> にWebSocketハンドラーを登録するだけです。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::SSLServer </span><span style="color:#6699cc;">svr</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">cert.pem</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">key.pem</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">WebSocket</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/ws</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::Request &, httplib::ws::WebSocket &ws) {
|
||||
</span><span style="color:#d3d0c8;"> std::string msg;
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(ws.</span><span style="color:#6699cc;">read</span><span style="color:#d3d0c8;">(msg)) {
|
||||
</span><span style="color:#d3d0c8;"> ws.</span><span style="color:#6699cc;">send</span><span style="color:#d3d0c8;">(msg);
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">0.0.0.0</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8443</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::SSLServer </span><span style="font-weight:bold;color:#795da3;">svr</span><span style="color:#323232;">(</span><span style="color:#183691;">"cert.pem"</span><span style="color:#323232;">, </span><span style="color:#183691;">"key.pem"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.WebSocket(</span><span style="color:#183691;">"/ws"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::Request </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">, httplib::ws::WebSocket </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">ws) {
|
||||
</span><span style="color:#323232;"> std::string msg;
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(ws.read(msg)) {
|
||||
</span><span style="color:#323232;"> ws.send(msg);
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"0.0.0.0"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8443</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>クライアント側は <code>wss://</code> スキームを使います。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::ws::WebSocketClient </span><span style="color:#6699cc;">client</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">wss://localhost:8443/ws</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::ws::WebSocketClient </span><span style="font-weight:bold;color:#795da3;">client</span><span style="color:#323232;">(</span><span style="color:#183691;">"wss://localhost:8443/ws"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>次のステップ</h2>
|
||||
<p>WebSocketの基本がわかりましたね。ここまでで Tourは終わりです。</p>
|
||||
<p>次のページでは、Tourで取り上げなかった機能をまとめて紹介します。</p>
|
||||
<p><strong>次:</strong> <a href="../09-whats-next">What's Next</a></p>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,460 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>What's Next - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="active">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>What's Next</h1>
|
||||
<p>Tourお疲れさまでした! cpp-httplibの基本はひと通り押さえましたね。でも、まだまだ便利な機能があります。Tourで取り上げなかった機能をカテゴリー別に紹介します。</p>
|
||||
<h2>Streaming API</h2>
|
||||
<p>LLMのストリーミング応答や大きなファイルのダウンロードでは、レスポンス全体をメモリに載せたくないですよね。<code>stream::Get()</code> を使えば、データをチャンクごとに処理できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:11434</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> result = </span><span style="color:#6699cc;">httplib::stream::Get</span><span style="color:#d3d0c8;">(cli, "</span><span style="color:#99cc99;">/api/generate</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">(result) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">while </span><span style="color:#d3d0c8;">(result.</span><span style="color:#6699cc;">next</span><span style="color:#d3d0c8;">()) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout.</span><span style="color:#6699cc;">write</span><span style="color:#d3d0c8;">(result.</span><span style="color:#6699cc;">data</span><span style="color:#d3d0c8;">(), result.</span><span style="color:#6699cc;">size</span><span style="color:#d3d0c8;">());
|
||||
</span><span style="color:#d3d0c8;"> }
|
||||
</span><span style="color:#d3d0c8;">}
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:11434"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> result </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">httplib::stream::Get(cli, </span><span style="color:#183691;">"/api/generate"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">if </span><span style="color:#323232;">(result) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">while </span><span style="color:#323232;">(result.next()) {
|
||||
</span><span style="color:#323232;"> std::cout.write(result.data(), result.size());
|
||||
</span><span style="color:#323232;"> }
|
||||
</span><span style="color:#323232;">}
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>Get()</code> に <code>content_receiver</code> コールバックを渡す方法もあります。こちらはKeep-Aliveと併用できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/stream</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const char </span><span style="color:#d3d0c8;">*data, size_t len) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout.</span><span style="color:#6699cc;">write</span><span style="color:#d3d0c8;">(data, len);
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">cli.Get(</span><span style="color:#183691;">"/stream"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const char *</span><span style="color:#323232;">data, </span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> len) {
|
||||
</span><span style="color:#323232;"> std::cout.write(data, len);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">true</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>サーバー側には <code>set_content_provider()</code> と <code>set_chunked_content_provider()</code> があります。サイズがわかっているなら前者、不明なら後者を使ってください。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// サイズ指定あり(Content-Length が設定される)
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/file</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> size = </span><span style="color:#6699cc;">get_file_size</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">large.bin</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content_provider</span><span style="color:#d3d0c8;">(size, "</span><span style="color:#99cc99;">application/octet-stream</span><span style="color:#d3d0c8;">",
|
||||
</span><span style="color:#d3d0c8;"> [](size_t offset, size_t length, httplib::DataSink &sink) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// offset から length バイト分を送る
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#747369;">// サイズ不明(Chunked Transfer Encoding)
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/stream</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_chunked_content_provider</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">",
|
||||
</span><span style="color:#d3d0c8;"> [](size_t offset, httplib::DataSink &sink) {
|
||||
</span><span style="color:#d3d0c8;"> sink.</span><span style="color:#6699cc;">write</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">chunk</span><span style="color:#66cccc;">\n</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">6</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">; </span><span style="color:#747369;">// falseを返すと終了
|
||||
</span><span style="color:#d3d0c8;"> });
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// サイズ指定あり(Content-Length が設定される)
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/file"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> size </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">get_file_size(</span><span style="color:#183691;">"large.bin"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> res.set_content_provider(size, </span><span style="color:#183691;">"application/octet-stream"</span><span style="color:#323232;">,
|
||||
</span><span style="color:#323232;"> [](</span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> offset, </span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> length, httplib::DataSink </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">sink) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// offset から length バイト分を送る
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">true</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-style:italic;color:#969896;">// サイズ不明(Chunked Transfer Encoding)
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/stream"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_chunked_content_provider(</span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">,
|
||||
</span><span style="color:#323232;"> [](</span><span style="color:#0086b3;">size_t</span><span style="color:#323232;"> offset, httplib::DataSink </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">sink) {
|
||||
</span><span style="color:#323232;"> sink.write(</span><span style="color:#183691;">"chunk</span><span style="color:#0086b3;">\n</span><span style="color:#183691;">"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">6</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return </span><span style="color:#0086b3;">true</span><span style="color:#323232;">; </span><span style="font-style:italic;color:#969896;">// falseを返すと終了
|
||||
</span><span style="color:#323232;"> });
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>大きなファイルのアップロードには <code>make_file_provider()</code> が便利です。ファイルを全部メモリに読み込まず、ストリーミングで送れます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Post</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/upload</span><span style="color:#d3d0c8;">", {}, {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#6699cc;">httplib::make_file_provider</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">file</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">/path/to/large-file.zip</span><span style="color:#d3d0c8;">")
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Post(</span><span style="color:#183691;">"/upload"</span><span style="color:#323232;">, {}, {
|
||||
</span><span style="color:#323232;"> httplib::make_file_provider(</span><span style="color:#183691;">"file"</span><span style="color:#323232;">, </span><span style="color:#183691;">"/path/to/large-file.zip"</span><span style="color:#323232;">)
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Server-Sent Events (SSE)</h2>
|
||||
<p>SSEクライアントも用意しています。自動再接続や <code>Last-Event-ID</code> による再開にも対応しています。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost:8080</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">httplib::sse::SSEClient </span><span style="color:#6699cc;">sse</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">cli</span><span style="color:#d3d0c8;">, "</span><span style="color:#99cc99;">/events</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">sse.</span><span style="color:#6699cc;">on_message</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::sse::SSEMessage &msg) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << msg.</span><span style="color:#f2777a;">event </span><span style="color:#d3d0c8;"><< "</span><span style="color:#99cc99;">: </span><span style="color:#d3d0c8;">" << msg.</span><span style="color:#f2777a;">data </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">sse.</span><span style="color:#6699cc;">start</span><span style="color:#d3d0c8;">(); </span><span style="color:#747369;">// ブロッキング、自動再接続あり
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost:8080"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">httplib::sse::SSEClient </span><span style="font-weight:bold;color:#795da3;">sse</span><span style="color:#323232;">(cli, </span><span style="color:#183691;">"/events"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">sse.on_message([](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::sse::SSEMessage </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">msg) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> msg.event </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">": " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> msg.data </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">sse.start(); </span><span style="font-style:italic;color:#969896;">// ブロッキング、自動再接続あり
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>イベントタイプごとにハンドラーを分けることもできますよ。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">sse.</span><span style="color:#6699cc;">on_event</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">update</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> httplib::sse::SSEMessage &msg) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// "update" イベントだけ処理
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">sse.on_event(</span><span style="color:#183691;">"update"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const</span><span style="color:#323232;"> httplib::sse::SSEMessage </span><span style="font-weight:bold;color:#a71d5d;">&</span><span style="color:#323232;">msg) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// "update" イベントだけ処理
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>認証</h2>
|
||||
<p>クライアントにはBasic認証、Bearer Token認証、Digest認証のヘルパーを用意しています。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://api.example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_basic_auth</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">user</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">password</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_bearer_token_auth</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">my-token</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://api.example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_basic_auth(</span><span style="color:#183691;">"user"</span><span style="color:#323232;">, </span><span style="color:#183691;">"password"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_bearer_token_auth(</span><span style="color:#183691;">"my-token"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>圧縮</h2>
|
||||
<p>gzip、Brotli、Zstandardによる圧縮・展開に対応しています。使いたい方式のマクロを定義してコンパイルしましょう。</p>
|
||||
<table><thead><tr><th>圧縮方式</th><th>マクロ定義</th></tr></thead><tbody>
|
||||
<tr><td>gzip</td><td><code>CPPHTTPLIB_ZLIB_SUPPORT</code></td></tr>
|
||||
<tr><td>Brotli</td><td><code>CPPHTTPLIB_BROTLI_SUPPORT</code></td></tr>
|
||||
<tr><td>Zstandard</td><td><code>CPPHTTPLIB_ZSTD_SUPPORT</code></td></tr>
|
||||
</tbody></table>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_compress</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// リクエストボディを圧縮
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_decompress</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// レスポンスボディを展開
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_compress(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// リクエストボディを圧縮
|
||||
</span><span style="color:#323232;">cli.set_decompress(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// レスポンスボディを展開
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>プロキシ</h2>
|
||||
<p>HTTPプロキシ経由で接続できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_proxy</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">proxy.example.com</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">8080</span><span style="color:#d3d0c8;">);
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_proxy_basic_auth</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">user</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">password</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_proxy(</span><span style="color:#183691;">"proxy.example.com"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">8080</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_proxy_basic_auth(</span><span style="color:#183691;">"user"</span><span style="color:#323232;">, </span><span style="color:#183691;">"password"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>タイムアウト</h2>
|
||||
<p>接続・読み取り・書き込みのタイムアウトを個別に設定できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_connection_timeout</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">5</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// 5秒
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_read_timeout</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">10</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// 10秒
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_write_timeout</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">10</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">); </span><span style="color:#747369;">// 10秒
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_connection_timeout(</span><span style="color:#0086b3;">5</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 5秒
|
||||
</span><span style="color:#323232;">cli.set_read_timeout(</span><span style="color:#0086b3;">10</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 10秒
|
||||
</span><span style="color:#323232;">cli.set_write_timeout(</span><span style="color:#0086b3;">10</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">); </span><span style="font-style:italic;color:#969896;">// 10秒
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Keep-Alive</h2>
|
||||
<p>同じサーバーに何度もリクエストするなら、Keep-Aliveを有効にしましょう。TCP接続を再利用するので効率的です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">https://example.com</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_keep_alive</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"https://example.com"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_keep_alive(</span><span style="color:#0086b3;">true</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>サーバーのミドルウェア</h2>
|
||||
<p>リクエスト処理の前後にフックを挟めます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_pre_routing_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// すべてのリクエストの前に実行される
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> httplib::Server::HandlerResponse::Unhandled; </span><span style="color:#747369;">// 通常のルーティングに進む
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_post_routing_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#747369;">// レスポンスが返された後に実行される
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_header</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">X-Server</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">cpp-httplib</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_pre_routing_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// すべてのリクエストの前に実行される
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return</span><span style="color:#323232;"> httplib::Server::HandlerResponse::Unhandled; </span><span style="font-style:italic;color:#969896;">// 通常のルーティングに進む
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.set_post_routing_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-style:italic;color:#969896;">// レスポンスが返された後に実行される
|
||||
</span><span style="color:#323232;"> res.set_header(</span><span style="color:#183691;">"X-Server"</span><span style="color:#323232;">, </span><span style="color:#183691;">"cpp-httplib"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p><code>req.user_data</code> を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_pre_routing_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> req.</span><span style="color:#f2777a;">user_data</span><span style="color:#d3d0c8;">["</span><span style="color:#99cc99;">auth_user</span><span style="color:#d3d0c8;">"] = </span><span style="color:#6699cc;">std::string</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">alice</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> httplib::Server::HandlerResponse::Unhandled;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/me</span><span style="color:#d3d0c8;">", [](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> user = std::</span><span style="color:#6699cc;">any_cast</span><span style="color:#d3d0c8;"><std::string>(req.</span><span style="color:#f2777a;">user_data</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">at</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">auth_user</span><span style="color:#d3d0c8;">"));
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Hello, </span><span style="color:#d3d0c8;">" + user, "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_pre_routing_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> req.user_data[</span><span style="color:#183691;">"auth_user"</span><span style="color:#323232;">] </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">std::string(</span><span style="color:#183691;">"alice"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">return</span><span style="color:#323232;"> httplib::Server::HandlerResponse::Unhandled;
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.Get(</span><span style="color:#183691;">"/me"</span><span style="color:#323232;">, [](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> </span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> user </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#323232;">std::any_cast<std::string>(req.user_data.at(</span><span style="color:#183691;">"auth_user"</span><span style="color:#323232;">));
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Hello, " </span><span style="font-weight:bold;color:#a71d5d;">+</span><span style="color:#323232;"> user, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<p>エラーや例外のハンドラーもカスタマイズできますよ。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_error_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Custom Error Page</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/html</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_exception_handler</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">auto </span><span style="color:#d3d0c8;">&res, std::exception_ptr ep) {
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;">= </span><span style="color:#f99157;">500</span><span style="color:#d3d0c8;">;
|
||||
</span><span style="color:#d3d0c8;"> res.</span><span style="color:#6699cc;">set_content</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">Internal Server Error</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">text/plain</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_error_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Custom Error Page"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/html"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="color:#323232;">svr.set_exception_handler([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">auto &</span><span style="color:#323232;">res, std::exception_ptr ep) {
|
||||
</span><span style="color:#323232;"> res.status </span><span style="font-weight:bold;color:#a71d5d;">= </span><span style="color:#0086b3;">500</span><span style="color:#323232;">;
|
||||
</span><span style="color:#323232;"> res.set_content(</span><span style="color:#183691;">"Internal Server Error"</span><span style="color:#323232;">, </span><span style="color:#183691;">"text/plain"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>ロギング</h2>
|
||||
<p>サーバーでもクライアントでもロガーを設定できます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_logger</span><span style="color:#d3d0c8;">([](</span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&req, </span><span style="color:#cc99cc;">const auto </span><span style="color:#d3d0c8;">&res) {
|
||||
</span><span style="color:#d3d0c8;"> std::cout << req.</span><span style="color:#f2777a;">method </span><span style="color:#d3d0c8;"><< " " << req.</span><span style="color:#f2777a;">path </span><span style="color:#d3d0c8;"><< " " << res.</span><span style="color:#f2777a;">status </span><span style="color:#d3d0c8;"><< std::endl;
|
||||
</span><span style="color:#d3d0c8;">});
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="color:#323232;">svr.set_logger([](</span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">req, </span><span style="font-weight:bold;color:#a71d5d;">const auto &</span><span style="color:#323232;">res) {
|
||||
</span><span style="color:#323232;"> std::cout </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> req.method </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">" " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> req.path </span><span style="font-weight:bold;color:#a71d5d;"><< </span><span style="color:#183691;">" " </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> res.status </span><span style="font-weight:bold;color:#a71d5d;"><<</span><span style="color:#323232;"> std::endl;
|
||||
</span><span style="color:#323232;">});
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>Unix Domain Socket</h2>
|
||||
<p>TCP以外に、Unix Domain Socketでの通信にも対応しています。同じマシン上のプロセス間通信に使えます。</p>
|
||||
<div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// サーバー
|
||||
</span><span style="color:#d3d0c8;">httplib::Server svr;
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">set_address_family</span><span style="color:#d3d0c8;">(AF_UNIX);
|
||||
</span><span style="color:#d3d0c8;">svr.</span><span style="color:#6699cc;">listen</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/tmp/httplib.sock</span><span style="color:#d3d0c8;">", </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">);
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// サーバー
|
||||
</span><span style="color:#323232;">httplib::Server svr;
|
||||
</span><span style="color:#323232;">svr.set_address_family(AF_UNIX);
|
||||
</span><span style="color:#323232;">svr.listen(</span><span style="color:#183691;">"/tmp/httplib.sock"</span><span style="color:#323232;">, </span><span style="color:#0086b3;">0</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div><div class="code-block-wrapper"><div data-code-theme="dark"><pre style="background-color:#2d2d2d;">
|
||||
<span style="color:#747369;">// クライアント
|
||||
</span><span style="color:#d3d0c8;">httplib::Client </span><span style="color:#6699cc;">cli</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">http://localhost</span><span style="color:#d3d0c8;">");
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_address_family</span><span style="color:#d3d0c8;">(AF_UNIX);
|
||||
</span><span style="color:#d3d0c8;">cli.</span><span style="color:#6699cc;">set_hostname_addr_map</span><span style="color:#d3d0c8;">({{"</span><span style="color:#99cc99;">localhost</span><span style="color:#d3d0c8;">", "</span><span style="color:#99cc99;">/tmp/httplib.sock</span><span style="color:#d3d0c8;">"}});
|
||||
</span><span style="color:#d3d0c8;">
|
||||
</span><span style="color:#cc99cc;">auto</span><span style="color:#d3d0c8;"> res = cli.</span><span style="color:#6699cc;">Get</span><span style="color:#d3d0c8;">("</span><span style="color:#99cc99;">/</span><span style="color:#d3d0c8;">");
|
||||
</span></pre>
|
||||
</div><div data-code-theme="light"><pre style="background-color:#ffffff;">
|
||||
<span style="font-style:italic;color:#969896;">// クライアント
|
||||
</span><span style="color:#323232;">httplib::Client </span><span style="font-weight:bold;color:#795da3;">cli</span><span style="color:#323232;">(</span><span style="color:#183691;">"http://localhost"</span><span style="color:#323232;">);
|
||||
</span><span style="color:#323232;">cli.set_address_family(AF_UNIX);
|
||||
</span><span style="color:#323232;">cli.set_hostname_addr_map({{</span><span style="color:#183691;">"localhost"</span><span style="color:#323232;">, </span><span style="color:#183691;">"/tmp/httplib.sock"</span><span style="color:#323232;">}});
|
||||
</span><span style="color:#323232;">
|
||||
</span><span style="font-weight:bold;color:#a71d5d;">auto</span><span style="color:#323232;"> res </span><span style="font-weight:bold;color:#a71d5d;">=</span><span style="color:#323232;"> cli.Get(</span><span style="color:#183691;">"/"</span><span style="color:#323232;">);
|
||||
</span></pre>
|
||||
</div></div>
|
||||
<h2>さらに詳しく</h2>
|
||||
<p>もっと詳しく知りたいときは、以下を参照してください。</p>
|
||||
<ul>
|
||||
<li>Cookbook — よくあるユースケースのレシピ集</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README.md">README</a> — 全APIのリファレンス</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README-sse.md">README-sse</a> — Server-Sent Eventsの使い方</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README-stream.md">README-stream</a> — Streaming APIの使い方</li>
|
||||
<li><a href="https://github.com/yhirose/cpp-httplib/blob/master/README-websocket.md">README-websocket</a> — WebSocketサーバーの使い方</li>
|
||||
</ul>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,141 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja" data-base-path="/cpp-httplib">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>A Tour of cpp-httplib - cpp-httplib</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/cpp-httplib/favicon.svg">
|
||||
<link rel="stylesheet" href="/cpp-httplib/css/main.css">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('preferred-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<a href="/cpp-httplib/ja/" class="header-title">cpp-httplib <span style="font-size:0.75em;font-weight:normal;margin-left:4px">v0.35.0</span></a>
|
||||
<div class="header-spacer"></div>
|
||||
<nav class="header-nav">
|
||||
<a href="/cpp-httplib/ja/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/cpp-httplib/ja/tour/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
||||
Tour
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/yhirose/cpp-httplib" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="header-tools">
|
||||
<button class="search-btn" aria-label="Search (⌘K)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<button class="theme-toggle" aria-label="Toggle theme"></button>
|
||||
<div class="lang-selector">
|
||||
<button class="lang-btn" aria-label="Language">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
JA
|
||||
</button>
|
||||
<ul class="lang-popup">
|
||||
|
||||
<li><a href="#" data-lang="en">EN</a></li>
|
||||
|
||||
<li><a href="#" data-lang="ja">JA</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" aria-label="Menu">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="layout has-sidebar">
|
||||
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="nav-section">
|
||||
<a href="/cpp-httplib/ja/tour/" class="nav-section-title active">A Tour of cpp-httplib</a>
|
||||
|
||||
<ul class="nav-list">
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/01-getting-started/" class="">Getting Started</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/02-basic-client/" class="">Basic Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/03-basic-server/" class="">Basic Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/04-static-file-server/" class="">Static File Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/05-tls-setup/" class="">TLS Setup</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/06-https-client/" class="">HTTPS Client</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/07-https-server/" class="">HTTPS Server</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/08-websocket/" class="">WebSocket</a></li>
|
||||
|
||||
<li><a href="/cpp-httplib/ja/tour/09-whats-next/" class="">What's Next</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<article>
|
||||
<h1>A Tour of cpp-httplib</h1>
|
||||
<p>cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。</p>
|
||||
<ol>
|
||||
<li><a href="01-getting-started">Getting Started</a> — httplib.h の入手とHello Worldサーバー</li>
|
||||
<li><a href="02-basic-client">Basic Client</a> — GET/POST・パスパラメーターのリクエスト送信</li>
|
||||
<li><a href="03-basic-server">Basic Server</a> — ルーティング、パスパラメーター、レスポンスの組み立て</li>
|
||||
<li><a href="04-static-file-server">Static File Server</a> — 静的ファイルの配信</li>
|
||||
<li><a href="05-tls-setup">TLS Setup</a> — OpenSSL / mbedTLS のセットアップ</li>
|
||||
<li><a href="06-https-client">HTTPS Client</a> — HTTPSサイトへのリクエスト</li>
|
||||
<li><a href="07-https-server">HTTPS Server</a> — HTTPSサーバーの構築</li>
|
||||
<li><a href="08-websocket">WebSocket</a> — WebSocket通信の基本</li>
|
||||
<li><a href="09-whats-next">What's Next</a> — さらなる機能の紹介</li>
|
||||
</ol>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
© 2026 yhirose. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- Search modal -->
|
||||
<div class="search-overlay" id="search-overlay">
|
||||
<div class="search-modal">
|
||||
<div class="search-input-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" spellcheck="false">
|
||||
<kbd class="search-esc">ESC</kbd>
|
||||
</div>
|
||||
<ul class="search-results" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/cpp-httplib/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,297 +0,0 @@
|
||||
// Language selector
|
||||
(function () {
|
||||
var btn = document.querySelector('.lang-btn');
|
||||
var popup = document.querySelector('.lang-popup');
|
||||
if (!btn || !popup) return;
|
||||
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
popup.classList.toggle('open');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function () {
|
||||
popup.classList.remove('open');
|
||||
});
|
||||
|
||||
popup.addEventListener('click', function (e) {
|
||||
var link = e.target.closest('[data-lang]');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
var lang = link.getAttribute('data-lang');
|
||||
localStorage.setItem('preferred-lang', lang);
|
||||
var basePath = document.documentElement.getAttribute('data-base-path') || '';
|
||||
var path = window.location.pathname;
|
||||
// Strip base path prefix, replace lang, then re-add base path
|
||||
var pathWithoutBase = path.slice(basePath.length);
|
||||
var newPath = basePath + pathWithoutBase.replace(/^\/[a-z]{2}\//, '/' + lang + '/');
|
||||
window.location.href = newPath;
|
||||
});
|
||||
})();
|
||||
|
||||
// Theme toggle
|
||||
(function () {
|
||||
var btn = document.querySelector('.theme-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
// Feather Icons: sun (light mode) and moon (dark mode)
|
||||
var sunSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
||||
var moonSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
||||
|
||||
function getTheme() {
|
||||
var stored = localStorage.getItem('preferred-theme');
|
||||
if (stored) return stored;
|
||||
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
btn.innerHTML = theme === 'light' ? sunSVG : moonSVG;
|
||||
}
|
||||
|
||||
applyTheme(getTheme());
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
var current = getTheme();
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem('preferred-theme', next);
|
||||
applyTheme(next);
|
||||
});
|
||||
})();
|
||||
|
||||
// Mobile sidebar toggle
|
||||
(function () {
|
||||
var toggle = document.querySelector('.sidebar-toggle');
|
||||
var sidebar = document.querySelector('.sidebar');
|
||||
if (!toggle || !sidebar) return;
|
||||
|
||||
toggle.addEventListener('click', function () {
|
||||
sidebar.classList.toggle('open');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!sidebar.contains(e.target) && e.target !== toggle) {
|
||||
sidebar.classList.remove('open');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Site search (⌘K / Ctrl+K)
|
||||
(function () {
|
||||
var overlay = document.getElementById('search-overlay');
|
||||
var input = document.getElementById('search-input');
|
||||
var resultsList = document.getElementById('search-results');
|
||||
if (!overlay || !input || !resultsList) return;
|
||||
|
||||
var searchBtn = document.querySelector('.search-btn');
|
||||
var pagesData = null; // cached pages-data.json
|
||||
var activeIndex = -1;
|
||||
|
||||
function getCurrentLang() {
|
||||
return document.documentElement.getAttribute('lang') || 'en';
|
||||
}
|
||||
|
||||
function getBasePath() {
|
||||
return document.documentElement.getAttribute('data-base-path') || '';
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
overlay.classList.add('open');
|
||||
input.value = '';
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
input.focus();
|
||||
loadPagesData();
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
overlay.classList.remove('open');
|
||||
input.value = '';
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
}
|
||||
|
||||
function loadPagesData() {
|
||||
if (pagesData) return;
|
||||
var basePath = getBasePath();
|
||||
fetch(basePath + '/pages-data.json')
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) { pagesData = data; })
|
||||
.catch(function () { pagesData = []; });
|
||||
}
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function highlightText(text, query) {
|
||||
if (!query) return text;
|
||||
var escaped = escapeRegExp(query);
|
||||
var re = new RegExp('(' + escaped + ')', 'gi');
|
||||
return text.replace(re, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
function buildSnippet(body, query) {
|
||||
if (!query || !body) return '';
|
||||
var lower = body.toLowerCase();
|
||||
var idx = lower.indexOf(query.toLowerCase());
|
||||
var start, end, snippet;
|
||||
if (idx === -1) {
|
||||
snippet = body.substring(0, 120);
|
||||
} else {
|
||||
start = Math.max(0, idx - 40);
|
||||
end = Math.min(body.length, idx + query.length + 80);
|
||||
snippet = (start > 0 ? '...' : '') + body.substring(start, end) + (end < body.length ? '...' : '');
|
||||
}
|
||||
return highlightText(snippet, query);
|
||||
}
|
||||
|
||||
function search(query) {
|
||||
if (!pagesData || !query) {
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
var lang = getCurrentLang();
|
||||
var q = query.toLowerCase();
|
||||
|
||||
// Score and filter
|
||||
var scored = [];
|
||||
for (var i = 0; i < pagesData.length; i++) {
|
||||
var page = pagesData[i];
|
||||
if (page.lang !== lang) continue;
|
||||
|
||||
var score = 0;
|
||||
var titleLower = page.title.toLowerCase();
|
||||
var bodyLower = (page.body || '').toLowerCase();
|
||||
|
||||
if (titleLower.indexOf(q) !== -1) {
|
||||
score += 10;
|
||||
// Bonus for exact title match
|
||||
if (titleLower === q) score += 5;
|
||||
}
|
||||
if (bodyLower.indexOf(q) !== -1) {
|
||||
score += 3;
|
||||
}
|
||||
if (page.section.toLowerCase().indexOf(q) !== -1) {
|
||||
score += 1;
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
scored.push({ page: page, score: score });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
scored.sort(function (a, b) { return b.score - a.score; });
|
||||
|
||||
// Limit results
|
||||
var results = scored.slice(0, 20);
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsList.innerHTML = '<li class="search-no-results">No results found.</li>';
|
||||
activeIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
var r = results[j];
|
||||
var snippet = buildSnippet(r.page.body, query);
|
||||
html += '<li data-url="' + r.page.url + '">'
|
||||
+ '<div class="search-result-title">' + highlightText(r.page.title, query) + '</div>'
|
||||
+ (snippet ? '<div class="search-result-snippet">' + snippet + '</div>' : '')
|
||||
+ '</li>';
|
||||
}
|
||||
resultsList.innerHTML = html;
|
||||
activeIndex = -1;
|
||||
}
|
||||
|
||||
function setActive(index) {
|
||||
var items = resultsList.querySelectorAll('li[data-url]');
|
||||
if (items.length === 0) return;
|
||||
// Remove previous active
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
items[i].classList.remove('active');
|
||||
}
|
||||
if (index < 0) index = items.length - 1;
|
||||
if (index >= items.length) index = 0;
|
||||
activeIndex = index;
|
||||
items[activeIndex].classList.add('active');
|
||||
items[activeIndex].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function navigateToActive() {
|
||||
var items = resultsList.querySelectorAll('li[data-url]');
|
||||
if (activeIndex >= 0 && activeIndex < items.length) {
|
||||
var url = items[activeIndex].getAttribute('data-url');
|
||||
if (url) {
|
||||
closeSearch();
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event: search button
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
openSearch();
|
||||
});
|
||||
}
|
||||
|
||||
// Use capture phase to intercept keys before browser default behavior
|
||||
// (e.g. ESC clearing input text in some browsers)
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
overlay.classList.contains('open') ? closeSearch() : openSearch();
|
||||
return;
|
||||
}
|
||||
if (!overlay.classList.contains('open')) return;
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSearch();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
navigateToActive();
|
||||
}
|
||||
}, true); // capture phase
|
||||
|
||||
// Event: click overlay background to close
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) {
|
||||
closeSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: click result item
|
||||
resultsList.addEventListener('click', function (e) {
|
||||
var li = e.target.closest('li[data-url]');
|
||||
if (!li) return;
|
||||
var url = li.getAttribute('data-url');
|
||||
if (url) {
|
||||
closeSearch();
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
// Event: input for live search
|
||||
var debounceTimer = null;
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(function () {
|
||||
search(input.value.trim());
|
||||
}, 150);
|
||||
});
|
||||
})();
|
||||