【Python】ソケット通信でHTTPクライアントを開発

PythonでのHTTPクライアントの作り方解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは Python での HTTP クライアントの開発の仕方について解説していきます。具体的には、ソケット通信(socket モジュール)を利用した HTTP クライアントの開発の仕方について解説していきます。

冒頭でいきなりこんなことを言われると戸惑う方もおられるかもしれませんが、実用的な HTTP クライアントをソケット通信を利用して開発するのは困難で、実用性を求めるのであれば、 HTTP クライアントとしては下記ページで紹介している requests モジュールや urllib モジュールを利用する方が良いです。この理由については後述の requests モジュールや urllib モジュールの利用がオススメな理由 で解説しますが、このページを読み進めていっていただければ何となく察していただけると思います。

requestsモジュールの解説ページアイキャッチ 【Python】reqeustsモジュールについて解説 requestsモジュールでのウェブアプリの操作の仕方の解説ページアイキャッチ 【Python】requestsモジュールでPOSTメソッドのリクエストを送信(ウェブアプリの操作)

ただし、実用性求めるのではなく、HTTP や HTTP クライアントについての知識を深めたい方や、ソケット通信の知識や使い方の知見を広めたい方に関しては HTTP クライアントの開発は非常に有益な経験になると思います。なので、こういった目的の方は是非ページを読み進めていただければと思います!

ソケット通信と HTTP の関係

まずソケット通信と HTTP について解説しておきます。

ソケット通信とはソケットを利用した通信であり、Python の場合は socket モジュールを利用してソケット通信を実現することが多いです。このソケット通信についての詳細は下記ページにまとめていますので、詳しく知りたい方は下記ページを参照していただければと思います。

Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)

このソケット通信では、TCP / IP モデルでいうトランスポート層までがサポートされるようになっています。すなわち、ソケット通信では、他の通信端末の特定のソケット(ソケットを持つプログラム)に対してデータを正常に届けるための様々な制御が行われます。また、ソケット通信を行う関数やメソッドに指定したパラメーターに応じてトランスポート層のプロトコルが選択され、そのプロトコルに従った通信・制御が行われるようになっています。

TCP/IPモデルとソケット通信との関係性を示した図

なんですが、複数のプログラムの間で、どういったデータをどういう順序でやりとりするのかについてはソケット通信は関与しません。このような、送受信のルールを定めているのがアプリケーション層のプロトコルになります。

アプリケーション層のプロトコルについての説明図

そして、ソケット通信を利用する開発者は、これらを自分で決めてプログラムの開発を行う or 既に決められたアプリケーション層のプロトコルに従って開発を行う必要があります。そして、この既に決められたアプリケーション層のプロトコルの1つが HTTP となります。

HTTPがアプリケーション層のプロトコルの1つであることを示す図

つまり、ソケット通信はトランスポート層をサポートする通信で、ソケット通信を利用することで相手のプログラムにデータを届けたり、相手のプログラムからデータを受け取ったりすることができるようになります。なんですが、データの送受信ができたとしても、通信プログラムを開発するためには、送受信するためのデータのルール(データのフォーマットや送受信する順序など)を決め、それらに従ってデータの送受信を行うように設計・実装する必要があります。

でないと、期待するデータが受信できなかったり、送受信の順序がバラバラになって通信に不整合が発生することになります。例えば、送受信のルールが破綻していたり、ルールに従わずにプログラムが開発したりすると、下の図のようにお互いのプログラムが受信待ちをずっとしてデータのやりとりが開始されないようなことになる可能性があります。

お互いにデータの受信待ちをしていてデータのやりとりが開始されない様子

そんなことにならないように、従うべき送受信ルールを定め、それに従ってプログラムを開発することが重要となります。そして、そのルールを定めたものがアプリケーション層のプロトコルであり、そのプロトコルの1つが HTTP となります。

また、前述のとおり HTTP はアプリケーション層のプロトコルですが、HTTP で通信を行う場合、その下位のトランスポート層のプロトコルとしては TCP が採用されることが多いです。そのため、このページではトランスポート層のプロトコルに TCP を利用して、さらにアプリケーション層のプロトコルに HTTP を利用するケースをベースに解説していきます。

HTTPを採用する場合はトランスポート層のプロトコルとしてTCPを採用することが多いことを示す図

HTTP クライアント

こういった通信プログラムはクライアントサーバーモデルに従って開発することが多いです。サーバーとはサービスを提供するプログラムで、クライアントはサーバーにサービスの提供要求するプログラムになります。

クライアントとサーバーの関係を示す図

HTTP に従って通信を行うプログラムもクライアントサーバーモデルに従って開発され、特に HTTP に従って通信を行うクライアントは HTTP クライアントHTTP に従って通信を行うサーバーを HTTP サーバーと呼びます(ウェブサーバーと呼ぶこともあります)。

また、HTTP サーバーが提供するサービスは HTTP クライアントからのリクエストの内容によって異なります。このリクエストの内容の詳細に関しては次の章で解説しますが、基本的にはメソッドと URL によってリクエスト内容が決まり、HTTP サーバーは、そのリクエストの内容に応じた処理の実行とデータの返却を行います。

HTTP クライアントの代表例はウェブブラウザーで、リンクをクリックした際には、そのリンクに設定された URL に対するメソッド GET のリクエストが HTTP サーバーに送信されることになります。同様に、URL バーに URL を入力したような際には、その URL バーに入力された URL に対するメソッド GET のリクエストが HTTP サーバーに送信されることになります。

ウェブブラウザーがHTTPクライアントであることを示す図1

そして、そのリクエストを受け取った HTTP サーバーは、そのリクエストに応えるためのデータをレスポンスとしてウェブブラウザーに返却します。このレスポンスを受け取ったウェブブラウザーは、その受け取ったレスポンスに基づいてページを描画します。これにより、ウェブブラウザー上にページが表示され、ユーザーはクリックしたリンク先のページや URL バーに入力した URL のページを閲覧することができることになります。

ウェブブラウザーがHTTPクライアントであることを示す図2

このページでは、この HTTP クライアントの開発手順を解説していきます。ただ、ウェブブラウザーの場合、受け取ったデータに基づいてページを描画するような機能まで備えていますが、ここでは単純に HTTP クライアントの通信部分のみ、つまり、HTTP に従ったリクエストを送信し、HTTP に従ったレスポンスを受信することのみを行う HTTP クライアントの開発手順を解説していきます。もちろん、これを拡張すれば、あなた独自のウェブブラウザーも開発することができます。が、かなり大変…。

スポンサーリンク

HTTP 通信でのデータの送受信のルール

次は、本題である HTTP 通信でクライアントとサーバーとの間でやりとりされるデータのルール、具体的には送受信するデータの順序と送受信するデータのフォーマットについて解説していきます。

送受信するデータの順序

まず、クライアント・サーバー間で送受信されるデータの順序について解説します。

バージョンアップを重ねるごとに複雑にはなっていっているのですが、特にバージョン 1.0 の HTTP の場合、クライアント・サーバー間で送受信されるデータの順序は非常にシンプルです。その順序とは、クライアントがリクエストを送信し、サーバーがそのリクエストに対するレスポンスを返却するという流れのみになります。

HTTPでのデータの送受信の流れ

なので、ソケット通信観点で言えば、クライアントはリクエストを一度送信し、さらにサーバーから送信されてきたデータを全て受信すれば、リクエストに応じたデータの取得を行うことができることになります。

事前に接続の確立が必要

ただし、トランスポート層のプロトコルが TCP の場合は、これらのデータの送受信の前に接続の確立が必要となります。HTTP クライアントの通信相手となる HTTP サーバーはトランスポート層のプロトコルに TCP を採用することが一般的になっているので、基本的には HTTP クライアントでもトランスポート層のプロトコルに TCP を採用し、リクエストの送信前に接続要求を HTTP サーバーに対して接続を確立する必要があると考えて良いです。

そして、接続を確立した HTTP サーバーに対してリクエストの送信や HTTP クライアントからのリクエストの受信を行うことになります。

HTTP クライアントの場合、前述のとおり通信相手は HTTP サーバーとなり、HTTP サーバーは基本的にポート番号 80 で接続要求の受付待ちを行っているので、接続の確立を行うために HTTP クライアントは「HTTP サーバーの IP アドレス&ポート番号 80」に対して接続要求を送信する必要があります。より具体的には、Python の socket モジュールを利用する場合は、connect(('HTTP サーバーの IP アドレス, 80)) を実行して接続要求を送信する必要があります。

MEMO

練習用に HTTP サーバーを動作させるような場合は 80 ではないポート番号(80008080 など)が利用されることが多いです

接続の確立さえ完了すれば、後は sendsendall メソッドを実行すれば接続の確立相手の HTTP サーバーにリクエストが送信されますし、recvrecvfrom メソッドを実行すれば接続確立相手の HTTP サーバーからレスポンスを受信することが可能です。

TCP通信におけるconnectとsend/recvの関係性

ということで、あとは送信するリクエストを HTTP に従ったフォーマットで用意し、さらに受信するレスポンスを HTTP に従ったフォーマットで扱ってやれば HTTP クライアントが完成することになります。

また、上記では connect メソッドの引数に HTTP サーバーの IP アドレス を指定することを前提として解説を行っていますが、HTTP の場合は IP アドレスよりもドメイン名やホスト名を指定することの方が多いです。

例えば、http://daeudaeu.work/ のページを表示するような場合は connect(('daeudaeu.work', 80) を実行して HTTP サーバーと接続を確立することができます(ただし、この場合は HTTP クライアントを実行する PC が DNS サーバーと接続されている必要があります。

ドメイン名を指定してconnectを行う様子

また、接続の切断に関しては毎回必須というわけではなく、HTTP 1.1 の場合、ヘッダーの Connection フィールドに keep-alive を指定することで、サーバーがレスポンスを送信した後も接続の確立を維持し続けたまま新たなリクエストの受付を行うことが可能となっています。

ただ、今回は Connection には close を指定することを前提とした解説を行っていきます。この場合は HTTP クライアントはレスポンス受信後にソケットの close を毎回行う必要がありますが、処理が毎回同じになるのでクライアントの作りとしてはシンプルになります。

リクエストのフォーマット

次は、HTTP に従ったリクエストのデータフォーマットについて説明します。この HTTP におけるリクエストは HTTP リクエストと呼ばれます。

HTTP リクエストのデータのフォーマットの大枠は下記のようになります。このように、HTTP リクエストは4つの種類のデータから構成され、1行目がリクエストライン、2行目以降がヘッダー空行を挟んで、以降がボディになります。リクエストラインとヘッダーフィールド、および空行の各行は改行コード \r\n (CRLF) で区切られます。

HTTPリクエストのデータフォーマット

スポンサーリンク

リクエストライン

HTTP リクエストにおいて一番重要なデータは「リクエストライン」となります。クライアントがサーバーに対してリクエストする内容のほとんどはリクエストラインに情報が詰まっていると言っても過言ではありません。

リクエストラインは下記の3つの情報から構成されるデータになります。これらのデータはスペースで区切られます。

データの種類 値の例
メソッド GETPOST など
URL //index.html など
HTTP のバージョン HTTP/1.0HTTP/1.1 など

URI でリクエスト対象の「リソース」メソッド でリクエストの「目的」を示すようになっており、これらの情報から「何をどうしたいのか?」という情報がサーバーに伝わるようになっています。あとは、クライアントが対応している HTTP のバージョンを HTTP のバージョン で指定するようになっています。

例えば下記のようなリクエストラインの場合、クライアントはサーバーに対して HTTP/1.1 の通信で /index.htmlGET したい (取得したい) とサーバーにリクエストすることになります。

GET /index.html HTTP/1.1\r\n

このリクエストを受け取ったサーバーは、/index.html のページを表示するためのデータをレスポンスとしてクライアントに返却することになります。そして、これにより、クライアントの /index.htmlGET したいというリクエストが満たされることになります。

メソッド

メソッド には下記のような種類が存在し、クライアントはリクエストの目的に応じて メソッド を適切に指定する必要があります。

  • GET:データを取得したい
  • POST:データを新規作成したい
  • PUT:データの全体を更新したい
  • PATCH:データの一部分のみを更新したい
  • DELETE:データを削除したい

例えばウェブブラウザーでウェブページを表示するのであれば、ウェブページを表示するためのデータ(HTML や CSS や画像など)を取得することを目的にリクエストを送信するため、その メソッド には GET が指定されることになります。また、ウェブブラウザーで掲示板等にコメントを新規投稿するのであれば、コメントというデータを新規作成することを目的にリクエストを送信するため、その メソッド には POST が指定されることになります。

こんな感じで、メソッド を適切に設定することで、クライアントのリクエストの目的をサーバーに伝えることができます。

GETPOST 以外に関しては少し分かりにくいかもしれませんが、例えば REST API 等を利用する場合は、どのメソッドを利用してリクエストを送信すべきかが API の仕様書等に記載されているはずなので、それに従ってメソッドを指定してやれば良いことになります。

URL

そして、上記のようなメソッドに応じた要求を「何に」対して行いたいのかを指定するのがリクエストラインにおける URL の役割になります。

具体例で考えると、やっぱりウェブブラウザーでのウェブページの表示が分かりやすくて、前述のとおり、ウェブブラウザーの URL バーに URL を指定すると、その URL を表示したいというリクエストが HTTP サーバーに対して送信されることになります。

ウェブブラウザーのURLバーにURLを指定すると、そのURLに対するリクエストがウェブブラウザーに送信される様子

このとき、URL バーに入力した URL が http://daeudaeu.work/ であったとすると、メソッドに GET が、URL に / が指定されたリクエストラインのリクエストが HTTP サーバーに対して送信されることになります。

GET / HTTP/1.1\r\n

上記の例を見て、リクエストラインの URLhttp://daeudaeu.work/ になっていないことに疑問を感じた方もおられるかもしれません。これは単に URL の指定方法の違いによるものだけで、別にリクエストラインの URL に http://daeudaeu.work/ のような URL 全体を指定しても問題はありません。このような URL の指定の仕方は “絶対パス指定” と呼ばれます。それに対し、上記のような / は、URL からドメイン名(上記の場合は daeudaeu.work)までの部分を省略した “ルートパス指定” による URL の指定の仕方であり、この URL の指定の仕方でもリクエストラインの URL は指定可能です。リクエストラインの場合、このルートパス指定で URL を指定することの方が多いと思います。

URLの指定の仕方についての説明図

ちなみに、事前に接続の確立が必要 で少し説明しましたが、ドメイン名の部分はリクエストラインの URL には不要ですが、接続を確立する際に必要となります。

ドメイン名に対応する HTTP サーバーと接続を確立してしまえば、後はその接続を確立した相手同士でリクエストやレスポンスのやり取りが行われることになります。なので、ドメイン名に対応する HTTP サーバーと接続さえ確立してやれば、後は URL 等でドメイン名を指定することは不要となります。

リクエストのヘッダー

ヘッダーはリクエストの詳細をサーバーに伝えるためのデータとなります。ヘッダーは複数の行から構成され、各行は フィールド名: 値\r\n という形式になります。

この フィールド名 には多種多様なものが存在します。代表的なものを下記の表にまとめておきます。例えば、HTTP リクエストのボディの情報をサーバーに伝えるようなフィールドとして Content-LengthContent-Type がありますし、逆に HTTP サーバーから「こういうボディを送信してきて欲しい」と要求するような AcceptAccept-Encoding も存在します。

フィールド名 意味
Host 送信先のホスト名・ポート番号
Content-Length リクエストのボディのサイズ
Content-Type リクエストのボディのフォーマット名
User-Agent クライアントのソフトの情報
Connection 接続の維持 / 切断
Accept クライアントが受付可能なボディのフォーマット
Accept-Language クライアントが受付可能なボディの言語
Accept-Encoding クライアントが受付可能なボディの符号化手法

これらのフィールド全てをヘッダーに指定する必要があるというわけではありません。ただし、HTTP サーバーや HTTP サーバーの設定、メソッド等によっては必須になるフィールドも存在するため、その点には注意してください。

リクエストのボディ

ボディはサーバーに送信するデータの本文となります。例えば、掲示板に投稿したい場合は、投稿したい文章をボディにセットした状態でリクエストを送信することになります。また、画像をウェブサイトにアップロードするような場合も、その画像データはボディとして HTTP サーバーに送信されることになります。

HTTPリクエストにおけるボディの役割を示す図

このように、サーバーに対してデータの新規作成やデータの更新等をリクエストする場合は、どんなデータを新規作成したり、どんなデータに更新したいのかをサーバーに伝える必要があります。この「どんなデータ」という情報をサーバーに伝えるために、このボディをサーバーにリクエストに含めて送信することが必要となります。この情報無しに、単にデータを新規作成して欲しいとリクエストされても、サーバーは「どんなデータ」を新規作成すれば良いかが分からず困ってしまいますよね…。

POSTやPATCHメソッドにおけるボディの重要性を説明する図

逆に、サーバーに対してデータの取得を要求するだけであれば、このボディは不要となることが多いです。つまり、リクエストの目的によってはボディが不要になる場合もあります。前述のとおり、リクエストの目的はリクエストラインのメソッドで伝えられることになり、このメソッドが GET の場合はボディが不要になることが多く、逆に POSTPATCHPUT などのメソッドの場合はボディが必要となります。

また、このボディのデータのフォーマットはヘッダーの Content-Type によりサーバーに伝えることができますし、ボディのサイズは Content-Length により伝えることができます。

スポンサーリンク

レスポンスのフォーマット

次は、サーバーから送信されてくるレスポンスのフォーマットについて解説します。この HTTP 通信でのレスポンスは HTTP レスポンスと呼ばれます。

HTTP レスポンスのデータのフォーマットの大枠は下記のようになります。見ていただければ分かる通り、大枠に関して言うと HTTP リクエストとほぼ同じで、HTTP レスポンスは4つの種類のデータから構成され、1行目がステータスライン、2行目以降がヘッダー空行を挟んで、以降がボディになります。ステータスラインとヘッダーフィールド、および空行の各行は改行コード \r\n (CRLF) で区切られます。

HTTPレスポンスのデータフォーマット

ステータスライン

ステータスラインは下記の3つのデータから構成されます。これらの各データはスペースで区切られます。

データの種類 値の例
HTTP のバージョン HTTP/1.0HTTP/1.1 など
ステータスコード 200400 など
ステータスコードの詳細 OKAccepted など

ステータスラインの中で特に重要なるのが ステータスコード で、この ステータスコード により、クライアントはリクエストの結果を知ることができるようになってます。ステータスコード は3桁の数値として表現され、一番左側の桁の値によってステータスコードは下記のように分類されます(1xx は省略しています)。

  • 2xx:リクエストの処理に成功
  • 3xx:URL が移動
  • 4xx:クライアントが原因でリクエストの処理に失敗
  • 5xx:サーバーが原因でリクエストの処理に失敗

そして、残りの2桁でサーバーでの処理結果の詳細が伝えられることになります。一番重要なステータスコードは 200 で、これは単純に送信した HTTP リクエストの処理に成功したことを示すコードになっています。

前述のとおり、リクエストの結果を示すのが ステータスコード であるため、クライアントはレスポンスを受け取ったら、まず自身が送信した HTTP リクエストの結果を知るために ステータスコード から確認する必要があります。

レスポンスのヘッダー

ヘッダーは、リクエスト同様に、レスポンスの詳細を伝えるためのデータとなります。また、レスポンスのヘッダーも複数の行から構成され、各行は フィールド名: フィールドの値\r\n という形式になります。

特にクライアントにおいて、このレスポンスのヘッダーは重要です。リクエストの場合、クライアントの都合の良いようにフィールドを指定したり、サーバー側で必須となるフィールドのみを指定してやれば良いだけですが、特に次に説明するボディのデータの受信の仕方やボディのデータの扱い方がヘッダーに記載されているため、クライアントはヘッダーの各種フィールドの値に応じてボディを受信する処理を変更したり、さらには受信したボディのデータの扱い方(ボディのデコード時の文字コードの指定の仕方や分割されたデータの結合の仕方)を適切に切り替える必要があります。

例えば、ヘッダーに Content-Length が存在すれば、ボディを全て受信するためには、recv メソッドで受信する処理を Content-Length の値分のデータを受信するまで繰り返し実行する必要があります。

また、ヘッダーに Content-Type: text/html; charset=euc-jp が存在すれば、ボディは HTML として扱い、さらに文字コードに euc-jp を指定してボディのデコードを行う必要があります。

さらに、ヘッダーに Transfer-Encoding: chunkedContent-Encoding: gzip が存在すれば、ボディは1つのデータではなく複数のデータの塊(チャンク)としてサーバーから送信されてくることになり、さらに各データの塊は gzip と呼ばれる圧縮形式で圧縮されていることになります。なので、複数のデータを結合したり、gzip 形式のデータをデコードしたりすることが必要となります。

単にボディを全て受信するだけであれば簡単にクライアントは開発できますが、上記のように、本来であればクライアントはヘッダーに応じて適切に処理が実行されるように開発する必要があります。

スポンサーリンク

レスポンスのボディ

ボディはサーバーから受信するデータの本文となります。

前述でも説明しましたが、ボディがどういった形式のデータであるかはヘッダーに記載されています。具体的には、ヘッダーの Content-Type フィールドに記載されています。 

特にメソッドに GET を指定してリクエストを送信した場合、このボディこそがクライアントが求めているデータとなります。例えば、ウェブページを表示するための HTML や画像ファイル・CSS ファイル等が、このボディとしてサーバーから送信されてくることになります。なので、特にメソッドに GET を指定してリクエストを送信した場合は、このボディが非常に重要なデータとなります。

メソッドGETのリクエストに対するレスポンスのボディの重要性を説明する図

逆に、メソッドに POSTPATCH 等を指定してサーバーにデータの新規作成や更新をリクエストした場合、サーバーから受信するデータよりも、サーバーで実行されるデータの新規作成処理や更新処理の結果の方が重要になります。そして、それらが成功したか否かは ステータスコード により確認できるため、これらのメソッドでリクエストを送信する場合はボディよりもステータスラインが重要なデータとなると言えます。ただし、上記のような処理に失敗した場合、その失敗した理由等を伝えるメッセージがボディとしてサーバーから送信されてくることもありますので、このような場合はボディも重要なデータとなると言えます。

HTTP クライアントの作り方

ということで、ここまで HTTP や HTTP クライアントについて解説してきました。

ここからは、実際にソケット通信を利用した HTTP クライアントの Python での作り方を解説していきたいと思います。ただ、ここまで解説してきた内容を全て踏まえて HTTP クライアントを開発するとなると、開発の仕方も解説内容も複雑かつ大規模になってしまいます…。そのため、ここでは基本的には「リクエストラインの メソッド は GET のみ」、さらに「HTTP レスポンスのヘッダーには必ず Content-Length が存在する」という前提の基に、機能を制限した簡単な HTTP クライアントの作り方を解説していきたいと思います。

かなり機能を制限した HTTP クライアントにはなるのですが、それでも HTTP クライアントの作り方の基本は理解していただけると思いますし、ここで作成したものを拡張していけば、もっと多くのリクエストやレスポンスを処理できる HTTP クライアントに仕立てていくことが可能です。

基本は TCP 通信

では、ソケット通信を利用した HTTP クライアントの作り方を説明していきます。

HTTP クライアントを開発する際には、まず TCP 通信を行うクライアントを作成しましょう。それが HTTP クライアントのベースとなります。

世の中に存在する HTTP サーバーの多くはトランスポート層のプロトコルとして TCP を採用しているものが多いです。それらの HTTP サーバーと通信を行うためには、 HTTP クライアントもトランスポート層のプロトコルは TCP を採用する必要があります。

そして、基本的な TCP 通信を行うクライアントのスクリプトは下記のような構造になります。TCP 通信では、データの送受信の前にサーバー・クライアント間で接続を確立する必要があります。この接続を確立するために connect 関数を実行する必要があるという点が、TCP 通信を行うクライアント開発時のポイントになると思います。

TCP通信のクライアント
import socket
import sys

hostname = sys.argv[1]
port = sys.argv[2]
url = sys.argv[3]

def main():
    # ソケットを生成
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 接続要求を送信して接続を確立
    sock.connect((hostname, int(port)))

    # HTTP リクエストを送信
    send_http_request(sock, hostname, port, url)

    # HTTP レスポンスを受信
    status_line, headers, body = recv_http_response(sock)
    
    # ボディをファイルに保存
    with open('body.html', 'wb') as f:
        f.write(body)
    
    # ソケットをクローズ
    sock.close()

if __name__ == '__main__':
    main()

TCP 通信を行うクライアントの作り方については下記ページで詳しく解説していますので、詳細は下記ページを参照していただければと思います。

Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)

これで、HTTP クライアントのベースが出来上がったことになります。上記のクライアントのスクリプトは、HTTP でのデータの送受信の順序に従うように、まず HTTP リクエストとなるデータを send_http_request 関数でサーバーに対して送信し、さらにサーバーから HTTP レスポンスを recv_http_request 関数で受信する流れで処理を行うようになっています。そして、受信したレスポンスのボディ部分を body.html という名前でファイル保存するようにしています。

ただ、上記のスクリプトでは、HTTP リクエストを送信する send_http_request 関数と、HTTP レスポンスを受信する recv_http_response 関数とが定義されていません。これらは、まさに HTTP クライアント開発時のポイントになる部分になりますので、これらの関数の定義の仕方については後述で詳細を解説します。とりあえず、上記では HTTP に従った順序でデータの送受信を行うようになっているため、後は HTTP リクエスト送信を行う関数 send_http_request と HTTP レスポンス受信を行う関数 recv_http_response を実装すれば HTTP 通信が実現できることになります。

また、この HTTP クライアントはコマンドライン引数で下記の3つが指定されることを前提とした作りになっています。

  • 引数1:IP アドレス or ホスト名
  • 引数2:ポート番号
  • 引数3:URL

そして、この HTTP クライアントは 引数1引数2 で指定された IP アドレス (ホスト名) &ポート番号に対して connect を実行して HTTP サーバーと接続を確立し、その後 HTTP に従ってデータの送受信を行うようになっています。また、クライアントから送信する HTTP リクエストにおけるリクエストラインの URL 部分は 引数3 となるようになっています。

こういう作りにしているため、動作確認時に使用する HTTP サーバーやリクエスト対象の URL に応じてコマンドライン引数を指定してやれば、いろんな HTTP サーバーとの HTTP 通信の動作確認を行うことが可能です。

スポンサーリンク

HTTP リクエストの送信

次は、HTTP リクエストの送信処理を行う send_http_request 関数を作成していきましょう!

ここまで説明してきたように、HTTP クライアントから送信するデータは HTTP リクエストのフォーマットに従う必要があります。HTTP リクエストのフォーマットについては リクエストのフォーマット で記載した通りで、1行目のデータがリクエストライン、2行目以降のデータがヘッダー、さらに空行を挟んでボディが続くフォーマットとなります。

HTTPリクエストのデータフォーマット

なので、HTTP 通信を行うのであれば、クライアントは上記のフォーマットに合わせたデータを作成し、それを送信する必要があります。具体的には、リクエストラインとなる文字列、ヘッダーとなる文字列、空行となる文字列、さらに必要に応じてボディとなるデータを用意し、これらを結合した結果を送信する必要があります。

リクエストラインの生成

ということで、まずはリクエストラインを作成していきましょう!

前述で紹介したスクリプトのとおり、リクエストラインにおける URL はコマンドライン引数で指定されることを想定したスクリプトになっており、url = sys.argv[3] によって url にセットされることになります。そして、今回はメソッドとして GET のみに対応することとしています。さらに、HTTP のバージョンを 1.1 とすれば、リクエストラインの文字列は下記によって生成できることになります。

リクエストライン
request_line = f'GET {url} HTTP/1.1\r\n'

改行コードには \r\n を指定する必要がある点に注意しましょう。

また、今回はメソッドを GET にのみ対応としていますが、他のメソッドでのリクエストを送信したい場合は上記の GET 部分を POSTPATCH 等に変更すればよいことになります。

ヘッダーの生成

次はヘッダーを生成していきます。

ヘッダーは複数行で構成されることになり、各行は フィールド名: フィールドの値\r\n という形式になります。 

例えばですが、フィールド ConnectionHost を指定するヘッダーは下記のように生成することができます。hostnameport はコマンドライン引数で指定されたホスト名およびポート番号となります(他の HTTP クライアントで試してみたらポート番号が 80 の場合は Host にポート番号は記載しないようだったので、それを真似て 80 以外の場合のみポート番号をヘッダーに出力するようにしています)。

ヘッダーの追加
header = 'Connection: close\r\n'
if port == '80':
    header += f'Host: {hostname}\r\n'
else:
    header += f'Host: {hostname}:{port}\r\n'

これらの処理を見ていただければ分かる通り、1行分の フィールド名: フィールドの値\r\n の形式の文字列を用意し、後は、その文字列に対して フィールド名: フィールドの値\r\n の形式の文字列をどんどん結合していけばヘッダーが出来上がることになります。

空行の生成

HTTP リクエストにおいては空行が重要な意味合いを持っており、この空行もしっかり HTTP リクエストのデータに含ませる必要があります。この空行は、要は単なる \r\n となりますので、このデータを生成してやればよいだけです。

空行の生成
blank = '\r\n'

ボディの生成

今回はメソッドが GET であることを前提としており、メソッドが GET の場合はボディが不要になることが多いと思います。他のメソッドでボディを送信する必要があるのであればボディも一緒に HTTP リクエストとして送信する必要があります。また、おそらくですが、その場合はボディのデータのフォーマットやボディのサイズのフィールドをヘッダーに追加してやる必要があるので注意も必要です。

例えば、下記のような JSON のデータをボディとして送信するのであれば、

ボディの例
body = '{"username": "YamadaHanako", "message": "Hello World!"}'

ヘッダーには下記のようなフィールドを追加してやる必要があります。

ボディの情報のヘッダーへの追加
header += f'Content-Length: {len(body.encode())}'
header += 'Content-Type: application/json'

こんな感じで、ボディも含めて送信する場合は、そのボディがどのようなコンテンツであるかをヘッダーを使ってサーバーに伝える必要がある点に注意してください。

また、どのようなボディを送信する必要があるかについては、通信相手となる HTTP サーバーやウェブアプリ等の仕様によって異なります。例えば REST API の場合などは仕様書にボディの形式等が明記されているはずなので、それを読んで送信するボディを決めてやる必要があります。

HTTP リクエストの生成

ここまでの処理でリクエストライン+ヘッダー+空行(+必要に応じてボディ)のデータが用意できたので、後はこれらを結合してやることで HTTP リクエストの文字列が生成されることになります。

さらに、この文字列を encode メソッドを利用してバイト型のデータに変換してやれば、HTTP サーバーに送信すべき HTTP リクエストが完成したことになります!

HTTPリクエストの生成
request_str = request_line + header + blank
request = request_str.encode()

送信

あとは、HTTP サーバーとの接続を確立済みのソケットを利用して HTTP リクエストを送信してやれば良いだけです。全てのデータが送信できるよう、send メソッドではなく sendall メソッドを利用して送信するのが良いと思います。

HTTPリクエストの送信
# sock:HTTPサーバーと接続確率済みのソケット
sock.sendall(request)

以上で、HTTP リクエストの HTTP サーバーへの送信処理が完了したことになります。ここで紹介したコードを統合してやれば、HTTP リクエストの送信処理を行う send_http_request 関数が完成することになります。この send_http_request 関数の全体のコードに関しては、後述の HTTP クライアントのサンプル で紹介します。

HTTP レスポンスの受信

次は、HTTP レスポンスの受信を行う recv_http_request 関数を作成していきましょう!

HTTP サーバーは、クライアントからの HTTP リクエストを受信すると HTTP レスポンスを返却するようになっています。これは、そういうふうに HTTP としてプロトコルが規定されているからになります。つまり、クライアント側の視点で言えば、HTTP リクエストを送信した後に recv メソッドを実行すれば HTTP レスポンスのフォーマットのデータが受信できることになります。

この HTTP レスポンスのフォーマットについては レスポンスのフォーマット で記載した通りで、1行目のデータがステータスライン、2行目以降のデータがヘッダー、さらに空行を挟んでボディが続くフォーマットとなります。

HTTPレスポンスのデータフォーマット

HTTP レスポンス全体の受信の実現方法

ただし、下記ページでも解説しているように recv メソッドでは受信するデータが途切れる可能性があるため、相手が送信してきたデータ全体を確実に受信するためには recv メソッドを繰り返し実行する必要があります。

受信データが途切れてしまう問題の解決方法の解説ページアイキャッチ ソケット通信で受信したデータが途切れてしまう問題の解決方法

この時に「全てのデータが受信できたかどうかをどう判断すれば良いの?」という点がポイントになります。これが判断できるのであれば、データが全て受信できるまで recv メソッドを繰り返せばよいだけです。で、これに関しては、HTTP の場合は上記ページでも解説している次のいずれかの方法で判断可能です。

今回は HTTP レスポンスのヘッダーに Content-Length が存在することを前提としており、この Content-Length が上記の方法①における “送信するデータのサイズ” に該当します(念のため補足しておくと、この “送信する” の主語は HTTP サーバーになります)。今回は、この Content-Length を利用し、上記における方法①で HTTP レスポンス全てを受信する処理を実現していきたいと思います。具体的には、受信したボディのサイズが Content-Length 未満の間、繰り返し recv メソッドを実行することで HTTP レスポンスの全体を受信することを実現していきます。

で、これを実現するためには、まずヘッダー全体を受信して Content-Length の値を取得することが必要となります。

ただし、クライアントは事前に HTTP レスポンスのヘッダーのサイズを知ることができないため、クライアントはヘッダーの最後までのデータをキリよく受信するような制御はできません。また、先ほど示したページでも解説しているように、recv メソッドで一度に受信できるサイズも状況によって変化する可能性があります。なので、recv メソッドを一度実行しても、ヘッダーの途中までしか受信できないケースもありますし、ヘッダー全体だけでなくボディの一部まで一度に受信されるケースもあります。

recvメソッドで受信できる範囲がクライアントでは制御できないことを示す図

したがって、HTTP レスポンスの受信処理は、こういった様々なケースを考慮し、これらのどんなケースでも対応できるように実装する必要があります。前述の例で言えば、まだヘッダー全体が受信できていないのであれば、まずはヘッダー全体を受信できるまで recv メソッドを繰り返し実行する必要があります。また、既にボディの一部まで受信できているのであれば、ヘッダーから Content-Length の値を取得し、Content-Length - 既に受信済みのボディのサイズ 分のデータの受信を追加で行う必要があります。

こんな感じで、ソケット通信で HTTP クライアントを開発する場合、まずはステータスラインを含むヘッダーまでを受信し、次にヘッダーの値に基づきボディの受信を行うような2段階的なデータの受信処理が必要となります。

HTTPクライアントが2段階的に受信を行うことが必要であることを示す図

このような受信処理についてもう少し詳しく説明していきます。

ステータスライン+ヘッダーの受信

この「ヘッダー全体が受信できたかどうか」を判断する時に活躍するのが HTTP レスポンスに含まれる空行 になります。この空行の実体は \r\n であり、さらにヘッダーの各行は \r\n で区切られます。したがって、受信したデータの中に \r\n\r\n が存在すれば既にヘッダー全体を受信できたと判断することが可能です。つまり、ヘッダー全体を受信するためには、受信したデータに \r\n\r\n が含まれていない間、recv を繰り返し実行してやれば良いことになります。

したがって、ヘッダー全体(ステータスラインを含む)の受信に関しては下記のような処理で実現できます。

ヘッダーの受信処理
# ステータスラインとヘッダーの受信
    data = b''
    while b'\r\n\r\n' not in data:
        # 空行が含まれるまでrecvを繰り返す
        recv_data = sock.recv(1024)

        # 受信したデータを結合
        data += recv_data

        if len(recv_data) == 0:
            # 相手のソケットがシャットダウン
            break

Content-Length の取得

上記を実行すれば確実にヘッダー全体を受信することが可能です。そして、このヘッダーには所望の Content-Length フィールドも存在します(前述の通り、今回は Content-Length がヘッダーに存在することを前提とした解説をしています)。

ただ、受信したデータはヘッダーだけでなくボディも含まれる可能性があります。さらに、受信したデータにはステータスラインも含まれることになります。ステータスラインからステータスコードを取得したり、ヘッダーから所望のフィールドを取得したり、ボディの受信をしたりする上では、これらの各データを分割しておいた方が楽ですので、ここで受信したデータの「ステータスライン」「ヘッダー」「ボディ」への分割を行なっておきます。

まず、ヘッダーとボディの間には \r\n\r\n が必ず存在しますので、受信したデータに対して split('\r\n\r\n') を実行すればステータスライン+ヘッダーとボディに分割することが可能です。

空行による分割で受信したデータをステータスライン+ヘッダーとボディに分割する様子

さらに、ステータスラインやヘッダーの各行は \r\n で区切られますので、ステータスライン+ヘッダーのデータに対して split('\r\n') を実行すれば行ごとのリストに分割することが可能です。そして、そのリストの先頭のデータがステータスラインであり、それよりも後ろ側のデータが全てヘッダーということになります。

空行で分割したデータを\r\nで分割してステータスラインとヘッダーに分離する様子

こんな感じでデータの分割を行えば、受信したデータをステータスライン・ヘッダー・ボディの各データに分割することができます。

具体的には、下記の parse_http_response 関数によりデータの分割を実現することができます。引数 data は事前に受信したデータとなります。2つ目の返却値 headers は、ヘッダーの各行を要素とするリストとなり、他の返却値は単純なバイト型のデータになります。

各データへの分割
def parse_http_response(data):
    """受信したデータを分割"""

    # ステータスライン+ヘッダーとボディを分割
    status_header__body = data.split(b'\r\n\r\n')
    status_header = status_header__body[0]
    body = status_header__body[1]

    # ステータスライン+ヘッダーを行ごとに分割
    headers = status_header.split(b'\r\n')

    # 1行目がステータスライン
    status_line = headers[0]

    # 2行目以降がヘッダー
    del headers[0]

    return status_line, headers, body

次はヘッダーから Content-Length のフィールドの値を取得しましょう!これは単純にヘッダーのリストの中から Content-Length という文字列が存在する行を探索し、さらにその行を : で分割してやることで実現できます。これは、前述の通り、ヘッダーの各行は フィールド名: フィールドの値 という形式で構成されているからになります。

つまり、ヘッダーの各行を要素とするフィールドとするリストを引数 headers で受け取るとすれば、下記の get_content_length 関数により Content-Length のフィールドの値の取得を実現することができます。

Content-Lengthの取得
def get_content_length(headers):
    """Content-Lengthの値を取得"""
    
    for header in headers:
        if b'Content-Length' in header:
            # フィールドの値を取得してint型に変換
            field_name, field_value = header.split(b':')
            content_length = int(field_value.decode())
            return content_length

    return None

ボディの受信

Content-Length の値が取得できれば、あとは2段階目の受信処理としてボディの受信を行えば良いだけです。サーバーが送信してくるボディのサイズは Content-Length となりますので、受信済みのボディのサイズが Content-Length 未満の間 recv メソッドを繰り返せば良いだけです。具体的には、下記のような関数でボディ全体の受信を実現できます。引数 sock は HTTP サーバーと接続確率済みのソケット、引数 body は既に受信済みのボディ、引数 content_lengthContent-Length の値をそれぞれ指定して実行されることを想定した関数になっています。

ボディ全体の受信
def recv_by_content_length(sock, body, content_length):
    """残りのボディを全て受信"""
    
    # 残りのボディを受信
    while len(body) < content_length:
        recv_data = sock.recv(1024)
        body += recv_data

        if len(recv_data) == 0:
            # 相手のソケットがシャットダウン
            return body

    return body

こんな感じで、段階的に繰り返し recv メソッドを実行すれば、まずヘッダー全体を受信して所望のフィールドの値を取得し、その取得した値に応じてボディを受信することでボディ全体を確実に受信することが可能となります。

また、上記のような処理は、ヘッダーとボディの間に \r\n\r\n が存在することを前提としたものになっています。つまり、HTTP で、レスポンスのフォーマットが “ヘッダーとボディの間には空行が存在する” と規定されているために実現可能な処理になっています。このことからも、HTTP レスポンスのフォーマットにおける空行の重要性に気付いていただけるのではないかと思います。

受信後の処理の流れ

HTTP レスポンスが受信できれば、あとは HTTP クライアントで HTTP レスポンスを利用して実行したい処理を実行すれば良いだけです。

例えばステータスラインのステータスコードに応じてメッセージを出力しても良いですし、ボディをファイルとして保存するようにしても良いです。もちろん、本格的なウェブブラウザを作るのであればボディを解析してページを描画するようにしても問題ないです。

HTTP レスポンスを受信した後の処理は、あなた自身が開発したい HTTP クライアントに合わせて適当に実装していただければと思います。今回は、ボディをファイルとして保存する処理のみを行う HTTP クライアントのスクリプトを紹介します。

ヘッダーに応じた処理の切り替え

また、サーバーの実装や、サーバの設定、さらには HTTP リクエストの内容によって、サーバーから送信されてくる HTTP レスポンスのボディの形式等が異なるという点に注意してください。今回の例は Content-Length がヘッダーに含まれることを前提とした例になりますが、ヘッダーに Content-Length が含まれない場合もあります。そういった場合でも HTTP レスポンスの全体を受信できるように適切にクライアントを開発する必要があります。本格的な HTTP クライアントをソケット通信を利用して開発する場合、HTTP レスポンスの様々なケースを考慮し、色んなケースに対応できるように作りこむ必要がある点が一番開発の難しい点になると思います。

HTTP クライアントのサンプル

最後に HTTP クライアントのスクリプトのサンプルを示しておきます。

この HTTP クライアントは、処理を簡単にするために下記の制限を加えています。

  • HTTP リクエスト:
    • メソッドは GET 限定
    • ヘッダーは HostConnection フィールドのみ
      • Connection の値は close 固定
    • ボディは無し
  • HTTP レスポンス:
    • ヘッダーに必ず Content-Length が存在
    • ボディは符号化なし

このような制限も存在するため、サーバーによってはレスポンスが上手く受信できない場合もあり、あくまでも簡易版の HTTP クライアントという位置づけである点に注意してください。また、あくまでも作成するのは HTTP クライアントであり、HTTPS には対応していないので注意してください。

スポンサーリンク

サンプルスクリプト

HTTP クライアントのサンプルスクリプトは下記になります。基本は HTTP クライアントの作り方 で紹介したスクリプトや関数を統合した形で作成していますので、わからない点は HTTP クライアントの作り方 を参照していただければと思います。一部、ステータスコードの取得などは追加していますが、ここまでの説明を理解してくださっていれば処理内容も理解できると思いますので説明は省略させていただきます。

http_client.py
import socket
import sys

hostname = sys.argv[1]
port = sys.argv[2]
uri = sys.argv[3]

def create_http_request(hostname, port, uri):
    """HTTPリクエストを作成"""

    # リクエストライン
    request_str = f'GET {uri} HTTP/1.1\r\n'

    # ヘッダー
    if port == '80':
        request_str += f'Host: {hostname}\r\n'
    else:
        request_str += f'Host: {hostname}:{port}\r\n'
    request_str += 'Connection: close\r\n'
    
    # 空行
    request_str += '\r\n'

    # バイト型のデータに変換
    request = request_str.encode()

    print(request_str)

    return request

def parse_http_response(data):
    """受信したデータを分割"""

    # ステータスライン+ヘッダーとボディを分割
    status_header__body = data.split(b'\r\n\r\n')
    status_header = status_header__body[0]
    body = status_header__body[1]

    # ステータスライン+ヘッダーを行ごとに分割
    headers = status_header.split(b'\r\n')

    # 1行目がステータスライン
    status_line = headers[0]

    # 2行目以降がヘッダー
    del headers[0]

    return status_line, headers, body

def get_content_length(headers):
    """Content-Lengthの値を取得"""
    
    for header in headers:
        if b'Content-Length' in header:
            # フィールドの値を取得してint型に変換
            field_name, field_value = header.split(b':')
            content_length = int(field_value.decode())
            return content_length

    return None

def get_status_code(status_line):
    """ステータスコードを取得"""

    statuses = status_line.split(b' ')
    version = statuses[0]
    status_code = statuses[1]
    reason = b' '.join(statuses[2:])
    print(version, status_code, reason)
    return int(status_code)

def recv_by_content_length(sock, body, content_length):
    """残りのボディを全て受信"""
    
    # 残りのボディを受信
    while len(body) < content_length:
        recv_data = sock.recv(1024)
        body += recv_data

        if len(recv_data) == 0:
            # 相手のソケットがシャットダウン
            return body

    return body


def recv_response(sock):
    """HTTPレスポンスを受信する"""

    # ステータスラインとヘッダーの受信
    data = b''
    while b'\r\n\r\n' not in data:
        # 空行が含まれるまでrecvを繰り返す
        recv_data = sock.recv(1024)

        # 受信したデータを結合
        data += recv_data

        if len(recv_data) == 0:
            # 相手のソケットがシャットダウン
            break

    # ステータスライン・ヘッダー・ボディに分割
    status_line, headers, body = parse_http_response(data)

    # ステータスコードを取得
    status_code = get_status_code(status_line)
    if status_code != 200:
        print(f'エラーが発生しました:{status_code}')
        return
    
    # Content-Lengthフィールドの値を取得
    content_length = get_content_length(headers)

    if content_length is not None:
        # Content-Lengthフィールドが存在する場合のボディを受信
        body = recv_by_content_length(sock, body, content_length)
    
    else:
        print('ヘッダーに必要なフィールドが存在しません')
        return

    return status_line, headers, body

def main():
    # HTTPリクエストを生成
    request = create_http_request(hostname, port, uri)

    # ソケットを生成
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 接続要求を送信して接続を確立
    sock.connect((hostname, int(port)))

    # HTTP リクエストを送信
    sock.sendall(request)

    # HTTP レスポンスを受信
    status_line, headers, body = recv_response(sock)
    
    # ボディをファイルに保存
    with open('body.html', 'wb') as f:
        f.write(body)
    
    # ソケットをクローズ
    sock.close()

if __name__ == '__main__':
    main()

HTTP クライアントの動作確認

続いて、先ほど示した HTTP クライアントのスクリプトを実際に実行して動作確認を行ってみましょう!先ほど示したスクリプトのファイル名が http_client.py であることを前提に動作確認の手順を示していきたいと思います。

HTTP クライアントの動作は実際にウェブページの表示のリクエストを HTTP サーバーに送信してみることで確認可能です。

具体的には、ウェブブラウザーでウェブページを表示し、その時の URL を下記のように分解してコマンドライン引数に指定して http_client.py を実行すれば、そのウェブページの HTML を取得することが可能です。2つ目の引数に関しては 80 を指定すれば良いはずです。

URLとコマンドライン引数との関係性を示す図

ただ、このクライアントはあくまでも HTTP にしか対応しておらず、現在ではほとんどのウェブページが HTTPS にしか対応していないはずです。なので、HTTP に対応しているウェブページを見つけること自体がちょっと大変だと思います。そんな方は、是非下記のコマンドで http_client.py の動作確認をしてみていただければと思います。下記の daeudaeu.work は私が取得しているドメインで、超てきとうに作っているウェブページですが、HTTP にも対応しているので HTTP クライアントの動作確認に利用することが可能です。

python http_client.py daeudaeu.work 80 /

http_client.py を実行して上手く HTTP サーバーと通信が行われれば、HTTP サーバーから受信した HTTP レスポンスのボディ部分が body.html というファイル名で保存されます。

その body.html をウェブブラウザーにドラッグ&ドロップすれば、ウェブページっぽいものがウェブブラウザーに表示されるはずです。例えば上記のコマンドを実行した場合、ウェブブラウザーに下記の URL を指定したときと同様のページが表示されるはずです。

ページの表示の仕方の説明図

この body.html のページとしての描画自体はウェブブラウザーによって行われているのですが、HTML の受信自体は http_client.py によって行われています。つまり、保存された HTML ファイルのドラッグ&ドロップによってページが表示されるのは、http_client.py で HTTP サーバーと受信が行われ、HTTP レスポンスをサーバーから受信できているからになります。このことより http_client.py が HTTP クライアントとして正常に機能していることが確認できたことになります。

今回作成したスクリプトでは HTML をページとして描画する機能は存在しませんし、対応しているメソッドやヘッダーのフィールドも制限された簡単な HTTP クライアントにはなっていますが、それでも HTTP というアプリケーション層のプロトコルに従ったプログラムの開発が経験できたと考えて良いのではないかと思います。

もちろん、上記のスクリプトを発展させていけば、もっと本格的な HTTP クライアントを開発することも可能です。

requests モジュールや urllib モジュールの利用がオススメな理由

ただ、ページの冒頭でも触れたとおり、実用的な HTTP クライアントを求めるのであれば、ソケット通信を利用して HTTP クライアントを開発するよりも、既に HTTP クライアントとして出来上がっている requests モジュールや urllib モジュールを利用することをオススメします。

最後まで記事を読んでいただいた方には理解していただけたのではないかと思いますが、HTTP クライアントを開発するのはかなり大変です。今回紹介したスクリプトはメソッドは GET のみ、さらに HTTP レスポンスのヘッダーも Content-Length が存在することを前提としたものとなっています。それでも結構難しいスクリプトになっていますよね…。本格的な HTTP クライアントを開発しようと思うと、もっと他のメソッドにも対応する必要がありますし、HTTP サーバーから送信されてくる HTTP レスポンスのヘッダーに Content-Length が存在しない場合やボディが符号化されて送信されてくる場合など、様々なケースを考慮し、それらのケース全てに対応できるように HTTP クライアントを開発する必要があります。

それだけでなく、今回紹介したスクリプトでは例外時の処理を省略しており、本来であれば、データの送受信中に通信が途切れるなど、様々なケースに対応できるように例外処理を追加する必要があります。さらに、今回は HTTP のみに対応したクライアントの作り方を説明しましたが、セキュリティのことを考えるとクライアントは HTTPS に対応する必要もあり、暗号化等の知識も必要となります。

こういった点を考慮しながら HTTP クライアントを開発するよりも、既に上記のような様々なケースに対応済みの外部モジュール、具体的には requests モジュールや urllib モジュールを利用する方がオススメです。機能的にも品質的にも効率的にもオススメです。

なんですが、ソケット通信が全く利用価値のない通信・モジュールかというとそういうわけではなく、複数のプログラム間でちょっとした通信などを行うような場合は独自のプロトコルを採用することも多く、そういった場合はソケット通信が有効です。したがって、実用的な HTTP クライアントを開発することを目的にするのではなく、ソケット通信の知識を深めたり、さらにはプロトコルの知識を深める目的で HTTP クライアントをソケット通信を利用して開発してみるのは有益ですので、こういった目的であれば HTTP クライアントを作りこんでみることもオススメです!

ちなみに、上記で紹介した requests モジュールの使い方に関しては下記ページで紹介していますので、実用的な HTTP クライアントを利用したい場合は是非下記ページを参考にしていただければと思います。

requestsモジュールの解説ページアイキャッチ 【Python】reqeustsモジュールについて解説 requestsモジュールでのウェブアプリの操作の仕方の解説ページアイキャッチ 【Python】requestsモジュールでPOSTメソッドのリクエストを送信(ウェブアプリの操作)

スポンサーリンク

まとめ

このページでは、Python で socket モジュールを利用した HTTP クライアントの作り方について解説しました!

HTTP クライアントを開発するためには、HTTP リクエストのフォーマットをしっかり理解して HTTP リクエストを作成して送信すること、および、HTTP レスポンスのフォーマットをしっかり理解して HTTP レスポンスを受信することが重要となります。

プロトコルとしてはシンプルなのですが、意外としっかり作りこもうと思うと大変なので、実用的な HTTP クライアントを利用したいのであれば自身で開発するのではなく既に HTTP クライアントとして出来上がっている requestsurllib 等の外部モジュールを利用することをオススメします。

とはいえ、ソケット通信の使いこなしを理解したり、ソケット通信の知識を深める上では、今回のような特定のプロトコルのクライアントやサーバーを開発してみるのは非常に有益です!是非、HTTP クライアントだけでなく HTTP サーバーを開発してみたり、他のプロトコルのクライアント・サーバーの開発にも挑戦してみてください!

同じカテゴリのページ一覧を表示