Win32/64ネットワーク開発 004 winsockを使ったhttp通信
前の記事:Win32/64ネットワーク開発 003 wininetを使ったftp通信
次の記事:[[Win32/64ネットワーク開発 005]]
概要
WinSockという通信の窓口を使って、ネットワークを通信してみたいと思います。自分ではネットワークの基礎となる装置を作るのは大変なのでWindowsではコレを使うのが最も基礎的なネットワークのプログラムを始める原点になると思います。
まずはhttp通信を処理するプログラムを作りますが、今はhttpsという通信が主要なので、あまり役に立たないですね。でも、https通信を行うとなると一気に難しくなります。httpsはSSL(Secure Socket Layer)やTLS(Transport Layer Security)という暗号方式で通信を行います。暗号通信おそるべし。そしてftpにもSSLがあります。ftpsですね。リモートにあるサーバを端末から操作する際、通信される一切の情報を暗号化するSSH(Secure SHell)を使ったSFTPもあるか。UNIX系のシェルが備えられている場合に使われるSCP(Secure Copy Protocol)もあるね。いろいろあります。ネットワーク通信楽しいけど、通信の道を出歩くときはちゃんと装備をして出歩かないと、街で強盗に合うような、通信の傍受をされることもあります。悪いことをする人がいなくなるといいんだけど、不平不満がなくならない世界に悪はなくならない。我慢が足りない人はたくさん存在するし、裕福な人の生活があり、そこをうらやむ人がいて、埋めようとする人がいる。ソフトウェアの勉強でもして人生を消費するのも楽しいのにね。Windows/Linux/Macパソコンがあるだけで裕福だと思うし、ましてや電力契約・ネットワーク通信契約が存在する環境があるなんて裕福過ぎる。スマホだと開発はあまりできないから、SNSに時間を消費したり暇になっちゃうかな。ここの管理人はSNSはやらないっすけどね。使い方だけちょろっと知って、あとはそっとしておくっす。
なんにしても、複雑なことをする前に、風前の灯となりつつあるHTTP通信の習得からですね。
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <Windows.h>
#include <cstdio>
#include <winsock2.h>
#include <iostream>
#include <string>
#pragma comment( lib, "ws2_32.lib" )
int main()
{
WSADATA WSADATAwsaData;
LPHOSTENT LPHOSTENTlpHostnet;
SOCKET SOCKETs;
int iReturn;
SOCKADDR_IN SOCKADDR_INsockadd;
char szServer[256], szURL[256], szStrRcv[1024], szPort[8], szYesNo[4];
wchar_t wchStr[256];
u_short u_shortPort;
unsigned int uiAddr;
while (1) {
if (WSAStartup(MAKEWORD(1, 1), &WSADATAwsaData) != 0) {
perror("WSAStartupエラー\n");
return -1;
}
printf("Webサーバー名(ex.www.yo-net.jp):");
std::cin >> szServer;
printf("ポート番号(ex.80):");
std::cin >> szPort;
if (strcmp(szPort, "") == 0) {
strcpy_s(szPort, strlen("80"), "80");
}
u_shortPort = (u_short)atoi(szPort);
printf("URL(ex.http://www.yo-net.jp/index.html):");
std::cin >> szURL;
if (strcmp(szURL, "") == 0){
strcpy_s(szURL, strlen("/"), "/");
}
//ソケット処理
SOCKETs = socket(PF_INET, SOCK_STREAM, 0);
if (SOCKETs == INVALID_SOCKET) {
perror("ソケットをオープンできない。\n");
WSACleanup();
return -2;
}
//ホスト情報取得処理
LPHOSTENTlpHostnet = gethostbyname(szServer);
if (LPHOSTENTlpHostnet == NULL) {
uiAddr = inet_addr(szServer);
LPHOSTENTlpHostnet = gethostbyaddr((char*)&uiAddr, 4, AF_INET);
if (LPHOSTENTlpHostnet == NULL) {
int iUTF16Len = MultiByteToWideChar(932, 0, szServer, -1, NULL, 0);
std::wstring wstringCP932Str(iUTF16Len, 0);
if (iUTF16Len > 0) {
MultiByteToWideChar(932, 0, szServer, -1, &wstringCP932Str[0], iUTF16Len);
}
wsprintf(wchStr, L"%sが見つからない\n", wstringCP932Str.c_str());
int iCP932Len = WideCharToMultiByte(932, 0, wchStr, -1, NULL, 0, NULL, NULL);
std::string stringCP932Str(iCP932Len, 0);
if (iCP932Len > 0) {
WideCharToMultiByte(932, 0, wchStr, -1, &stringCP932Str[0], iCP932Len, NULL, NULL);
}
perror(stringCP932Str.c_str());
WSACleanup();
return -3;
}
}
//SOCKADDR型構造体への値設定処理
memset(&SOCKADDR_INsockadd, 0, sizeof(SOCKADDR_INsockadd));
SOCKADDR_INsockadd.sin_family = AF_INET;
SOCKADDR_INsockadd.sin_port = htons(u_shortPort);
SOCKADDR_INsockadd.sin_addr = *((LPIN_ADDR)*LPHOSTENTlpHostnet->h_addr_list);
//接続処理
if (connect(SOCKETs, (PSOCKADDR)&SOCKADDR_INsockadd, sizeof(SOCKADDR_INsockadd)) != 0) {
perror("サーバーソケットに接続失敗\n");
closesocket(SOCKETs);
WSACleanup();
return -4;
}
//送信メッセージ形成処理
int iUTF16Len = MultiByteToWideChar(932, 0, szURL, -1, NULL, 0);
std::wstring wstringCP932Str(iUTF16Len, 0);
if (iUTF16Len > 0) {
MultiByteToWideChar(932, 0, szURL, -1, &wstringCP932Str[0], iUTF16Len);
}
wsprintf(wchStr, L"GET %s HTTP/1.0\r\n\r\n", wstringCP932Str.c_str());
int iCP932Len = WideCharToMultiByte(932, 0, wchStr, -1, NULL, 0, NULL, NULL);
std::string stringCP932Str(iCP932Len, 0);
if (iCP932Len > 0) {
WideCharToMultiByte(932, 0, wchStr, -1, &stringCP932Str[0], iCP932Len, NULL, NULL);
}
//送信処理
iReturn = send(SOCKETs, stringCP932Str.c_str(), (int)strlen(stringCP932Str.c_str()), 0);
//受信処理 受信最終文字までループ継続
while (1) {
memset(szStrRcv, '\0', sizeof(szStrRcv));
iReturn = recv(SOCKETs, szStrRcv, (int)sizeof(szStrRcv) - 1, 0);
printf("%s", szStrRcv);
if (iReturn == 0){
break;
}
if (iReturn == SOCKET_ERROR) {
perror("recvエラー\n");
break;
}
}
//終了処理 シャットダウン→クローズ→WSAクリア
if (shutdown(SOCKETs, SD_BOTH) != 0) {
perror("ソケットシャットダウン失敗\n");
}
closesocket(SOCKETs);
WSACleanup();
//プログラム継続判定
printf("\n再実行(Y/N):");
std::cin >> szYesNo;
if (strcmp(szYesNo, "n") == 0 || strcmp(szYesNo, "N") == 0){
break;
}
}
return 0;
}
上記のコードはコマンドプロンプトで動くコンソールアプリケーションです。プロジェクトのプロパティでリンカーの入力の追加の依存ファイルに;WSock32.lib;ws2_32.libを追加しなければなりません。
では少しづつ、動きを確認してみましょう。
1行目の#define WIN32_LEAN_AND_MEANがある場合Windows.hから以下のインクルードが除外されます。
- #include <cderr.h>
- #include <dde.h>
- #include <ddeml.h>
- #include <dlgs.h>
- #include <lzexpand.h>
- #include <mmsystem.h>
- #include <nb30.h>
- #include <rpc.h>
- #include <shellapi.h>
- #include <winperf.h>
- #include <winsock.h>
- #include <wincrypt.h>
- #include <winefs.h>
- #include <winscard.h>
- #include <winspool.h>
- #include <ole2.h>
- #include <commdlg.h>
次に#define _WINSOCK_DEPRECATED_NO_WARNINGSで、Winsockの関連関数を使うときにエラーとするようになっている部分を解除します。
WinSockを使う場合はwinsock2.hをインクルードすると関連関数が導入され使えるようになります。
WinSockとはいったい何なのかという疑問もありますが、考察をすると長くなります。でも知っておいた方がいいような気もする。歴史的な背景とかね。バージョンとかね。winsock2だもんね。知識を吸収する部分は本家ウィキペディアでも難しいことはあまりないのでそちらに説明を譲ります。https://ja.wikipedia.org/wiki/Winsockですね。深追いするとなかなか難しいことになってきます。ほどよい距離感で知るとよいでしょう。このサイトでは、この項目以降で、WinSockの技術についてはある程度深追いもするつもりです。まずは暗号化もないhttp通信の基本から学びます。
main関数の最初の部分ではこれから使う変数などを定義しています。そして、プログラム全体を繰り返して使うための永久ループwhileを使っています。whileから出るときは最後の部分で入力をうけつけて、それがある一定の入力値なら、break;して抜け出して処理を終了します。
最初に実行すべきは、winsock関数利用時の初期化処理です。23行目がそれにあたります。
全体の流れとしては、
WSAStartup(winsockバージョン番号指定, 受け取る変数)で初期化
↓
socket(ソケット方式指定)関数でsocket値を取得
↓
gethostbyneame(ホスト名)でLPHOSTENT型値にアドレス情報を取得構築する
↓
SOCKADDR_IN型にアドレス情報と方式とポート番号情報を指定して、接続情報を構築
↓
connect関数で接続。socket値とSOCKADDR_IN型値を使います。
↓
通信メッセージをsend関数で送信。直前までに送信するメッセージを構築しておかないと駄目ですね。
↓
送信した応答をrecv関数で受け取り、受け取り時は一定の大きさの文字列変数で受け取り終わるまで繰り返すような呼び出しをする仕組みがあります。
↓
通信が終わったら、ソケット番号のシャットダウン
↓
ソケットのクローズ
↓
WSACleanupで処理を終了
というような、処理の流れになります。
上記の流れを細かく追いつつ、主要な関数の使い方の説明をしていきます。まずはwinsockを使うときの初期化にあたる、WSAStartup関数から見ていきましょう。WSAとつく関数はwinsock関連の関数であることを表しています。Windows Sockets APIを意味していると思います。
■WSAStartup(WORD, LPWSADATA)
第1引数:WORD wVersionRequested
MAKEWORD関数が二つのByte型(8bit)引数をとり、WORD型(16bit)に変換してくれますので、WinSockのバージョン1.1を使うときは、MAKEWORD(1, 1)のように指定すると、0x0101(10進数で257)という値を渡すことができます。WORD値の上位バイトがマイナーバージョンで、下位バイトがマイナーバージョンなのですが、MAKEWORDマクロは最初の引数が下位バイトで、次が上位バイトを受け取ることになっているので、バージョン2.0のときはMAKEWORD(2, 0)と指定します。
第2引数:LPWSADATA lpWSAData
WSADATA型の変数の参照を渡すと、引数値に初期化の結果返される値が格納されます。今回はWSADATAwsaData.wVersionに257。WSADATAwsaData.wHighVersionに514(0x0202で2.2が最新版)。WSADATAwsaData.iMaxSocketsに32767(0x7fff)。WSADATAwsaData.iMaxSocketsに32767。WSADATAwsaData.UdpDgに65467(0xffbb)。WSADATAwsaData.szDescriptionに"WinSock 2.0"。WSADATAwsaData.szSystemStatusに"Running"。というような内容が格納されます。WSADATAwsaData.lpVendorInfo char*型変数は初期化されず不定値です。
■socket(int, int, int)
第1引数:int af
ソケットのアドレス ファミリの仕様を指定します。
いちばんよく使う指定が、
- PF_INET(2):インターネット プロトコル バージョン 4 (IPv4) アドレス ファミリ。
その他はhttps://learn.microsoft.com/ja-jp/windows/win32/api/winsock2/nf-winsock2-socketが詳しい。
- AF_UNSPEC:指定なし。
- AF_IPX:IPX/SPX アドレス ファミリ。
- AF_APPLETALK:AppleTalk アドレス ファミリ
- AF_NETBIOS:NetBIOS アドレス ファミリ。
- AF_INET6:インターネット プロトコル バージョン 6 (IPv6) アドレス ファミリ。
- AF_IRDA:赤外線データ関連付け (IrDA) アドレス ファミリ。
- AF_BTH:Bluetooth アドレス ファミリ。
第2引数:int type
ソケットの型指定。
よく使うのは
- Sock_stream
その他には
- SOCK_DGRAM:コネクションレスで信頼性の低いバッファーであるデータグラムをサポートするソケット型。
- SOCK_RAW:上位層プロトコル ヘッダーを操作できるようにする生のソケットを提供するソケットの種類。
- SOCK_RDM:信頼性の高いメッセージ データグラムを提供するソケットの種類。
- SOCK_SEQPACKET:データグラムに基づいて擬似ストリーム パケットを提供するソケットの種類。
第3引数:int protocol
プロトコルを指定します。値 0 を指定した場合、呼び出し元はプロトコルを指定せず、サービスプロバイダーは使用するプロトコルを選択します。以下のような値があります。
- IPPROTO_ICMP:インターネット制御メッセージ プロトコル (ICMP)。
- IPPROTO_IGMP:インターネット グループ管理プロトコル (IGMP)。
- BTHPROTO_RFCOMM:Bluetooth 無線周波数通信 (Bluetooth RFCOMM) プロトコル。
- IPPROTO_TCP:伝送制御プロトコル (TCP)。
- IPPROTO_UDP:ユーザー データグラム プロトコル (UDP)。
- IPPROTO_ICMPV6:インターネット制御メッセージ プロトコル バージョン 6 (ICMPv6)。
- IPPROTO_RM:信頼性の高いマルチキャスト用の PGM プロトコル。
次にWebサーバ名や、ポート名やURL名を取得する処理をします。ポート番号は数値なのですが、文字列として受け取った数値を純粋な数値に変換する処理が必要です。(u_short)atoi(szPort)のようにatoi関数で文字列を数値に変換することができます。ポート番号は16bitで表すように規格が決まっているので、ひとつのIPアドレスをもつ端末には65535のポートまでがあります。ひとつの端末に6万もあれば、どんなに複雑な端末の利用をしていても、足りなくなるような事態にはなりません。コト足りているのが現状です。
次にLPHOSTENTlpHostnet = gethostbyname(szServer)のようにLPHOSTNET型変数にホスト名から算出されるIPアドレスを取得します。DNSサーバへの問い合わせで解決してIPVer4アドレスを取得しています。www.yo-net.jpなら160(0xa0).251(0xfb).151(0x97).160(0xa0)です。LPHOSTENTlpHostnet.h_addr_list[0~3]にそれぞれの値が格納されます。LPHOSTENTlpHostnet.h_addr_typeは2で、LPHOSTENTlpHostnet.h_addr_lengthは4となります。
次にSOCKADDR_IN型の変数SOCKADDR_INsockaddのsin_family, sin_port, sin_addrメンバ変数に適切な値を格納します。sin_familyにはプロトコルファミリの番号をsin_portにはポート番号を、sin_addrにはIPアドレスを設定します。sin_portはビッグエンディアンでの数値である必要があるので、変換関数を使いますそれが、htons関数です。httpの標準ポート番号80は16bitの2進数で0000 0000 0101 0000です。16進数で0x00 0x50です。これをビッグエンディアンにすると 0x50 0x00なので2進数では、0101 0000 0000 0000 となります。これは10進数で表現すると20480です。というような計算をhtons(host to network short)関数が処理してくれます。似ているものには、htonlという関数もあり、その逆のntohsやntohlというものもあります。
そして、接続処理を行います。
■connect(SOCKET, const struct sockaddr FAR*, int)
第1引数:SOCKET s
ソケットを識別する記述子。
第2引数:const struct sockaddr FAR * name
接続を確立する必要がある sockaddr(SOCKADDR_INは同等だけどキャストが必要になります) 構造体へのポインター。(PSOCKADDR)型にキャストしたSOCKADDR_IN型の参照を引数にします。
第3引数:int namelen
第2引数の変数のサイズを指定します。sizeof(name)のようなカタチでサイズが取得できます。
次にsend関数で使う送信文字列を作成する処理をして、接続処理を行います。
■send(SOCKET, const char FAR*, int, int)
第1引数:SOCKET s
ソケットを識別する記述子。
第2引数:const char FAR* buf
送信するASCII文字列
第3引数:int len
第2引数の送信する文字列の長さ。文字列変数に格納されている最後の\0は送信不要です。
第4引数:int flags
呼び出しの方法を指定するフラグのセット。
- MSG_DONTROUTE:データをルーティングの対象にしないことを指定します。
- MSG_OOB:OOB(アウト・オブ・バンド)データを送信します (SOCK_STREAM などのストリーム スタイルのソケットのみ)。
■recv(SOCKET, char FAR*, int, int)
この関数は何度か呼び出す都度、受信文字列の位置が進みます。InternetReadFile関数とは違って、読み取り位置を調整したりはできません。読み取りが完了する都度、バッファから削除されていきます。読み取り位置を調整しようとする場合は受信プログラム側で更なる工夫が必要になります。
1.自前のバッファを用意する:
一度受信したデータを格納するためのバッファを用意し、必要に応じてそのバッファから読み取りを行います。この場合、既に読み取ったデータを保持することができます。
2.非同期ソケットと WSAAsyncSelect を使用する:
WSAAsyncSelect を使用して非同期ソケットを実装することで、データが到着するたびに通知を受け取り、データを都度処理することが可能です。
3.ioctlsocket を使用して FIONREAD を調べる:
ioctlsocket 関数を使用して FIONREAD を取得することで、まだ読まれていないデータのバイト数を知ることができます。
これらの手法により、データが到着したかどうかや、読み取り位置を変更することができますが、具体的な実現についてはまた別の機会に考えることにします。まずは基本を習得しましょう。
第1引数:SOCKET s
ソケットを識別する記述子。
第2引数:char FAR * buf
受信する文字列情報。文字コードはプログラム側で解析して解釈しないとなりません。
第3引数:int len
受信した文字の数。
第4引数:int flags
呼び出しの方法を指定するフラグのセット。
- MSG_PEEK:受信データをピークします。
- MSG_OOB:帯域外 (OOB) データを処理します。
- MSG_WAITALL:受信要求はイベントが発生した場合に完了します。
通信が完了したらshutdown関数でソケットを閉じる準備をします。
■shutdown(SOCKET, char FAR*, int, int)
第1引数:SOCKET s
ソケットを識別する記述子。
第2引数:int how
許可されなくなる操作の種類を示すフラグ。
- SD_RECEIVE:受信
- SD_SEND:送信
- SD_BOTH:両方
そして、closesocket関数でソケットを閉じます。
■closesocket(SOCKET)
第1引数:SOCKET s
ソケットを識別する記述子。
そして、Winsock 2 DLL (Ws2_32.dll) の使用を終了するため、WSACleanup関数を実行します。
■WSACleanup()
引数無し。
HTTPで特定のアドレスのHTML文書を得たいとき、送信するメッセージは
GET http://www.yo-net.jp/index.html HTTP/1.0\r\n\r\n
のようなものを送信します。その結果、以下のような文字列の応答が返ってきます。
HTTP/1.1 301 Moved Permanently
Date: Fri, 01 Dec 2023 22:00:00 GMT
Server: Apache
Location: https://www.yo-net.jp/index.html
Content-Length: 240
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://www.yo-net.jp/index.html">here</a>.</p>
</body></html>
管理人のWebsiteでは、トップページのhttpアクセスはhttpsへ転送されるような動作をするように指示するようなHTML文書が応答されるようになっています。Location:https://www.yo-net.jp/index.htmlで自動転送されるようになります。自動転送が処理されないようなWebBrowserの場合にはThe document has moved <a href="https://www.yo-net.jp/index.html">here</a>.のような文書を表示して、手動で転送先を開くように促しています。
前の記事:Win32/64ネットワーク開発 003 wininetを使ったftp通信
次の記事:[[Win32/64ネットワーク開発 005]]