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

文字コード関係 編集

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 <stdlib.h>
#include <windows.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があったが、UTF-8にはそもそもバイトオーダーは1つなので意味がなくShellBang!との親和性もなく、Windows 10 19H1以降はBOMなしが既定となった。

2024年2月時点では『メモ帳』で保存しても、BOMはつかない。

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

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

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

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

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

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

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

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

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

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


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