SPDY対応アプリケーションをC言語で実装する方法

昨日、SPDYのCライブラリであるspdylayがめでたく1.0.0としてリリースされたので早速使ってみました。まずはmacOSX10.8.3で試してみました。ビルド方法はメモに書いていますので参考にして下さい。

今回はSPDYで通信できるクライアントをCで書く際に、どのような実装の流れになるかを紹介したいと思います。本エントリで実装流れを把握したら、spdylay/spdylay.hを読む事をおすすめします。

SPDYのCライブラリspdylayの概要

spdylayのAPIはコールバックベースで実装されています。ただ、今回はコールバックされる関数の詳細な実装まで説明すると、全体的な流れが見えにくくなるので、コールバック関数の実装の仕方は省略します。コールバック関数周りの詳細を知りたい方はspdylay/spdylay.hを読むと良いと思います。

まずは、どういう流れでSPDY対応アプリケーションを実装するかをみていきましょう。

まずはconnect

普通にCでconnectしましょう。普通にsocket作ってfdをもらう感じですね。大体以下のようになります。

[program lang=’c’ escaped=’true’]

static int connect_to(const char *host, uint16_t port)
{
  struct addrinfo hints;
  int fd = -1;
  int rv;
  char service[NI_MAXSERV];
  struct addrinfo *res, *rp;
  snprintf(service, sizeof(service), "%u", port);
  memset(&hints, 0, sizeof(struct addrinfo));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  rv = getaddrinfo(host, service, &hints, &res);
  if(rv != 0) {
    dief("getaddrinfo", gai_strerror(rv));
  }
  for(rp = res; rp; rp = rp->ai_next) {
    fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
    if(fd == -1) {
      continue;
    }
    while((rv = connect(fd, rp->ai_addr, rp->ai_addrlen)) == -1 &&
          errno == EINTR);
    if(rv == 0) {
      break;
    }
    close(fd);
    fd = -1;
  }
  freeaddrinfo(res);
  return fd;
}

[/program]

SSLのハンドシェイク実装

SPDYなのでSSLセッションをはる必要があります。

基本的にはSSLのハンドシェイクをするだけなのですが、SPDYはNPN(Next Protocol Negotiation)というSPDYを含む使用可能なプロトコルを選択するための実装が追加で必要になります。

普通にSSL_CTX_new()とかして、SSLやSSL_CTXをいつもどおりゴニョゴニョした後に、NPNの実装を追加します。
NPN実装はコールバック関数を引数としてSSL_CTX_*の関数に渡します。このコールバックは全体の流れでも重要なので、関数まで説明します。

[program lang=’c’ escaped=’true’]

SSL_CTX_set_next_proto_select_cb(ssl_ctx, select_next_proto_cb, spdy_proto_version);

[/program]

コールバックされる関数は例えば以下のように実装しておきます。ここで呼ばれる関数の中で、spdylayのAPIであるspdylay_select_next_protocolによって、SPDYのプロトコルバージョンを得ます。

[program lang=’c’ escaped=’true’]

static int select_next_proto_cb(SSL* ssl,
                                unsigned char **out, unsigned char *outlen,
                                const unsigned char *in, unsigned int inlen,
                                void *arg)
{
  int rv;
  uint16_t *spdy_proto_version;

  rv = spdylay_select_next_protocol(out, outlen, in, inlen);
  if(rv <= 0) {
    die("Server did not advertise spdy/2 or spdy/3 protocol.");
  }
  spdy_proto_version = (uint16_t*)arg;
  *spdy_proto_version = rv;
  return SSL_TLSEXT_ERR_OK;
}

[/program]

これ以降は、connectから得られたfdを介してSSLハンドシェイクを行い、fdをノンブロックにしたり、ソケットをTCP_NODELAYにしたりします。

[program lang=’c’ escaped=’true’]

fcntl(fd, F_SETFL, flags ¦ O_NONBLOCK)
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &val, (socklen_t)sizeof(val));

[/program]

SPDYセッションの生成

ここまできて、ようやくSPDYセッションを貼るためのNPNやSSL通信の準備ができたので、SPDYのセッションをはります。
今回はクライアント側の実装なので、以下のようなspdylay APIを呼び出します。

[program lang=’c’ escaped=’true’]

spdylay_session_client_new(&connection.session, spdy_proto_version, &callbacks, &connection);

[/program]

ここで、第一引数にはspdylay_session構造体(connection構造体にはSSL構造体とspdylay_session構造体をメンバとしています)、第二引数にはSPDYプロトコルのバージョン、第三引数にはSPDYで通信している各フェイズでコールバックする関数群を登録した構造体、第四引数には任意のユーザデータを渡します。connection構造体は以下のように定義しています。

[program lang=’c’ escaped=’true’]

struct Connection {
  SSL *ssl;
  spdylay_session *session;
  int want_io;
};

[/program]

第三引数に渡すspdylay_session_callbacks *callbacksには以下のように、SPDYの各種通信フェイズでコールバックする関数を登録することができます。

[program lang=’c’ escaped=’true’]

callbacks->send_callback = send_callback;
callbacks->recv_callback = recv_callback;
callbacks->before_ctrl_send_callback = before_ctrl_send_callback;
callbacks->on_ctrl_send_callback = on_ctrl_send_callback;
callbacks->on_ctrl_recv_callback = on_ctrl_recv_callback;
callbacks->on_stream_close_callback = on_stream_close_callback;
callbacks->on_data_chunk_recv_callback = on_data_chunk_recv_callback;

[/program]

上記のように、データを送受信するフェイズや、コントロールデータ送受信フェイズやその前(ストリームIDを得るフェイズ)、ストリーム終了時や、データチャンク受信時等で関数をコールバックすることができます。

このあたりは、実際にspdylay/spdylay.hを見て、色々試してみると良いでしょう。コールバック時に必要なデータ、例えばSSL構造体等はspdyのセッション生成時の第四引数のユーザデータ(上記の例では&connection)として受け渡せば良いと思います。

SPDY上でリクエストの送信

そして、SPDYセッション生成後は、リクエストを送信(メッセージをキューイング)してあげます。例えば以下のように書きます。

[program lang=’c’ escaped=’true’]

int pri = 0;
int rv;
const char *nv[15];
nv[0] = ":method";     nv[1] = "GET";
nv[2] = ":path";       nv[3] = req->path;
nv[4] = ":version";    nv[5] = "HTTP/1.1";
nv[6] = ":scheme";     nv[7] = "https";
nv[8] = ":host";       nv[9] = req->hostport;
nv[10] = "accept";     nv[11] = "*/*";
nv[12] = "user-agent"; nv[13] = "spdylay/"SPDYLAY_VERSION;
nv[14] = NULL;
rv = spdylay_submit_request(connection->session, pri, nv, NULL, req);

[/program]

SPDY上のHTTPレイヤーで扱われるSPDYヘッダーには、HTTPヘッダーから幾つかSPDYヘッダーにマージした上で、コロン(:)がヘッダーの先頭に付けられ、小文字になります。これは、そもそものHTTPヘッダーの命名においてはコロンは不正なシーケンスで、既に幾つかのWebアプリケーションで独自のHTTPヘッダーを使っていた場合にも絶対に衝突しないようにするためです。

これをsprylyのAPIであるspdylay_submit_requestによってリクエストを送信します。

第五引数のreqは、以下のような構造体になります。

[program lang=’c’ escaped=’true’]

struct Request {
  char *host;
  uint16_t port;
  char *path;
  char *hostport;
  int32_t stream_id;
  spdylay_gzip *inflater;
};

[/program]

SPDYセッション上でイベントループによるデータの送受信

リクエストメッセージをキューイングした後は、SPDYセッションの状態からspdylay_session_want_readあるいはspdylay_session_want_write APIによって、送信か受信かのステータスを得た後、イベントループに入ります。そして、都度キューに溜まったフレームを送信する場合はspdylay_session_send、受信する場合はspdylay_session_recv APIによって、フレームの送受信を行います。実際にはcallbacksに登録されているコールバック関数でバイトデータが送受信されます。

イベントループとコールバックI/Oによる実装によって、SPDY上の並列したストリーム上でフレームの送受信が行われます。例えば以下のように実装します。

[program lang=’c’ escaped=’true’]

static void exec_io(struct Connection *connection)
{
  int rv;
  rv = spdylay_session_recv(connection->session);
  if(rv != 0) {
    diec("spdylay_session_recv", rv);
  }
  rv = spdylay_session_send(connection->session);
  if(rv != 0) {
    diec("spdylay_session_send", rv);
  }
}

static void ctl_poll(struct pollfd *pollfd, struct Connection *connection)
{
  pollfd->events = 0;
  if(spdylay_session_want_read(connection->session) ¦¦
     connection->want_io == WANT_READ) {
    pollfd->events ¦= POLLIN;
  }
  if(spdylay_session_want_write(connection->session) ¦¦
     connection->want_io == WANT_WRITE) {
    pollfd->events ¦= POLLOUT;
  }
}

pollfds[0].fd = fd;
ctl_poll(pollfds, &connection);

while(spdylay_session_want_read(connection.session) ¦¦ spdylay_session_want_write(connection.session)) {
  int nfds = poll(pollfds, npollfds, -1);
  if(nfds == -1) {
    dief("poll", strerror(errno));
  }
  if(pollfds[0].revents & (POLLIN ¦ POLLOUT)) {
    exec_io(&connection);
  }
  if((pollfds[0].revents & POLLHUP) ¦¦ (pollfds[0].revents & POLLERR)) {
    die("Connection error");
  }
  ctl_poll(pollfds, &connection);
}

[/program]

実際に送受信したデータの扱いは、callbacksに登録したコールバック関数で行います。実装の流れ上、コールバック関数を一部紹介しておきます。例えば、単純なデータの標準出力表示であれば、

[program lang=’c’ escaped=’true’]

callbacks->on_data_chunk_recv_callback = on_data_chunk_recv_callback

[/program]

で以下のような関数をコールバックすると良いでしょう。gzipされている場合はそれを解凍してやる必要があります。

[program lang=’c’ escaped=’true’]

static void on_data_chunk_recv_callback(spdylay_session *session, uint8_t flags,
                                        int32_t stream_id,
                                        const uint8_t *data, size_t len,
                                        void *user_data)
{
  struct Request *req;
  req = spdylay_session_get_stream_user_data(session, stream_id);
  if(req) {
    if(req->inflater) {
      while(len > 0) {
        uint8_t out[MAX_OUTLEN];
        size_t outlen = MAX_OUTLEN;
        size_t tlen = len;
        int rv;
        rv = spdylay_gzip_inflate(req->inflater, out, &outlen, data, &tlen);
        if(rv == -1) {
          spdylay_submit_rst_stream(session, stream_id, SPDYLAY_INTERNAL_ERROR);
          break;
        }
        fwrite(out, 1, outlen, stdout);
        data += tlen;
        len -= tlen;
      }
    } else {
      fwrite(data, 1, len, stdout);
    }
    printf("¥n");
  }
}

[/program]

SPDYリソースを開放

そして、データの送受信が終わった後は、SPDYやSSLのリソースを綺麗にします。spdylay APIでは、spdylay_session_delを使います。大体以下のようになるでしょう。

[program lang=’c’ escaped=’true’]

spdylay_session_del(connection.session);
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ssl_ctx);
shutdown(fd, SHUT_WR);
close(fd);

[/program]

これがspdylayを使ったクライアント側からSPDYで通信するための一連の流れになります。

最後に

今回は、SPDYで通信するための一連の流れを解説するために、あえてコールバックされる関数の詳細な実装は省きました。
次書くとしたら、実際にSPDY通信の各フェイズでコールバックされる関数をどのように書くかを、簡単なサンプルを元に紹介したいと思います。本エントリで実装の流れを掴んだ人はspdylay/spdylay.hを読んで実際に実装してみると良いでしょう。

これでガンガンSPDY対応アプリケーションを書く事ができますね!