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