Windows Socket APIとは、Windowsアプリ上でネットワーク通信を行うためのAPIです。
プログラマはWinSockをリンク、インクルードすることによってTCP/IP通信をすぐに実装できます。
WinSockはWindowsアプリケーションで通信を行う標準的な方法です。
WinSockを使用するにはC言語の知識とネットワーク概論の知識を必要とします。それらに自信がない方は、なんらかのリファレンスを当たって下さい。
C言語に関しては、↓のページから勉強することができます。
わかりやすい C言語入門
ネットワーク概論に関しては、↓のページから勉強することができます。
ネットワーク概論
また、通信系のプログラムは書ききったところで9割方動きません。
その理由は単純で、スタンドアロン(単独で動くプログラム)ソフトウェアと違い、アルゴリズムの挙動以外にも、サーバーサイドプログラムのミスや、ルーター、ファイアウォールの設定ミス、ネットワークが不安定なことによるパケットロス、パケットレシーブのタイミング、レコード境界の作成など、考えるべき事柄が膨大だからです。
では通信系のプログラムを作成するのは難しいのかと問われると、そんなことはありません。
ソースコード自体は100行以内に収まることが多いです。
80行あれば1対1でネット対戦するゲームが作れます(Shadowverseやポケットモンスターのオンライン対戦など)
単純に煩雑で、考えることが多くて、詳しく説明している書籍やWEBサイトがない為に、初学者が困惑しているというのが現状です。
WinSock自体が古いAPIだからというのもありますけどね。
前置きはこの辺りにして、早速プログラムを書きましょう。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
サーバーサイドプログラムから書きましょう。
記念すべき第1行目はWinSockをリンクすることです。
本来であればVisual Studioの設定から、追加の依存関係にWs2_32.libを指定するのですが、WinSockはWindowsが提供している機能なので、このように書くこともできます。
というか、こう書いた方が良いです。リリースする際に依存関係を指定し忘れたり、そもそも依存関係を指定する手順が面倒なので、一行書いてしまいましょう。これなら他のプロジェクトへコピペしてもそのまま動くので、移植性も高いです。(Windows環境に限る)
さて、これでWinSockはリンクできました。次はインクルードします。
なぜリンクしたのにインクルードするのかと言いますと、リンクはプロジェクト全体へ依存関係を指定するのに対し、インクルードはソースファイルへ関数を教える役目を持つからです。
#pragma comment(lib, "Ws2_32.lib")はプロジェクト内のどこかのソースファイルへ書けばそれで構いません。複数回書く必要もないですし、main関数以外に適当なソースファイルを作成してそこへ単行で書いても問題なく動きます。
それに対し、#include <winsock.h>はWinSockの関数を使用するファイルへは必ず書いて下さい。これは今まで通りなので特に説明する必要はありませんね。
続いて、WSADATA型(構造体)の変数を宣言しました。C++では={0};で構造体内部の変数を全て0で初期化できるので、その機能を使用しています。
この構造体には、WinSockの低レイヤーな情報を格納します。具体的にはWinSockのバージョン情報であったり、ベンダーの説明だったりなのですが……ぶっちゃけ必要ありません。
後述するWSAStartup関数でWinSockの使用を準備するのですが、その際WSADATA構造体のアドレスをWSAStartup関数へ渡してあげる必要があるので、仕方なく作っているだけです。実際の通信に必要なIPアドレスやポート番号を格納する訳でもないので、使おうとしない限り使うことはないです。
要は、WSAStartup関数が発動すると、WinSockの低レイヤーな情報が発生するので、それを格納しておく変数を作っておきたかっただけです。自分で作った変数へ格納してもよいです。ですが最初から用意されているのでWSADATA構造体を使いましょう。アプリケーションによってはWinSockのバージョン情報をユーザーへ表示したいこともあるでしょうから、そういうときに使って下さい。
main関数へ突入しました。先述したWSAStartup関数です。
この関数へは使用するWinSockのバージョンと、WSADATA構造体のアドレスを渡す必要があります。
WSAStartupの第一引数はWORD型です。WORD型とは、unsigned short型のことです。unsigned short型とは、2バイトの整数型のことです。
WSAStartupの第一引数はとてもややこしくて、2バイトの整数を受け取る内の、上位1バイトへWinSockのマイナーバージョンを、下位1バイトへWinSockのメジャーバージョンを指定します。
しかし、この第一引数を決定する計算式はとてもややこしいです。↓
((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
こんなもの理解しようとしないで下さい。その前に発狂します。
この計算を肩代わりしてくれる、計算式を定義したマクロがありますので、それを利用しましょう。
MAKEWORD(1, 1) // これは左側の引数にメジャーバージョンを、右側の引数にマイナーバージョンを指定すると、ビット演算を代わりに行ってくれます。別にWSAStartupの為に用意された訳ではありません。しかし、WSAStartupで使用することが多いです。
ちなみにMAKEWORDを使うのはこれが最初で最後です。
WSAStartup関数の第二引数へは、WSADATA構造体のアドレスを指定します。&演算子はその変数のアドレスを表現させる効力を持つのでしたね。
と、ここまで読んでみてどうでしょうか? 大分煩雑ですよね。
これはWinSockが古いAPIだからというのもありますし、最適化をしている結果こうなったというのもありますし、WinSockの祖先であるBSDSocketとの移植性を考慮しているからというのもありますし、とにかく色々と複雑な事情があってこのような若干使いにくいAPIになっているのです。
では続けますよ。
SOCKADDR_IN構造体の変数を作成しました。
この構造体には、サーバーの基本的な情報を格納します。
WSADATA構造体と違って、サーバーとして使用するコンピュータのIPアドレスや、どのポート番号で待ち受けるのか、プロトコル(TCPやUDPなど)は何を使用するのかなどを格納します。
更に、今後使用する関数でもSOCKADDR_IN構造体へ格納した情報を参照するので、WSADATA構造体と違って極めて重要な変数と言えるでしょう。
ServerInfo.sin_family = AF_INET;
にて早速定数を格納しています。AF_INETは整数の2を表しています。これは使用するネットワークの種類を意味します。TCPかUDPを使用する時は、AF_INETを指定します。
ちなみに、構造体のメンバ変数へアクセスする際には.演算子を使用します。
ServerInfo.sin_port = htons(54924);
にてサーバーサイドプログラム(このプログラム)が専有するポート番号を指定しています。
ここで以前、2バイト以上のデータはネットワークバイトオーダーへ変換する必要があるとお話したのを覚えているでしょうか?
MAKEWORDを使用した際には、1バイトずつ指定したので関係なかったのですが、
ポート番号を指定する時にはそうもいきません。
ネットワークバイトオーダーへ変換する時は、htons関数か、htonl関数を使用します。これはunsigned short型(0 ~ 65,535)のネットワークバイトオーダーへ変換するのか、unsigned long型(unsigned int型と同じ)のネットワークバイトオーダーへ変換するのかで変わります。
少し怖くなってきたかもしれませんが、落ち着いて考えて下さい。
そもそもポート番号をネットワークバイトオーダーへ変換する理由はなんでしょうか。
そうです、クライアント側から発見される為です。クライアント側もポート番号をネットワークバイトオーダーへ変換して探し回ります。それなのにサーバーのポート番号がネットワークバイトオーダーへ変換されていなかったらどうでしょうか? 運がよければ見つかるかもしれませんが、下手すると永久に見つからないかもしれません。
なのでポート番号はネットワークバイトオーダーへ変換する必要があるのです。
しかし、クライアントとのやり取りは基本、文字や文字列を使用するのでこれ以降使うことはないです。使うこともできます。
ポート番号を指定する時はhtons関数を使ってネットワークバイトオーダーへ変換して渡すということを覚えておいて下さい。なぜ変換が必要なのかは心の片隅にでも置いておいて下さい。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ServerInfo.sin_addr.S_un.S_addr
へはサーバーサイドプログラムを稼働させるコンピュータのIPアドレスを指定します。
その際、inet_addr関数を使用しています。inet_addr関数は1バイト毎に.で区切られた文字列を受け取り、それをIPアドレスとして解釈します。また、ネットワークバイトオーダーへの変換も同時に行ってくれます。
少しメンバ変数側がややこしいですが、今まで通り指定するだけなので気にしなくてよいです。
また、今はローカルIPアドレスを指定して、同じLAN内(ルーター内)での通信プログラムを作成しますが、グローバルIPアドレスを指定すれば、すぐにWAN上(インターネット全域)で稼働できるプログラムになります。最初はローカルネットワークで練習して、慣れてきたらグローバルIPアドレスを使ってみましょう。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ネットワークプログラミングを行う上で重要な概念をお伝えしておきます。
Socket(ソケット)です。ソケットとは、パケットがアプリケーションへ届いた時、アプリケーション側でデータを待ち受けている論理デバイスのことです。

これまでサーバー側の設定を行ってきましたが、いよいよそれらを使ってクライアントを待ち受ける準備をします。
ソケットデバイスを作成するにはSOCKET型の変数を宣言します。その後、Socket関数で初期化します。
Socket関数の第一引数へは、この通信で使用するアドレスファミリを指定します。アドレスファミリとは、ネットワークの種類のことです。今回はTCP通信を行うので、TCPまたはUDPを示すAF_INETを指定します。
Socket関数の第二引数へは、この通信で使用するプロトコルを指定します。
TCPを使用するときはSOCK_STREAMを指定します。
Socket関数は上記の設定をしたSOCKET型を返却するので、作成したSOCKET型へ代入してやればよいのです。
今後はこのソケットをアプリケーション上で待機させ、相手方のパケットをキャッチします。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ソケットはそのままでは使用できません。なぜなら、ソケットはサーバーの情報をなにも知らないからです。そこでソケットとサーバーとの情報を紐つける必要が生じるのですが、手動でやるのは大変なので、bind関数が用意されています。
bindの第一引数へは、紐つける対象のソケットを指定します。
bindの第二引数へは、紐つける対象のサーバーの情報を指定します。
この指定にはSOCKADDR構造体のアドレスを用います。
あれ? ボク達が作成したのはSOCKADDR_IN構造体でしたよね?
実はbindは汎用的な関数でして、他にもSOCKADDR_OSI構造体などのアドレスを受け取ることもあります。その上で聞いて欲しいのですが、bindは1つしか存在しません。C言語では同じ名前の関数を作成することはできません。なので、SOCKADDR_INを直接受け取れるようにbindを作成すると、SOCKADDR_OSIを受け取れなくなってしまいます。
そこで考え出されたのが、データの汎用型を受け取ることです。
bindが受け取るSOCKADDR構造体はSOCKADDR_INやSOCKADDR_OSIなどあらゆるソケットアドレス構造体を格納できる巨大な構造体です。
CやC++キャストを行うと大きなデータ型へ合わせて計算を行うのを覚えているでしょうか?
つまり、SOCKADDR構造体でSOCKADDR構造体より小さい構造体をキャストすると、大きさがSOCKADDR構造体と同じになるのです。これを利用すればSOCKADDR構造体でキャストしたとしてもSOCKADDR_IN構造体の中身は失われません。int型をdouble型へキャストしても情報がロスしないのと同じです。
bindが1つしか存在できないので、汎用性を持たせる為にこのようなインターフェースとなっているのです。
また、*演算子を付けて、引数のSOCKADDR構造体はアドレス変数であることを表現しています。
(SOCKADDR*)&SOCKADDR_INとは、SOCKADDR_INを示すアドレスをSOCKADDRを示すアドレスでキャストするということですね。intを示すアドレスをdoubleを示すアドレスでキャストすると考えれば分かりやすいでしょうか。そして関数へはdoubleしか渡さない……と。よく考えますね。
ボクは普通にbindを複数作ればよかったのでは? bind_in, bind_osiなどと思うのですが、BSDSocketがこういうインターフェースなのでそれに習ったのでしょう。この発想は素晴らしいですが、若干分かりにくくなっていますね。
bindの第三引数へは、SOCKADDR_IN構造体の大きさを渡します。sizeof演算子はその変数をメモリへ配置する為に必要なバイト数を調べます。
sizeof演算子を扱う時にsizeofが返す値へ+1あるいは-1するべきか悩む方がいますが、sizeofが返却するのは、オペランドがメモリ上で専有するバイト数です。整数型をsizeofすれば4になりますし、char型の要素数[10]の配列をsizeofすれば10になります。この場合、0~9までの10個の領域が確保されているので全く問題ないですね。sizeof演算子は特に不都合がなければ+1や-1する必要はありません。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Listen関数はソケットを接続可能状態へ変化させるスイッチのような関数です。
Listenの第一引数は、接続可能状態へ変化させるソケットを指定します。
Listenの第二引数は、連続してパケットが届いた場合、接続を保留させるパケット数を指定します。
接続を保留させるパケットとは一体何なのでしょうか?
実は1つのソケットにつき、4つか5つ程度、パケットを同時に受け取ることができます。
Listenの第二引数は、パケットをたくさん受け取った時、最大何個まで保持しておくかを指定します。
例えばあなたがオンライン格闘ゲームをTCPで(普通はUDPで作りますが)作成しているとします。プレイヤー1がAボタンを押した1フレーム後、今度はBボタンを押しました。ボタンを押すごとにパケットを発送するので、Aボタンのパケットを送出した1フレーム後に、Bボタンのパケットを送出することになりました。
この際、サーバー側でAボタンのパケットの処理が1フレーム以内終わるでしょうか?
答えは……
Listenの第二引数はプログラマの負担を軽減してくれます。サーバーの処理を高速化させる為に躍起になる必要が少し減るということです。
今回のサンプルコードでは0を指定しています。つまり、連続(かなり短い間隔)してパケットが届いた場合、後から届いたパケットは破棄することになるかもしれません。
と言いたいところですが、今回のサンプルコードではすぐに処理をするので問題にならないことが多いです。
実用的なアプリケーションを書く時は、4か5あたりを指定しておきましょう。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
いよいよクライアントからの接続を実際に受け入れます。
その為に、クライアントの情報を格納する構造体を作成しましょう。
サーバーの情報を格納する構造体と同じSOCKADDR_IN構造体です。
accept関数は実際に接続を待ち受ける関数です。
この関数が実行された時、クライアントからの接続を待ち受けるモードになります。
acceptはソフトウェアをブロッキングします。ブロッキングとは、動きを止めるということです。
意味が分からないかもしれませんが、acceptを発動するとaccept以外の全ての処理が動作を停止します。accept以下に書かれた処理は発動しません。格闘ゲームならキャラクターはその場で動かなくなりますし、制限時間を表現するタイマーも動きません。
acceptのような関数をブロッキング関数と呼びます。
acceptは、クライアントからのSYNを待ち受ける為に、プログラムを停止してしまうのです。
acceptがプログラムをブロックしてくれないと、コンソールなので一瞬でプログラムが終了してしまいます。今回のプログラムでは、むしろacceptがブロッキングを起こして待機してくれる方が都合が良いですね。acceptがブロッキングを起こすか否かはプログラマが決められます。
もしブロッキングを起こしたくない場合(例えば格闘ゲームの場合)、相手からの接続がくるまではacceptを発動しないモードにすることもできます。
この時注意して欲しいのですが、acceptが受け取るのはプログラマが定義したパケットではなく、SYNです。SYNを相手から受け取るまで待ちます。
acceptの第一引数には、待ち受けるソケットを指定します。
acceptの第二引数には、クライアントの情報を格納する構造体のアドレスを指定します。その際、上で述べた様々な背景により、(SOCKADDR*)へキャストする必要があります。
acceptの第三引数には、クライアントの情報を格納する構造体の大きさが格納された変数のアドレスを指定する必要があります。
acceptの上の行でClientInfoのサイズを測っているのはacceptの第三引数の為です。
また、sizeofの戻り値はunsigned int か unsigned shortであり、処理系により違うので、とりあえずintでキャストしています。acceptの第三引数には、int型を指定する必要があるからです。
ところで先程からしれっと新しい変数が宣言されていたことをご存知でしたか?
SOCKET CommunicationSocket; ですね。
acceptはSYNを受け付けたソケットをベースに新しいソケットを返却します。
この新しいソケットを使用して、実際のパケットをやり取りします。acceptにてクライアントの接続を待ち受け、クライアントが接続してきた時、そのクライアントの情報はSOCKADDR_INへ格納されます。このSOCKADDR_INとソケットを紐つける為にaccept内で処理を行い、結果、紐付けられた新しいソケットが返却されるのです。
今後はCommunicationSocketを使用してクライアントと実際のデータをやり取りします。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
closesocket関数はSOCKET型が専有していたメモリの領域を解放します。
今後はacceptが返却したCommunicationSocketを使うので、ListenSocketの役目は終了です。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━」
実はacceptがソケットを返却した時点で、このアプリケーションはパケットを受け取れる状態になっており、送出されてきたパケットは実際受け取っています。しかし、アプリケーションが受け取ったパケットは、厳密にはアプリケーションとは別の領域(ソケットバッファ)に格納され、読み出されるのを待機している状態になります。
recv関数は、ソケットバッファに格納されたパケットを読み出します。
今回のプログラムでは、クライアントは文字列を送信していることを想定しています。
recvの第一引数には、パケットを受け取ったソケットを指定します。
recvの第二引数には、もしパケットを読み出すことに成功したら格納する領域のアドレスを指定します。配列名は配列の先頭アドレスを表現します。
recvの第三引数には、パケットをどの程度まで読み出して良いかを指定します。本来はsizeof(ClientDateBuffer)で問題ないのですが、仕様により、読み出されたパケットからNULL文字が切り捨てられてしまう為、読み出したパケットに手動でNULL文字を付加するとよいかもしれません。その為、配列の最後尾から1つ手前の領域までは読み込んでも良いと指定しています。
NULL文字の付加は次の項目でやります。
recvの第四引数には、recvの挙動を指定します……が、読み込んだデータをソケットバッファから削除しない機能だったり、バンド外データの受信だったりと、通常使わない機能しかないので、基本的に0を指定します。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WinSockの話から脱線してしまうのですが、
recvは読み込んだ文字数を返却します。これを利用してNULL文字の付加を簡単に行うことができます。
例えば5文字読み込んだ場合、recvRETURNは5になります。
つまり、配列の6番目の要素にNULL文字を格納すればよいのです。配列の6番目の要素は[5]なので、そのまま代入すればよい訳です。配列の添え字は0から始まる。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
またもやWinSockの話から脱線してしまうのですが、読み込んだ文字列を画面へ表示しましょう。
printfでClientDateBufferを表示するだけですね。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
これでクライアントから送出されてきた文字列を表示するプログラムが一応できました。
しかしまだ終わりではありません。使い終わったら片付けなきゃいけないのが世の常です。
shutdown関数は指定したソケットがパケットを受け取らないようにします。
第二引数へは、0または1または2を指定します。
0を指定した時は、パケットを受け取らなくなります。
1を指定した時は、パケットを送信できなくなります。
2を指定した時は、上記のどちらともできなくなります。
別にこの関数を使わずとも、closesocketで解放してやればよいのですが、ソケットを閉じて、ソケットの領域を解放する方がきれいなプログラムだと思うので、このようにしています。
closesocketは上で説明しましたね。
WSACleanup関数は、WinSockの使用を終了します。
WSACleanupが発動すると、WinSock.dllのあらゆるリソースが自動解放されます。これはclosesocketと同じ効果を持ちます。
実はclosesocketは内部的にはメモリ領域を解放していません。解放しているように見せかけているだけです。WSACleanupを実行して初めて、様々な領域を解放するので、この関数を忘れてはいけません。
お疲れ様でした。かなり長かったですが、これにてサーバーサイドアプリケーションの完成です。
次回は対となるクライアントアプリケーションを制作します。
Next 【1限目】クライアント
コメント