HTTP通信

編集

sockaddr_in 構造体を使う場合

編集

MSDNのコードではsockaddr_in 構造体を使わずにコードを書いている。(MSDNのコードでは addrinfo* 構造体でソケット通信のコードを書いている。)

だが、Windowsでも、addrinfo* 構造体をつかわずに、sockaddr_in 構造体でも、WinSockのコードを下記のようにして書ける。

下記コードでは、ローカルホスト(手元のパソコン自身)に接続している。

コード例
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 古い関数をいくつか使っているので。古いのを置き換えできるなら不要。

#pragma comment( lib, "ws2_32.lib" )

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

    // WinSockの初期化など
    WSADATA data;
    WSAStartup(MAKEWORD(2, 0), &data);

    // ポート番号
    unsigned short port = 8080; // 8080 はローカルホストを意味する伝統的な番号。「9876」とかでもいい。


    // sockaddr_in 構造体の確保
    struct sockaddr_in destAddr;


    // sockaddr_in 構造体の設定
    memset(&destAddr, 0, sizeof(destAddr));
    destAddr.sin_port = htons(port);
    destAddr.sin_family = AF_INET;


    char destIP_text[] = "127.0.0.1"; // 127.0.0.1 はローカルホストを意味する番号
    inet_pton(AF_INET, destIP_text, &destAddr.sin_addr.s_addr); // 下記コードからの置き換えが必要。
    // destAddr.sin_addr.s_addr = inet_addr(destIP_text); // ←これだと古くてエラーになる。

    printf("接続しようとしているIPアドレス %s \n", destIP_text);


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

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


    // 接続
    connect(destSocket, (struct sockaddr*)&destAddr, sizeof(destAddr));

    char SendMsg[50] = "aaaa"; // 初期化のため、なんらかの文字列が必要. MSG はメッセージのつもり 
    strcpy_s(SendMsg, 50, "GET /index.html HTTP/1.1 "); // のちの作業のためGETメッセージにしているだけ。
    // メッセージはBBBBとか適当なものでもよい。

    printf("送ろうとするメッセージ: %s\n", SendMsg);


    // メッセージ送信
    printf("送信中...\n");
    send(destSocket, SendMsg, strlen(SendMsg) , 0);


    // 今後のHTTP処理のためのコード。まだ機能してない。
    int rVal;
    char buf[5000];
    int size = 4000;
    while (1) {
        rVal = recv(destSocket,buf,size,0);
        if (rVal==SOCKET_ERROR) { 
            break; 
        }
    }


    // WinSockの終了
    closesocket(destSocket);
    WSACleanup();
}


// Winsock用パラメータ // 無いとコンパイル時にエラーになる。
// 下記のパラメータは、宣言されてさえいれば、どこでもいい。
int status;
int numsnt;

実務などでエラー処理などのための場合分けが必要になるだろうだが、しかし上記コードにおいてはエラー処理コードがあるとコードが煩雑になって学習者に分かりづらくなる恐れがあるので、上記コードからはエラー処理を除去してある。


なお、 inet_pton とは、「pton」の「P」が Presentation の略だと言われており、ここでの「P」は文字列などの意味であり、「n」はNetwaorkの意味であり、

Presentation to Network (プレゼンからネットへ)

の意味であり、ptonは、文字列などを、ソケット関連の構造体のもつ情報に変換する命令である。


逆に、 inet_ntop なら、

Network to Presentation

なので、ソケット関連の構造体のもつ情報を、文字列などに置き換える命令である。


ローカルのXAMPP上のHTMLファイルとの通信

編集

Apathce などのサーバー上のHTMLファイルとどうやって通信するのか、読者は疑問に思ってるだろう。

さて、実験したいところであるが、しかし、いきなり外部と通信をすると、アクセスが集中したりして迷惑だし、アクセス遮断されたりしかねないので、まず手元のパソコンで試そう。

Windowsの場合、XAMPPというサーバソフトがあるので、これを手元のパソコンにインストールすれば、ローカルのHTTPサーバを立てられる。

文字コードの設定など、やや面倒ではあるが、しかしソケット通信のテストなら、英語のままでも構わないので、インストールしてしまおう。

PHP/確実に動作させるまで にXAMPPの設定方法など書いてあるので、参考に。


さて、まず、Windows版XAMPPの場合、フォルダ htdocs に、サーバにアップロードしたいファイルを入れる。このhtdocsのような、そこに入れたファイルをサーバにアップロードするフォルダのことを「ドキュメント・ルート」という。

ともかく、このhtdocsフォルダに、HTMLファイルでもPHPファイルでも何でもいいので、名前をつけてファイルを入れる。

ただし、「index.html」など一部の名前は、特別な用途が決まっているので、このような名前は避ける必要がある。

などで、とりあえず「aaaa.html」とか「detarame.html」(デタラメ.html の意味)とか、特別な用途を予約されたファイル名とは重ならないファイル名をつけて、htdocsに入れよう。

detarame.html に「seikou」(成功)とでも書いておこう。そのメッセージが読めたら、ソケット通信の成功である。

日本語はパソコンでは非アスキーコードという意味でマルチバイト文字であるが、しかしWindowsのchar型はシングルバイト文字対応の型であり、そしてWinSockのrecv関数はchar型しか認めていないので、どうあがいても、recv関数だけでは、日本語のようなマルチバイト文字を認識できない。なので、WinSockで日本語を処理するのは、文字コード処理に関する、かなり高度なスキルを要求され、面倒である。

よって、初心者レベルでは、WinSockで日本語を送受信するのは、諦めよう。


さて、XAMPP および Apacheを起動するのを忘れてはいけない。XAMPPの起動後に表れるXAMPPコントロールパネルから、モジュール『Apache』の欄にある「Start」ボタンを押せばいい。すると、Apatcheが手元のパソコン内で立ち上がる。

これさえ出来れば、あとは下記のコードをVisual Studio に入力して、実行・ビルドすればいいだけである。


コード例
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 古い関数をいくつか使っているので。古いのを置き換えできるなら不要。

#pragma comment( lib, "ws2_32.lib" )


// Winsock用パラメータ // 無いとコンパイル時にエラーになる。
// なぜだか、早めに宣言する必要がある。
int status;
int numsnt;


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

    // WinSockの初期化など
    WSADATA data;
    WSAStartup(MAKEWORD(2, 0), &data);

    // ポート番号
    unsigned short port = 80; // 8080 はローカルホストを意味する伝統的な番号。「9876」とかでもいい。


    // sockaddr_in 構造体の確保
    struct sockaddr_in destAddr;


    // sockaddr_in 構造体の設定
    memset(&destAddr, 0, sizeof(destAddr));
    destAddr.sin_port = htons(port);
    destAddr.sin_family = AF_INET;


    char destIP_text[] = "127.0.0.1"; // 127.0.0.1 はローカルホストを意味する番号
    inet_pton(AF_INET, destIP_text, &destAddr.sin_addr.s_addr); // 下記コードからの置き換えが必要。
    // destAddr.sin_addr.s_addr = inet_addr(destIP_text); // ←これだと古くてエラーになる。

    printf("接続しようとしているIPアドレス %s \n", destIP_text);


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

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


    // 接続
    connect(destSocket, (struct sockaddr*)&destAddr, sizeof(destAddr));

    char SendMsg[50] = "aaaa"; // 初期化のため、なんらかの文字列が必要. MSG はメッセージのつもり 
    // printf("送ろうとするメッセージ: %s\n", SendMsg);

    // メッセージ送信
    printf("送信中...\n");

    strcpy_s(SendMsg, 50, "GET /detarame.html HTTP/1.0\r\n\r\n");
    send(destSocket, SendMsg, strlen(SendMsg) +, 0);

    // ↓ なくても動くが、今後の拡張用やアップデート対策などに残す
    // strcpy_s(SendMsg, 50, "Host: localhost\r\n"); 
    // send(destSocket, SendMsg, strlen(SendMsg) , 0);

    // strcpy_s(SendMsg, 50, "Connection: close\r\n\r\n"); // localhost
    // send(destSocket, SendMsg, strlen(SendMsg) , 0);


    int rVal;
    char buf[5000];
    int size = 4000;

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

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

    }

    printf("終\n");

    // WinSockの終了
    closesocket(destSocket);
    WSACleanup();
}

これを実行すると、下記のような表示結果になる。

表示結果
接続しようとしているIPアドレス 127.0.0.1
送信中...
受信: HTTP/1.1 200 OK
Date: Wed, 08 Jul 2020 16:44:34 GMT
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1f PHP/7.4.4
Last-Modified: Wed, 08 Jul 2020 16:06:53 GMT
ETag: "7-5a9f04be00a8f"
Accept-Ranges: bytes
Content-Length: 7
Connection: close
Content-Type: text/html

seikou!
終

C:\Users\ユーザ名\source\repos\sockTestClient\x64\Release\sockTestClient.exe (プロセス
 5756) は、コード 0 で終了しました。
デバッグが停止したときに自動的にコンソールを閉じるには、[ツール] -> [オプション]
 -> [デバッグ] -> [デバッグの停止時に自動的にコンソールを閉じる] を有効にします
。
このウィンドウを閉じるには、任意のキーを押してください...


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

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

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


解説

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

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

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

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

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

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


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

strcpy_s(SendMsg, 50, "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() 関数の返り値について

recv() 関数は、終了時に 0を返し、エラー時に -1 を返すという実装である。

また、一般にWinSockではエラー時の値は SOCKET_ERROR で表すので、念のため上記コードでは recvの直後のif文に SOCKET_ERROR も加えている。


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

if ( rVal == 0  ) {

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


一方、

        if (rVal == -1) {

とか

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

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


send(destSocket, SendMsg, strlen(SendMsg) , 0); の strlen の長さ

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


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

strcpy_s(SendMsg, 50, "Host: localhost\r\n"); 
send(destSocket, SendMsg, strlen(SendMsg) , 0);

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

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


HTTP1.1

編集

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

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

"Host: localhost:80"

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


コード例
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 古い関数をいくつか使っているので。古いのを置き換えできるなら不要。

#pragma comment( lib, "ws2_32.lib" )


// Winsock用パラメータ // 無いとコンパイル時にエラーになる。
// なぜだか、早めに宣言する必要がある。
int status;
int numsnt;


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

    // WinSockの初期化など
    WSADATA data;
    WSAStartup(MAKEWORD(2, 0), &data);

    // ポート番号
    unsigned short port = 80; // 8080 はローカルホストを意味する伝統的な番号。「9876」とかでもいい。


    // sockaddr_in 構造体の確保
    struct sockaddr_in destAddr;


    // sockaddr_in 構造体の設定
    memset(&destAddr, 0, sizeof(destAddr));
    destAddr.sin_port = htons(port);
    destAddr.sin_family = AF_INET;


    char destIP_text[] = "127.0.0.1"; // 127.0.0.1 はローカルホストを意味する番号
    inet_pton(AF_INET, destIP_text, &destAddr.sin_addr.s_addr); // 下記コードからの置き換えが必要。
    // destAddr.sin_addr.s_addr = inet_addr(destIP_text); // ←これだと古くてエラーになる。

    printf("接続しようとしているIPアドレス %s \n", destIP_text);


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

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


    // 接続
    connect(destSocket, (struct sockaddr*)&destAddr, sizeof(destAddr));

    char SendMsg[50] = "aaaa"; // 初期化のため、なんらかの文字列が必要. MSG はメッセージのつもり 
    // printf("送ろうとするメッセージ: %s\n", SendMsg);

    // メッセージ送信
    printf("送信中...\n");

    strcpy_s(SendMsg, 50, "GET /detarame.html HTTP/1.1\r\n");
    send(destSocket, SendMsg, strlen(SendMsg) , 0);

    strcpy_s(SendMsg, 50, "Host: localhost:80\r\n");
    send(destSocket, SendMsg, strlen(SendMsg) , 0);

    strcpy_s(SendMsg, 50, "\r\n");
    send(destSocket, SendMsg, strlen(SendMsg), 0);


    // ↓ なくても動くが、今後の拡張用やアップデート対策などに残す

    // strcpy_s(SendMsg, 50, "Connection: close\r\n\r\n"); // localhost
    // send(destSocket, SendMsg, strlen(SendMsg) , 0);


    int rVal;
    char buf[5000];
    int size = 4000;

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

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

    }

    printf("終\n");

    // WinSockの終了
    closesocket(destSocket);
    WSACleanup();
}

IPv6対応

編集

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

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


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

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

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


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


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

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

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

0000:0000:0000:0000:0000:0001

という意味です。なお、(2進数ではなく)16進数です。


なので、たとえば、コードは下記のようになります。

コード例
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#define _WINSOCK_DEPRECATED_NO_WARNINGS // 古い関数をいくつか使っているので。古いのを置き換えできるなら不要。

#pragma comment( lib, "ws2_32.lib" )


// Winsock用パラメータ // 無いとコンパイル時にエラーになる。
// なぜだか、早めに宣言する必要がある。
int status;
int numsnt;


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

    // WinSockの初期化など
    WSADATA data;
    WSAStartup(MAKEWORD(2, 0), &data);

    // ポート番号
    unsigned short port = 80; // 80 はローカルホストを意味する伝統的なポート番号。


    // sockaddr_in 構造体の確保
    struct sockaddr_in6 destAddr; // IPv6対応!


    // sockaddr_in 構造体の設定
    memset(&destAddr, 0, sizeof(destAddr));
    destAddr.sin6_port = htons(port);  // IPv6対応!
    destAddr.sin6_family = AF_INET6;  // IPv6対応!


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

    printf("接続しようとしているIPアドレス %s \n", destIP_text);


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

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


    // 接続
    connect(destSocket, (struct sockaddr*)&destAddr, sizeof(destAddr));

    char SendMsg[50] = "aaaa"; // 初期化のため、なんらかの文字列が必要. MSG はメッセージのつもり 
    // printf("送ろうとするメッセージ: %s\n", SendMsg);

    // メッセージ送信
    printf("送信中...\n");

    strcpy_s(SendMsg, 50, "GET /detarame.html HTTP/1.1\r\n");
    send(destSocket, SendMsg, strlen(SendMsg) , 0);

    strcpy_s(SendMsg, 50, "Host: localhost:80\r\n");
    send(destSocket, SendMsg, strlen(SendMsg) , 0);


    strcpy_s(SendMsg, 50, "\r\n");
    send(destSocket, SendMsg, strlen(SendMsg), 0);

    // ↓ なくても動くが、今後の拡張用やアップデート対策などに残す

    // strcpy_s(SendMsg, 50, "Connection: close\r\n\r\n"); // localhost
    // send(destSocket, SendMsg, strlen(SendMsg) , 0);


    int rVal;
    char buf[5000];
    int size = 4000;

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

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

    }

    printf("終\n");

    // WinSockの終了
    closesocket(destSocket);
    WSACleanup();
}


文字コード関係

編集
リンク先の内容: UTF-8対応、BOM の話題など

webページのHTTP通信を見るには

編集

FirefoxでもGoogle Chromeでも何でもいいですがwebブラウザで、ファンクションキー F12 を押すと、開発者モードになり、ページ下部にブラウザ内などにある情報が表れます。

そこで、いくつも出て来る項目のうち、「ネットワーク」を押すことで、どんなヘッダが送受信されているかを見ることができます。

実際、この「WinSock/HTTP通信」で開発者モードでヘッダを見てみると、

(抜粋)
メソッド ドメイン ファイル 初期化 タイプ
GET ja.wikibooks.org HTTP通信 browsing-centext... html
GET ja.wikibooks.org load.php?lang=ja&modules=startup&only=scripts&ra... stylesheet css

のような情報を見ることができます。


ローカルホストで作成したApacheサーバ上にあるwebページでも同様の方法で、通信ヘッダを見ることができます。


telnet

編集

実は、Visual Studio を使わなくても、telnet (テルネット) という国際規格で決まっている通信形式を使って、WindowsではコマンドプロンプトからHTTP通信が出来ます。

なお、LInuxなどでも telnet 通信が可能です。

当然ながらVisual Studio を経由しない通信なので、(visual Studioとは異なり)マイクロソフトへの会員登録も不要です。


もしWindowsでtelnet通信する場合、まずはコントロールパネルにある「Windowsの機能の有効化または無効化」から(OSバージョンによって多少は手順が異なる)、telnetをインストールします。


その後、コマンドラインで

telnet

と入力すると、下記のような画面に移り変わります。


まず、文字が表示されて無い場合があるので、コマンド

set localecho

で文字表示をするように設定します。

すると、成功すれば、

Microsoft Telnet クライアントへようこそ

エスケープ文字は 'CTRL+]' です

Microsoft Telnet>

のような画面になります。


失敗したり、何かおかしくなったら、コマンド

q

または

quit

でtelnetを終了できます。

この後、コマンド

open localhost 80

で、ローカルホストにポート80番で接続できます。

その後、

Microsoft Telnet クライアントへようこそ

エスケープ文字は 'CTRL+]' です

Microsoft Telnet> open localhost 80
接続中: localhost...

のような画面に移り、もう接続しています。


Windows番では、カーソル位置がおかしな位置に移り、たとえば冒頭の「Microsoft Telnet クライアントへようこそ」の行頭に移ったりしますが、仕様です。

その後、コマンドで

GET /detarame.html HTTP/1.0

とコマンド入力します。

GET /detarame.html HTTP/1.0トへようこそ

エスケープ文字は 'CTRL+]' です
Microsoft Telnet> open localhost 80

のような画面になります。

なお、BackSpace キーや Delete キーは使えません。見た目では、文字は同じでも、telnetはBackSpaceなどのキー情報ごと送信してしまうので、間違えたら、t接続から、やり直す必要があります。

さらに、コマンド入力は、円ターキーを2回押す必要があります。

さて、成功すれば、下記のようにメッセージが返ってきます。

HTTP/1.1 200 OK
Date: Sat, 11 Jul 2020 09:11:02 GMT
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1f PHP/7.4.4
Last-Modified: Wed, 08 Jul 2020 16:06:53 GMT
ETag: "7-5a9f04be00a8f"
Accept-Ranges: bytes
Content-Length: 7
Connection: close
Content-Type: text/html

seikou!

ホストとの接続が切断されました。

続行するには何かキーを押してください...