Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active February 23, 2026 19:07
Show Gist options
  • Select an option

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

Select an option

Save masakielastic/c9f7ffd11c5b43a85404f4ebce3b55d5 to your computer and use it in GitHub Desktop.
nghttp2 でブロッキングな h2c サーバー (TLS なしの HTTP/2 サーバー)

nghttp2 でブロッキングな h2c サーバー (TLS なしの HTTP/2 サーバー)

gcc -Wall -Wextra -O2 server.c -o server $(pkg-config --cflags --libs libnghttp2)
masakielastic@penguin:~/http-server$ ./server
Listening on 127.0.0.1:8080 (h2c prior-knowledge)
Try: nghttp -v http://127.0.0.1:8080/ --no-tls --pri
nghttp -v http://127.0.0.1:8080/ --ktls
[  0.000] Connected
[  0.000] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.000] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.000] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.000] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.000] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.000] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.000] send HEADERS frame <length=38, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: http
          :authority: 127.0.0.1:8080
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.52.0
[  0.000] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[  0.000] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.000] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.041] recv (stream_id=13) :status: 200
[  0.041] recv (stream_id=13) content-type: text/plain; charset=utf-
[  0.041] recv (stream_id=13) content-length: 13
[  0.041] recv HEADERS frame <length=25, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
Hello HTTP/2
[  0.041] recv DATA frame <length=13, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.042] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

server.c

// blocking_h2_server.c
// Minimal blocking HTTP/2 server (cleartext, prior-knowledge) using libnghttp2.
// Handles a single connection at a time, responds "Hello HTTP/2\n" to any request.

#define _POSIX_C_SOURCE 200809L

#include <nghttp2/nghttp2.h>

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif

typedef struct {
  int fd;
} conn_t;

typedef struct {
  const uint8_t *data;
  size_t len;
  size_t off;
} stream_body_t;

static void die(const char *msg) {
  perror(msg);
  exit(1);
}

static ssize_t send_callback(nghttp2_session *session,
                             const uint8_t *data, size_t length,
                             int flags, void *user_data) {
  (void)session; (void)flags;
  conn_t *conn = (conn_t *)user_data;

  // Blocking send (may block). For a production server you should handle
  // partial writes and EAGAIN with nonblocking sockets.
  ssize_t n = send(conn->fd, data, length, MSG_NOSIGNAL);
  if (n < 0) {
    if (errno == EINTR) return NGHTTP2_ERR_WOULDBLOCK;
    return NGHTTP2_ERR_CALLBACK_FAILURE;
  }
  return n;
}

static ssize_t data_read_callback(nghttp2_session *session,
                                  int32_t stream_id,
                                  uint8_t *buf, size_t length,
                                  uint32_t *data_flags,
                                  nghttp2_data_source *source,
                                  void *user_data) {
  (void)session; (void)stream_id; (void)user_data;
  stream_body_t *body = (stream_body_t *)source->ptr;

  size_t remain = body->len - body->off;
  size_t ncopy = remain < length ? remain : length;
  if (ncopy > 0) {
    memcpy(buf, body->data + body->off, ncopy);
    body->off += ncopy;
  }
  if (body->off >= body->len) {
    *data_flags |= NGHTTP2_DATA_FLAG_EOF;
  }
  return (ssize_t)ncopy;
}

static int on_frame_recv_callback(nghttp2_session *session,
                                  const nghttp2_frame *frame,
                                  void *user_data) {
  (void)user_data;

  // When we get the request HEADERS with END_STREAM, respond immediately.
  if (frame->hd.type == NGHTTP2_HEADERS &&
      frame->headers.cat == NGHTTP2_HCAT_REQUEST &&
      (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) {

    static const char resp_payload[] = "Hello HTTP/2\n";
    stream_body_t *body = (stream_body_t *)calloc(1, sizeof(stream_body_t));
    if (!body) return NGHTTP2_ERR_CALLBACK_FAILURE;
    body->data = (const uint8_t *)resp_payload;
    body->len  = sizeof(resp_payload) - 1;
    body->off  = 0;

    char content_length[32];
    snprintf(content_length, sizeof(content_length), "%zu", body->len);

    nghttp2_nv hdrs[] = {
      {(uint8_t *)":status", (uint8_t *)"200", 7, 3, NGHTTP2_NV_FLAG_NONE},
      {(uint8_t *)"content-type", (uint8_t *)"text/plain; charset=utf-8",
       12, 24, NGHTTP2_NV_FLAG_NONE},
      {(uint8_t *)"content-length", (uint8_t *)content_length,
       14, (uint16_t)strlen(content_length), NGHTTP2_NV_FLAG_NONE},
    };

    nghttp2_data_provider dp;
    memset(&dp, 0, sizeof(dp));
    dp.source.ptr = body;
    dp.read_callback = data_read_callback;

    int rv = nghttp2_submit_response(session, frame->hd.stream_id,
                                    hdrs, sizeof(hdrs)/sizeof(hdrs[0]),
                                    &dp);
    if (rv != 0) {
      free(body);
      return NGHTTP2_ERR_CALLBACK_FAILURE;
    }
  }

  return 0;
}

static int on_stream_close_callback(nghttp2_session *session,
                                    int32_t stream_id,
                                    uint32_t error_code,
                                    void *user_data) {
  (void)session; (void)error_code; (void)user_data;

  // Free the body object we attached via data provider:
  // We stored it in nghttp2_data_source.ptr, but nghttp2 doesn't keep that
  // accessible here. For a real server, keep per-stream state in your own map.
  // This minimal example uses static body payload, so we skip cleanup complexity.
  (void)stream_id;
  return 0;
}

static nghttp2_session *setup_h2_session(conn_t *conn) {
  nghttp2_session_callbacks *cbs = NULL;
  nghttp2_session *session = NULL;

  if (nghttp2_session_callbacks_new(&cbs) != 0) return NULL;

  nghttp2_session_callbacks_set_send_callback(cbs, send_callback);
  nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv_callback);
  nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close_callback);

  // Create server session
  if (nghttp2_session_server_new(&session, cbs, conn) != 0) { // :contentReference[oaicite:1]{index=1}
    nghttp2_session_callbacks_del(cbs);
    return NULL;
  }

  nghttp2_session_callbacks_del(cbs);

  // Server must send SETTINGS first
  nghttp2_settings_entry iv[1] = {
    {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}
  };
  if (nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv, 1) != 0) {
    nghttp2_session_del(session);
    return NULL;
  }

  return session;
}

static int create_listen_socket(const char *ip, uint16_t port) {
  int fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd < 0) die("socket");

  int yes = 1;
  if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
    die("setsockopt");
  }

  struct sockaddr_in addr;
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);
  if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
    fprintf(stderr, "inet_pton failed for %s\n", ip);
    exit(1);
  }

  if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) die("bind");
  if (listen(fd, 16) < 0) die("listen");
  return fd;
}

static void serve_one_connection(int client_fd) {
  conn_t conn = {.fd = client_fd};

  nghttp2_session *session = setup_h2_session(&conn);
  if (!session) {
    fprintf(stderr, "failed to setup nghttp2 session\n");
    close(client_fd);
    return;
  }

  // Send initial SETTINGS
  if (nghttp2_session_send(session) != 0) {
    nghttp2_session_del(session);
    close(client_fd);
    return;
  }

  uint8_t buf[16 * 1024];

  for (;;) {
    ssize_t r = recv(client_fd, buf, sizeof(buf), 0);
    if (r == 0) {
      // peer closed
      break;
    }
    if (r < 0) {
      if (errno == EINTR) continue;
      perror("recv");
      break;
    }

    // Feed received bytes into nghttp2
    ssize_t nread = nghttp2_session_mem_recv(session, buf, (size_t)r);
    if (nread < 0) {
      fprintf(stderr, "nghttp2_session_mem_recv error: %zd\n", nread);
      break;
    }

    // Let nghttp2 send pending frames (responses, WINDOW_UPDATE, etc.)
    int rv = nghttp2_session_send(session);
    if (rv != 0) {
      fprintf(stderr, "nghttp2_session_send error: %d\n", rv);
      break;
    }

    if (nghttp2_session_want_read(session) == 0 &&
        nghttp2_session_want_write(session) == 0) {
      break;
    }
  }

  nghttp2_session_del(session);
  close(client_fd);
}

int main(void) {
  signal(SIGPIPE, SIG_IGN);

  const char *ip = "127.0.0.1";
  uint16_t port = 8080;

  int lfd = create_listen_socket(ip, port);
  fprintf(stderr, "Listening on %s:%u (h2c prior-knowledge)\n", ip, port);
  fprintf(stderr, "Try: nghttp -v http://%s:%u/ --no-tls --pri\n", ip, port);

  for (;;) {
    struct sockaddr_in caddr;
    socklen_t clen = sizeof(caddr);
    int cfd = accept(lfd, (struct sockaddr *)&caddr, &clen);
    if (cfd < 0) {
      if (errno == EINTR) continue;
      die("accept");
    }
    serve_one_connection(cfd); // sequential / blocking
  }

  close(lfd);
  return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment