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」の部分は残る。
なので、もしブラウザなどを作りたい場合、手順はおおむね、
- 最初の「Content-type」を探し、そこに配列のポインタを合わせる。
- Content-typeの次に最初に見つかった改行復帰 dada を探し、その直後に配列のポインタを合わせる。
- そして、3バイトぶんの文字を読み取り、BOMであるかどうかを判定する。
- BOMであれば、ポインタを3バイトすすめ、そこから文字のワイド文字への変換を始める。
- BOMでなければ、そのまま文字のワイド文字の変換を始める。
のような手順になるだろう。
BOMのある場合、基本的にこのBOMの直後からテキスト本文が始まるハズなので、文字列の変換する場合には、BOM以降のビットパターンから変換していく必要がある。
誤って、BOMごと変換しようとしても、(コンパイルできても)エラーになったりして、うまく変換できない。
- (※ 調査中: ) アップロードしたテキストに「いつつ」という語句があると、なぜかそこで変換エラーが起きてしまい、そこから先の文字をうまく読み取れずに、終了してしまったり、いくつかの行が読み飛ばされてしまう。