• 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 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