1Tutorial: HTTP/2 client 2========================= 3 4In this tutorial, we are going to write a very primitive HTTP/2 5client. The complete source code, `libevent-client.c`_, is attached at 6the end of this page. It also resides in the examples directory in 7the archive or repository. 8 9This simple client takes a single HTTPS URI and retrieves the resource 10at the URI. The synopsis is: 11 12.. code-block:: text 13 14 $ libevent-client HTTPS_URI 15 16We use libevent in this tutorial to handle networking I/O. Please 17note that nghttp2 itself does not depend on libevent. 18 19The client starts with some libevent and OpenSSL setup in the 20``main()`` and ``run()`` functions. This setup isn't specific to 21nghttp2, but one thing you should look at is setup of ALPN. Client 22tells application protocols that it supports to server via ALPN:: 23 24 static SSL_CTX *create_ssl_ctx(void) { 25 SSL_CTX *ssl_ctx; 26 ssl_ctx = SSL_CTX_new(SSLv23_client_method()); 27 if (!ssl_ctx) { 28 errx(1, "Could not create SSL/TLS context: %s", 29 ERR_error_string(ERR_get_error(), NULL)); 30 } 31 SSL_CTX_set_options(ssl_ctx, 32 SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | 33 SSL_OP_NO_COMPRESSION | 34 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); 35 36 SSL_CTX_set_alpn_protos(ssl_ctx, (const unsigned char *)"\x02h2", 3); 37 38 return ssl_ctx; 39 } 40 41Here we see ``SSL_CTX_get_alpn_protos()`` function call. We instructs 42OpenSSL to notify the server that we support h2, ALPN identifier for 43HTTP/2. 44 45The example client defines a couple of structs: 46 47We define and use a ``http2_session_data`` structure to store data 48related to the HTTP/2 session:: 49 50 typedef struct { 51 nghttp2_session *session; 52 struct evdns_base *dnsbase; 53 struct bufferevent *bev; 54 http2_stream_data *stream_data; 55 } http2_session_data; 56 57Since this program only handles one URI, it uses only one stream. We 58store the single stream's data in a ``http2_stream_data`` structure 59and the ``stream_data`` points to it. The ``http2_stream_data`` 60structure is defined as follows:: 61 62 typedef struct { 63 /* The NULL-terminated URI string to retrieve. */ 64 const char *uri; 65 /* Parsed result of the |uri| */ 66 struct http_parser_url *u; 67 /* The authority portion of the |uri|, not NULL-terminated */ 68 char *authority; 69 /* The path portion of the |uri|, including query, not 70 NULL-terminated */ 71 char *path; 72 /* The length of the |authority| */ 73 size_t authoritylen; 74 /* The length of the |path| */ 75 size_t pathlen; 76 /* The stream ID of this stream */ 77 int32_t stream_id; 78 } http2_stream_data; 79 80We create and initialize these structures in 81``create_http2_session_data()`` and ``create_http2_stream_data()`` 82respectively. 83 84``initiate_connection()`` is called to start the connection to the 85remote server. It's defined as:: 86 87 static void initiate_connection(struct event_base *evbase, SSL_CTX *ssl_ctx, 88 const char *host, uint16_t port, 89 http2_session_data *session_data) { 90 int rv; 91 struct bufferevent *bev; 92 SSL *ssl; 93 94 ssl = create_ssl(ssl_ctx); 95 bev = bufferevent_openssl_socket_new( 96 evbase, -1, ssl, BUFFEREVENT_SSL_CONNECTING, 97 BEV_OPT_DEFER_CALLBACKS | BEV_OPT_CLOSE_ON_FREE); 98 bufferevent_enable(bev, EV_READ | EV_WRITE); 99 bufferevent_setcb(bev, readcb, writecb, eventcb, session_data); 100 rv = bufferevent_socket_connect_hostname(bev, session_data->dnsbase, 101 AF_UNSPEC, host, port); 102 103 if (rv != 0) { 104 errx(1, "Could not connect to the remote host %s", host); 105 } 106 session_data->bev = bev; 107 } 108 109``initiate_connection()`` creates a bufferevent for the connection and 110sets up three callbacks: ``readcb``, ``writecb``, and ``eventcb``. 111 112The ``eventcb()`` is invoked by the libevent event loop when an event 113(e.g. connection has been established, timeout, etc.) occurs on the 114underlying network socket:: 115 116 static void eventcb(struct bufferevent *bev, short events, void *ptr) { 117 http2_session_data *session_data = (http2_session_data *)ptr; 118 if (events & BEV_EVENT_CONNECTED) { 119 int fd = bufferevent_getfd(bev); 120 int val = 1; 121 const unsigned char *alpn = NULL; 122 unsigned int alpnlen = 0; 123 SSL *ssl; 124 125 fprintf(stderr, "Connected\n"); 126 127 ssl = bufferevent_openssl_get_ssl(session_data->bev); 128 129 SSL_get0_alpn_selected(ssl, &alpn, &alpnlen); 130 131 if (alpn == NULL || alpnlen != 2 || memcmp("h2", alpn, 2) != 0) { 132 fprintf(stderr, "h2 is not negotiated\n"); 133 delete_http2_session_data(session_data); 134 return; 135 } 136 137 setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&val, sizeof(val)); 138 initialize_nghttp2_session(session_data); 139 send_client_connection_header(session_data); 140 submit_request(session_data); 141 if (session_send(session_data) != 0) { 142 delete_http2_session_data(session_data); 143 } 144 return; 145 } 146 if (events & BEV_EVENT_EOF) { 147 warnx("Disconnected from the remote host"); 148 } else if (events & BEV_EVENT_ERROR) { 149 warnx("Network error"); 150 } else if (events & BEV_EVENT_TIMEOUT) { 151 warnx("Timeout"); 152 } 153 delete_http2_session_data(session_data); 154 } 155 156Here we validate that HTTP/2 is negotiated, and if not, drop 157connection. 158 159For ``BEV_EVENT_EOF``, ``BEV_EVENT_ERROR``, and ``BEV_EVENT_TIMEOUT`` 160events, we just simply tear down the connection. 161 162The ``BEV_EVENT_CONNECTED`` event is invoked when the SSL/TLS 163handshake has completed successfully. After this we're ready to begin 164communicating via HTTP/2. 165 166The ``initialize_nghttp2_session()`` function initializes the nghttp2 167session object and several callbacks:: 168 169 static void initialize_nghttp2_session(http2_session_data *session_data) { 170 nghttp2_session_callbacks *callbacks; 171 172 nghttp2_session_callbacks_new(&callbacks); 173 174 nghttp2_session_callbacks_set_send_callback2(callbacks, send_callback); 175 176 nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, 177 on_frame_recv_callback); 178 179 nghttp2_session_callbacks_set_on_data_chunk_recv_callback( 180 callbacks, on_data_chunk_recv_callback); 181 182 nghttp2_session_callbacks_set_on_stream_close_callback( 183 callbacks, on_stream_close_callback); 184 185 nghttp2_session_callbacks_set_on_header_callback(callbacks, 186 on_header_callback); 187 188 nghttp2_session_callbacks_set_on_begin_headers_callback( 189 callbacks, on_begin_headers_callback); 190 191 nghttp2_session_client_new(&session_data->session, callbacks, session_data); 192 193 nghttp2_session_callbacks_del(callbacks); 194 } 195 196Since we are creating a client, we use `nghttp2_session_client_new()` 197to initialize the nghttp2 session object. The callbacks setup are 198explained later. 199 200The `delete_http2_session_data()` function destroys ``session_data`` 201and frees its bufferevent, so the underlying connection is closed. It 202also calls `nghttp2_session_del()` to delete the nghttp2 session 203object. 204 205A HTTP/2 connection begins by sending the client connection preface, 206which is a 24 byte magic byte string (:macro:`NGHTTP2_CLIENT_MAGIC`), 207followed by a SETTINGS frame. The 24 byte magic string is sent 208automatically by nghttp2. We send the SETTINGS frame in 209``send_client_connection_header()``:: 210 211 static void send_client_connection_header(http2_session_data *session_data) { 212 nghttp2_settings_entry iv[1] = { 213 {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}}; 214 int rv; 215 216 /* client 24 bytes magic string will be sent by nghttp2 library */ 217 rv = nghttp2_submit_settings(session_data->session, NGHTTP2_FLAG_NONE, iv, 218 ARRLEN(iv)); 219 if (rv != 0) { 220 errx(1, "Could not submit SETTINGS: %s", nghttp2_strerror(rv)); 221 } 222 } 223 224Here we specify SETTINGS_MAX_CONCURRENT_STREAMS as 100. This is not 225needed for this tiny example program, it just demonstrates use of the 226SETTINGS frame. To queue the SETTINGS frame for transmission, we call 227`nghttp2_submit_settings()`. Note that `nghttp2_submit_settings()` 228only queues the frame for transmission, and doesn't actually send it. 229All ``nghttp2_submit_*()`` family functions have this property. To 230actually send the frame, `nghttp2_session_send()` has to be called, 231which is described (and called) later. 232 233After the transmission of the client connection header, we enqueue the 234HTTP request in the ``submit_request()`` function:: 235 236 static void submit_request(http2_session_data *session_data) { 237 int32_t stream_id; 238 http2_stream_data *stream_data = session_data->stream_data; 239 const char *uri = stream_data->uri; 240 const struct http_parser_url *u = stream_data->u; 241 nghttp2_nv hdrs[] = { 242 MAKE_NV2(":method", "GET"), 243 MAKE_NV(":scheme", &uri[u->field_data[UF_SCHEMA].off], 244 u->field_data[UF_SCHEMA].len), 245 MAKE_NV(":authority", stream_data->authority, stream_data->authoritylen), 246 MAKE_NV(":path", stream_data->path, stream_data->pathlen)}; 247 fprintf(stderr, "Request headers:\n"); 248 print_headers(stderr, hdrs, ARRLEN(hdrs)); 249 stream_id = nghttp2_submit_request2(session_data->session, NULL, hdrs, 250 ARRLEN(hdrs), NULL, stream_data); 251 if (stream_id < 0) { 252 errx(1, "Could not submit HTTP request: %s", nghttp2_strerror(stream_id)); 253 } 254 255 stream_data->stream_id = stream_id; 256 } 257 258We build the HTTP request header fields in ``hdrs``, which is an array 259of :type:`nghttp2_nv`. There are four header fields to be sent: 260``:method``, ``:scheme``, ``:authority``, and ``:path``. To queue the 261HTTP request, we call `nghttp2_submit_request2()`. The ``stream_data`` 262is passed via the *stream_user_data* parameter, which is helpfully 263later passed back to callback functions. 264 265`nghttp2_submit_request2()` returns the newly assigned stream ID for 266the request. 267 268The next bufferevent callback is ``readcb()``, which is invoked when 269data is available to read from the bufferevent input buffer:: 270 271 static void readcb(struct bufferevent *bev, void *ptr) { 272 http2_session_data *session_data = (http2_session_data *)ptr; 273 nghttp2_ssize readlen; 274 struct evbuffer *input = bufferevent_get_input(bev); 275 size_t datalen = evbuffer_get_length(input); 276 unsigned char *data = evbuffer_pullup(input, -1); 277 278 readlen = nghttp2_session_mem_recv2(session_data->session, data, datalen); 279 if (readlen < 0) { 280 warnx("Fatal error: %s", nghttp2_strerror((int)readlen)); 281 delete_http2_session_data(session_data); 282 return; 283 } 284 if (evbuffer_drain(input, (size_t)readlen) != 0) { 285 warnx("Fatal error: evbuffer_drain failed"); 286 delete_http2_session_data(session_data); 287 return; 288 } 289 if (session_send(session_data) != 0) { 290 delete_http2_session_data(session_data); 291 return; 292 } 293 } 294 295In this function we feed all unprocessed, received data to the nghttp2 296session object using the `nghttp2_session_mem_recv2()` function. 297`nghttp2_session_mem_recv2()` processes the received data and may 298invoke nghttp2 callbacks and queue frames for transmission. Since 299there may be pending frames for transmission, we call immediately 300``session_send()`` to send them. ``session_send()`` is defined as 301follows:: 302 303 static int session_send(http2_session_data *session_data) { 304 int rv; 305 306 rv = nghttp2_session_send(session_data->session); 307 if (rv != 0) { 308 warnx("Fatal error: %s", nghttp2_strerror(rv)); 309 return -1; 310 } 311 return 0; 312 } 313 314The `nghttp2_session_send()` function serializes pending frames into 315wire format and calls the ``send_callback()`` function to send them. 316``send_callback()`` has type :type:`nghttp2_send_callback2` and is 317defined as:: 318 319 static nghttp2_ssize send_callback(nghttp2_session *session _U_, 320 const uint8_t *data, size_t length, 321 int flags _U_, void *user_data) { 322 http2_session_data *session_data = (http2_session_data *)user_data; 323 struct bufferevent *bev = session_data->bev; 324 bufferevent_write(bev, data, length); 325 return (nghttp2_ssize)length; 326 } 327 328Since we use bufferevent to abstract network I/O, we just write the 329data to the bufferevent object. Note that `nghttp2_session_send()` 330continues to write all frames queued so far. If we were writing the 331data to the non-blocking socket directly using the ``write()`` system 332call, we'd soon receive an ``EAGAIN`` or ``EWOULDBLOCK`` error, since 333sockets have a limited send buffer. If that happens, it's possible to 334return :macro:`NGHTTP2_ERR_WOULDBLOCK` to signal the nghttp2 library 335to stop sending further data. When writing to a bufferevent, you 336should regulate the amount of data written, to avoid possible huge 337memory consumption. In this example client however we don't implement 338a limit. To see how to regulate the amount of buffered data, see the 339``send_callback()`` in the server tutorial. 340 341The third bufferevent callback is ``writecb()``, which is invoked when 342all data written in the bufferevent output buffer has been sent:: 343 344 static void writecb(struct bufferevent *bev _U_, void *ptr) { 345 http2_session_data *session_data = (http2_session_data *)ptr; 346 if (nghttp2_session_want_read(session_data->session) == 0 && 347 nghttp2_session_want_write(session_data->session) == 0 && 348 evbuffer_get_length(bufferevent_get_output(session_data->bev)) == 0) { 349 delete_http2_session_data(session_data); 350 } 351 } 352 353As described earlier, we just write off all data in `send_callback()`, 354so there is no data to write in this function. All we have to do is 355check if the connection should be dropped or not. The nghttp2 session 356object keeps track of reception and transmission of GOAWAY frames and 357other error conditions. Using this information, the nghttp2 session 358object can state whether the connection should be dropped or not. 359More specifically, when both `nghttp2_session_want_read()` and 360`nghttp2_session_want_write()` return 0, the connection is no-longer 361required and can be closed. Since we're using bufferevent and its 362deferred callback option, the bufferevent output buffer may still 363contain pending data when the ``writecb()`` is called. To handle this 364situation, we also check whether the output buffer is empty or not. If 365all of these conditions are met, then we drop the connection. 366 367Now let's look at the remaining nghttp2 callbacks setup in the 368``initialize_nghttp2_setup()`` function. 369 370A server responds to the request by first sending a HEADERS frame. 371The HEADERS frame consists of response header name/value pairs, and 372the ``on_header_callback()`` is called for each name/value pair:: 373 374 static int on_header_callback(nghttp2_session *session _U_, 375 const nghttp2_frame *frame, const uint8_t *name, 376 size_t namelen, const uint8_t *value, 377 size_t valuelen, uint8_t flags _U_, 378 void *user_data) { 379 http2_session_data *session_data = (http2_session_data *)user_data; 380 switch (frame->hd.type) { 381 case NGHTTP2_HEADERS: 382 if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE && 383 session_data->stream_data->stream_id == frame->hd.stream_id) { 384 /* Print response headers for the initiated request. */ 385 print_header(stderr, name, namelen, value, valuelen); 386 break; 387 } 388 } 389 return 0; 390 } 391 392In this tutorial, we just print the name/value pairs on stderr. 393 394After the HEADERS frame has been fully received (and thus all response 395header name/value pairs have been received), the 396``on_frame_recv_callback()`` function is called:: 397 398 static int on_frame_recv_callback(nghttp2_session *session _U_, 399 const nghttp2_frame *frame, void *user_data) { 400 http2_session_data *session_data = (http2_session_data *)user_data; 401 switch (frame->hd.type) { 402 case NGHTTP2_HEADERS: 403 if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE && 404 session_data->stream_data->stream_id == frame->hd.stream_id) { 405 fprintf(stderr, "All headers received\n"); 406 } 407 break; 408 } 409 return 0; 410 } 411 412``on_frame_recv_callback()`` is called for other frame types too. 413 414In this tutorial, we are just interested in the HTTP response HEADERS 415frame. We check the frame type and its category (it should be 416:macro:`NGHTTP2_HCAT_RESPONSE` for HTTP response HEADERS). We also 417check its stream ID. 418 419Next, zero or more DATA frames can be received. The 420``on_data_chunk_recv_callback()`` function is invoked when a chunk of 421data is received from the remote peer:: 422 423 static int on_data_chunk_recv_callback(nghttp2_session *session _U_, 424 uint8_t flags _U_, int32_t stream_id, 425 const uint8_t *data, size_t len, 426 void *user_data) { 427 http2_session_data *session_data = (http2_session_data *)user_data; 428 if (session_data->stream_data->stream_id == stream_id) { 429 fwrite(data, len, 1, stdout); 430 } 431 return 0; 432 } 433 434In our case, a chunk of data is HTTP response body. After checking the 435stream ID, we just write the received data to stdout. Note the output 436in the terminal may be corrupted if the response body contains some 437binary data. 438 439The ``on_stream_close_callback()`` function is invoked when the stream 440is about to close:: 441 442 static int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, 443 nghttp2_error_code error_code, 444 void *user_data) { 445 http2_session_data *session_data = (http2_session_data *)user_data; 446 int rv; 447 448 if (session_data->stream_data->stream_id == stream_id) { 449 fprintf(stderr, "Stream %d closed with error_code=%d\n", stream_id, 450 error_code); 451 rv = nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR); 452 if (rv != 0) { 453 return NGHTTP2_ERR_CALLBACK_FAILURE; 454 } 455 } 456 return 0; 457 } 458 459If the stream ID matches the one we initiated, it means that its 460stream is going to be closed. Since we have finished receiving 461resource we wanted (or the stream was reset by RST_STREAM from the 462remote peer), we call `nghttp2_session_terminate_session()` to 463commence closure of the HTTP/2 session gracefully. If you have 464some data associated for the stream to be closed, you may delete it 465here. 466