WinSock

WinSockとは、プログラミングの際、Windowsでインターネット通信をするためのAPIなどである。webブラウザを作成したりしたい場合、このようなAPI(ソケット通信API)が必要である。もし単にホームページを作りたい場合には、Winsock は不要である(ホームページの作り方は、簡単なものなら HTML を使って作れる)。
Windowsに限らず、UnixやLinuxなどでも、OSが提供している、ネット通信をするためのAPI群を「ソケット」(socket)といい、ソケットを用いて通信を実現することを「ソケット通信」などという。
この科目でいっている「クライアント」側のプログラムとは、たとえばwebブラウザそのもののようなプログラムである(HTMLファイルのことではないので、混同しないように)。

とりあえず、動くコード(MSDN Creating a Socket for the Client などにあるコードを繋げただけ)
MSDN のページをもとに作成したコードだが、しかしMSDNのサイトでは「int main(int argc, char** argv)」の引数 「(int argc, char** argv) 」の部分の説明が抜けているのである。
「(int argc, char** argv) 」とは、C言語界隈で慣用的によく用いられる引数であり、「プログラム仮引数」という。(詳しくは『C言語/中級者向けの話題#プログラム仮引数を持つ関数』を参照。)
さて、構造体の部分は、
(以下略) は、Windows仕様やあるいは規格か何かでこう書くと決まっているらしく、 MSDNにもこう構造体が書いてあるので、
読者はそのまま受け入れてもらうしかない。
上記のMSDNのコードの予備を、諸般の事情のため、wikibooksでも保管する。
WSAStartup が成功した場合、0を返す仕様である。
つまり、もし返り値が 0 でなければ、エラーである。
なので、続くif文で、返り値が 0 かどうかを見ている。

IPv4 のころは gethostbyname が使われていたが現在ではあまりサポートされておらず、 2010年以降の現代では代わりに getaddrinfo() を使うのが推奨されています。 (上述MSDNの完成形コード例でも gethostbyname は使われておらず、getaddrinfo() だけが使われています。)
getaddrinfo()を使うには winsock2 が必要ですのでインクルードする必要があります(つまりバージョン1ではダメ)。

残念ながら日本のネットにあるwinsockについての古い解説サイトでは gethostbyname でいまだに解説しているサイトもありますが、単にそのサイトの作者が更新をサボっているだけなので無視しましょう。
getaddrinfo() は成功したら返り値として 0 を返却します。
なお、 getaddrinfo() で確保されたメモリは freeaddrinfo() で解放されます。

Linuxのソケットプログラミングにも同様の関数がありますが、しかし仕様が微妙に異なるので、読者はwinsockプログラミングではwindows用のサイトを参考にしましょう。
とりあえず、下記のようなコードで、サイトのドメイン名から、IPv4対応のIPアドレスを知ることが出来ます。(※いきなりIPv6対応のコードを書くのは難しいので、まずIPv4対応のコードから書こう。)
※ 架空の企業のIPアドレスですので、数字はデタラメです。また、一部、伏字にしています。
のような結果になる。
※ 上記コードは2023年のwindows11でも問題なく動作する。

のMAKEWORDというのは、この場合、WinSockのバージョンが2.2であることを表している。 あまり仕様の詳細のハッキリしない関数であるが、MSDNでこの関数(正確にはマクロ)が使われているので、それに従おう。
なお、生成されるWinSockのバージョンは
の書式である。
また、もし
のようにバージョン2.0を指定しても、実行時に生成されるWinSockのバージョンは自動的に2.2のような実行時の最新安定版のバージョンになる。
あまり重要ではないので、詳しくは後述する。

さて、かつて inet_ntoa という関数があったが、これは2020年代のVisual Studio では使えないので、 inet_ntop に置き換える必要がある。

さて、それとは別件だが、
は、何をやっているかというと、大まかには、まず、getaddrinfo()の結果が構造体 res (またはそのポインタ)に入り、
そしてgetaddrinfo(hostname, NULL, &hints, &res); の第4引数でアドレスを指定した構造体(&res)は、addrinfo構造体というものに格納される。 そして、Windowsの場合、addrinfo構造体は、要素として
と言う要素を持つ仕様になっている。(一方、BSDでは、OPアドレス情報は sockaddr に含まないらしい。)
この sockaddr *ai_addr は構造体のポインタなので、利用の際にはアロー演算子->で指定しなければいけない(なぜ構造体そのものではなく構造体のポインタなのか気になるかもしれないが、規格などでそう決まってしまっているようなので、従うしかない。外国のサイトを見ても、同様のコードである)。
本来のBSDなどのsockaddr と sockaddr_in は、違いとして、あて先IPアドレスの情報を含んでいるかどうかの違いがある。sockaddr は、その構造体変数の内部情報には、IPアドレスの情報を含まない。一方、 sockaddr_in はIPアドレス情報を含む。なので、上記コードでは、最終的にIPアドレス情報を含む sockaddr_in に変換しているわけである。
BSD本来の sockaddr はIPアドレス情報を含まないが、Windowsでは sockaddr がなんらかの方法でIPアドレス情報を含んでおり、ともかくWindowsでは sockaddr_in に変換できるように実装をされている。
IPアドレス情報をテキスト文字化できる関数 inet_ntop が、sockaddr_in だけに対応しているので、これに変換しなけれんばならない。
とあるが、
のような書式である。
なお、むりやり sockaddr 構造体の情報を引数に入れてもコンパイルできてしまうが、まったく違うIPアドレスが表示されてしまう。
および
は、まったく見当違いのアドレスを表示するだけである。
なので、まず、 nameaddr.s_addr = ((struct sockaddr_in*)(res->ai_addr))->sin_addr.s_addr; は、addrinfo構造体の中から、IPアドレス関連の要素を抜き出すために、
という項目がある。
そして、
によって、addrinfo構造体のメンバとしての sockaddr 構造体の情報を、独立した sockaddr_in 構造体のポインタとなるように転写している。
さて、 sockaddr_in 構造体は、メンバ変数として「sin_addr.s_addr」というのを持つが、
(struct sockaddr_in*)(res->ai_addr)は、 sockaddr_in 構造体ではなく、 sockaddr_in 構造体のポインタ なので、なので、「sin_addr.s_addr」にアクセスしたい場合には、アロー演算子でアクセスしなければならない。

なお、もし、冒頭の WSAStartup のコードを変えてしまって、
と変えてもコンパイルできてしまい、実行時に表示されるIPアドレスも相手先のアドレスであるが(※ 2020年にVisual Studio 2019で確認)、しかしMSDNでは推奨していない書式なので、MAKEWORDで書いたほうが安全だろう。
なお、「0x」とは「16進数で」という意味。
WORDは、「16進数で4ケタ」の値という意味。
なお、16進数の1ケタは、4ビットである。(2×2×2×2=16なので。)
なので、1 WORD は、4×4=16より、1 WORD は16ビット長である。
そして、1バイトは8ビットなので、つまりWORDとは2バイトである。
8ビットで0から256の数値を扱えるので、2バイトだと0から65536の数値を扱える。つまり、WORDは0から65536の数値を扱える。
MAKEWORDマクロ で作られるビット列も、1 WORDである。なので、16進数で4ケタを指定してもいいのである。
なお、DWORD とは「16進数で8ケタ」である。つまりDWORDなら合計4バイトである。DWORDは4バイトなので 0から 4,294 967,296 の数値を扱える。
ちなみに、0xでWSAStartupを直接指定した場合、
の書式である。MAKEWORDで生成した場合とは、マイナーバージョンとメジャーバージョンの順序が逆なので、気をつけよう。
なので、
でもコンパイルが通るし、表示されるIPアドレスも正常である。
一方、
では実行しても、IPアドレスが表示されない。
ちなみに、
というコードをWinSockの生成以降からクローズ前の任意の場所に付け足すことで、バージョンを表示すことができる。 >> というのはシフト演算。上記コード例の場合なら、右に8ビットのシフトをするという意味。
上記コード中の「data」とは、
の第2引数の&dataに由来している。なので、もし引数名が別の名前(たとえば wsadata)で、ソケット生成時のコードも
なら、
のようにコードが変わる。
追加コードの表示結果は、
のように表示される。バージョン2.2であっても、「WinSock 2.0」と表示される。
下記コードでgetaddrinfoでIPv6でDNSサーバと通信できる。
socket() 関数の返却値は、intではなく、SOCKET型という専用の型である必要があります。
そのため、事前に SOCKET型の変数 ConnectSocket を宣言しています。
またsocket() 関数は失敗すると INVALID_SOCKET という返却値を返します。
socket()関数は3つの引数を取ります。
これら3つの引数の組み合わせで、通信方法を指定しています。
第1引数はアドレスファミリというものの指定です。

MSDNのコード中に
とありますが、
これは既にwindowsによって定義されている addrinfo 構造体についてのポインタ変数 result を作成する宣言です(Unixでも、構造体の名前は違いますが、ソケット通信の専用の構造体が最初から用意されており、その構造体の変数を書き換えることで通信する方法です)。
さて、Windowsの上記の宣言では、 addrinfo は作成されません(なぜなら、すでにaddrinfo構造体は作成されているので)。

ソケット作成のコードで
とあります。 「->」はアロー演算子というもので、構造体のポインタ の メンバ にアクセスするときに使います。
ポインタ ptr が出てきて厄介ですが、
読みとくと、ptr は単なる構造体 addrinf のポインタ変数です。
そしてWindowsの既に用意している addrinfo 構造体には、すでに ai_family メンバがありますので 『ADDRINFOA (ws2def.h) - Win32 apps | Microsoft Docs』『ADDRINFOA structure』 、
よって、 ptr->ai_family により、addrinfo構造体の ai_family メンバにアクセスできるのです。
hints 関連の項目にある AF_INET というのはIPv4のアドレスファミリですが、なぜかIPv6時代の現代でも流用されており、これで色々と通用します。
さて WindowsにかぎらずLinuxなどでも一般に hints では、通信方法を指定する情報が扱われている仕様である。
しかしWinSockのこのコードの hints は単にaddrinfo から流用した構造体変数である。 実際、MSDN の ADDRINFOA structure を見ると、ページ中ほどにある ai_family の項目一覧の中に「 AF_INET 」という項目があります。
ZeroMemory() というのは指定した変数のメモリにゼロ 0 を書き込む関数型APIマクロ。
ZeroMemory() の第1引数は、0を書き込みたいインスタンスへのポインタ。
第2引数は 0 を書き込む文字の長さ。
telnet(テルネット) でのHTTP通信についても、上記リンク先にある。
Windowsでは、実はWinSockを使わずとも、HTTPやFTPなど、既存の有名な通信プロトコルに対応した、簡略的に通信できる関数を持った関数グループがWindowsには既に用意されており、たとえばWinInetと言われる関数グループがそのような目的のために用意されている。

下記に、WinInetを用いたHTTP通信のコード例を示す。
下記コードを実験するには、事前に XAMPP や Apache などの webサーバ を立ち上げること。なおwindowsの場合、apacheを直接インストールするのは難しいので、xamppをインストールするのが入門的である(なお『PHP/確実に動作させるまで』にXAMPPなどの設定の解説あり)。
実験用のHTMLファイルには、detarame.html というHTMLファイルに、「seikou」(成功の意味)という文字列が書いてあるとする。

※ http://localhost/detarame.html のページ内容と同じ内容が、そのまま表示される。
たとえば、webブラウザで上記ページにアクセスした場合に「seikou」と表示されるなら、上記C++コードを実行した際には、コマンドプロンプトで「seikou」と表示される。
のように表示される。
WinInetのHTTP通信の場合、ポート番号などの指定の必要がない。
指定するのは、ほぼURLのみで、関数 InternetOpenURL でアクセス先 URL を指定する。「URLのどこからどこがドメイン名か?」とか、ユーザーは意識の必要がない。
その他、InternetOpen などが用意されており、割とC言語のファイル入出力の雰囲気に近いようなユーザーフレンドリーな感じで操作できるように、マイクロソフトが仕様を作っている。
先にInternetOpen してからでないと、関数 InternetOpenURL を使えないハズである。

さて、C言語のファイル入出力では、ファイルポインタというものを介して、ファイルのOpenやcloseなどの処理をする。
Win32APIではファイルハンドルというものが用意されており、ファイルポインタと似たような使いかたをする。
InternetOpenの内容を、ファイルハンドルに保管する。
通信が終わったら、使用したファイルハンドルを片付けるため、関数 InternetCloseHandleでクローズする。

GlobalAllocは、Windows特有の、メモリの動的確保の関数。標準C言語でいうmallocのようなメモリ確保の関数である。
本WinInet通信では実装の都合などで、もしコレを使わずに直接にchar型の配列に受信した文字を格納しようとしても、文字化けで
以下略のように表示されてしまう。
メモリ確保したら、本来なら、使用が終わったらGlobalFree()でメモリ解放しないといけない。なので、上記コードでは、メモリ解放してある。
なお、mallocも、free()関数でメモリ解放するのが、行儀よい、とされる。なぜなら、mallocなどのメモリ確保の関数を使う場合、使用後にはメモリ解放をしないと、バグとして「メモリリーク」というメモリ圧迫のバグの原因になりやすいからである。
メモリ確保したら、本来なら、使用が終わったらGlobalFree()でメモリ解放しないといけない。なので、上記コードでは、メモリ解放してある。 実は、この程度の長さのコードの場合、Windowsが気を利かして、コード終了時にメモリを解放してくれるのでGlobalFree()の省略も可能である。だが今後、別の長いコードにコピーする場合など将来的な学習も考え、上記コードではメモリを行儀よく解放してある。
なお、もし単に GlobalAlloc(GPTR, 1000); の部分をそのまま malloc(1000); に置き換えても、Windowsの実装の都合なのか、文字化け「seikouヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘ」が出てしまう。
malloc だと、GlobalAlloc引数の「GPRT」などのフラグ設定を出来ない。

マイクロソフトは、WinInetについてはサンプルコードのひとつも公式MSDNに紹介していない(2020年7月に確認)。せっかくユーザフレンドリーなWinInetがあるのに、サービス心が中途半端である。


InternetReadFile の仕様として、ファイルを読み終わると、 InternetReadFile(hFile, outBuf, readSizeMax, (DWORD*)&realRead); の最後の引数 realRead に 0 が代入される。
なので、この仕様を使うことで、HTTP通信における受信のwhileループを下記のように書き換えることもできる。

※ windows11で試したところ、realRead を (int) で変換しないといけなかった。変換しないとビルドエラーになった。
ソケットプログラミングについての出版状況は、まるで Visual C++ の Windows APIプログラミングの書店での不足ぶりと類似の状況であり、あまり市販の入門書ではソケットプログラミングが解説されていない状態である。
そのため当面はネットの解説サイトを参考にするしかない。
下記のサイトに、充実したWinSockの解説がある。