• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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