WinSock/HTTP通信/文字コード関係

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

文字コード関係[編集]

UTF8対応[編集]

まず、実際にUTF-8でXAMPP上にアップロードされた日本語ファイルを、コマンドラインで表示をできるコードを示す。 ただし、アップロードされるテキストファイルの文字数によって、冒頭のヘッダ文の内容が少々変わるので、若干、テキストファイル本文の開始位置がズレる。

下記コードの newArray[i] = buf[252+i]; の252の部分をプラス2したりプラス4したりすると、おおむね、位置があう。つまり newArray[i] = buf[254+i]; に変えると、うまくいく場合がある。(詳しくは配列の中身を解析のこと。)

252の数字の部分は、環境によって違う可能性があるので、正確には、HTTPヘッダ文字列を解析して値を決定すること。


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

#include <windows.h>
#include <stdlib.h>

#include <locale.h>

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

#define _CRT_SECURE_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_in6 destAddr;


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


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

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


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

    // ソケット生成
    destSocket = socket(AF_INET6, 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);


    int rVal;
    char buf[5000];
    int size = 4000;
    wchar_t wbuf[1000];

    setlocale(LC_ALL, "JPN");

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

        if (rVal == 0 || rVal == -1 || rVal == SOCKET_ERROR) {
            break;
        }

        printf("bufもじれつ: %s\n", buf);

    }


    printf("解析中...\n");
    printf("240: %c\n", buf[240]);
    printf("241: %c\n", buf[241]);
    printf("242: %c\n", buf[242]);
    printf("243: %c\n", buf[243]);
    printf("244: %c\n", buf[244]);
    printf("245: %c\n", buf[245]); // "l" HTMLのl


    printf("246: %c\n", buf[246]);
    printf("247: %c\n", buf[247]);
    printf("248: %c\n", buf[248]);
    printf("249: %c\n", buf[249]);
    printf("250: %c\n", buf[250]);
    printf("251: %c\n", buf[251]);
    printf("252: %c\n", buf[252]);
    printf("253: %c\n", buf[253]); 
    printf("254: %c\n", buf[254]); 
    printf("255: %c\n", buf[255]); 
    printf("256: %c\n", buf[256]);
    printf("257: %c\n", buf[257]);


    printf("\n");
    printf("ヘッダ以降の文字列を新規の配列に入れています...\n");
    char newArray[100];

    for (int i = 0; i < 50; i = i + 1) {
        newArray[i] = buf[252+i];

    }

    printf("コード変換前の新配列の内容です\n");
    printf("%s\n", newArray);
    printf("\n");

    printf("ヘッダ以降の文字列を変換しています...\n");
    MultiByteToWideChar(CP_UTF8, 0, newArray, 15, wbuf, sizeof(wbuf));

    printf("受信した文字列: %ls\n", wbuf);


    printf("終\n");

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

また、アップロードするテキストファイルには、

成功

とだけ書いてあるとしよう。


実行結果
接続しようとしているIPアドレス: ::1:
送信中...
bufもじれつ: HTTP/1.1 200 OK
Date: Wed, 15 Jul 2020 05:07:28 GMT
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1f PHP/7.4.4
Last-Modified: Wed, 15 Jul 2020 04:51:36 GMT
ETag: "9-5aa73adc124aa"
Accept-Ranges: bytes
Content-Length: 9
Content-Type: text/html

・ソ謌仙粥
解析中...
240: /
241: h
242: t
243: m
244: l
245:
246:

247:
248:

249: ・250: サ
251: ソ
252: ・253: ・254: ・255: ・256: ・257: ・
ヘッダ以降の文字列を新規の配列に入れています...
コード変換前の新配列の内容です
謌仙粥

ヘッダ以降の文字列を変換しています...
受信した文字列: 成功
終

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


表示結果が長いが、ちゃんと

受信した文字列: 成功

というふうに、アップロードした「成功」という文字列が表示されている。


解説

まず、XAMPPのアップロードの際に、UTF-8でアップロードしようが、最初に送られてくるHTTPヘッダの文字列

HTTP/1.1 200 OK
Date: Wed, 15 Jul 2020 05:07:28 GMT
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1f PHP/7.4.4

のメッセージが、XAMPPの設定にもよるが、普通はUTF-8でなく、ANSIなど別の文字コードで送られてしまうので、このヘッダ部分を、なんらかの方法で上手く除去しないといけない。

なぜなら、もしそのまま、 Windows.hにある文字コード変換の関数

MultiByteToWideChar

を使っても、冒頭のANSI的な別コードの文字列の影響のせいで、(コンパイルはできるが)エラーになってしまい、文字列がうまく表示されないからである。


そこで、ヘッダの文字列の最後の、

Content-Type: text/html

の文字列をなんらかの方法で探し出し、

その行の次の文字列から、新規のchar型の配列を作れば、その新規配列にはUTF-8文字列しかないので、変換の際の ANSI と UTF-8 との混在を防げる。

※ ただし、文字列検索の機能の実装をするとコードが複雑化するので、上記のコードでは、
    printf("240: %c\n", buf[240]);
    printf("241: %c\n", buf[241]);
    printf("242: %c\n", buf[242]);
みたいなチカラ技で、実際に配列の何番目に何の文字が入っているかを、プログラマーに解析させている。(もしブラウザなどを実装する際は、このチカラ業の部分を、文字列検索のコードなどに置き換えよう。)


あとは、とにかく、UTF-8だけのコードさえ抜粋できてしまえば(そして、その抜粋した内容を新規の配列にまとめれば)、Windows.hにある文字コード変換の関数

MultiByteToWideChar

を使えば、UTF-8コード(マルチバイト)を、古いWindowsでも表示できるワイド文字列に変換できるので、この MultiByteToWideChar() 関数で変換すればいいだけである。


BOM関係[編集]

まず、printf()関数のフォーマット指定子(%dとか%sとかのヤツ)を「%x」とすると、その変数にある機械語が16進数で見れる。

recv()関数で受信した配列の中身を表示する事により、何を受信したかが明確に機械語で表示できる。


#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#include <windows.h>
#include <stdlib.h>

#include <locale.h>

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

#define _CRT_SECURE_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_in6 destAddr;


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


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

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


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

    // ソケット生成
    destSocket = socket(AF_INET6, 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 /detarame2.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);



    int rVal;
    char buf[5000];
    int size = 4000;
    wchar_t wbuf[1000];

    setlocale(LC_ALL, "JPN");

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

        if (rVal == 0 || rVal == -1 || rVal == SOCKET_ERROR) {
            break;
        }

        printf("bufもじれつ: %s\n", buf);

    }


    printf("解析中...\n");

    for (int i = 0; i < 30; i = i + 1) {
        printf("%d: %x %c\n", 240+i, buf[240+i], buf[240+i]);
    }


    printf("\n");
    printf("ヘッダ以降の文字列を新規の配列に入れています...\n");
    char newArray[100];

    for (int i = 0; i < 100; i = i + 1) {
        newArray[i] = buf[254+i];

    }

    printf("コード変換前の新配列の内容です\n");
    printf("%s\n", newArray);
    printf("\n");

    printf("ヘッダ以降の文字列を変換しています...\n");
    MultiByteToWideChar(CP_UTF8, 0, newArray, 50, wbuf, sizeof(wbuf));

    printf("受信した文字列: %ls\n", wbuf);


    printf("終\n");

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


実行結果
接続しようとしているIPアドレス: ::1:
送信中...
bufもじれつ: HTTP/1.1 200 OK
Date: Wed, 15 Jul 2020 07:59:56 GMT
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1f PHP/7.4.4
Last-Modified: Wed, 15 Jul 2020 07:58:22 GMT
ETag: "3b-5aa7649a72893"
Accept-Ranges: bytes
Content-Length: 59
Content-Type: text/html

・ソ謌仙粥
2陦檎岼
縺・>縺、縺、
縺輔s縺弱g縺・a
蝗・

解析中...
240: 78 x
241: 74 t
242: 2f /
243: 68 h
244: 74 t
245: 6d m
246: 6c l
247: d
248: a

249: d
250: a

251: ffffffef ・252: ffffffbb サ
253: ffffffbf ソ
254: ffffffe6 ・255: ffffff88 ・256: ffffff90 ・257: ffffffe5 ・258: ffffff8a ・
259: ffffff9f ・260: d
261: a

262: 32 2
263: ffffffe8 ・264: ffffffa1 。
265: ffffff8c ・266: ffffffe7 ・267: ffffff9b ・268: ffffffae ョ
269: d

ヘッダ以降の文字列を新規の配列に入れています...
コード変換前の新配列の内容です
謌仙粥
2陦檎岼
縺・>縺、縺、
縺輔s縺弱g縺・a
蝗・


ヘッダ以降の文字列を変換しています...
受信した文字列: 成功
2行目
いいつつ
�終 ぎょうめ

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


さて、

247: d
248: a

249: d
250: a

とあるが、これは

ラインフィードバック・復帰(十六進数で「0d」)と
改行・キャリッジリターン(十六進数で「0a」)

を意味している。


さて、

251: ffffffef ・252: ffffffbb サ
253: ffffffbf ソ

とあるが、

これはBOM(バイトオーダーマーク、略称BOMは「ボム」と発音)という、制御用の文字コードの一種であり、UTF-8の場合には「ef bb bf」という機械語がつく。(詳しくはw:バイトオーダーマーク)


BOMがあるか無いかは、OSによって異なるが、Windowsは基本的にBOMがある。

『メモ帳』で保存すると、BOMがつくハズである。

また、フリーソフトのTeraPad も、文字コード指定して保存する際に「UTF-8」を選択したとき、BOMがつく。もしTeraPadでBOMなしを選びたいなら、「UTF-8N」で保存しないといけない。

他のテキストエディタだと、「UTF-8」でもBOMなしの場合もあるので、テキストエディタの説明書などを確認のこと。


さて、LinuxやBSD(マックもBSD派生)では、BOMをつけないのが主流である。そして、サーバ業界ではLinuxが主流である。

なので、将来的にソケット関係のプログラマーはLinuxの方式に合わせることになる可能性が高い。


なお、BOMなしで保存した場合でも、改行+復帰の2回ぶんの「dada」の部分は残る。


なので、もしブラウザなどを作りたい場合、手順はおおむね、

  1. 最初の「Content-type」を探し、そこに配列のポインタを合わせる。
  2. Content-typeの次に最初に見つかった改行復帰 dada を探し、その直後に配列のポインタを合わせる。
  3. そして、3バイトぶんの文字を読み取り、BOMであるかどうかを判定する。
    1. BOMであれば、ポインタを3バイトすすめ、そこから文字のワイド文字への変換を始める。
    2. BOMでなければ、そのまま文字のワイド文字の変換を始める。

のような手順になるだろう。

BOMのある場合、基本的にこのBOMの直後からテキスト本文が始まるハズなので、文字列の変換する場合には、BOM以降の機械語から変換していく必要がある。

誤って、BOMごと変換しようとしても、(コンパイルできても)エラーになったりして、うまく変換できない。


(※ 調査中: ) アップロードしたテキストに「いつつ」という語句があると、なぜかそこで変換エラーが起きてしまい、そこから先の文字をうまく読み取れずに、終了してしまったり、いくつかの行が読み飛ばされてしまう。