2007年6月16日土曜日

SOCKETプログラミング

このページではBerkeley SOCKET、Winsock及びJava Socketのプログラミングについて紹介する。SOCKETも好きだな。

 記述例およびサンプルに含まれるファイルの全部、または一部を使用したことによる損害等について、一切の責任を負いません。また、サンプルの文字コードはS-JISで提供しますので、ご使用の際はWindowsからFTPするなどして適切な文字コードに変換してください。尚、サンプル中には説明の簡略化のため意味のないコードや、実用上問題のあるコードも含まれていますのでご注意ください。

1. [UNIX]スレッドでrecv
2. [UNIX]selectを使う
3. [Winsock]スレッドでrecv
4. [Winsock]selectを使う
5. [Winsock]イベントを使う
6. [Java]Socket(クライアント)
7. [Java]ServerSocket(サーバー)

[UNIX]スレッドでrecv
 SOCKETを使う上で考慮しなければならないのが受信の実現方法だ。データを受信する関数であるrecvがブロッキングするからだ。僕が一番好きなのはスレッドを使用する方法だ。受信専用のスレッドを新たに生成すればrecvがブロッキングしても影響はそのスレッドのみに限定されるので、例えばキー入力などは平行して処理されるので問題はなくなる。

 今までスレッドを使用したことがない方は、スレッドと聞くだけで躊躇されるかもしれないが、実際にやってみればそれほど難しいものではない。スレッドに関しては別にページを用意しているのでそちらも参照して欲しい。

<受信スレッドの生成>

/* 受信スレッド生成 */
pthread_create(&tid,NULL,waitReceiveThread,NULL);
pthread_join(tid,NULL); /* 受信スレッド終了待ち */

<受信スレッドルーチン>

void*
waitReceiveThread(void* pParam)
{
char buf[1024];
int recvSize;

while(1)
{
/* データ受信待ち */
recvSize = recv(fd,buf,1024,0);

printf("Received %d bytes\n",recvSize);

if(recvSize == 0) /* 相手CLOSE? */
{
printf("check\n");
close(fd);
break;
}
}

return NULL;
}

 受信スレッドルーチンは単にrecvのループになっている。recvはデータを受信するまで戻ってこないのでCPU時間は消費しない。SOCKETがクローズされるとrecvは戻り値=0で戻ってくるので、その場合にはループをbreakする処理を入れている。受信スレッドの生成元スレッドは pthread_joinで新たに生成したスレッドの終了を待機しているので、受信スレッドルーチンが終了すれば生成元スレッドも終了する。
[UNIX]selectを使う
 正直言って僕はselectはあまり好きではない。でも、とりあえず載せておく。まず、FD_ZEROマクロでfd_setを初期化します。次にFD_SETマクロでselectで待ちたいSOCKETの fdを設定する。このfd_setを使ってselectを呼び出すと、データを受信するまでselectはブロックするがCPU時間は消費しない。 selectから戻ったところでFD_ISSETマクロを用いて該当fdの読み込みデータがあるかどうかを調べ、データがある場合はrecvを呼び出す。

int
waitReceive(void)
{
fd_set fds;
char buf[1024];
int recvSize;
/* バッチ共通初期処理 */
while(1) /* メッセージループ */
{
FD_ZERO(&fds); /* fd_set初期化 */
FD_SET(fd,&fds); /* fd設定 */
select(fd + 1,&fds,NULL,NULL,NULL); /* データ受信待ち */

if(FD_ISSET(fd,&fds)) /* 該当fd読み込みデータあり? */
{
/* データ受信 */
recvSize = recv(fd,buf,1024,0);

printf("Received %d bytes\n",recvSize);

if(recvSize == 0) /* 相手CLOSE? */
{
printf("check\n");
close(fd);
break;
}
}
}

return 0;
}

[Winsock]スレッドでrecv
 Win32でもスレッドが使用できるので、新たにスレッドを生成して通常の処理は平行して行いながらrecvで待つことができる。Win32のスレッドモデルはpthreadに比べるとやや煩雑になっており、加えてWinsockでは様々な処理方式を選択可能なのだが、スレッドを使用した方式が最もシンプルであろう。この例ではCのライブラリを使用するために、_beginthreadexを使用しているが、CreateThreadでも同じだ。スレッドに関しては別にページを用意しているのでそちらも参照して欲しい。

<受信スレッドの生成>

/* 受信スレッド生成 */
hThread = (HANDLE)_beginthreadex(NULL,0,waitReceiveThread,NULL,0,&threadId);

WaitForSingleObject(hThread,INFINITE); /* 受信スレッド終了待ち */
CloseHandle(hThread); /* ハンドルクローズ */

<受信スレッドルーチン>

DWORD WINAPI
waitReceiveThread(LPVOID pParam)
{
char buf[1024];
int recvSize;

while(1)
{
/* データ受信待ち */
recvSize = recv(fd,buf,1024,0);

printf("Received %d bytes\n",recvSize);

if(recvSize == 0) /* 相手CLOSE? */
{
printf("check\n");
closesocket(fd); /* CLOSE */
break;
}
}

return 0;
}

 この例でも受信スレッドルーチンの生成元スレッドは、pthreadのときと同様に新たに生成したスレッドの終了を待機している。Win32ではスレッドのハンドルがシグナル状態になるのをWaitForSingleObjectで待機する。受信スレッドルーチンが終了すれば生成元スレッドも終了する。

スレッドrecvのサンプル(ソースはLinuxでもコンパイルできるようになっている。UNIXとWindowsの違いを見てみて欲しい。)
[Winsock]selectを使う
 selectを使う場合はUNIXのSOCKETとまったく同じだ。よりWindowsっぽく書くなら、WSAAsyncSelectを使うという手があるが、こちらはメッセージを使用する。メッセージを使用するにはウィンドウが必要になる僕はメッセージの仕組みをあまり詳しくないので専らイベントを使用する。
[Winsock]イベントを使う
 Winsock2.0ではWin32のイベントオブジェクトを使用することができる。メッセージを利用しないのでウィンドウが必要なく、GUIがないプログラムで使いやすい。Anpsockもこの方式を使っている。ところが、イベントを使ったWinsockの解説書はあまりない。そもそも、Winsock2.0の解説書が少ない。

 イベントに関しては別にページを用意しているのでそちらも参照して欲しい。

int
waitReceive(void)
{
char buf[1024];
int recvSize;
int nRet;
HANDLE hEvent = WSACreateEvent();
WSANETWORKEVENTS events;

WSAEventSelect(fd,hEvent,FD_READ|FD_CLOSE);

while(1)
{
/* イベント待ち */
nRet = WSAWaitForMultipleEvents(1,&hEvent,FALSE,WSA_INFINITE,FALSE);

if(nRet == WSA_WAIT_FAILED)
{
printf("!!!!!!!!!!!!!!!!!!!!1\n");
break;
}

/* イベントの調査 */
if(WSAEnumNetworkEvents(fd,hEvent,&events) == SOCKET_ERROR)
{
printf("???\n");
}
else
{
/* CLOSE */
if(events.lNetworkEvents & FD_CLOSE)
{
printf("close\n");
closesocket(fd);
break;
}
/* READ */
if(events.lNetworkEvents & FD_READ)
{
recvSize = recv(fd,buf,1024,0);
printf("Received %d bytes\n",recvSize);
}
}
}

WSACloseEvent(hEvent); /* イベントクローズ */

return 0;
}

イベントrecvのサンプル
[Java]Socket(クライアント)
 Javaでクライアントのソケットを作成するにはホストとポートを指定してSocketをnewするだけである。たったのこれだけでCで言うところのsocketからconnectの呼び出しまで済んでしまうから驚きである。

// クライアントソケット作成
Socket conn = new Socket("localhost",port);
System.out.println("Connecting to port " + port);
// データ受信スレッド生成
RecvData rd = new RecvData(conn.getInputStream());
rd.start();

 データの受信は他の言語と同様スレッドを用いている。実際にサンプルを見ていただければお分かりになると思うが、わずか5、60行でクライアントのプログラムが書けるのである。素晴らしい!

class RecvData extends Thread {
InputStream st;

public RecvData(InputStream st) {
this.st = st;
}

public void run() {
int data;

try {
// データ読み込み
while((data = st.read()) != -1) {
System.out.print((char)data);
}
} catch(IOException e) {
System.out.println(e);
}
}
}

[Java]ServerSocket(サーバー)
 Javaでサーバーソケットを作成する場合はポートを指定してServerSocketをnewする。

// サーバーソケット作成
ServerSocket server = new ServerSocket(port);
System.out.println("Listening on port " + port);

 作成したサーバーソケットがAcceptするとクライアントの項で既出のSocketを返すので、以降同様に処理してやれば良い。サンプルでは受信スレッドのクラスはクライアントと共有している。

// Accept
Socket conn = server.accept();
System.out.println("Accept");
// データ受信スレッド生成
RecvData rd = new RecvData(conn.getInputStream());
rd.start();

ソケットのサンプル(サーバー、クライアント)

0 件のコメント: