資料館/ネトゲ製作/002

Last-modified: 2008-11-03 (月) 16:26:29

とりあえず通信する

とりあえず通信するか。何事もトライアンドエラー、いわゆる、試行錯誤ね。

PCBnet2でTCP通信する際の大まかな手順

基本的にTCP通信であれば、他のライブラリも似たような段取りのはずだ。

  1. ホスト(サーバー)が、通信待機を開始する
  2. クライアントが接続する
  3. クライアントのアクセスを確認したホストは、通信を承認する
    1. 新たなソケット(後述)を作成し、通信できる状態となる
  4. ソケットを通じてデータのやり取りを行う


大まかにこうなる。ではここで基礎知識のちぇっく。

  • ステップ2でクライアントがホストに接続する最必要なものは→ホストのIPアドレス
  • ホストに接続した際、データの振り分け先プログラムを識別するものは→ポート番号
  • ポート番号が他のアプリケーションと被ってしまった場合に起こる問題は?(まるばつ)
    • クライアントはホストにたどり着く事ができない→×IPがあればたどり着く事はできる
    • データの振り分け先を指定できないため、ホストは待機を開始できない→
    • データの振り分け先を指定できないため、クライアントはデータを送信できない→×送信は出来るが、ホストが受け取れない

基礎知識編を読んでいれば、IPアドレスによってホストの位置を決め、ポート番号によってアプリケーションを決める、という事がわかっているはず。

ソケットってなんじゃらほい

一つのポートで、複数の相手とやり取りをする時に番号を振る事ができる。イメージ映像を用意しておいたのでフィーリングでわかれ。
sock.jpg
LANケーブルの中にポートという窓が何個もあり、さらにその窓の中をソケットという小さな窓で区切り、複数の相手と通信する事ができる。だってそうしないと、3人以上で対戦するときにポートが3つも4つも必要になって大変じゃないか。以上。

まずは接続処理だけ作ってみる

  • 任意のポートで接続待機を開始する
  • クライアントの接続を感知し承認する

という機能を持つサーバープログラムと

  • サーバーにアクセスする
  • 接続が承認されたら知らせる

という最低限の機能を持ったサンプルを作った。両方とも、#include文を含めてキッチリ10行にまとめたので、読解力に自信の無いボーイ達でも安心だ。

サーバー側の処理

filesaba1.hsp
#include "pcbnet2.as"
	title "?T?[?o?[?v???O????"
	tcpmake opensocket,4649
	repeat
		tcpwait opensocket
		if stat=1:tcpaccept socket,opensocket:break
		await 10
	loop
	mes "?N???C?A???g??????????
	stop

まずsaba1.hspから見ていく。3行目のtcpmakeによって接続待機を開始する。
待機を開始したら、次はクライアントが着ているか確認しなければならない。この辺なんかめんどくさいな。無限ループによって、定期的にtcpwaitを呼び出し、ソケットにクライアントがアクセスしてきていないかを調べている。メールボックスを定期的に覗く感じに近いか。ゲーム中にもプレイヤーの接続を許容するなら、メインループの中にでも置いておけばよろしい。
クライアントからのアクセスを検出すると、statが1になるため、tcpaccseptでそのクライアントのアクセスを承認し、新しくソケットを作り、通信できる状態となる。


ここで新しくソケットを作る意味を理解しておこう。一番最初に待機を開始するときに作ったソケットは、いわゆる受付窓口である。受付にきた客は、応対された後、大抵は別の窓口へ移動する。受付窓口でずっと話し込んでいると、後ろの客が入ってこれないためだ。携帯電話とか買いに行ったことあるならわかるだろう。この受付→応対の流れが、そっくりそのまま内部で起こっている。


クライアントが到達したら、受付用ソケットを確認するループを抜けておしまいとなる。




4649は今回適当に選んだポート番号である。ただし、実際にはこの番号は余りよろしくない。ポート番号は0~65535番まで利用できるが、既に幅広く使われており、利用できないポート番号もある。今回ゴロで選んだ4649は登録済みポート番号というカテゴリに属し、被ってる可能性が(実際には被ってなかったけど)ある。使われてなかったけど。普通我々シロートがゲームを作る際には、49152~65535までの間で私用するのが良い。*1
あと、1024番より下(うぇるのうん)は絶対に使っちゃダメ!僕とのお約束。




ところで、ネットゲームをやろうとしていると、ファイヤーウォールが警告を出してくる事はないだろうか。この鯖プログラムの場合、tcpmakeが実行された時点でファイヤーウォールが作動し警告を発する。

  • 通信を許可する
  • 通信をブロックする(選んじゃダメよ)

という2通りの動作を選択させる場合が多いが、どちらを選んでもHSP側でエラーになる事は無い。通信待機は成功しているからである。鍵のかかった牢獄で延々と股を開いている状況を想像していただきたい。そんな感じ。
なのでどうしても通信できないよう、という時は火壁が通信をブロックしていないか確認しておこう。

クライアント側の処理

次にkura1.hspだ。

filekura1.hsp
#include "pcbnet2.as"
	title "?T?[?o?[??????N???C?A???g"
	tcpopen socket,"127.0.0.1",4649
	repeat
		tcpiscon socket
		if stat=1:mes "?T?[?o?[????????:break
		if stat=2:dialog "unko":end
		await 10
	loop
	stop

tcpopenによって、サーバーへの接続を開始する。このときサーバーのIPを指定しなければならない。127.0.0.1というのはループバックアドレスという特殊なアドレスで、自分自身を指す。自分で鯖起動→クラでループバックを使ってアクセスする事で、自分のパソコンでテストを行う事ができる。


自分自身のIPとポート番号を指定した。従って現在自分のPCで起動している全てのプログラムのうち、ポート4649で待ち受けているプログラムにアクセスする事になる。
アクセスが成功したら、向こうが承認してくれるかどうかを定期的にチェックする。合格発表に近いか。鯖側がtcpacceptを呼び出して承認したら、速やかにstatが1になり、承認された事がクライアントに伝わる。

  • 一定時間応答が無かった。(tcpacceptが鯖側で呼び出されなかった等)
  • ソモソモアクセスが届かなかった(IPアドレス、ポート番号違い、ファイヤーウォールの陰謀など)

これらの理由により接続処理がタイムアウトしてしまった場合、statが2になるので、エラー表示をする。

色々データを送受信してみる

数値を送り、文字列で返答する

まずは基本中の基本ということで、数値型および文字列データの送受信をしてみる事とする。
流れとしては、サーバーが数値データを送信する→クライアントがその応答として文字列を返す、という感じで行く。saba2.hspを実行した後にkura2.hspを実行すると、双方のデータが送受信されている事がわかるだろう。

filesaba2.hsp
#include "pcbnet2.as"
	title "?T?[?o?[?v???O????"
	tcpmake opensocket,4649
	repeat
		tcpwait opensocket
		if stat=1:tcpaccept socket,opensocket:break
		await 10
	loop
	mes "?N???C?A???g??????????
;-------------------???l?f?[?^???M
	senddata=5963;int?^????
	mes "???["+senddata+"]?????????M????
	tcpsend senddata,0,4,socket
;-----------------???????M
	recivedata="";?????^??????`
	repeat
		tcpget recivedata,64,socket
		if stat!0:break
		await 10
	loop
	mes "?????"+recivedata+"]???M?????B"
	stop

接続処理は全く変わっていない。10行目以降を見ていこう。まず適当に整数を定義する。細かい話は省くがWinXP環境では整数型変数のサイズは4byteである。なので送信するデータのサイズも4byteという事になる。tcpsendで、受付用ソケットではなく、接続処理で新たに作成したソケットに向かって、senddataを投げつけよう。これで送信は完了だ。続きを読む前に、クライアントの受信処理の方を見た方が流れがわかりやすいかもしれない。






さて、次はクライアントから送られてくるデータを受け取らなければなるまい。チョッと気をつけなければならないのだが、15行目で文字列型変数を一つ宣言している。
ためしにこの行をコメントアウトするとわかるが、送られてきた文字列が数字に変換されて表示されてしまう。HSPだと色々変数の型とか勝手にやってくれるので忘れてしまいがちだが、PCBNET2を用いた通信では事前に必ず必要だと思われる変数を、必要だと思われる型必要だと思われるサイズで宣言しておく事がマスト。特に必要だと思われるサイズ、というのはシャレにならない。ほんとに。絶対に確保する事。もしサイズがわかんないとかそんなこと言うようなら充分に余裕を持って確保しておく事。少し専門用語を出すと、バッファオーバーフロー*2が発生する。
なので本当に確保する事。僕とのお約束。


さて、変数さえ確保できていれば、クライアントと似たような受信処理で、文字列の受信が完了する。今回送られてくる文字列のサイズがわかんなかったので適当に64にしといた。

filekura2.hsp
#include "pcbnet2.as"
	title "?T?[?o?[??????N???C?A???g"
	tcpopen socket,"127.0.0.1",4649
	repeat
		tcpiscon socket
		if stat=1:mes "?T?[?o?[????????:break
		if stat=2:dialog "unko":end
		await 10
	loop
;--------------------???l?f?[?^???M
	repeat 
		tcprecv recivedata,0,4,socket
		if stat!0:break
		await 4
	loop
	mes "?f?[?^["+recivedata+"]???M?????B"
;--------------------???????M
	senddata="?m?????????
	tcpput senddata,socket
	mes "?????"+senddata+"]????M?????B"
	stop

クライアントはまず、サーバーから送られてくる整数を受け取る事にする。今回は数値型変数一つが送られてくることがわかっているので、最大受信サイズにはとりあえず4byteを入れとく。送られてくるデータより大きくても問題は無い。無限ループでソケットにデータが来ていないか確認し、来ていれば受信する。statに読み取ったデータのサイズが入る。ここが0以上なら、何らかのデータが受信されたという事になる。
詳しい方法は後述するが、とりあえず今回はstatが0以上になった瞬間、データが入っている事だろう。


受信が終わったら、今度は文字列を返す。適当に宣言しとこう。文字列の送信は、tcpputという関数が用意されており、サクッと送信する事ができる。勿論、tcpsendを用いて送信する事もできる。
送信が完了したら、クライアントの動作は終了だ。

配列変数を送りつける

一つの変数を送ってみたが、今度は配列変数を丸ごと送りつけてみよう。

配列についてフィーリングで理解しとく

さて、配列変数は、同じ型、同じサイズの変数が数珠繋ぎになったモンである。数値型変数の送信例で、数値型変数は一個4byteだと説明した。同じ型、サイズの変数が繋がっているのだから、数値型配列変数全体のサイズは4*要素数となる。文字列型配列変数の場合は最大文字数*要素数となる。*3

配列変数の送信

filesaba3.hsp
#include "pcbnet2.as"
	title "?T?[?o?[?v???O????"
	tcpmake opensocket,4649
	repeat
		tcpwait opensocket
		if stat=1:tcpaccept socket,opensocket:break
		await 10
	loop
	mes "?N???C?A???g??????????
;-------------------???l?f?[?^???M
	dim senddata,5;int?^??z????
	repeat 5
		senddata.cnt=5-cnt
	loop
	mes "?z??????????M????
	tcpsend senddata,0,4*5,socket
	mes "???M?????B"
	stop

saba2.hspを改変し、配列方変数を送信してみよう。こんかいはサイズ5の数値型配列を用意し、(0)~(4)までに5~1までの数字を入れた。これを先程のtcpsendを用いて送信する。送信サイズには、4*要素数を指定する。これは頭から計数されるので、4*5を4*4とか4*3にすると、配列変数の4,3個目までを送信するといった事も一応可能である。

受信

filekura3.hsp
#include "pcbnet2.as"
	title "?T?[?o?[??????N???C?A???g"
	tcpopen socket,"127.0.0.1",4649
	repeat
		tcpiscon socket
		if stat=1:mes "?T?[?o?[????????:break
		if stat=2:dialog "unko":end
		await 10
	loop
;--------------------???l?f?[?^???M
	dim recivedata,5
	repeat 
		tcprecv recivedata,0,4*5,socket
		if stat!0:break
		await 4
	loop
	mes "?f?[?^???M?????B"
	repeat 5
		mes "recivedata."+cnt+"??+recivedata.cnt+"???
	loop
	stop

受信側である。
絶対に配列を確保する事
それが出来ていれば、あとは受信サイズの指定をして終了。きちんと確保さえ出来ていれば、整数型変数一つの送信となんら変わる事はないことは自明の理だろう。

データを途中から送信したい場合

何の因果か、データを途中から送信したい場合があるだろう。例えばdata(0)~data(4)のうち、data(2)とdata(3)の二つだけを送りたいとか、そういう曲面が無いとも言い切れない。
そういう時は、送信側でオフセット値を指定する事で解決する。オフセット値にbyte単位で値を指定すると、その分だけデータの先頭からずらした所より処理してくれる。
先の送信を例に取るとtcpsendを以下のように書き換える事で配列の3番目と4番目のみを送信する事ができる。

	tcpsend senddata,4*2,4*2,socket

まず一番最初の4*2は、8byte、つまり配列の先頭から、数値型変数二つ分ずらした所から送信を始める事を意味する。次の4*2は、送信するサイズの指定であるので、二つ変数を送りたい場合の8byteを指定する。

色々な種類のデータをまとめて送受信する

これがかなり重要になるであろうこと。異なる配列変数同士や、そもそも文字列とバイナリデータの混在など、今まで学んだ範囲では一度に送信する手立てが無いものも、パツイチで送信する事ができる。

データをパックする

複数のデータを一つの変数にまとめることをパックという。PCBNet2にはpack関数が用意されているので、そいつを使用するとらくちんぽん。
以下の例では、文字列と整数値を一つの変数にまとめている。
絶対に領域を確保する事

	alloc senddata,1024;1KB分変数を確保
	data1=4649
	data2=5963
	data3="文字列も送れる"
	data4=1234
	data5="間に数字とか挟んでもおk"

	pack senddata,"iia30ia30",data1,data2,data3,data4,data5;パック
	size=stat
	mes senddata;なにこの文字列
	mes "全部で"+size+"バイトだよ"

まず始めに、格納する変数を定義する。良くわかんなかったら充分に余裕を持って定義する事。格納したい文字列と数字を適当に宣言した後、pack関数によりsenddataにdata1~5までをぶっこむ。
pack関数を理解するうえで重要なのはフォーマット文字列である。packすべきデータがどのような型、サイズなのかを表すものであり、左から順に読まれる。この例では数値、数値、文字列30文字、数値、文字列30字、の順に指定してある。data1~5の型と対応している事がわかるだろう。フォーマットの詳しい内容は、pcbnet2のヘルプにでも書いてあるので音読しておく事。


ためしにsenddataを出力してみるとわかるが、良く判らん文字列が出てくる。これがpackした結果である。さて、packした事により、このdata1~5は、senddataという一つの変数にまとめられた。ではこの変数のサイズは実際いくつぐらいになっただろうか。計算してみよう。文字列30文字=30byteとして、足し算すればいい。





答え:4byteの数値3個+30字の文字列2個=4*3+30*2=72 (byte

packした後、システム変数statにpackしたサイズが入っているので、それを計算した値と照らし合わせてみると、ピッタンコカンカンである。1024byte分領域を確保しているのに72byteしか使わなかったことになる。後でデータが増える事も踏まえ、あらかじめ確保するサイズを128程度に落としても問題なさそうである。

データをアンパックする

パックされたデータを復元する作業がアンパックだ。pcbnet2には、unpack関数が用意されているのでそいつを使う。unpackは、packと殆ど記述方法が同じなのでコピペするとやりやすい。先の方法でパックされたデータを復元する方法は以下の通りだ。

	sdim data8,30:sdim data10,30
	unpack senddata,"iia30ia30",data6,data7,data8,data9,data10

フォーマット文字列には、パックした時と全く同じ値を指定する。あらかじめ別のところで定義しておくといいだろう。データがパックされている変数と、アンパック先を指定する。data6~10を出力してみると、1~5とそっくり同じデータが表示されるはずだ。

おまとめ

さて、このページを完璧に理解したならば、とりあえず瞬間的なデータの送受信についてはマスターした事になるだろう。
一瞬で送受信が完了しないようなデカイファイル、無限ループでリアルタイムにデータを受け渡す、などといった実践的な送受信は、ページが長くなってきたので次回以降に持ち越す事にする。さよならちゃん。


*1 ポート番号
*2 バッファオーバーラン
*3 ただし文字が入りきらなかった場合などにHSPが勝手にサイズ変更を行うため、自分が指定した値そのままにならないこともある。超注意。