HTTP2最速実装をmain()関数だけで簡単に説明する(非SSL編)

  • 54
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

C++のmain関数に全てベタ書きで書いてHTTP2の通信を説明します。
最速実装なので最低限の通信に限定しています。
MacとWindows両方でビルドできます。

接続先は「nghttp2.org」を使わせていただきます。
変更したい人はhostの部分を書き換えてください。(その場合はHEADERS_FRAMEも書き換える必要があります)

今回は平文で流れる通信です。
SSL版はこちらです。
ただし、main関数だけといいつつも、3つだけ冗長な処理を関数化してます。数行程度の定型処理です。

参考にしたのはこちらの資料です。
https://speakerdeck.com/syucream/2-zui-su-shi-zhuang-v3

依存

windowsの場合は下記の依存ライブラリを追加します。
 Ws2_32.lib

目的

HTTP2の最初のハードルを超えるための勉強。
main関数に全て書く事によって処理の流れを明確にする。
バイナリもそのままソースに打ち込むことによってどのようなデータが流れているのかを確認できるようにする。

実装

[https://github.com/0xfffffff7/HelloHttp2]

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string>

#ifdef WIN32
#include <winsock2.h>
#include <windows.h>
#pragma warning(disable:4996)
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define SOCKET int
#define SD_BOTH SHUT_WR
#endif


#define READ_BUF_SIZE 4096
#define BUF_SIZE 4097
#define PORT 80
#define BINARY_FRAME_LENGTH 9


// 3バイトのネットワークオーダーを4バイト整数へ変換する関数.
char* to_framedata3byte(char *p, int &n);

int get_error();

void close_socket(SOCKET socket);

int main(int argc, char **argv)
{


    //------------------------------------------------------------
    // 接続先ホスト名.
    // HTTP2に対応したホストを指定します.
    //------------------------------------------------------------
    std::string host = "nghttp2.org";




    //------------------------------------------------------------
    // TCPの準備.
    //------------------------------------------------------------
#ifdef WIN32
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        return 0;
    }
#endif

    int error = 0;
    struct hostent *hp;
    struct sockaddr_in addr;
    SOCKET _socket;

    if (!(hp = gethostbyname(host.c_str()))){
        return -1;
    }
    memset(&addr, 0, sizeof(addr));
    addr.sin_addr = *(struct in_addr*)hp->h_addr_list[0];
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);

    if ((_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))<0){
        return -1;
    }
    if (connect(_socket, (struct sockaddr *)&addr, sizeof(addr))<0){
        return -1;
    }



    //------------------------------------------------------------
    // HTTP2の準備.
    // SSLでない場合は、下記の24オクテットのデータを送信することで、
    // これからHTTP2通信を始めることを伝える.
    //------------------------------------------------------------
    std::string pri = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
    int r = (int)::send(_socket, pri.c_str(), pri.length(), NULL);


    //------------------------------------------------------------
    // 全てのデータはバイナリフレームで送受信される
    // バイナリフレームは共通の9バイトヘッダと、データ本体であるpayloadを持つ
    //
    // ●ヘッダ部分のフォーマット
    //
    //   1-3バイト目  payloadの長さ。長さにヘッダの9バイトは含まれない。.
    //   4バイト目 フレームのタイプ.
    //   5バイト目 フラグ.
    //   6-9バイト目 ストリームID.(最初の1bitは予約で必ず0)
    //
    //  |Length(24bit)|Type(8bit)|Flags(8bit)|Reserve(1bit)|Stream Identifier(31bit)|
    //  |Frame Payload(Lengthバイト分)|
    //
    //
    // [フレームのタイプ]
    //
    // DATA(0x00)  リクエストボディや、レスポンスボディを転送する
    // HEADERS(0x01)  圧縮済みのHTTPヘッダーを転送する
    // PRIORITY(0x02)  ストリームの優先度を変更する
    // RST_STREAM(0x03)  ストリームの終了を通知する
    // SETTINGS(0x04)  接続に関する設定を変更する
    // PUSH_PROMISE(0x05)  サーバーからのリソースのプッシュを通知する
    // PING(0x06)  接続状況を確認する
    // GOAWAY(0x07)  接続の終了を通知する
    // WINDOW_UPDATE(0x08)   フロー制御ウィンドウを更新する
    // CONTINUATION(0x09)  HEADERSフレームやPUSH_PROMISEフレームの続きのデータを転送する
    //
    // それぞれのリクエストやレスポンスにはストリームIDが付与される.
    // クライアントから発行されるストリームIDは奇数.
    // サーバーから発行されるストリームIDは偶数.
    // ストリームには優先順位が付けられています.
    // 今回はストリームID「1」だけを使用します.
    //------------------------------------------------------------

    //------------------------------------------------------------
    // HTTP2通信のフロー
    //
    // まず最初にSettingフレームを必ず交換します.
    // Settingフレームを交換したら、設定を適用したことを伝えるために必ずACKを送ります.
    //
    // Client -> Server  SettingFrame
    // Client <- Server  SettingFrame
    // Client -> Server  ACK
    // Client <- Server  ACK
    //
    // Client -> Server  HEADERS_FRAME (GETなど)
    // Client <- Server  HEADERS_FRAME (ステータスコードなど)
    // Client <- Server  DATA_FRAME (Body)
    // 
    // Client -> Server  GOAWAY_FRAME (送信終了)
    //------------------------------------------------------------

    //------------------------------------------------------------
    // Settingフレームの送信.
    // フレームタイプは「0x04」
    // 全てデフォルト値を採用するためpayloadは空です。
    // SettingフレームのストリームIDは0です.(コネクション全体に適用されるため)
    //
    // 今回は空ですがSettingフレームのpayloadは次のフォーマットです.
    //
    // |Identifer(16bit)|Value(32bit)|
    // 上記を設定値の数だけ連結させ、最終的な長さをヘッダフレームのLengthに記述します.
    //
    // Identiferは次のものが定義されています。
    // SETTINGS_HEADER_TABLE_SIZE (0x1)  初期値は 4,096 オクテット
    // SETTINGS_ENABLE_PUSH (0x2)  初期値は1
    // SETTINGS_MAX_CONCURRENT_STREAMS (0x3)  初期状態では無制限
    // SETTINGS_INITIAL_WINDOW_SIZE (0x4)   初期値は 2^16-1 (65,535)
    // SETTINGS_MAX_FRAME_SIZE (0x5)    初期値は 2^24-1 (16777215)
    // SETTINGS_MAX_HEADER_LIST_SIZE (0x6)   初期値は無制限
    //------------------------------------------------------------
    const char settingframe[BINARY_FRAME_LENGTH] = { 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };

    r = (int)::send(_socket, settingframe, BINARY_FRAME_LENGTH, NULL);
    if (r == -1){
        error = get_error();
        ::shutdown(_socket, SD_BOTH);
        close_socket(_socket);
        return 0;
    }


    //------------------------------------------------------------
    // Settingフレームの受信.
    //------------------------------------------------------------
    char buf[BUF_SIZE] = { 0 };
    char* p = buf;

    r = (int)::recv(_socket, p, READ_BUF_SIZE, NULL);
    if (r == -1){
        error = get_error();
        ::shutdown(_socket, SD_BOTH);
        close_socket(_socket);
        return 0;
    }



    //------------------------------------------------------------
    // ACKの送信.
    // ACKはSettingフレームを受け取った側が送る必要がある.
    // ACKはSettingフレームのフラグに0x01を立ててpayloadを空にしたもの.
    //
    // フレームタイプは「0x04」
    // 5バイト目にフラグ0x01を立てます。
    //------------------------------------------------------------
    const char settingframeAck[BINARY_FRAME_LENGTH] = { 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00 };

    r = (int)::send(_socket, settingframeAck, BINARY_FRAME_LENGTH, NULL);
    if (r == -1){
        error = get_error();
        ::shutdown(_socket, SD_BOTH);
        close_socket(_socket);
        return 0;
    }


    // サーバーからのACKの受信は下でやります..

    //------------------------------------------------------------
    // HEADERSフレームの送信.
    //
    // フレームタイプは「0x01」
    // このフレームに必要なヘッダがすべて含まれていてこれでストリームを終わらせることを示すために、
    // END_STREAM(0x1)とEND_HEADERS(0x4)を有効にします。
    // 具体的には5バイト目のフラグに「0x05」を立てます。
    // ストリームIDは「0x01」を使います.
    //
    // ここまででヘッダフレームは「ペイロードの長さ(3バイト), 0x01, 0x05, 0x00, 0x00, 0x00, 0x01」になります.
    //
    //
    // ●HTTP1.1でのセマンティクス
    //   "GET / HTTP1/1"
    //   "Host: nghttp2.org
    //
    // ●HTTP2でのセマンティクス
    //      :method GET
    //      :path /
    //      :scheme http
    //      :authority nghttp2.org
    //
    // 本来HTTP2はHPACKという方法で圧縮します.
    // 今回は上記のHTTP2のセマンティクスを圧縮なしで記述します.
    //
    // 一つのヘッダフィールドの記述例
    //
    // |0|0|0|0|      0|   // 最初の4ビットは圧縮に関する情報、次の4ビットはヘッダテーブルのインデクス.(今回は圧縮しないのですべて0)
    // |0|            7|   // 最初の1bitは圧縮に関する情報(今回は0)、次の7bitはフィールドの長さ
    // |:method|           // フィールドをそのままASCIIのオクテットで書く。
    // |0|            3|   // 最初の1bitは圧縮に関する情報(今回は0)、次の7bitはフィールドの長さ
    // |GET|               // 値をそのままASCIIのオクテットで書く。
    //
    // 上記が一つのヘッダフィールドの記述例で、ヘッダーフィールドの数だけこれを繰り返す.
    //
    //------------------------------------------------------------
    const char headersframe[69] = { 
    0x00, 0x00, 0x3c, 0x01, 0x04, 0x00, 0x00, 0x00, 0x01,  // ヘッダフレーム
    0x00,                                                  // 圧縮情報
    0x07, 0x3a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,        // 7 :method
    0x03, 0x47, 0x45, 0x54,                                // 3 GET
    0x00,                                                  // 圧縮情報
    0x05, 0x3a, 0x70, 0x61, 0x74, 0x68,                    // 5 :path
    0x01, 0x2f,                                            // 1 /
    0x00,                                                  // 圧縮情報
    0x07, 0x3a, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65,        // 7 :scheme
    0x04, 0x68, 0x74, 0x74, 0x70,                          // 4 http
    0x00,                                                  // 圧縮情報
    0x0a, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79,      // 10 :authority
    0x0b, 0x6e, 0x67, 0x68, 0x74, 0x74, 0x70, 0x32, 0x2e, 0x6f, 0x72, 0x67   // 11 nghttp2.org
    };

    r = (int)::send(_socket, headersframe, 69, NULL);
    if (r == -1){
        error = get_error();
        ::shutdown(_socket, SD_BOTH);
        close_socket(_socket);
        return 0;
    }


    //------------------------------------------------------------
    // HEADERSフレームの受信.
    //------------------------------------------------------------
    int payload_length = 0;
    int frame_type = 0;

    // まずはヘッダフレームを受信してpayloadのlengthを取得する。
    while (1){

        memset(buf, 0x00, BINARY_FRAME_LENGTH);
        p = buf;

        r = (int)::recv(_socket, p, BINARY_FRAME_LENGTH, NULL);
        if (r == -1){
            error = get_error();
            ::shutdown(_socket, SD_BOTH);
            close_socket(_socket);
            return 0;
        }

        // ACKが返ってくる場合があるのでACKなら無視して次を読む。
        if (memcmp(buf, settingframeAck, BINARY_FRAME_LENGTH) == 0){
            continue;
        }
        else{

            // payloadの長さを取得する。
            p = to_framedata3byte(p, payload_length);

            // フレームタイプがHEADERS_FRAMEではなかったら読み飛ばす。
            memcpy(&frame_type, p, 1);
            if (frame_type != 1){

                r = (int)::recv(_socket, p, payload_length, NULL);
                if (r == -1){
                    error = get_error();
                    ::shutdown(_socket, SD_BOTH);
                    close_socket(_socket);
                    return 0;
                }

                continue;
            }
            break;
        }
    }

    // 次にHEADERSフレームのpayloadを受信する。
    memset(buf, 0x00, payload_length);
    p = buf;
    r = (int)::recv(_socket, p, payload_length, NULL);
    if (r == -1){
        error = get_error();
        ::shutdown(_socket, SD_BOTH);
        close_socket(_socket);
        return 0;
    }


    //------------------------------------------------------------
    // DATAフレームの受信.
    //------------------------------------------------------------

    // まずはヘッダフレームを受信してpayloadのlengthを取得する。
    memset(buf, 0x00, BINARY_FRAME_LENGTH);
    p = buf;
    r = (int)::recv(_socket, p, BINARY_FRAME_LENGTH, NULL);
    to_framedata3byte(p, payload_length);

    // 次にpayloadを受信する。
    while (payload_length > 0){

        memset(buf, 0x00, BUF_SIZE);
        p = buf;

        r = (int)::recv(_socket, p, READ_BUF_SIZE, NULL);
        if (r == -1){
            error = get_error();
            ::shutdown(_socket, SD_BOTH);
            close_socket(_socket);
            return 0;
        }
        payload_length -= r;

        printf("%s", p);
    }


    //------------------------------------------------------------
    // GOAWAYの送信.
    //
    // これ以上データを送受信しない場合はGOAWAYフレームを送信します.
    // フレームタイプは「0x07」
    // ストリームIDは「0x00」(コネクション全体に適用するため)
    //------------------------------------------------------------
    const char goawayframe[17] = { 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00 };

    r = (int)::send(_socket, goawayframe, 17, NULL);
    if (r == -1){
        error = get_error();
        ::shutdown(_socket, SD_BOTH);
        close_socket(_socket);
        return 0;
    }



    //------------------------------------------------------------
    // 後始末.
    //------------------------------------------------------------
    ::shutdown(_socket, SD_BOTH);
    close_socket(_socket);

    return 0;

}

void close_socket(SOCKET socket){
#ifdef WIN32
    ::closesocket(socket);
    WSACleanup();
#else
    ::close(socket);
#endif
}

int get_error(){
#ifdef WIN32
    return WSAGetLastError();
#endif
    return errno;
}

char* to_framedata3byte(char *p, int &n){
    u_char buf[4] = { 0 };
    memcpy(&(buf[1]), p, 3);
    memcpy(&n, buf, 4);
    n = ntohl(n);
    p += 3;
    return p;
}

  

結果

通信に問題なければ、コンソールに次のようなhtmlが表示されるでしょう。

<!DOCTYPE html>
<!--[if IEMobile 7 ]><html class="no-js iem7"><![endif]-->
<!--[if lt IE 9]><html class="no-js lte-ie8"><![endif]-->
<!--[if (gt IE 8)|(gt IEMobile 7)|!(IEMobile)|!(IE)]><!--><html class="no-js" lang="en"><!--<![endif]-->
<head>
  <meta charset="utf-8">
  <title>Nghttp2: HTTP/2 C Library - nghttp2.org</title>
  <meta name="author" content="Tatsuhiro Tsujikawa">


  <meta name="description" content="Nghttp2: HTTP/2 C Library Feb 16th, 2015 11:16 pm nghttp2 is an implementation of HTTP/2 in C.
HTTP/2 and HPACK has been approved by IETF,
we are &hellip;">


  <!-- http://t.co/dKP3o1e -->
  <meta name="HandheldFriendly" content="True">
  <meta name="MobileOptimized" content="320">
  <meta name="viewport" content="width=device-width, initial-scale=1">


  <link rel="canonical" href="//nghttp2.org">
  <link href="/favicon.png" rel="icon">
  <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css">
  <link href="/atom.xml" rel="alternate" title="nghttp2.org" type="application/atom+xml">
  <script src="/javascripts/modernizr-2.0.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script>!window.jQuery && document.write(unescape('%3Cscript src="./javascripts/libs/jquery.min.js"%3E%3C/script%3E'))</script>
  <script src="/javascripts/octopress.js" type="text/javascript"></script>
  <!--Fonts from Google"s Web font directory at http://google.com/webfonts -->
<link href="//fonts.googleapis.com/css?family=PT+Serif:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=PT+Sans:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">

  </head>

<body   >
  <header role="banner"><hgroup>
  <h1><a href="/">nghttp2.org</a></h1>

    <h2>HTTP/2 C library and tools</h2>

</hgroup>

</header>
  <nav role="navigation"><ul class="subscription" data-subscription="rss">
  <li><a href="/atom.xml" rel="subscribe-rss" title="subscribe via RSS">RSS</a></li>

</ul>

<form action="https://www.google.com/search" method="get">
  <fieldset role="search">
    <input type="hidden" name="q" value="site://nghttp2.org" />
    <input class="search" type="text" name="q" results="0" placeholder="Search"/>
  </fieldset>
</form>

<ul class="main-navigation">
  <li><a href="/">Top</a></li>
  <li><a href="/blog/">Blog</a></li>
  <li><a href="/blog/archives">Archives</a></li>
  <li><a href="/documentation/">Documentation</a></li>
  <li><a href="/httpbin">Httpbin</a></li>
  <li><a href="https://github.com/tatsuhiro-t/nghttp2/releases">Releases</a></li>
  <li><a href="https://github.com/tatsuhiro-t/nghttp2">Source Code</a></li>
</ul>

</nav>
  <div id="main">
    <div id="content">
      <div>
<article role="article">

  <header>
    <h1 class="entry-title">Nghttp2: HTTP/2 C Library</h1>
    <p class="meta">

<time class='entry-date' datetime='2015-02-16T23:16:00+09:00'><span class='date'><span class='date-month'>Feb</span> <span class='date-day'>16</span><span class='date-suffix'>th</span>, <span class='date-year'>2015</span></span> <span class='time'>11:16 pm</span></time></p>
  </header>

  <p>nghttp2 is an implementation of HTTP/2 in C.
<a href="https://www.ietf.org/blog/2015/02/http2-approved/">HTTP/2 and HPACK has been approved by IETF</a>,
we are now waiting for their publication as RFCs.</p>

<p>The framing layer of HTTP/2 is implemented as a form of reusable C
library.  On top of that, we have implemented HTTP/2 <a href="/documentation/nghttp.1.html">client</a>, <a href="/documentation/nghttpd.1.html">server</a>
and <a href="/documentation/nghttpx.1.html">proxy</a>.
We have also developed <a href="/documentation/h2load.1.html">load test and benchmarking tool</a> for HTTP/2 and SPDY.</p>

<p>We have participated in httpbis working group since HTTP/2 draft-04,
which is the first implementation draft.  Since then we have updated
nghttp2 library constantly to latest specification and nghttp2 is now
one of the most mature <a href="https://github.com/http2/http2-spec/wiki/Implementations">HTTP/2 implementations</a>.</p>

<p>All C APIs are <a href="/documentation/apiref.html">fully documented</a>.</p>

<p>HTTP/2 utilizes header compression method called <a href="http://http2.github.io/http2-spec/compression.html">HPACK</a>.  We offer
HPACK encoder and decoder are available as <a href="/documentation/tutorial-hpack.html">public API</a>.</p>

<p>nghttp2 library itself is a bit low-level.  The experimental <a href="/documentation/libnghttp2_asio.html">high level C++ API</a> is also available.</p>

<p>We have <a href="/documentation/python-apiref.html">Python binding</a> of this libary, but we have not covered
everything yet.</p>

     <footer>
      <p class="meta">

<time class='entry-date' datetime='2015-02-16T23:16:00+09:00'><span class='date'><span class='date-month'>Feb</span> <span class='date-day'>16</span><span class='date-suffix'>th</span>, <span class='date-year'>2015</span></span> <span class='time'>11:16 pm</span></time>

      </p>

        <div class="sharing">

  <a href="//twitter.com/share" class="twitter-share-button" data-url="//nghttp2.org/index.html" data-via="" data-counturl="//nghttp2.org/index.html" >Tweet</a>



</div>


    </footer>

</article>

</div>

<aside class="sidebar">

    <section>
  <h1>Recent Posts</h1>
  <ul id="recent_posts">

      <li class="post">
        <a href="/blog/2015/03/14/nghttp2-v0-7-7/">Nghttp2 v0.7.7</a>
      </li>

      <li class="post">
        <a href="/blog/2015/03/14/nghttp2-v0-7-6/">Nghttp2 v0.7.6</a>
      </li>

      <li class="post">
        <a href="/blog/2015/02/27/nghttp2-v0-7-5/">Nghttp2 v0.7.5</a>
      </li>

      <li class="post">
        <a href="/blog/2015/02/15/nghttp2-v0-7-4/">Nghttp2 v0.7.4</a>
      </li>

      <li class="post">
        <a href="/blog/2015/02/10/nghttp2-dot-org-enabled-http2-server-push/">nghttp2.org Enabled HTTP/2 Server Push</a>
      </li>

  </ul>
</section>

</aside>
    </div>
  </div>
  <footer role="contentinfo"><p>
  Copyright &copy; 2015 - Tatsuhiro Tsujikawa -
  <span class="credit">Powered by <a href="http://octopress.org">Octopress</a></span>
</p>

</footer>

  <script type="text/javascript">
    (function(){
      var twitterWidgets = document.createElement('script');
      twitterWidgets.type = 'text/javascript';
      twitterWidgets.async = true;
      twitterWidgets.src = '//platform.twitter.com/widgets.js';
      document.getElementsByTagName('head')[0].appendChild(twitterWidgets);
    })();
  </script>

</body>
</html>