資料館/ネトゲ製作/003

Last-modified: 2008-11-11 (火) 20:42:03

チャットサーバー作るぞ

pcbnet2付属サンプルには、懇切丁寧、チャットサーバーも用意されている。だが、HSP2.xを対称に書かれているため若干の修正を行う必要がある上に、uPnPを使ったりと、シンプルと銘打ってる割には結構色々やっている。ちなみに俺はuPnPのあたりで一回さじを投げたね。ホント。
なので、あのソースを読むことが出来ないチルドレンのために、本当にシンプルな、教科書に載せられるようなふつくしいチャットサーバーを、順序だてて作ってみようと思う。ウホ、親切。結婚して。


勿論、チャットサーバーとして最低限の機能しか持たないぞ。めんどくせーからな。とは言いつつも、一応複数人のユーザーが接続してくるので、ユーザーを識別できる程度に管理していく。

サーバーメモ

  • ホストに接続できるクライアントは最大で100人(夢はでっかく)
  • クライアント・サーバー方式で通信する。データは必ずホストを介し、送信者以外の全員に送信される。
  • ホストにはチャット機能はない。あくまでデータの受け渡しなどをするのみ。

クライアントメモ

  • クライアントはユーザー名を入力してサーバーに接続する
  • 一度に送信する文字は50文字まで。50文字に満たない場合も50になるまで空白を入れて送信する
    • 溢れた文字は無視
  • クライアントは、他のクライアントの名前などを直接知る事はない

まずは色々考えとく

ソフトウェア名(最重要)を考える

pcbnet2のサンプルに同梱されているチャットサンプルに、SimpleChatClientという文字がある。なので今回オイラが作るチャットシステムはそれよりさらにシンプルであるのでReallySimpleChatSystemとでも名づけようか。略してRSCSだ。何かカッコイイな。やる気出てきたぞ。以下この名前を使う。

送るデータと頻度について考える

前回の講座で説明した、packのフォーマットを決めるという事である。
まず通信頻度についてだが、基本的に通信頻度は少なければ少ないほど良い。そうなるように考えよう。今回は、誰かがEnterキーを押してテキストを送信した時のみ、サーバーが各ユーザーにデータを送信する事にしよう。これにより通信頻度はかなり下がる。だって人間がEnterキーを押さない限り通信しないんだもの。


次に送信するデータだ。まず思いつくのは

  • 入力されたテキストの内容
  • そのテキストの発言者の名前

が思い当たる。この二つを送信すれば、なるほど、サーバーはそのデータを全員に送信する事で、チャットシステムとしての体裁が整う。
だがここで注意しなければならないのは、クライアント→サーバーにデータを送信する際に果たしてユーザー名は必要かという事である。サーバーに接続した時にユーザー名をサーバーに保存しておけば、わざわざ何度も送信する必要はなくなる。サーバーが各ユーザーに送信する時に、名前を付与して送れば良いのだ。
さて、他に送信する内容はないかしら・・・無いな。RSCSの趣旨に反する。
なのでクライアント→サーバーにはテキスト、サーバー→クライアントにはテキストと送信者名を送信する、という取り決めにする。

ユーザーの管理方法を考える

ぶっちゃけ大したことはやらない。

  • ユーザーの名前
  • ユーザーのソケットID
  • そのユーザーが有効かどうかのフラグ

この辺を配列変数にして持っていればよさそうだ。

まとめ

サーバー側で必要な機能は

  • ユーザーの待ちうけ、接続処理
  • データを各ユーザーに送信する処理

この二つだけ。同様にクライアントで必要な機能は

  • サーバーに接続する処理
  • テキストを送信する処理
  • テキストと送信者の名前を受信する処理

この3つだけとなる。なんともシンプルだ。

サーバーとテストプログラムの作成

WHAT IS テストプログラム?

サーバーが正しく動作しているか調べるには、クライアントが完成していなければならないことは明白だ。ところがクライアントが正しく動作しているか調べるには、サーバーが完成していなければならないという循環に陥っている。そこで、サーバーの動作確認用に、接続処理やデータの送信だけを行う簡単なテストプログラムを作るといい。テストプログラムでちゃんと動いたら、今度はそれを元にクライアントに通信処理を実装する。例えば起動に5秒程度かかるゲームに通信対戦を入れようとした場合、クライアントに実装しながらテストを行うと非常に効率が悪いが、テストプログラムであらかたテストを済ませてしまえばだいぶ効率が良い。

変数の宣言とユーザー管理例

#define
;---------ユーザーデータ領域の確保
	sdim username,200,MAXUSER;ユーザー名
	dim usersocketID,MAXUSER;ユーザーのソケットID
	dim uservalidflag,MAXUSER;有効かどうかのフラグ

ユーザーをフラグによって管理する。ユーザーと接続が維持されている間は、uservalidflagを1にしておき、切断されたら0にする。新たにユーザーが接続してきた時はvalidflag.0から検索して行き、flagが0であるものをそのユーザーに割り当てる。見つからなかった(flagが全て1)=ユーザーが一杯である。

ユーザーの接続、切断

前回の講義で接続処理のあらましはわかっただろう。ここでは複数人のユーザーの接続をさばく方法などを簡単に解説する。


基本的に、受付で対応できる人数は1人である。ほぼ同時に複数人のユーザーが接続してきた場合、前のユーザーの接続処理が完了するまで待ちぼうけを食らうことになる。
さて、TCPWaitによってユーザーの接続を感知した。なのでそのユーザーに割り当てるナンバーを決定しよう。ナンバーが5の場合は、先に宣言した変数のusername.5などを使用することになる。

	tcpwait opensocket;受付にユーザーが来ていないかチェック
	if stat=1:{;着てる
		errorflag=1
		repeat MAXUSER;使われていないユーザーNoを検索
			if uservalidflag.cnt=0:{
				errorflag=0
				blankID=cnt
				break
			}
		loop
		if errorflag=1:return;満員状態
		tcpaccept usersocketID.blankID,opensocket;接続承認
		uservalidflag.blankID=1;有効フラグを立てる
		repeat
			tcpget username.blankID,64,usersocketID.blankID
			if stat!0:break
		loop
		username.blankID+="#"+blankID+"";名前にNo追加
		mes ""+username.blankID+"さんが接続してきました"
	}
  1. ユーザーナンバーの検索、決定
  2. 接続承認、有効フラグを立てる
  3. 文字列を受信し名前とする

という手順で動く。特に難しいことはやっていない。
ここで特筆すべき点はユーザーが一杯の時である。この例では、ユーザーが一杯になった時は何も処理していない。これによって規定数以上のユーザーを接続させないという処理が実装できるが、クライアントにはその理由がわからないだろう。今回は省くが、一度接続を承認し、ユーザーが一杯ですというメッセージをクライアントに送信してから切断すれば、クライアントはエラーで接続できないのか、ユーザー制限で切断されたのかわかるため、親切だといえる。


切断処理についてはもっとも単純な方法を利用する。その回線にエラーが生じたら切断するというものだ。

	tcpfail usersocketID.cnt;ソケットがエラー状態か調べる
	if stat!0:{;エラー状態
		tcpclose usersocketID.cnt
		uservalidflag.cnt=0
	}

エラー状態は様々な要因で起こる可能性があるが、一番考えられるのはクライアントが終了した時である。接続を切断し、有効フラグを0にしておこう。こうすれば、そのナンバーはまた別のユーザーに割り当てることが出来る。

テストプログラムを用いた接続テスト例

接続、切断処理のあらましが出来たところで、いったんテストしたくなるのが人情というもの。RSCS程度の規模ならいきなりクライアントを作ってしまって問題ないが、ゲームに後から追加する、というような状況も踏まえ、簡単なテストプログラムを作ってみる。自分で必要だと思われる処理を詰め込めばいい。*1

#include
#define
#define
	tcpopen socket,"127.0.0.1",SERVERPORT
	repeat
		tcpiscon socket
		if stat!0:break
		await 1
	loop
	tcpput USERNAME,socket
	wait 100
	end

オイラが今回利用したものはこんな感じだ。接続、名前送信、切断処理がシンプルにまとめられている。これを適宜書き換えつつ、サーバープログラムをテストしていくわけだ。こんなふうに。

testing.jpg

テキストの送受信

さて、肝となるテキストの送受信機能を作ったらサーバーは開発終了だ。ループによって有効なユーザーからのテキスト到着を待とう。

	repeat MAXUSER
		if uservalidflag.cnt=0:continue
		tcpget recivedata,64,usersocketID.cnt
		if stat!0:{
			;----------送信処理---------------------
		}
	loop

まあこんな感じだろう。
有効でないユーザーからテキストが届くわけがないので、有効フラグが立っていないユーザーはこの処理を真島クンみたいにすっ飛ばしてくれ。
ではここで問題。この送信処理には何を書けばいいだろう。キーワードは、有効なユーザーに送信する送信者には送る必要はない名前を付与して送る、といった感じか。回答例はサンプルとしてうpしておくが、実装方法は一通りではないのでテケトーにやってみてほしい。サンプルでは、pack命令を使用し、ログの表示のためにmesboxを作成し、ログ書き込み用のサブルーチンが追加されている。

クライアントの作成

いきなりクライアントの作成を読み始める天邪鬼は居ないと思うが、おながいですからサーバー作成を先に読んできてください。

接続処理

さて、クライアント側はサーバーに接続するところから始まるが、きちんと順序だてて作成してきた良い子達にはサンタさんからプレゼントがある。接続処理のあらましがサーバーテストプログラムにまとまっているのである。これをみると接続処理の際、ユーザー名が必要になるらしい。なのでまずは起動時にユーザー名を入力する処理を入れておこう。mesboxとかで適当に作れ。
接続処理についてはテストプログラムを作ったので大体いいだろう。テストプログラムからコピって来て適宜書き換えればよろしい。一応先のテストプログラムの例では、4行目~10行目が接続処理に相当する。

テキストの送信処理

クライアント側で適当に入力処理を作ろう。最低限以下のものがあれば事足りるだろう。

  • 送信するテキストを入力するところ
  • 送信するアクションを起こすところ
  • 受信したテキストを表示するところ

今回の実装では、自分が送信したメッセージはサーバーから送られてこないので、送信アクションの際に自分のテキストを表示する処理をすると良いだろう。あとはサーバーの仕様に従ってデータを送信する。今回は、入力されたテキストのみを送信すればサーバーが勝手にやってくれる。

クライアントの実装は一通りではない

さて、ここまで一連で見てきた人々は気付いただろう。ようはサーバーの仕様にそってデータの送受信が行えれば、どんな実装でもクライアントとして成立する。

;--------名前などの定義------------------
	username="TEST_USER"
	serverIP="127.0.0.1"

	screen 0,320,135
	title "RSCS Ver."+VER+" START"
	syscolor 15:boxf
	color 0,0,0
	mes "利用するユーザーネーム"
	input username,320,20,20
	mes "サーバーのIPアドレス"
	input serverIP,320,20,0
	objsize 320,60
	button "GO",*start
	stop

名前入力画面はこんな感じだ。もちろんあらかじめポート番号はサーバーの設定に則って定義している。ここではユーザー名を決定し、サーバーへ接続するアクションが起こせればそれでいい。
そしてチャット部分では、テキストの送受信が出来れば何でもいい。
テキストボックスで改行処理を行っても良いし、送信ボタンをつけても良い。この辺は各自適当に考えてみて欲しい。一応3パターンくらいサンプルを作っておいた。どのクライアントをいくつ起動しても、サーバーで決められた人数以内ならきちんとチャットできるはずだ。50人とか試してないからわからんけどな。

動作チェック

全て作り終えたらサーバーを一つとクライアントを好きなだけ起動し、チェックしてみよう。各々の通信がうまくいっているようなら、どうやら完成だ。インターネットを通じて世界中の人とチャットするステキなシステムを、我々は一つ生み出してしまった事になる。


もう一人の自分とチャットする寂しい男
chattest01.jpg
そのときのサーバーログ
chattest02.jpg

共通処理をまとめてみる

さて、実際作ってみると、サーバーとクライアントで共通な部分があることに気づく。ポートの宣言とかだ。通信ポートが変わるたびに両方を書き換えるのはまんどくさいよな。#include文を利用するなどして適宜共通ファイルにしておくといいだろう。

まとめ

以上でサーバー・クライアント方式のチャットシステムは完成だ。
クライアントは本当に送受信処理のみで、サーバーが全てを担っている事が良く判ったと思う。大規模なシステムになると、サーバーに依存すればするほど負荷が高くなるためクライアントで処理すべき事も増えてくるだろう。


次回は、このチャットシステムを、ピアツーピア式で構築してみよう。ただし、ユーザーを管理する簡単なサーバーは置く。これによって、ピア・ツー・ピア式とサーバー・クライアント式における、サーバーとクライアントへの依存度の違いがわかるだろう。

サンプルソースのダウソ

改変は自由、批判は断わる!


*1 テストについてはここをみるとよい