マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

Socket.tcp_server_loopでかんたんHTTP

ちゃんとしたHTTPサーバーを作るのって難しいんですが、それっぽいものを作るなら実はそんなに難しく無いです。

HTTPサーバーの処理の流れ

とってもざっくり説明すると、HTTPサーバーの処理はこんな流れです。

  1. TCPコネクションを確立する
  2. HTTPリクエストを受信する
  3. HTTPレスポンスを送信する
  4. TCPコネションを切断する

「1. TCPコネクションを確立する」でまとめてしまっていますが、この中にもいくつか手順があります。TCPソケットをbindしてlistenしてacceptします。混乱したでしょう。そうだと思います。

  1. TCPソケットを作成する
    コンピュータがデータを送受信するとき、何らかの通信方式に従っている必要があります。英語しかしゃべれない人が訪ねてくる施設の受付に日本語しかしゃべれない人を置いたらまずいのと同じです。何らかの通信方式でデータを送受信するための出入り口がソケットです。
    ソケットにはいくつか種類がありますが、HTTPはTCPという通信方式で接続してくるため、TCPソケットを使います。
    ※) RFCではHTTPは特定のトランスポートプロトコルに依存しない、としている。が、基本的にはTCPと考えて良い。RFC 7230 — HTTP/1.1: Message Syntax and Routing (日本語訳)
  2. bindする TCPソケットが、コンピュータの持つポートのうちどこに繋げるかを支持します。普通、HTTPサーバーはTCPの80番ポートに接続します。
  3. listenする ソケットを接続待ちの状態にします。これで外部から接続できるようになります。
  4. acceptする 接続されるまでプログラムの実行を停止(ブロック)します。接続されたら、接続してきたクライアントと通信可能な状態にして、プログラムを再開します。

大まかにこんな流れでTCPコネクションを確立します。大変です。C言語で実装しようものなら、謎の構造体や識別子をいっぱい覚えないといけないので非常に大変です。

Socket.tcp_server_loop

TCPコネクションの確率が大変なのはわかっていただけたと思います。ですが安心してください、Rubyには、「1. TCPコネクションを確立する」を代わりにやってくれるメソッドがあります。スゴーイ。

docs.ruby-lang.org

これを使うと、TCPコネクションを確立して、Socket.tcp_server_loopに渡したブロックを処理してくれます。

コネクションを確立したら、socketからメッセージを読み取り、加工して相手に送信するプログラムはこのようにかけます。9999は接続を待ち受けるTCPのポート番号です。

require 'socket'

Socket.tcp_server_loop(9999) do |sock|
  msg = sock.recv 1000
  sock.sendmsg "#{msg.strip}を受信しました"
ensure
  sock.close
end

これは、ncを使うと動作確認できます。

$ echo hello |  nc localhost 9999
helloを受信しました

注意点としては、このプログラムでは、複数のリクエストを同時に扱うことができない点があります。マルチスレッディングやforkを利用すると、この問題に対処できますがここでは扱いません

HTTPサーバーもどき

TCPサーバーが簡単に作れることがわかったので、HTTPサーバーもどきを作ります。

メッセージフォーマット

HTTP1.1のレスポンスは、ざっくりこのようなフォーマットに従っていることを期待します

ステータスライン
レスポンスヘッダー
CRLF(改行)
メッセージ本体

レスポンスヘッダーは省略可能です。 ステータスラインはHTTPのバージョンとHTTPステータスの組み合わせです。 メッセージ本体には、HTMLなど、なんでもいいのでデータが入ります。

つまり、最低限こんな風になっていればHTTPリクエストとしては正しいです。

HTTP1.1 200 OK

こんにちは

これを踏まえて、HTMLを返すようなプログラムはこんなかんじにかけます

Socket.tcp_server_loop(9999) do |sock, _client_addrinfo|
  sock.write <<~RESP
    HTTP/1.1 200 OK

    <html>
    <body>
    <h1>こんにちは 世界</h1>
    <p>HTTPを使って通信するサーバーを作ったよ。</p>
    </body>
    </html>
  RESP
ensure
  sock.close
end

え?ほんとに動くの?という感じですね。 ブラウザでhttp://localhost:9999/を開きます。

f:id:ytnk531:20201020084516p:plain

うごいてます。HTTP自体はそんなに難しくない気がしてきましたね。

おわりに

以上、HTTPのサーバーもどきは意外と簡単に作れるという話でした。

注意ですが、今回作ったプログラムは、クライアントの要求を無視してただただ単純なリクエストを返すだけのものなので、まだまだサーバーと呼べる代物ではありません。 少なくとも、リクエストメッセージのパース、要求されたファイルの返却、GET、POST、HEADなどのHTTPメソッドへの対応、複数リクエストの同時処理…などなどやらなければいけないことはたくさんあります。 ただ、鬼門となりがちなTCPのコネクション確立が、ほとんどなにも知らなくてもできるっていうのは結構すごいことなんじゃないでしょうか。Rubyでネットワークプログラミングってけっこう楽しいかもしれません。