Socket を利用した簡単な HTTP クライアントの実装

Python でも Socket による低レベルのネットワーク通信を利用できます。

ここでは Socket を用いて簡単な HTTP クライアントを作成し、その動作確認をしてみます。

尚、念のために書くと Socket は主に TCP より下の部分についてカバーするものです。

Socket でネットワーク通信をするプログラムを記述する場合には、アプリケーション層でのプロトコル(今回の場合で言うと HTTP)については、 自前で実装する必要があります。

ここでは HTTP のリクエストとしては、簡単な GET リクエストを Keep-Alive 無し (Connection: close) で送り、応答を受信したらブチ切るだけのものとします。

Socket を利用した簡単な HTTP クライアントの実装

ここでは当サイト (python.keicode.com) に test.txt というテキストファイルを置いて、それを HTTP 1.1 の GET リクエストで取得する状況を考えます。

test.txt は次のようなメッセージが書いているだけのファイルです。

Hello! Python Socket!

接続先は python.keicode.com、ポート番号は HTTP 標準の 80。

ここで送信する GET リクエストの説明

送信する HTTP リクエストは次の通りです。

GET /test.txt HTTP/1.1(CRLF)
Host: python.keicode.com(CRLF)
Connection: close(CRLF)
(CRLF)

HTTP プロトコルの説明を簡単にすると、GET はメソッド。/test.txt は URI と言ってサーバー上でのリソースの場所です。HTTP/1.1 は HTTP プロトコルのバージョン指定です。

一行改行して、次に Host ヘッダーとしてホスト名を指定します。これは HTTP 1.1 では必須です。あるサーバー上で同じポート 80 番に複数のウェブサイトをホストできるようにするために、 ここでホスト名を指定するのです。

次の行の Connection: close はクライアントからサーバーに対して、「そっちが応答を返し終わったら、接続を切ってくれ!」と伝えています。 もしこれが無いと、基本的に接続は切断されません。接続されたままになります。これを Keep-Alive といいます。

接続が Keep-Alive (生かし続ける、とでも訳せばいいかな?)であれば、同じ接続セッション上でクライアントから別の HTTP リクエストを続けて送ることができます。

しかし、今回作る HTTP クライアントでは Keep-Alive はサポートしないので、Connection: close としています。なぜ明示的に切るかというと、接続を切らないと、サーバーからの応答がどこで終わったかわからない(分かりにくい)からです。 Keep-Alive の状態でサーバーの応答の終了を判別するには、サーバーからの応答に含まれるHTTP レスポンスヘッダーから Content-Length を読み取り、何バイト読み込んだらサーバーが応答を返し終わったことにするのか判定しなければいけません。 今回は面倒くさいので簡略化のため、「サーバーが接続を切ったらそこでレスポンス終了」とします。

さて、リクエストヘッダーの説明にもどります。Connection の行の次は一行空行 (CRLF) があります。これはクライアントからサーバーに対して「ここでリクエスト (ヘッダー) が終わりましたよ」と知らせる部分ですので、ただの空行ですが重要な意味を持ちます。

以上はウェブサーバーとの取り決め、すなわち HTTP 1.1 の話です。ソケットを利用してちゃんと動くプログラムを作るためには、サーバー側がどういうときに、どういう動きをするのか、ちゃんと理解していないといけないです。

Python での HTTP クライアント

以上の HTTP リクエストを送信して、サーバーからの応答を表示するプログラムは次のようになります。

import socket

msg = 'GET /test.txt HTTP/1.1\r\n'
msg = msg + 'Host: python.keicode.com\r\n'
msg = msg + 'Connection: close\r\n'
msg = msg + '\r\n'

msglen = len(msg)
total_sent = 0

s = socket.socket(
  socket.AF_INET,
  socket.SOCK_STREAM)
s.connect( (
  'python.keicode.com',
  80) )

while total_sent < msglen:
  sent = s.send(msg[total_sent:])
  if sent == 0:
    raise RuntimeError("Error")
  total_sent = total_sent + sent

#s.shutdown(socket.SHUT_WR)

chunks = []
total_received = 0

while True:
  chunk = s.recv(1024)
  if chunk == '':
    break
  chunks.append(chunk)
  total_received = total_received + len(chunk)

s.shutdown(socket.SHUT_RDWR)
s.close()

print ''.join(chunks)
print total_sent
print total_received

最後に送信バイト数と受信バイト数も表示しています。

この結果は次のようになりました。

HTTP/1.1 200 OK
Date: Tue, 26 Aug 2014 13:39:07 GMT
Server: Apache
Last-Modified: Tue, 25 Aug 2014 20:24:39 GMT
Accept-Ranges: bytes
Content-Length: 21
Vary: Accept-Encoding
Connection: close
Content-Type: text/plain

Hello! Python Socket!
71
249

確かに test.txt の内容が読み取れていますね。

socket の使い方

定数の名前などは標準的な名前で定義されているので、他の言語でソケットを利用したことがある人には分かりやすいと思います。

まず import socket でソケットをインポートします。ソケット系の関数、定数は socket.* とします。

socket.socket でソケットを作成します。connect でサーバーに接続します。send で HTTP リクエストを送信します。 ここでは変数 msg がリクエストなので、msg を送っています。

一度の send 呼び出しでリクエスト全体が送信できると限らないので、while ループで繰り返して呼び出しています。

recv で受信します。こちらも送ってきたものを全て受信するために while で複数回呼び出しています。 もし、Connection: close としないと、ずっとサーバーからの応答待ってしまいます。ここではサーバーから接続を切ってきたタイミングで recv が返ります。

次に shutdown で TCP レベルで接続を切断し、close でクローズします。

shutdown はどういうこと?

ソケットに慣れていないと、なぜわざわざ shutdown と close が別々に存在するのかピンと来ないかもしれないので、一応補足しておきます。

TCP ではハーフクローズという考え方があります。サーバーからクライアントが FIN パケットというのを送ると、サーバーからクライアントに対して、「接続を切断しますよ(もう何も送りませんよ)」 と伝えることができます。クライアントからサーバーに対して FIN を送ると、「こちらからはもうそちら側に何も送りませんよ。(でもサーバーの応答は受信します)」ということになります。 片側が「もうこっちは終わり」という状態がハーフクローズです。

両側が「こっちは終わり」といって、その終了を確認しあったら、それがクローズ (切断状態) です。

TCP では、どこからどこまでデータを受信したか、ということを ACK というフラグで確認しあうことによって、データを間違いなく送受信できる仕組みになっています。もし受信したパケットに抜けがあれば、 クライアントはサーバーに再送をお願いしたりします。そういう仕組みなので FIN で「ここで終わり!」とハッキリ宣言するのは、お互いが意図したデータを全て送受信できたか確認しあうために重要なのです。

上記のコードでは、クライアントの send → サーバーからのデータの recv → shutdown → close という風に Socket のファンクションを呼び出しています。

この結果、次のようなパケットのシーケンスになります。

黄色の FIN がサーバーからクライアントに送ってきたものです。薄紫色はクライアントからサーバーへの FIN です。

今回は recv を永久にブロックさせないように Connection: close を指定しました。確かに意図したように、サーバーからの応答に引き続き、 FIN (切断) が来ています。それを受けて、 クライアント側で shutdown を呼び出しているので、クライアントからサーバーに FIN を送っています。

ちなみに、上記ソースコード内でコメントアウトしてある箇所でクライアント側から shutdown(socket.SHUT_WR) とするとどうなるでしょうか。

前述の通り「このソケットには、もう書き込みません」という意味になるので FIN パケットをサーバーに送ってハーフクローズするはずですね。

実際にパケットを確認しても、確かに次のようにリクエストを送信した直後に FIN を送っています。

では、どちらが良いか、というと今回の状況では本来は先にクライアントから shutdown を呼ぶほうが良いと思います。リソースは早めに解放すべきという考えです。

サンプルコードではいきなり shutdown してしまったら、ソケットに不慣れな人にはわけが分からなくなると思ったので、最後にクリーンアップをまとめて書いたのでした。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Python 入門