Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active March 10, 2026 03:30
Show Gist options
  • Select an option

  • Save masakielastic/0780aa80692336cb0022b303cb7d3f93 to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/0780aa80692336cb0022b303cb7d3f93 to your computer and use it in GitHub Desktop.
nghttp2 でブロッキングな HTTP クライアント (イベントループなし)

nghttp2 でブロッキングな HTTP クライアント (イベントループなし)

gcc -Wall -Wextra -O2 client.c -o client $(pkg-config --cflags --libs libnghttp2 openssl)
./client httpbin.org 443 /get
H: :status: 200
H: date: Mon, 23 Feb 2026 18:39:06 GMT
H: content-type: application/json
H: content-length: 273
H: server: gunicorn/19.9.0
H: access-control-allow-origin: *
H: access-control-allow-credentials: true
Received response headers (stream=1)
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "httpbin.org", 
    "User-Agent": "nghttp2-blocking-client/0.1", 
    "X-Amzn-Trace-Id": "Root=1-699c9eca-3eeb8b001fc71ec719ae3e3f"
  }, 
  "origin": "115.162.203.179", 
  "url": "https://httpbin.org/get"
}

Stream closed (id=1, error=0)
/*
* Minimal HTTP/2 client with nghttp2 + OpenSSL (blocking I/O, no event loop lib).
*
* Build:
* cc -O2 -Wall -Wextra -pedantic h2_client.c -o h2_client \
* $(pkg-config --cflags --libs libnghttp2 openssl)
*
* Run:
* ./h2_client example.com 443 /
*
* Notes:
* - Requires server to support HTTP/2 over TLS (ALPN "h2").
* - Prints response body to stdout; headers/status are printed to stderr.
*/
#include <nghttp2/nghttp2.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAKE_NV(NAME, VALUE) \
(nghttp2_nv){ \
(uint8_t *)(NAME), (uint8_t *)(VALUE), \
(uint16_t)strlen(NAME), (uint16_t)strlen(VALUE), \
NGHTTP2_NV_FLAG_NONE \
}
typedef struct {
SSL *ssl;
int32_t stream_id;
int stream_closed;
} client_ctx;
/* ---- Utility: TCP connect ---- */
static int tcp_connect(const char *host, const char *port) {
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *res = NULL;
int gai = getaddrinfo(host, port, &hints, &res);
if (gai != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai));
return -1;
}
int fd = -1;
for (struct addrinfo *rp = res; rp; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0) continue;
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0) fprintf(stderr, "tcp_connect: failed\n");
return fd;
}
/* ---- TLS setup (ALPN h2) ---- */
static SSL_CTX *sslctx_create(void) {
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) return NULL;
/* Verify server cert */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if (SSL_CTX_set_default_verify_paths(ctx) != 1) {
SSL_CTX_free(ctx);
return NULL;
}
/* ALPN: offer h2 */
static const unsigned char alpn_protos[] = { 2, 'h', '2' };
if (SSL_CTX_set_alpn_protos(ctx, alpn_protos, sizeof(alpn_protos)) != 0) {
SSL_CTX_free(ctx);
return NULL;
}
return ctx;
}
static int ssl_handshake(SSL_CTX *ctx, int fd, const char *host, SSL **out_ssl) {
SSL *ssl = SSL_new(ctx);
if (!ssl) return -1;
/* SNI */
SSL_set_tlsext_host_name(ssl, host);
SSL_set_fd(ssl, fd);
if (SSL_connect(ssl) != 1) {
fprintf(stderr, "SSL_connect failed\n");
SSL_free(ssl);
return -1;
}
/* Check ALPN result is h2 */
const unsigned char *alpn = NULL;
unsigned int alpn_len = 0;
SSL_get0_alpn_selected(ssl, &alpn, &alpn_len);
if (!(alpn_len == 2 && memcmp(alpn, "h2", 2) == 0)) {
fprintf(stderr, "Server did not negotiate h2 via ALPN\n");
SSL_free(ssl);
return -1;
}
/* Basic hostname verification (OpenSSL 1.1.0+) */
X509 *cert = SSL_get_peer_certificate(ssl);
if (!cert) {
fprintf(stderr, "No server certificate\n");
SSL_free(ssl);
return -1;
}
long vr = SSL_get_verify_result(ssl);
if (vr != X509_V_OK) {
fprintf(stderr, "Certificate verify failed: %s\n", X509_verify_cert_error_string(vr));
X509_free(cert);
SSL_free(ssl);
return -1;
}
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
if (X509_check_host(cert, host, 0, 0, NULL) != 1) {
fprintf(stderr, "Hostname verification failed\n");
X509_free(cert);
SSL_free(ssl);
return -1;
}
#endif
X509_free(cert);
*out_ssl = ssl;
return 0;
}
/* ---- nghttp2 callbacks ---- */
static ssize_t send_cb(nghttp2_session *session,
const uint8_t *data, size_t length,
int flags, void *user_data) {
(void)session; (void)flags;
client_ctx *c = (client_ctx *)user_data;
size_t off = 0;
while (off < length) {
int n = SSL_write(c->ssl, data + off, (int)(length - off));
if (n <= 0) {
int err = SSL_get_error(c->ssl, n);
fprintf(stderr, "SSL_write error: %d\n", err);
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
off += (size_t)n;
}
return (ssize_t)length;
}
static int on_header_cb(nghttp2_session *session,
const nghttp2_frame *frame,
const uint8_t *name, size_t namelen,
const uint8_t *value, size_t valuelen,
uint8_t flags, void *user_data) {
(void)session; (void)flags;
client_ctx *c = (client_ctx *)user_data;
if (frame->hd.type == NGHTTP2_HEADERS &&
frame->headers.cat == NGHTTP2_HCAT_RESPONSE &&
frame->hd.stream_id == c->stream_id) {
fprintf(stderr, "H: %.*s: %.*s\n",
(int)namelen, (const char *)name,
(int)valuelen, (const char *)value);
}
return 0;
}
static int on_frame_recv_cb(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data) {
(void)session;
client_ctx *c = (client_ctx *)user_data;
if (frame->hd.stream_id == c->stream_id) {
if (frame->hd.type == NGHTTP2_HEADERS &&
frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
fprintf(stderr, "Received response headers (stream=%d)\n", c->stream_id);
}
if (frame->hd.type == NGHTTP2_DATA) {
/* data chunks handled in on_data_chunk_recv_cb */
}
}
return 0;
}
static int on_data_chunk_recv_cb(nghttp2_session *session,
uint8_t flags, int32_t stream_id,
const uint8_t *data, size_t len,
void *user_data) {
(void)session; (void)flags;
client_ctx *c = (client_ctx *)user_data;
if (stream_id == c->stream_id) {
/* Body to stdout */
fwrite(data, 1, len, stdout);
fflush(stdout);
}
return 0;
}
static int on_stream_close_cb(nghttp2_session *session,
int32_t stream_id, uint32_t error_code,
void *user_data) {
(void)session;
client_ctx *c = (client_ctx *)user_data;
if (stream_id == c->stream_id) {
fprintf(stderr, "\nStream closed (id=%d, error=%u)\n", stream_id, error_code);
c->stream_closed = 1;
}
return 0;
}
static void die(const char *msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}
int main(int argc, char **argv) {
if (argc != 4) {
fprintf(stderr, "Usage: %s <host> <port> <path>\n", argv[0]);
return 2;
}
const char *host = argv[1];
const char *port = argv[2];
const char *path = argv[3];
/* OpenSSL init */
SSL_library_init();
SSL_load_error_strings();
int fd = tcp_connect(host, port);
if (fd < 0) return 1;
SSL_CTX *ssl_ctx = sslctx_create();
if (!ssl_ctx) die("SSL_CTX create failed");
SSL *ssl = NULL;
if (ssl_handshake(ssl_ctx, fd, host, &ssl) != 0) {
SSL_CTX_free(ssl_ctx);
close(fd);
return 1;
}
/* nghttp2 session init */
nghttp2_session_callbacks *cbs = NULL;
if (nghttp2_session_callbacks_new(&cbs) != 0) die("callbacks_new failed");
nghttp2_session_callbacks_set_send_callback(cbs, send_cb);
nghttp2_session_callbacks_set_on_header_callback(cbs, on_header_cb);
nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv_cb);
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(cbs, on_data_chunk_recv_cb);
nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close_cb);
client_ctx ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.ssl = ssl;
ctx.stream_id = -1;
ctx.stream_closed = 0;
nghttp2_session *session = NULL;
if (nghttp2_session_client_new(&session, cbs, &ctx) != 0) die("client_new failed");
nghttp2_session_callbacks_del(cbs);
/* Send initial SETTINGS */
nghttp2_settings_entry iv[1];
iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS;
iv[0].value = 100;
if (nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv, 1) != 0)
die("submit_settings failed");
/* Build request headers */
char authority[512];
snprintf(authority, sizeof(authority), "%s:%s", host, port);
nghttp2_nv hdrs[] = {
MAKE_NV(":method", "GET"),
MAKE_NV(":scheme", "https"),
MAKE_NV(":authority", authority),
MAKE_NV(":path", path),
MAKE_NV("user-agent", "nghttp2-blocking-client/0.1"),
MAKE_NV("accept", "*/*"),
};
ctx.stream_id = nghttp2_submit_request(session, NULL, hdrs,
(size_t)(sizeof(hdrs) / sizeof(hdrs[0])),
NULL, NULL);
if (ctx.stream_id < 0) die("submit_request failed");
/* Main blocking loop:
* - nghttp2_session_send() writes pending frames via send_cb (SSL_write)
* - SSL_read() blocks until bytes arrive
* - feed bytes into nghttp2_session_mem_recv()
*/
uint8_t rbuf[16 * 1024];
while (!ctx.stream_closed) {
int rv = nghttp2_session_send(session);
if (rv != 0) {
fprintf(stderr, "session_send: %s\n", nghttp2_strerror(rv));
break;
}
int n = SSL_read(ssl, rbuf, (int)sizeof(rbuf));
if (n <= 0) {
int err = SSL_get_error(ssl, n);
if (err == SSL_ERROR_ZERO_RETURN) {
fprintf(stderr, "TLS connection closed\n");
} else {
fprintf(stderr, "SSL_read error: %d\n", err);
}
break;
}
ssize_t fed = nghttp2_session_mem_recv(session, rbuf, (size_t)n);
if (fed < 0) {
fprintf(stderr, "mem_recv: %s\n", nghttp2_strerror((int)fed));
break;
}
}
/* Flush any remaining outbound (e.g., GOAWAY/ACK) */
(void)nghttp2_session_send(session);
nghttp2_session_del(session);
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ssl_ctx);
close(fd);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment