- PR -

データの連続送受信に関して。

投稿者投稿内容
にとろ
会議室デビュー日: 2003/11/30
投稿数: 4
投稿日時: 2003-11-30 06:37
はじめまして、にとろと申します。
こちらには初めて投稿いたしますが、よろしくお願いします。

現在、複数のクライアント(10人ほど)とのTCPでのデータ送受信を目的とした、
サーバのプログラム(チャットサーバのようなもの)を作成しております。

はじめは、セレクタを利用したソケットチャネルで、受信したパケットをスレッドプールに渡し、
各処理を実行する設計でプログラムしましたが、以下の様な問題が発生しました。

TCPで高速に送信した場合、1パケットで複数のメッセージ(メッセーの分割もあり)が送信される場合がある。

  クライアント送信パケットの内容
    パケットAの内容:AAAAAAAAAAAA
    パケットBの内容:BBBBBBBBBBBB
    パケットCの内容:CCCCCCCCCCCC
    パケットDの内容:DDDDDDDDDDDD
    パケットEの内容:EEEEEEEEEEEE
    
  サーバ受信パケットの内容
    パケットAの内容:AAAAAAAAAAAABBBBBB
    パケットBの内容:BBBBBB
    パケットCの内容:CCCCCCCCCCCCDDDDDDDDDDDD
    パケットEの内容:EEEEEEEEEEEE

この場合、1パケット1メッセージで考えるとメッセージの破損が発生します。
したがって、ノンブロッキングモードでクライアントからデータを読み取り、各受信パケットをマルチスレッドで解析・処理する事が難しくなってます。
パケットが結合する場所やサイズは不明です。

そのため、各クライアント受信・解析処理をシングルスレッドで処理する必要があると判断し、以下の様な設計を考えました。
 
 受信スレッド処理
   各クライアントのスレッドを立てて、ブロッキングモードで受信する。
   受信パケットしたパケットを解析するスレッドプールオブジェクトを生成する。
   解析スレッドのオブジェクトをリストに最後尾に格納し、受信処理に戻る。

 解析スレッド処理
   正常な1メッセージ分のオブジェクトが完成した場合、メッセージ処理を行うスレッドプール
   にデータを渡す。他のパケットが結合していなければ、リストから削除し処理を終了する。
  
   破損パケットを発見した場合、前後の解析スレッドオブジェクトに通知し待機する??
   以下の処理未定・・・


なんとか(強引ですが・・・)受信処理と解析処理を分割して処理でき、解析処理もプチマルチスレッド処理にできるようになりましたが、前後のスレッドオブジェクトへの通知以下の処理の設計がうまく思いつきません。

どなたか、TCPパケットが結合・分裂する理由や規則性をご存知の方、またはこのような事象が発生した場合の対処法、設計方法(クライアント・サーバ問わず)をご存知の方がいらっしゃいましたら、返信お願いします。




一休
常連さん
会議室デビュー日: 2003/11/26
投稿数: 20
投稿日時: 2003-11-30 11:15
確認です。
「破損パケット」とは「メッセージの一部分のデータ」という意味で
使われているのでしょうか。

例えば、
メッセージ:abcdefghijk
を「abc」、「defghi」、「jk」という3つのパケットで受信した場合
それぞれを破損パケットと呼んでいらっしゃるのでしょうか。

(本来の「破損パケット」とは全く違う意味ですが。。。)
にとろ
会議室デビュー日: 2003/11/30
投稿数: 4
投稿日時: 2003-11-30 14:43
[quote]
一休さんの書き込み (2003-11-30 11:15) より:
確認です。
「破損パケット」とは「メッセージの一部分のデータ」という意味で
使われているのでしょうか。
 →その通りだと思います。

例えば、
メッセージ:abcdefghijk
を「abc」、「defghi」、「jk」という3つのパケットで受信した場合
それぞれを破損パケットと呼んでいらっしゃるのでしょうか。
 →現状、TCPレベル??でパケットが分割されて送受信される認識が無かったので、
  本来「abcdefghijk」であるものを、「abc」、「defghi」、「jk」
  とアプリレベルで個々に受信した場合、パケットが破損と言うよりは、メッセージとして
  正常ではないと判断しています。

(本来の「破損パケット」とは全く違う意味ですが。。。)
 →確かに破損パケット言う認識は間違っています・・・
  一回の受信で得られるメッセージが破損している言う方が正しいですね。
一休
常連さん
会議室デビュー日: 2003/11/26
投稿数: 20
投稿日時: 2003-11-30 15:24
なるほど。

以下のような処理で1メッセージ分のパケットを
全部受信するまでループさせるという方法が考えられます。

受信スレッドではクライアントからの接続要求受け付けと、
そのクライアント用のスレッド生成のみを行う。
つまり、サーバ上に受信スレッドは1オブジェクトのみ存在し、無限ループさせる。


各クライアントスレッドでは InputStream のreadメソッドを、戻り値(受信バイト数)が-1になるまで
ループさせる。
ループの中では、受信したバイト列を用意しておいたバッファへコピーして繋げていく。

これで、1メッセージ受信完了です。

※クライアントスレッドと言っているのは、サーバ側でクライアントの接続要求の数だけ生成されるオブジェクトです。
クライアント側で処理するという意味ではありません。

にとろさんがここまでどう実装されているかわかりませんが、
ソケットを使用したコーディング例です。
動作確認等はしてません。

受信スレッド
コード:
class Rcv extends Thread {
	final static int SERVER_PORT = 5555;

	public Rcv() {
		this.start();
	}

	public void run() {
		ServerSocket svSoc;
		try {
			svSoc = new ServerSocket( SERVER_PORT );
		} catch ( Exception e) {
		}

		// 接続要求受付、クライアントスレッド生成
		while( true ) {
			Socket		soc;
			try {
				soc = svSoc.accept();
				new Client(soc);
			} catch ( Exception e) {
			}
		}
	}
}



クライアントスレッド
コード:
class Client extends Thread {
	private Socket		soc;
	private InputStream	in;
	private int		len;
	private int		total_len;
	private byte[]		temp_buf	= new byte[1024];
	private byte[]		buf = new byte[5000];

	public Client(Socket soc) {
		this.soc = soc;
		run();
	}

	public void run() {
		try {
			in = soc.getInputStream();

			for ( len = 0, total_len = 0;
				( len = in.read( temp_buf, 0, temp_buf.length ) )!= -1;
				total_len += len )
			{
				System.arraycopy( temp_buf, 0, buf, total_len, len );
			}
			in.close();
		} catch ( Exception e ) {
		}
		
		//bufにメッセージが格納させているので要件に応じて処理。
		・
		・
		・



見当違いのレスをしていたら申し訳ありません。
にとろ
会議室デビュー日: 2003/11/30
投稿数: 4
投稿日時: 2003-11-30 18:39
サンプルソースありがとうございます。
私が考えた設計も一休さんとほぼ同じです。

しかし、一人のクライアントから複数のメッセージを一つのソケットで受信する場合、少々問題があります。
【問題内容】
・クライアント側はソケットのストリームのクローズ処理を行わないため、InputStreamの戻り値が-1にな ることが無い。(複数のメッセージを同一ソケットで送信するため)
・一回の受信データに複数のメッセージが存在する可能性がある。
・メッセージを分割して受信する場合がある。(一休さんのサンプルソースで解決)
・クライアントが送信するメッセージは可変長である。
・クライアントの送信間隔は高速(50〜500msec)である。
 (受信と解析の処理を分断する必要がある??)

基本的には、各クライアントのスレッド立てて受信処理を行う方針で問題は無いと思いますが、ちょっと送信間隔が高速なため、受信と解析の処理をシングルスレッドで行う事に不安を感じます。

手詰まり気味なのでいい案があれば返信お願いします。
一休
常連さん
会議室デビュー日: 2003/11/26
投稿数: 20
投稿日時: 2003-11-30 22:15
引用:
・クライアント側はソケットのストリームのクローズ処理を行わないため、InputStreamの戻り値が-1になることが無い。(複数のメッセージを同一ソケットで送信するため)
・一回の受信データに複数のメッセージが存在する可能性がある。
・クライアントが送信するメッセージは可変長である。


上記に関しては、クライアント側で送信時にヘッダ情報を追加する等の解決方法があるかと思われます。

例えば送信メッセージのLengthをメッセージの頭に付加し、
サーバ側では受信バイト数がLengthになったところでメッセージの終わり(というより区切り?)を判定する。

といった方法です。

ただし、この方法ですとクライアント側ではヘッダ情報付加の為、
サーバ側ではその解析の為にそれぞれ時間的コストが掛かってしまいますね。

Lengthの取得や文字列解析にどの程度のコストが掛かるのかに関しては
私も勉強不足で お力になれません。
ただ、TCPでの通信速度と上記のような単純な文字列処理を比較すると後者の方が高速に処理できそうなので、問題なさそうな気もしますが。。。(←すみません。これは推測です。通信環境、サーバの処理能力 etc.環境にも依りますしね。)

引用:
・クライアントの送信間隔は高速(50〜500msec)である。
 (受信と解析の処理を分断する必要がある??)
送信間隔が高速なため、受信と解析の処理をシングルスレッドで行う事に不安を感じます。


これについてどなたかアドバイス頂ければ私としても幸いです。
H2
ぬし
会議室デビュー日: 2001/09/06
投稿数: 586
お住まい・勤務地: 港
投稿日時: 2003-12-01 09:38
引用:

にとろさんの書き込み (2003-11-30 06:37) より:
どなたか、TCPパケットが結合・分裂する理由や規則性をご存知の方、またはこのような事象が発生した場合の対処法、設計方法(クライアント・サーバ問わず)をご存知の方がいらっしゃいましたら、返信お願いします。


確か、TCPのパケットサイズはクライアントとサーバーのプラットフォームによって変わってしまうはずです。(だったっけな?)もともとTCPはストリーミング用なので、パケットごとの通信であればUDPを使うべきです。ただUDPだとパケットの順序が変わったり、パケットが届かない可能性もあります。TCPを使用するのであれば、一休さんが言われるように、メッセージ内に各パケットのサイズを入れればいいはずです。メッセージサイズの分のバイト数を固定にすれば処理も楽になるはずですよ。

受信スレッド処理
   //各クライアントのスレッドを立てて、ブロッキングモードでのコネクションをする。
   while(true) {
    byte[] size = new byte[1]; //メッセージサイズは1バイト(0-255文字)
    in.read(size); //メッセージサイズがsize[0] に入る
    byte[] message = new byte[size[0]+128]; //byteは-128〜127なので、128を足してあげる
    in.read(message); //コネクションから、を読み込む
    //受信したmessageを解析スレッドに渡す。
   }

解析スレッド処理
   //必要な処理をする

送信側
   送信するデータを用意する
   頭(もしくは終わり)に区切り記号(もしくはメッセージサイズ)をくっつけて送信

で、いいのではないでしょうか。たかだか、1,2行増えたぐらいでは速度はほとんど変わらないと思います。
nil
会議室デビュー日: 2003/06/17
投稿数: 14
投稿日時: 2003-12-01 13:08
引用:

H2さんの書き込み (2003-12-01 09:38) より:
受信スレッド処理
   //各クライアントのスレッドを立てて、ブロッキングモードでのコネクションをする。
   while(true) {
    byte[] size = new byte[1]; //メッセージサイズは1バイト(0-255文字)
    in.read(size); //メッセージサイズがsize[0] に入る
    byte[] message = new byte[size[0]+128]; //byteは-128〜127なので、128を足してあげる
    in.read(message); //コネクションから、を読み込む
    //受信したmessageを解析スレッドに渡す。
   }


  • なぜInputStream#read();を使わないのでしょうか?
  • なぜ0-255の長さをそのまま1byteに格納しないのでしょうか?
  • in.read(message)が配列を満たすことなく返る場合がある事をお忘れではないでしょうか?


改行文字等で区切りを示す方法もありますね。
サーバー側は少し複雑になりますが、telnetコマンド等で試験できるのは便利ですよ:)

スキルアップ/キャリアアップ(JOB@IT)