Unixソケットプログラミング/HTTP通信

出典: フリー教科書『ウィキブックス(Wikibooks)』

HTTP[編集]

HTTPヘッダ[編集]

WinSock/外部通信の方法』で概要を説明してある。webブラウザなどのよるホームページなどの表示の仕組みは、原理的にはコレ。

HTTP通信できるコード[編集]

Unixの場合、下記のコードで、ローカルホスト上のApacheにアップロードしたhtmlファイルに、コマンドラインなどからソケット通信でアクセスできる。

あらかじめ、アップロード用のhtmlファイルを作成しておいて、Apacheのドキュメントルートフォルダに入れること。(ドキュメントルートが分からなければ、wikibooks『PHP/確実に動作させるまで』を参照するか、ググること。)


コード例
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int main() {
  struct sockaddr_in srcAdoresu;


  /* ソケットの作成 */

  // エラー処理用などの変数
  int destSocket;

  // ソケット生成
  destSocket = socket(AF_INET, SOCK_STREAM, 0);

  /* 接続先指定用構造体の準備 */
  srcAdoresu.sin_family = AF_INET;
  srcAdoresu.sin_port = htons(80); 
  srcAdoresu.sin_addr.s_addr = inet_addr("127.0.0.1");

  /* サーバに接続 */
  connect(destSocket, (struct sockaddr *)&srcAdoresu, sizeof(srcAdoresu));


  /* メッセージの送信 */
  char SendMsg[50] = "aaaa";
  strcpy(SendMsg, "GET /detarame.html HTTP/1.0\r\n\r\n");
  write(destSocket, SendMsg, strlen(SendMsg) );

  /* メッセージの受信 */
  int rVal;
  char buf[5000];
  int size = 4000;

  // 受信
  while (1) {
    rVal = recv(destSocket, buf, size, 0);

    if (rVal == 0 || rVal == -1) {
      break;
    }
    printf("受信: %s\n", buf);
  }

  /* socketの終了 */
  close(destSocket);

  return 0;
}
(※ Fedora32 で 2020年7月9日に動作を確認ずみ )

解説は、WinSock/HTTP通信 とほぼ同じなので、そちらを参照されたし。


実行結果
受信: HTTP/1.1 200 OK
Date: Thu, 09 Jul 2020 08:41:05 GMT
Server: Apache/2.4.43 (Fedora)
Last-Modified: Thu, 09 Jul 2020 08:23:36 GMT
ETag: "18-5a9fdf0de13dc"
Accept-Ranges: bytes
Content-Length: 24
Connection: close
Content-Type: text/html; charset=UTF-8

seikou!
2gyoume
3gyoume


なお、アップロードしたhtmlファイルは、上記コードの場合

detarame.html
seikou!
2gyoume
3gyoume

という内容である。(ファイル名は、デタラメhtmlのつもり)

意味的には

成功
2行目
3行目

のつもり。

なお、Linuxの場合は日本語の全角文字列でも送受信できる(※ Fedora32 で 2020年7月9日に確認ずみ )。Windowsの場合などは文字コードなどの理由で日本語の設定が難しい場合があるので、本wikiでは説明の簡単化のため英語にしてある。


コードの注意点として、もしWinSockのコードをコピーしたとき、Linuxでは strcpy_s を使えないので、strcpy に置き換える必要があり、引数なども調整する必要がある。

また、send()関数ではなくwrite()関数にすること。Linuxにも互換性のためかsend()関数が用意されているようだが、若干、WinSockとは仕様が違うらしく、WinSockのコードそのままでは動かない。ならば、いっそwrite()関数に置き換えたほうがLinuxではラクである。

解説[編集]

よくある設定ミスと対処法

上記コードを実行したら無限にループになる場合、Apacheの立ち上げを忘れている可能性があります。初日は本節を読んでいてApacheを立ち上げるように説明しているので忘れなくても、2日目以降によく立ち上げを忘れるでしょう。

コード改修で立ち上げ忘れを考慮したコードに改修する事も可能ですが(for文に置き換えるなど)、しかし本節では初学者への分かりやすさの観点から、上記コードはそのまま、ループ箇所をwhile文のままにしておきます。


解説

いわゆる外部ネットを含む、ウェブサイトとのHTTP通信を行う場合、ポート番号を「80」番に設定する必要があります。この理由は、そういうふうにHTTPの国際規格やTCP/IPの国際規格などがそう決まっているからです。

一般に、プロトコルの種類や通信の種類などによって、ポート番号が決まっています。

メール通信になると、なぜか上記とは別のポート番号を使います(読者の混同を防ぐため、メール通信のポート番号については、このページでは紹介しません)。

読者は「なぜ、プロトコルの種類に応じてポート番号が違うのか?」とか疑問はわくでしょうが、結局は単に昔の人がそう決めてしまっただけです。

「ポート」と聞くと、あたかも、ハードウェアの何かの端子の番号かのように思えるかもしれませんが、しかしハードウェア端子は一切、ポート番号とは無関係です。

「ポート番号」とは、その名に反して、実質的な意味はプロトコル番号です。

場合によっては複数のクライアント側コンピュータが1つのホストにアクセスする事もよくあるので、同じポート番号を使う場合が多々あり(たとえば、どのクライアントでもHTTPなら80番を使う)、ポート番号だけでは通信先を特定できないです[1]

よって、通信先の特定のために、ポート番号とIPアドレスの少なくとも2つ以上の関連付け(「アソシエーション」といいます)されたデータセットによって、通信が管理されています。[2]


コードの送信メッセージの

strcpy(SendMsg, "GET /detarame.html HTTP/1.0\r\n\r\n");

でGETメッセージを送っている。


なお、1回だけの

\r\n

は改行の意味である。ホスト情報など追加の情報を送る場合、1回だけ「\r\n」と入力し、追加情報を1行ずつ入力していく。

また

\r\n\r\n

と2個改行が続くことで、送信メッセージ全体がいったん終了する事を意味している(これはUnixソケット通信でも同様)。


なお、Bad Request の結果の表示は、

(長いので抜粋)
送信中...
受信: HTTP/1.1 400 Bad Request
Date: Wed, 08 Jul 2020 16:58:04 GMT
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1f PHP/7.4.4

のようになる。


recv() 関数の返り値について

Windowsでは、write()関数ではなくsend()関数でGETメッセージを送るのだが、Unixの場合、send関数で送っていも、recv()関数で受け取る事ができる。

Unixでもrecv() 関数の仕様は、正常な終了時には 0を返し、エラー時に -1 を返すという実装のようである。


実は、recvのブロックのif文は、

if ( rVal == 0  ) {

だけでも、とりあえず上記のコードは動くし、正常に終了する。


一方、

        if (rVal == -1) {

とか

if (rVal == SOCKET_ERROR) { // これだと終わらないのでダメ

だと、無限ループになってしまい、そのままでは終了しなくなるので、閉じるにはコマンドプロンプトのウィンドウ右上の「X」ボタンで閉じるしかない。


write(destSocket, SendMsg, strlen(SendMsg) ); の strlen の長さ

write(destSocket, SendMsg, strlen(SendMsg) ); の strlen の長さは、ピッタリと、 strlen(SendMsg)でなければなりません。


もし、「多いほうが安全かな?」と誤解して strlen(SendMsg)+1 のように大きくしてしまうと、2行目以降の送信メッセージが1文字ずれてしまうためか、もし、次のHostメッセージ

trcpy(SendMsg, "Host: localhost:80\r\n");
write(destSocket, SendMsg, strlen(SendMsg) );

で送信しようと思っても、送信に失敗します。

HTTP1.0ではHostメッセージは不要ですが、HTTP1.1ではHostメッセージが必ず必要なので、strlenはピッタリと過不足なくするように注意してください。


IPv6対応[編集]

※ 解説はWinSockとほぼ同じ。

sockaddr_in構造体はIPv4用の構造体なので、IPv6では使えません。

ですが、ほぼ同じ使い方のできるIPv6用の構造体 sockaddr_in6 がありますので、これを使うと IPv6対応のソケットプログラミングが簡単です。なお LinuxでもWindowsでも同様に sockaddr_in6 に置き換える方法で、IPv6対応できます。


さて、Linuxでも、 AF_INET を AF_INET6 に変える必要があります。

また、sin_portもsin6_portに、sin_family も sin6_family に変えるなど、 構造体のメンバもIpv6用に更新する必要があります。

s_addr も s6_addr に更新します。なお Linuxでも同様です。


手作業でやると大変なので、テキストエディタの一括変換の機能をつかうとラクだし、 更新し忘れによるエラーも防げるでしょう。


また、Ipv6のローカルホストは::1です。6ケタでなく3ケタなのは不思議に感じるかもしれませんが、国際規格(RFC 4291 など)でそう決まっています。(なお RFCとはw:Request for Commentsのこと)

なお、「::」というふうにコロンが2個続いた記号の意味は、のこりすべて「0」と言う意味です。

つまり、「::1」とは、一番右だけが1で、残りは0の、

0000:0000:0000:0000:0000:0000:0000:0001

という意味です。なお、(2進数ではなく)16進数です。なお、IPv6の4個の数字の並びは合計8グループです(6グループではないです)。(つまり、4×8=32個の数字がある。)

Ipv6のvとはバージョンの事です。数字のグループの個数とは無関係です。IPv4の数字が「127.0.0.1」のように4グループなのは、偶然の一致です。


ともかく、Unixの場合、IPv6対応のコードは、たとえば下記のようになります。


コード例
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int main() {
  struct sockaddr_in6 srcAdoresu;


  /* ソケットの作成 */

  // エラー処理用などの変数
  int destSocket;

  // ソケット生成
  destSocket = socket(AF_INET6, SOCK_STREAM, 0);

  /* 接続先指定用構造体の準備 */
  srcAdoresu.sin6_family = AF_INET6;
  srcAdoresu.sin6_port = htons(80); 
  srcAdoresu.sin6_family = AF_INET6; 

  char destIP_text[] = "::1"; // ここは「"localhost"」ではダメ(IPv4のホストが認識されてしまう)。
  inet_pton(AF_INET6, destIP_text, &srcAdoresu.sin6_addr.s6_addr);

  /* サーバに接続 */
  connect(destSocket, (struct sockaddr *)&srcAdoresu, sizeof(srcAdoresu));


  /* メッセージの送信 */
  char SendMsg[50] = "aaaa";
  strcpy(SendMsg, "GET /detarame.html HTTP/1.0\r\n\r\n");
  write(destSocket, SendMsg, strlen(SendMsg));

  /* メッセージの受信 */
  int rVal;
  char buf[5000];
  int size = 4000;

  // 受信
  while (1) {
    rVal = recv(destSocket, buf, size, 0);

    if (rVal == 0 || rVal == -1) {
      break;
    }
    printf("受信: %s\n", buf);
  }

  /* socketの終了 */
  close(destSocket);

  return 0;
}


HTTPは1.0でも1.1でも、どちらでも、IPv6対応が可能です。

HTTP1.1対応[編集]

※ 解説はWinSockとほぼ同じ。

HTTP1.1では、Hostメッセージが必ず必要です。

Apache のローカルホスト名はそのまま「localhost」ですので、ポート番号の80番とあわせてメッセージを

"Host: localhost:80"

のようにHostメッセージを加えて送信することになります。


コード例
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int main() {
  struct sockaddr_in6 srcAdoresu;


  /* ソケットの作成 */

  // エラー処理用などの変数
  int destSocket;

  // ソケット生成
  destSocket = socket(AF_INET6, SOCK_STREAM, 0);

  /* 接続先指定用構造体の準備 */
  srcAdoresu.sin6_family = AF_INET6;
  srcAdoresu.sin6_port = htons(80); 
  srcAdoresu.sin6_family = AF_INET6; 


  char destIP_text[] = "::1"; // ここは「"localhost"」ではダメ(IPv4のホストが認識されてしまう)。
  inet_pton(AF_INET6, destIP_text, &srcAdoresu.sin6_addr.s6_addr);

  /* サーバに接続 */
  connect(destSocket, (struct sockaddr *)&srcAdoresu, sizeof(srcAdoresu));


  /* メッセージの送信 */
  char SendMsg[50] = "aaaa";
  
  strcpy(SendMsg, "GET /detarame.html HTTP/1.1\r\n");
  write(destSocket, SendMsg, strlen(SendMsg) );

  strcpy(SendMsg, "Host: localhost:80\r\n");
  write(destSocket, SendMsg, strlen(SendMsg) );

  strcpy(SendMsg, "\r\n");
  write(destSocket, SendMsg, strlen(SendMsg) );

  /* メッセージの受信 */
  int rVal;
  char buf[5000];
  int size = 4000;

  // 受信
  while (1) {
    rVal = recv(destSocket, buf, size, 0);

    if (rVal == 0 || rVal == -1) {
      break;
    }
    printf("受信: %s\n", buf);
  }

  /* socketの終了 */
  close(destSocket);

  return 0;
}


外部サイトにアクセスする場合、Hostメッセージは

strcpy(SendMsg, "Host: www.企業名.com:80\r\n");

のように入力する事になる。

Hostに入れるのは、アクセス先のドメイン名とポート番号である。まちがえて、localhostを入れても、単なるエラーになる。

gethostbyname は別物[編集]

gethostbyname という、サイト名をもとにIPアドレスを検索できる関数がソケット通信にはあるが(なおWindowsでは、IPv6対応などの事情により、2020年現在では他の関数( getaddrinfo など)に置き換わっている)、

しかし、この gethostbyname の問い合わせ先は DNSサーバ であるので、HTTP通信とはあまり関係ない。(IPアドレス先サーバーに接続はしない関数である。単にサイト名からIPアドレス情報をDNSサーバに問い合わせて受け取るだけである。)

なお、gethostname という関数名の(hostとnameのあいだに「by」が無い)、似た名前のライブラリ関数もあるが、gethostbyname と gethostname とはまったく別の関数なので混同しないように(名称変更などではなく、そもそも別物の関数である。)。


しかも、企業のホームページの場合、たとえばどこかの企業の何らかのコンテンツをブラウザで閲覧した場合、アドレス欄に

https://www.企業名.com/コンテンツURL

のように各ページのURLが表示されるわけだが、

しかし通常、DNSサーバーに登録されているのは

https://www.企業名.com

の部分までである。


また、けっして gethostbyname は目的の企業サイトなどと接続するわけではない。下記コードのように、bind() や connect() などが無くても、 gethostbyname の結果を表示できる。

コード例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <unistd.h>
#include <string.h> 
#include <arpa/inet.h> 
#include <netdb.h> 

#include <stdlib.h>


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

    struct hostent *host;

    host = gethostbyname("localhost");

    printf("ホスト名 :s\n", host->h_name);
    printf("別名 : %s\n", host->h_aliases[0]);

    return 0;
}
(Linuxの Fedora 32 で 2020年5月17日に確認。)
結果
ホスト名 :localhost
別名 : localhost.localdomain


解説

構造体であらかじめ hostent 型というのがOSなどにより用意されている。なので、この構造体の「hostent」という部分の名前は変えてはいけない。

*host は単なるポインタ変数名なので、他のポインタ変数名に変えてもいい。


IPアドレスを表示したい場合は、さらに

// この上はprintf("別名 : %s\n", host->h_aliases[0]);
  struct in_addr *addr;

  addr = (struct in_addr *)(host->h_addr);
  printf("IPアドレス : %s\n", inet_ntoa(*addr));

// この直後に return 0;

というコードを付けたす。


つまり、

IPアドレスまで表示するコード例
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>

#include <stdlib.h>

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

  struct hostent *host;

  host = gethostbyname("localhost"); 

  printf("ホスト名 :%s\n", host->h_name);
  printf("別名 : %s\n", host->h_aliases[0]);


  struct in_addr *addr;

  addr = (struct in_addr *)(host->h_addr);
  printf("IPアドレス : %s\n", inet_ntoa(*addr));
  
  return 0;
}
表示結果
ホスト名 :localhost
別名 : localhost.localdomain
IPアドレス : 127.0.0.1

のようになる。

解説

struct in_addr *addr; のポインタ変数名 *addr は別にほかの変数名でもよいのだが(変えても動作することをLinuxのFedora32で2020年に確認ずみ)、慣習的に「*addr」という変数名をつかう場合が多い。

備考

余談だが、gethostbyname で自機にホスト名やIPアドレスなどを問い合わせるとき、わざわざサーバー側のプログラムを立てる必要は無い。

このことからも、最終目的のサーバーに問い合わせているのではなく、別のところ(一般にはDNSサーバー)に問い合わせている様子が分かるだろう。

getaddrinfo[編集]

予備知識

まず、コマンドラインで

nslookup ドメイン名

と入れると、そのドメインのIPアドレスが出て来る。

たとえば、

nslookup www.yaahule.co.jp

みたいにコマンドを入れる。(yaahuleは架空の企業名。)

もし後述コードで getaddrinfo によるソケット通信をした結果、nslookupで表示された結果と同じIPv6アドレスまたはIPv6アドレスが表示されれば、そのコードはDNSサーバとの通信に成功している。

IPv4版[編集]

Ipv4用のコードだが、getaddrinfoが次のコードで、Linux上で動く。(※ 2020年7月17日に、Fedora32上で動くことを確認ずみ。)

なお、下記コードの企業名、ドメイン名はデタラメなので、実験の際には、実在する検索エンジンのドメインに変えること。(下記のままだと、./a.out などで実行しても、何も表示されない。)

#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

#include <netdb.h> // これがないと、hints などを使えない

int main() {

    char hostname[20] = "www.yaahule.com"; // 架空の企業名
    struct addrinfo hints, * res;

    int err;

    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_family = AF_INET;

    hints.ai_protocol = IPPROTO_TCP;


    err = getaddrinfo(hostname, NULL, &hints, &res);

    if (err != 0) {
        return -1;
    }


    struct sockaddr_in* temp = (struct sockaddr_in*)(res->ai_addr) ;

    struct in_addr nameaddr;
    nameaddr.s_addr = (temp)->sin_addr.s_addr;

    char destbuf_text[500];

    inet_ntop(AF_INET, &(nameaddr.s_addr), destbuf_text, 100);
    printf("IPアドレス %s \n", destbuf_text);


    freeaddrinfo(res);

    return 0;
}
実行結果
IPアドレス 123.○△×.765.□△
※ 実行例の数字はデタラメです。

IPv6対応[編集]

#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

#include <netdb.h>


int main() {

    char hostname[20] = "www.yaahule.com"; // 架空の企業名
    struct addrinfo hints, * res;
    int err;

    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_family = AF_INET6;

    hints.ai_protocol = IPPROTO_TCP;


    err = getaddrinfo(hostname, NULL, &hints, &res);

    if (err != 0) {
        return -1;
    }

    // いっぺんに↓こう書いても、コンパイルできない。いろいろアレンジしてもダメ。
    // &nameaddr->sin6_addr = ((struct sockaddr_in6*)(res->ai_addr))->sin6_addr.s6_addr;

    struct sockaddr_in6* nameaddr; // in6_addr 型ではなく sockaddr_in6 に変えること

    nameaddr = (struct sockaddr_in6*)res->ai_addr;

    char destbuf_text[500];

    inet_ntop(AF_INET6, &(nameaddr->sin6_addr), destbuf_text, 200);
    printf("IP v6 アドレス - %s\n", destbuf_text);


    freeaddrinfo(res);

  return 0;
}
実行結果
IP v6 アドレス - 2345:6789:○○△△:654::2005
(※ 数字はデタラメです。)

telnet[編集]

telnet (テルネット)という国際規格に基づいたネット通信の方法が古くから(インターネット黎明期から)あり、Linuxでは、コマンドラインで telnet を利用できる。

※ telnetは、HTTP以外の通信もできるが、本wikiでは説明の都合上、HTTPの節で一緒に説明する。HTTPでtelnet通信するのが、もっとも入門しやすいだろうから。入門所でも、よくtelnetの例でHTTP通信の事例が紹介されている。

Fedora Linux (Fedora 32) の場合、最初からtelnetはインストールされているので新規インストール作業は不要だが、もし入っていなかったら、

sudo dnf install telnet

でインストールできる。


コマンドラインで

telnet

と入力すれば、コマンドラインがtelnet に移行する。 コマンド入力が成功すれば、

[ユーザ名@localhost ~]$ telnet
telnet> 

のような画面になる。

telnet をやめたい場合、

quit

または単に

q

と入力すればいい。

Linuxの場合、telnetの文字表示の設定が最初から済んでいるので、特に設定をいじる必要は無い。(なおWindowsは、初期設定ではtelnetが文字表示されない。)


Linux の telnet で、ローカルホストに接続するには、Apacheを立ち上げたあと、コマンド入力

open localhost 80

で接続できる。書式は

open ホスト名 ポート番号

の書式である。

コマンド入力が成功すれば、

[ユーザ名@localhost ~]$ telnet
telnet> open localhost 80
Trying ::1...
Connected to localhost.
Escape character is '^]'.

のような画面になる。


IPアドレスでもオープンできるが、

open 127.0.0.1 80

と書くと、数字が続いて見づらいので、なるべく「localhost」でオープンしたほうが良いだろう。

そして、Escape character なんとかの次の行にカーソルが移るので、

[ユーザ名@localhost ~]$ telnet
telnet> open localhost 80
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /detarame.html HTTP/1.0(リターン)
(リターン)

のように入力する。(リターン)とは、その位置でリターンキーを押すこと。けっして実際に「(リターン)」と文字入力してはいけない。

この結果、見た目は

[ユーザ名@localhost ~]$ telnet
telnet> open localhost 80
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /detarame.html HTTP/1.0

のようになる。

成功すれば、

HTTP/1.1 200 OK
Date: Sun, 12 Jul 2020 01:12:06 GMT

(長いので冒頭を抜粋)のような返事が返ってきて、以下のようになる。


実行結果
[ユーザ名@localhost ~]$ telnet
telnet> open localhost 80
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /detarame.html HTTP/1.0

HTTP/1.1 200 OK
Date: Sun, 12 Jul 2020 01:12:06 GMT
Server: Apache/2.4.43 (Fedora)
Last-Modified: Thu, 09 Jul 2020 08:48:49 GMT
ETag: "22-5a9fe4b09cb08"
Accept-Ranges: bytes
Content-Length: 34
Connection: close
Content-Type: text/html; charset=UTF-8

成功
日本語テスト
3行目
Connection closed by foreign host.
  1. ^ 村山公保『基礎からわかるTCP/IPネットワークコンピューティング入門』、オーム社、平成30年9月10日、226ページ
  2. ^ 村山公保『基礎からわかるTCP/IPネットワークコンピューティング入門』、オーム社、平成30年9月10日、226ページ