WinSock / BSDSocket

acceptは、発動した段階でソフトウェアを停止し、相手がconnectでSYNを送信するまでWindowsメッセージを処理しなくなります。
分かりやすくいうと、acceptを発動した時、SYNを受け取る動作以外無効化します。
B

illsut:江ノ木 椎茸(https://pawoo.net/@shitakenoki

しかし、acceptしている間、画面が止まるゲームは嫌ですよね。市販のゲームは通信待機中でも画面上をキャラが動いているものです。

そこで思いつく手法が、非同期通知です。
つまり、順番を逆にするのです。acceptで待ち受けてから、SYNを受け取るのではなく、SYNを受け取ってから、acceptを発動するのです。

SYNを受け取ったかどうかを判定するには、Windowsではイベントを作成すればよいでしょう。
イベントとは、いくつかの状態を持つスイッチのような変数です。

まずイベントを宣言し、SYNを受けとった時、言い換えると、acceptが必要な時にイベントをONにします。イベントがONになったら、acceptを発動します。

とりあえずイベントを宣言してみましょう。


イベントを宣言するにはWSAEVENT型を使います。
実はWinSockにはverson1とverson2があり、今まではverson1で学習を進めてきました。
しかし、WSAEVENT型はWinSock2にて定義されているので、今後はwinsock2をインクルードして下さい。これ以降の学習には、WinSock2を使用します。

それに伴い、inet_addrが警告を出すようになるため、警告を回避するプラグマを3行目で書いています。実用的なアプリケーションではinet_ptonなどを使用して下さい。

14行目でWSAEVENT型のイベントを宣言しています。
26行目でイベントを初期化しています。WSACreateEventは引数を持ちません。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
続いてソケットとイベントを紐付けます。bindやconnectと同じノリです。
ソケットとイベントを紐付けるにはWSAEventSelectを使用します。

第一引数へは紐付けるソケットを、第二引数へはイベント変数を、第三引数へは検知するイベントの種類を指定します。
このプログラムの関数を実行すると、ソケットがacceptを要する動作を検知すると、イベント変数の状態を勝手にシグナル状態へ変化させます。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
次のコードを表示する前に、WaitForSingleObject関数の説明をしておきます。
WaitForSingleObject(判定するイベント, 何秒停止するか)

WaitForSingleObjectはイベントの状態を判定します。
指定したイベントがシグナル状態のとき、WAIT_OBJECT_0を返します。

WaitForSingleObjectはブロッキングを起こします。しかしその時間は第二引数で指定できます。
0を指定すると、ブロッキングを起こさず、イベント変数の状態を判定してすぐに処理を返します。
これを利用しましょう。


このプログラムではwhile(1)で無限ループを作成し、ひたすら文字列を出力しています。
それに加えてWaitForSingleObjectでイベントの状態を毎ループ判定しています。第二引数は0なので、WaitForSingleObjectがソフトウェアを止めることはありません。

WaitForSingleObjectWAIT_OBJECT_0を返した時、SYNが届いているのでacceptを実行してbreak;でループを脱出します。break;は直近のループから脱出します。
ループを脱した後はacceptにて実際のやり取りを行うソケットが返却されているので、それを利用して今まで通りパケットを受け取ります。その後、画面に表示してWinSockとプログラムの終了処理です。

このプログラムにhellohello2という文字列パケットを送ると、実行結果は次のようになります。
16


もしacceptがブロッキングを起こすと、NowLoadingと1回だけ表示してアプリケーションは停止していたでしょう。でも……SYNパケットを待っている間アプリケーションが止まるのは嫌ですよね……
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ちなみにacceptが一度発動すると、紐つけたイベントのシグナル状態は勝手に解除されます。

補足ですが、WaitForSingleObjectの第二引数は1秒につき10000を指定して下さい。INFINITEを指定すると、イベント変数がシグナル状態になるまで永久に待機します。

また、非同期通知の手法はコンソールアプリケーション、Windowsアプリケーションで違いがありません。なぜなら、Win32APIの機能を使っているからです。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ちなみにconnectもブロッキングを起こすので、FD_CONNECTを使って同じように非同期通知を使用して下さい。
このエントリーをはてなブックマークに追加

例えば攻撃力を相手へ送信するとき、同じパケット内に、このパケットが攻撃力であることを示すメイン部分と、実際の攻撃力の数値を示すオプション部分を作りたいときがあります。
オプション付きパケットは、そのような時に使用します。

オプション付きパケットを作成する手法はレコード境界を作成する手法と全く同じです。
パケットのメイン部末尾に以降のデータはオプションであることを示す記号を付加し、
その記号からレコード境界までを読み取ります。
実装方法もレコード境界を作成する手法と同じなので、オプションが必要になったら各自記述して下さい。
このエントリーをはてなブックマークに追加

レコード境界とは、レコードの境界です。
レコードとは、データのことです。

前回、前々回作成したクライアント/サーバーサイドプログラムで考えます。
今回の議題は、クライアントが2つのパケットを送信した場合についてです。

例えばAボタンを押したことを示す「ButtonA」と、Bボタンを押したことを示す「ButtonB」というパケットを送信します。
send(ConnectSocket, "ButtonA", 7, 0);
send(ConnectSocket, "ButtonB", 7, 0);


2つのパケットがサーバーへ届いた後、recvを1度だけ実行して読み出すと、ClientDateBufferへはどのように文字列が格納されるでしょうか?



その為、1度のrecvで大量のパケットを読み出すことがあるのです。
これを防ぐための方法はいくつかあります。今回はレコード境界を作成する手法を学びます。
つまり、送信するパケットの末尾に記号を挿入しておき、その記号までのデータを実際のデータとして扱うということです。

レコード境界を用いたプログラムの処理手順は以下の通りです。
・クライアント:sendする文字列の末尾に記号を設定しておく。
・サーバーサイド:recvで読み出す。
・サーバーサイド:recvで読み出したデータから、末尾の記号が見つかるまで読み出すアルゴリズムを実行し、改めてデータを取り出す。


それでは実際にプログラミングしていきましょう。
sendする文字列の末尾に記号を設定しておく必要があります。
send(ConnectSocket, "ButtonA|", 8, 0);
send(ConnectSocket, "ButtonB|", 8, 0);


この状態で実行してみましょう。
2


ご覧のようにレコード毎に境界記号が設定されました。

続けてサーバー側で改めて境界記号まで文字を読み出す処理を書きます。


forrecvが受け取ったデータを1文字ずつ別の配列へ格納しています。
ClientDateBuffer[Seeki] != '|';
にて、|記号が見つかるまでforを継続していることが分かりますね。

また、forを脱出した後、NULL文字を末尾へ付加しています。
ちなみにforの初期化子は省略しています。
3


おまけ:2パケットとも取り出すプログラムを載せておきます。
2回目に取り出す時には、Seekiの値と配列の添え字が合わなくなるので、それを調整する必要があります。この辺は各自作りたいソフトウェアに合わせてきれいに書き直して下さい。

4
このエントリーをはてなブックマークに追加

前回作成したサーバーサイドプログラムは、クライアントが送信した文字列を画面へ表示するプログラムでした。
今回は、そのサーバーサイドプログラムへ文字列を送信するプログラムを書きます。
Visual Studio にて新しいプロジェクトを作成し、そちらに記述して下さい。


サーバーサイドプログラムと共通の関数を書いてみました。
実は、クライアントプログラムはサーバーサイドプログラムとほとんど変わりません。

SOCKADDR_IN構造体にてこちらでもServerInfoを作成しています。
この構造体へ格納するデータはサーバー側とあわせて下さい。
サーバーがどのようなネットワークの種類なのか、ポート番号とIPアドレスはなにかを指定するだけです。
また、クライアント側ではソケットを2つ作る必要はありません。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
それでは、クライアントならではの関数を紹介致しましょう。

connect関数は、サーバーへSYNを送信します。また、bindのようにソケットとSOCKADDR構造体の紐付けを行います。実はListenSYN ACKを返信します。
connectがうまくいくと、データを転送できる状態になります。

connectの第一引数へは、送信に使用するソケットを指定します。
connectの第二引数へは、サーバーの情報を格納したSOCKADDR構造体を指定します。前回話した通り、様々な事情からこのようなインターフェースとなっています。
connectの第三引数へは、第二引数のバイト単位のサイズを指定します。sizeof演算子を使えば良いです。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


実際のパケットを送信するにはsend関数を使用します。
sendの第一引数は、送信に使用するソケットを指定します。
sendの第二引数は、送信する文字列を指定します。
sendの第三引数は、送信する文字列の長さを指定します。strlen文字列の長さsizeof配列の長さを使用してもよいでしょう。
sendの第四引数は、sendの挙動を決めるオプションを指定するのですが、ルーティングテーブルの迂回や、バンド外データの送信など、ほとんど使わない機能なので通常は0を指定します
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
お疲れ様でした! これでクライアントソフトウェアの作成は完了です!
サーバーサイドプログラムを起動しつつ、クライアントプログラムを立ち上げてみて下さい。
17

ファイアウォールがアプリケーションをブロックします。今回のプログラムはローカルネットワークで運用するので、プライベートネットワーク(ローカルネットワーク)に許可をチェックしてアクセスを許可して下さい。
実用的なアプリケーションでは、パブリックネットワークを許可する必要があります。

もちろん同じPC内にサーバーサイドプログラムとクライアントプログラムは共存できます。

inet_addrのIPアドレスには、サーバーサイドプログラムもクライアントプログラムも同じローカルIPアドレスを指定して下さいね。

ローカルIPアドレスを調べるには、コマンドプロンプトからipconfigと打ちます。
3


ボクのデスクトップです。
13


実行結果
14

実行結果(サーバーサイドプログラムを拡大)
15

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
うまく動いた方はおめでとうございます。
うまく動かなかった方は、
IPアドレスの指定が間違っていないか、ポート番号の指定が間違っていないか……etc色々と確認してみて下さい。

IPアドレスが間違っている場合、例外が発生します。

もしWinSockのリンクそのものがうまくいかないのであれば、
黒翼猫のコンピュータ日記 2nd Edition #pragma commentでリンク指定しているのにリンクエラーになる問題
黒翼猫という方が詳しく解説しているのでそちらを参考に対処して下さい。

もちろん同じLAN内にあるコンピュータであれば、このプログラム達で通信が可能です。
PCを2台お持ちの方は、是非サーバーサイドプログラムと、クライアントプログラムをそれぞれのPCへ振り分けて実行してみて下さい。ボクはWin7のデスクトップとWin8.1のミニノートとWin10のゲーミングPCで確認しましたが、問題なく通信できました。
このエントリーをはてなブックマークに追加

Windows Socket API と サーバーサイド

Windows Socket APIとは、Windowsアプリ上でネットワーク通信を行うためのAPIです。
プログラマはWinSockをリンク、インクルードすることによってTCP/IP通信をすぐに実装できます。
WinSockはWindowsアプリケーションで通信を行う標準的な方法です。

WinSockを使用するには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(ソケット)です。ソケットとは、パケットがアプリケーションへ届いた時、アプリケーション側でデータを待ち受けている論理デバイスのことです。
11


これまでサーバー側の設定を行ってきましたが、いよいよそれらを使ってクライアントを待ち受ける準備をします。

ソケットデバイスを作成するには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_INSOCKADDR_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; ですね。
acceptSYNを受け付けたソケットをベースに新しいソケットを返却します。
この新しいソケットを使用して、実際のパケットをやり取りします。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を実行して初めて、様々な領域を解放するので、この関数を忘れてはいけません。

お疲れ様でした。かなり長かったですが、これにてサーバーサイドアプリケーションの完成です。
次回は対となるクライアントアプリケーションを制作します。
このエントリーをはてなブックマークに追加

↑このページのトップヘ