シングルスレッドで非同期socket通信

C言語でSocketを使って通信する物を作るのだが、

  • 1対多の通信
  • 非同期通信
  • プロセスとスレッドを使わない。

という条件になると、必然的にselectを使った通信になる。
レスポンスはこの際置いておいて、条件を満たせれば良いとする。
メモ書き程度なモノだし、間違っているかもしれない。

非同期通信

まず非同期通信の前に対義語の同期通信を知る必要がある。
同期通信とは、例えばキーボード入力する必要があるときは入力を終えるまで待つ。
非同期通信は、まんま逆で、入力を待たない。


どういうときに非同期通信をするのかといえば、例えば会話。
会話をするときは、1文話して1文聞く。
しかし、聞く為には、発言をするまで待たないといけない、とした場合、
自分が発言して相手の発言を待つ、と相手の発言を"待つ"状態ができる。
こうなると、相手が発言しない限り会話が進まなくなる。
これが同期。
それに実際は、連続で発言したい場合があるだろう。
そのため、相手の発言がない間は自分がいつでも発言できる状態にし、
相手の発言があったら相手の発言を聞く、と"待たない"状態になるように実装したい。
これが非同期。

プロセス(もしくはスレッド)で考えた場合。

まず会話するために、発言するプログラムと、聞くプログラムの2つを用意すれば、成り立つ。
これをプロセス単位(もしくはスレッド単位)で実装することで、非同期で実装することができる。
例えば、メッセージを受信した時点で、スレッドをつくり、処理する。

1対多の通信

ソケットには番号があり、TCPの場合は、accept関数で接続が確立された時点でその接続へのソケットを作るため、
普通に通信しても、接続を待ち受けるソケット、相手と通信するためのソケットの2つが存在することになる。
この時点で、1対多の通信ができる事がわかる。
listen関数で、そのソケットがいくつまでの接続を受け付けることができるか、を指定できる。
指定したら、後はacceptでできるソケットを記憶しておけば、
以降、切断するかされるまでは、確実な通信をすることができる。
UDPの場合は、1つのソケットで、複数の相手と通信できるが、
相手が存在しているのかも不明、データが壊れてたら再送要求をしないで破棄するため、確実な通信はできない。

プロセスとスレッドを使わないで、1対多の非同期通信の実装するには

TCP通信であれば、listenまでした後に、selectを使いlistenソケットを監視する。
listenソケットにくる通信要求はほぼ、"クライアント側からの接続の確立"なので、
acceptを呼び出し、クライアント毎のソケットを作る。
その後、クライアント毎のソケットを配列なりに入れてソケット番号を覚えておけばOK。
同時にFD_SETでselect時にこのソケット番号もチェックするようにフラグを書き換える。


配列に入れている場合は、FD_ISSETで接続中のソケットからのデータの受信があるかチェックして、
受信できるデータがあったら、それにあわせた処理。
配列の初期値としてacceptのエラー値をいれておくと、接続者数や空きのチェックが簡単になる。
単方向リストでも解決するので、配列を使うかリストを使うかこの辺りは自由。
容易さは配列、作業効率はリストかと思う。


上を踏まえて書いてみたソース ( Windows + GCC4.4.0 )

server.c
#include <stdio.h>

// socket
#define FD_SETSIZE 32
#define BUFFER 512

#include <windows.h>
#include <winsock2.h>


int main(){
	WSADATA wsaData;
	SOCKET sock;
	struct sockaddr_in addr;
	struct timeval t_val = {0, 1000};
	int select_ret;
	char buf[BUFFER];
	fd_set fds, readfds;
	int accept_list[FD_SETSIZE];
	int accept_num = 0;
	int i;
	
	// accept_list 初期化
	for(i=0; i<FD_SETSIZE; i++){
		accept_list[i] = INVALID_SOCKET;
	}
	
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(30000);
	addr.sin_addr.S_un.S_addr = INADDR_ANY;
	
	WSAStartup(MAKEWORD(2,0), &wsaData);
	
	if(
		(sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET ||
		bind(sock, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR ||
		listen(sock, FD_SETSIZE) == SOCKET_ERROR
		){
		printf("socket error\n");
		return 1;
	}
	FD_ZERO(&readfds);
	FD_SET(sock, &readfds);
	
	while(1){
		memcpy(&fds, &readfds, sizeof(fd_set));
		select_ret = select(0, &fds, NULL, NULL, &t_val);
		// timeoutでない場合
		if(select_ret != 0){
			// 待ちうけソケットにデータがある
			// 通信時にはacceptしたsocketを使うはずなので、待ちうけに来るものは必ずaccept対象
			if(FD_ISSET(sock, &fds)){
				struct sockaddr_in client;
				int len = sizeof(client);
				int client_sock = accept(sock, (struct sockaddr *)&client, &len);
				if(client_sock != INVALID_SOCKET){
					// 空いているところから登録
					int i=0;
					while(i < FD_SETSIZE && accept_list[i] != INVALID_SOCKET) i++;
					if(i != FD_SETSIZE){
						FD_SET(client_sock, &readfds);
						accept_list[i] = client_sock;
						printf("accept\n");
					}else{
						printf("空きがありません\n");
					}
				}else{
					printf("accept error\n");
				}
			}
			
			// 各ソケットの状況チェック
			for(i=0; i<FD_SETSIZE; i++){
				if(accept_list[i] != -1 && FD_ISSET(accept_list[i], &fds)){
					int x;
					memset(buf, 0, BUFFER);
					x = recv(accept_list[i], buf, BUFFER, 0);
					if(x != 0){
						// 受信データ処理
						buf[BUFFER-1] = '\0';
						printf("recv[%d][%d]: %s [status: %d]\n", i, accept_list[i], buf, x);
					}else{
						// 通信異常(相手からいきなり切断されると常にここが呼び出されたので。)
						printf("disconnect?[%d][%d]\n", i, accept_list[i]);
						closesocket(accept_list[i]);
						accept_list[i] = -1;
					}
				}
			}
		}
	}
	closesocket(sock);

	WSACleanup();

	return 0;
}
client.c
#include <winsock2.h>
#include <string.h>
#include <stdio.h>

#define BUFFER 512

int main(){
	WSADATA wsaData;

	SOCKET sock;
	struct sockaddr_in addr;
	struct timeval t_val = {0, 1000};
	fd_set fds, readfds;
	int select_ret;
	char buf[BUFFER];
	
	WSAStartup(MAKEWORD(2,0), &wsaData);
	
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(30000);
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	
	if(
		(sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET ||
		connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR
		){
		printf("socket error\n");
		return 1;
	}
	
	FD_ZERO(&readfds);
	FD_SET(sock, &readfds);
	
	while(1){
		memcpy(&fds, &readfds, sizeof(fd_set));
		select_ret = select(0, &fds, NULL, NULL, &t_val);
		
		if(select_ret != 0){
			// ソケットにデータがある
			if(FD_ISSET(sock, &fds)){
				// 受信データ処理
				memset(buf, 0, BUFFER);
				recv(sock, buf, BUFFER, 0);
				buf[BUFFER-1] = '\0';
				printf("%s\n", buf);
			}
		}else{
			if(fgets(buf,BUFFER-1, stdin) != NULL){
				// データ送信
				sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr));
			}
		}
	}
	
	closesocket(sock);

	WSACleanup();

	return 0;
}


-lws2_32をつけてコンパイル
clientに関してはサーバにデータを送れればいいというだけの実装しかしていないので、
標準入力の時点で待つ状態になるし、サーバからデータを送っているわけでもないので、受信データ処理も機能しない。