情報学部 菅沼ホーム 全体目次 演習解答例 付録 索引

第Ⅳ部

第18章 Network プログラミング

  1. 18.1 ソケットの利用
  2. (プログラム例 18.1 ) HTTP ポート
  3. (プログラム例 18.2 ) ファイルの転送
  4. (プログラム例 18.3 ) チャットルーム
  この章においては,Network プログラミングについて概説します.非常に基本的なことに対してだけの説明ですので,詳細については,他の書籍及び付録等を参照してください.なお,C/C++ に対するプログラム例は Linux 上で実行することを念頭に置いて作成してありますが,C/C++ のバージョンによっては動作しない可能性もあります.

18.1 ソケットの利用

  オペレーティングシステム( OS )の処理の単位をタスク,プロセス,スレッドなどと呼びます.呼び方は OS によって異なると共に,多少その意味も異なりますが,ここではほとんど同じ意味で使用していきます.

  2 つのプロセス間でデータ交換を行なうことをプロセス間通信IPCInterProcess Communication )といいます.通信を行なう 2 つのプロセスは,同一のシステムに存在しても,ネットワーク間に存在しても構いませんが,一般に,通信を行うためには,通信路が必要です.その際利用されるのがソケットです.ソケットとは,TCP/IP において利用するネットワーク用 API のことを指します.まず,ここで,TCP/IP 等について簡単に説明しておきます.

  TCP や UDP で通信をする場合,まずソケットを使って通信路を確保し,通常のファイル入出力用の read / write などを使って通信を行います.通信路を確保してしまえば,ファイル入出力と全く同じ感覚でプロセス間通信を行えます.

  TCP や UDP において,通信路を確保する際,アプリケーションまたは上位のプロトコルとデータの送受信を行うためのアドレスは,符号なし整数で表わされます.このアドレスをポート(番号)と呼びます.電子メールや FTP などのプロセス間通信用のアプリケーションにおいては,使用するポートが決まっています.このように,ポートには,アプリケーションが任意に使用可能なポートのほか,電子メールのような特定のアプリケーションが使用する Well-known ポートと呼ばれるものがあります.Well-known ポートは,特定のアプリケーションのために使用することが予約されており,その番号とアプリケーションの対応は,NICNetwork Information Center )によって統一的に管理されています.また,1 ~ 1023 番のポートは予約ポートと呼ばれ,これらのポートを UNIX などで使用するには,ルートの権限が必要になります.なお,代表的な Well-known ポートには,以下に示すようなものがあります.

ポート番号 ポート名 用途
20 ftp data File Transfer Protocolのデータ用ポート
21 ftp File Transfer Protocolの制御用ポート
23 telnet Telnet Protocol
80 http http
25 smtp Simple Mail Transfer Protocol
110 pop3 Post Office Protocol version 3
119 nntp Network News Transfer Protocol
123 ntp Network Time Protocol

(プログラム例 18.1 ) HTTP ポート

  サーバとクライアントのプロセス間で通信を行うためには,両者において,通信を行うためのプロセスが動作している必要があります.電子メール,ftp,http などが使用する Well-known ポートにおいては,通常,サーバ側のプロセスは常に動作しています.また,クライアント側においても,クライアント側のプロセスを起動するためのプログラムは OS によって準備されています.したがって,クライアント側で,適切なプログラムによって必要なプロセスを起動してやれば,プロセス間通信を行えます.たとえば,ftp においては,ftp コマンドを使用することによって簡単にプロセス間通信を行えますし,また,ホームページを閲覧するためには,Internet Explorer,Chrome,Firefox などのブラウザを起動することによって,HTTP ポートを利用したプロセス間通信が可能になります.

  しかし,Well-known ポートに対しても,クライアント側のプログラムを特別に書き,サーバ側のプロセスとのプロセス間通信を行うことが可能です.下に示したプログラムは,http ポートを利用し,ファイル「 http://cs-www/~suganuma/master.css 」の内容(テキストデータだけ)を表示するためのものです.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>   // 文字列操作,memset,FD_ZERO等
#include <netdb.h>   // ソケット(hostent等)
#include <sys/socket.h>   // ソケット(AF_INET, select等)
#include <netinet/in.h>    // in_port_t, sockaddr_in等
#include <unistd.h>   // fork, read, write, open, close

#define BUF_LEN 512   // バッファサイズ
#define PORT 80   // ポート番号

/************************************************/
/* ソケットの初期設定(クライアント,c_setup ) */
/*      return : ソケットのディスクリプタ       */
/************************************************/
//int c_setup(char *hostname, in_port_t port)
int c_setup(char *hostname, int port)    // コンパイラのバージョンによっては,
                                           // 上の行の代わりにこの行を使用
{
	int client_socket = -1;   // ソケット
	hostent *server_inf1;   // クライアント情報1
	sockaddr_in server_inf2;   // クライアント情報2
/*
		サーバ名からサーバ情報を得る
*/
	if ((server_inf1 = gethostbyname(hostname)) == NULL)
		printf("***error***  サーバ名が不適当です\n");
/*
		サーバ情報をsockaddr_in構造体に設定
*/
	else {
		memset((char *)&server_inf2, 0, sizeof(server_inf2));
		server_inf2.sin_family      = AF_INET;   // アドレスファミリー
		server_inf2.sin_port        = htons(port);   // htons(2バイト)
                                                     //   整数の上位及び下位バイトの指定
		memcpy((char *)&server_inf2.sin_addr.s_addr, server_inf1->h_addr, server_inf1->h_length);
/*
		ソケットを作成
*/
		if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
			printf("***error***  socketを作成できませんでした\n");
/*
		サーバへ接続
*/
		else {
			if (connect(client_socket, (sockaddr *)&server_inf2, sizeof(server_inf2)) == -1)
				client_socket = -1;
		}
	}

	return client_socket;
}

/*******************************/
/* HTTP クライアント( main ) */
/*      coded by Y.Suganuma    */
/*******************************/
int main(int argc, char *argv[])
{
	int n;
	int client_socket;   // 送受信用のソケット
	char buf[BUF_LEN];   // 送受信用バッファ
	char hostname[50] = "informatics.sist.ac.jp";   // ホスト名
	char adr[100] = "/suganuma/master.css";   // ファイル名
	char buf1[20];
/*
		初期設定
*/
	if ((client_socket = c_setup(hostname, PORT)) == -1) {
		printf("***error***  接続できませんでした\n");
		exit(1);
	}
/*
		サーバとの送信
*/
	else {
					// 送信内容の指定(サーバに対するリクエスト)
		strcpy(buf, "GET ");
		strcat(buf, adr);
		strcat(buf, " HTTP/1.1\r\n");
		write(client_socket, buf, strlen(buf));
					// ホスト名とポート
		strcpy(buf, "Host: ");
		strcat(buf, hostname);
		strcat(buf, ":");
		gcvt((double)PORT, 10, buf1);
		strcat(buf, buf1);
		strcat(buf, "\r\n");
		write(client_socket, buf, strlen(buf));
					// リクエスト(GET)の終了
		strcpy(buf, "\r\n");
		write(client_socket, buf, strlen(buf));
					// 受信内容の表示
		n = 1;
		while (n > 0) {
			n = read(client_socket, buf, BUF_LEN);
			if (n > 0)
				write(1, buf, n);
		}
	}

	close(client_socket);

	return 0;
}
		

  まず,c_setup 関数内において,socket 関数によってソケットを作成し,connect 関数によってサーバに接続しています.main 関数では,サーバ側のプロセスに対して以下に示す文字列(リクエスト.コメント部分は除く)を送信( write 関数)することによって,指定したファイルの内容を送信してもらい,その結果をクライアント側の画面に表示しています( while 文).一般的には,Well-known ポートに対してこのようなプログラムを書くことはないと思います.
GET /suganuma/master.css HTTP/1.1\r\n  // 送信内容の指定
Host: informatics.sist.ac.jp:80\r\n  // ホスト名とポート
\r\n  // リクエスト(GET)の終了		
(プログラム例 18.2 ) ファイルの転送

  次に,一般的なソケットの使用方法について考えてみます.ここで示す例は,クライアント側のファイルをサーバ側へ転送するためのプログラムです.ファイル転送を行う場合は,このようなプログラムを書かずに,ftp ポートを利用した ftp コマンドを利用することの方が実際的です.

  まず,クライアント側のプログラムを見て下さい.サーバ名や送信するファイル名を入力する点,ポート番号( 50000 ),ファイル内容の送信等の点で多少異なっていますが,基本的には,プログラム例 18.1 と同じです.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>   // 文字列操作,memset,FD_ZERO等
#include <netdb.h>   // ソケット(hostent等)
#include <sys/socket.h>   // ソケット(AF_INET, select等)
#include <netinet/in.h>    // in_port_t, sockaddr_in等
#include <fcntl.h>   // O_RDONLY等
#include <unistd.h>   // fork, read, write, open, close

#define BUF_LEN 512   // バッファサイズ
#define PORT 50000   // ポート番号

/************************************************/
/* ソケットの初期設定(クライアント,c_setup ) */
/*      hostname : サーバ名                     */
/*      port : ポート番号                       */
/*      return : ソケットのディスクリプタ       */
/************************************************/
int c_setup(char *hostname, in_port_t port)
//int c_setup(char *hostname, int port)    // コンパイラのバージョンによっては,
                                           // 上の行の代わりにこの行を使用
{
	int client_socket = -1;   // ソケット
/*
		サーバ名からサーバ情報を得る
*/
	hostent *server_inf1;   // サーバ情報1
	if ((server_inf1 = gethostbyname(hostname)) == NULL)
		printf("***error***  サーバ名が不適当です\n");
/*
		サーバ情報をsockaddr_in構造体に設定
*/
	else {
		sockaddr_in server_inf2;   // サーバ情報2
		memset((char *)&server_inf2, 0, sizeof(server_inf2));
		server_inf2.sin_family      = AF_INET;   // アドレスファミリー
		server_inf2.sin_port        = htons(port);   // htons(2バイト)
                                                     //   整数の上位及び下位バイトの指定
		memcpy((char *)&server_inf2.sin_addr.s_addr, server_inf1->h_addr, server_inf1->h_length);
/*
		ソケットを作成
*/
		if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
			printf("***error***  socketを作成できませんでした\n");
/*
		サーバへ接続
*/
		else {
			if (connect(client_socket, (sockaddr *)&server_inf2, sizeof(server_inf2)) == -1)
				client_socket = -1;
		}
	}
/*
		ディスクリプタを返す
*/
	return client_socket;
}

/***************************************/
/* ファイル転送(クライアント,main ) */
/*      coded by Y.Suganuma            */
/***************************************/
int main(int argc, char *argv[])
{
/*
		サーバ名
*/
	char hostname[50];   // ホスト名
	if (argc != 2) {
		printf("***error***  サーバ名を入力してください\n");
		exit(1);
    }
	else
		strcpy(hostname, argv[1]);
/*
		初期設定
*/
	int client_socket;   // 送受信用のソケット
	if ((client_socket = c_setup(hostname, PORT)) == -1) {
		printf("***error***  接続できませんでした\n");
		exit(1);
	}
/*
		ファイル名の入力とファイル内容の転送
*/
					// ファイル名の入力
	char buf[BUF_LEN];   // 送受信用バッファ
	int n = read(client_socket, buf, BUF_LEN);
	write(1, buf, n);
	scanf("%s", buf);
					// ファイル名の送信
	int in = open(buf, O_RDONLY);
//	int in = open(buf, O_BINARY | O_RDONLY);
//    OS(MS-DOSなど)によっては,上の行の代わりにこの行を使用しないと
//    バイナリファイルを正しく送信できない.
	if (in < 0) {
		printf("***error***  ファイルが存在しません\n");
		close(client_socket);
		exit(1);
	}
	write(client_socket, buf, strlen(buf)+1);
					// ファイル内容の送信
	int sw = 1;
	while (sw > 0) {
		n = read(in, buf, BUF_LEN);
		if (n > 0)
			write(client_socket, buf, n);
		else
			sw = 0;
	}
/*
		接続を切り,ファイルを閉じる
*/
	close(client_socket);
	close(in);

	return 0;
}
		

  次に,サーバ側のプログラムを見て下さい.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>   // 文字列操作,memset,FD_ZERO等
#include <sys/socket.h>   // ソケット(AF_INET, select等)
#include <arpa/inet.h>   // ソケット(sockaddr_in等)
#include <netinet/in.h>    // in_port_t, sockaddr_in等
#include <fcntl.h>   // O_RDONLY等
#include <sys/socket.h>   // ソケット(AF_INET, select等)
#include <netdb.h>   // ソケット(inet_ntoa等)
#include <unistd.h>   // fork, read, write, open, close

#define BUF_LEN 512   // バッファサイズ
#define PORT 50000   // ポート番号

/******************************************/
/* ソケットの初期設定(サーバ,s_setup ) */
/*      port : ポート番号                 */
/*      return : リスニングソケット       */
/******************************************/
int s_setup(in_port_t port)
//int s_setup(int port)    // コンパイラのバージョンによっては,
                           // 上の行の代わりにこの行を使用
{
	int listening_socket = -1;   // リスニングソケット
	sockaddr_in server_inf;   // サーバ情報
/*
		サーバ情報をsockaddr_in構造体に設定
*/
	memset((char *)&server_inf, 0, sizeof(server_inf));
	server_inf.sin_family      = AF_INET;   // アドレスファミリー
	server_inf.sin_addr.s_addr = htonl(INADDR_ANY);   // IPアドレス
	server_inf.sin_port        = htons(port);   // htonl(4バイト),htons(2バイト)
                                                //   整数の上位及び下位バイトの指定
/*
		リスニングソケットを作成
*/
	if ((listening_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		printf("***error***  接続できませんでした\n");
/*
		サーバのアドレスをソケットに設定する
*/
	else {
	    if (bind(listening_socket, (sockaddr *)&server_inf, sizeof(server_inf)) < 0)
			printf("***error***  設定に失敗しました\n");
/*
		待ち受ける(待機状態の準備)(第2引数は,待ち受けの数)
*/
		else {
			if (listen(listening_socket, 1) == -1 )
				printf("***error***  待ち受けに失敗しました\n");
		}
	}

	return listening_socket;
}

/********************************************/
/* ftpの実行( e_ftp )                     */
/*      connected_socket : 接続済みソケット */
/********************************************/
void e_ftp(int connected_socket)
{
	int n, sw, out;
	char buf[BUF_LEN];   // 送受信用バッファ
	hostent *client_inf1;   // クライアント情報1
	sockaddr_in client_inf2;   // クライアント情報2
	socklen_t len;
//	int len;    // コンパイラのバージョンによっては,
                // 上の行の代わりにこの行を使用
/*
		クライアント情報を取得できた場合,それを表示する
*/
	len = (socklen_t)sizeof(client_inf2);
//	len = sizeof(client_inf2);    // コンパイラのバージョンによっては,
                                  // 上の行の代わりにこの行を使用
	getpeername(connected_socket, (sockaddr *)&client_inf2, &len);
	client_inf1 = gethostbyaddr((char *)&client_inf2.sin_addr.s_addr, sizeof(client_inf2.sin_addr), AF_INET);
	if (client_inf1 != NULL) {
		printf("%s と接続しました(IP: %s, ポート %d)\n",
                client_inf1->h_name, inet_ntoa(client_inf2.sin_addr), ntohs(client_inf2.sin_port));
	}
/*
		送られてきたファイルを保存
*/
					// ファイル名
	write(connected_socket, "ファイル名を入力してください ", 29);
	read(connected_socket, buf, BUF_LEN);
					// 保存
	out = open(buf, O_WRONLY | O_CREAT, S_IREAD | S_IWRITE);
//	out = open(buf, O_BINARY | O_WRONLY | O_CREAT, S_IREAD | S_IWRITE);
//    OS(MS-DOSなど)によっては,上の行の代わりにこの行を使用しないと,
//    バイナリファイルを正しく送信できない.
	sw  = 1;
	while (sw > 0) {
		n = read(connected_socket, buf, BUF_LEN);
		if (n > 0)
			write(out, buf, n);
		else
			sw = 0;
	}

	close(out);
}

/*********************************/
/* ファイル転送(サーバ,main ) */
/*      coded by Y.Suganuma      */
/*********************************/
int main()
{
	int listening_socket;   // リスニングソケット
	int connected_socket;   // 接続済みソケット
	pid_t pid;   // プロセスID
/*
		リスニングソケットの生成
*/
	if ((listening_socket = s_setup(PORT)) != -1) {
/*
		接続待機
*/
		while(1) {
					// 接続済みソケット
			if ((connected_socket = accept(listening_socket, NULL, NULL)) != -1) {
					// 子プロセスの生成
						// 子プロセス
				if ((pid = fork()) == 0) {
					close(listening_socket);
					e_ftp(connected_socket);
					close(connected_socket);
					exit(0);
				}
						// 親プロセス
				else
					close(connected_socket);
			}
		}
	}

	return 0;
}
		
  まず,s_setup 関数内において,socket 関数によってリスニングソケットを作成し,bind 関数によってサーバのアドレスをソケットに設定し,次に,listen 関数によって接続待ちの状態に入ります.

  main 関数では,クライアント側からの接続要求に対して,accept 関数によって接続済みソケットを作成します.最も簡単にプログラムを書くとすれば,この時点でクライアントから送られてきたデータをファイルとして保存するようにすれば,ファイルの転送は終了します.しかし,このままでは,複数のクライアントから要求があった場合への対応が不十分になります(処理中のクライアントに対する処理が終了しない限り,次のクライアントの処理が行えない).

  そこで,この例では,fork 関数を使用して子プロセスを生成し,ファイル転送に関わる実際の処理は子プロセスで行うようにしています.fork 関数によって子プロセスを生成すると,上図に示すように,親プロセス及び子プロセスが,リスニングソケットと接続済みソケットで接続された状態になります.そこで,待ち受けだけを親プロセスで行い,ftp に関する実際の処理を子プロセスで行うため,親プロセスでは接続済みソケットを閉じ(×印),また,子プロセスではリスニングソケットを閉じ(×印)た後,e_ftp 関数によってファイル転送処理を行っています.ファイル転送処理を終了すると,子プロセスが終了すると共に,子プロセスの接続済みソケットも閉じられます.

(プログラム例 18.3 ) チャットルーム

  この例では,チャットルームを取り上げます.まず,クライアント側のプログラムを見て下さい.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>   // 文字列操作,memset,FD_ZERO等
#include <netdb.h>   // ソケット(hostent等)
#include <sys/socket.h>   // ソケット(AF_INET, select等)
#include <netinet/in.h>    // in_port_t, sockaddr_in等
#include <sys/types.h>   // fd_set
#include <unistd.h>   // fork, read, write, open, close

#define BUF_LEN 512   // バッファサイズ
#define PORT 50000   // ポート番号

/************************************************/
/* ソケットの初期設定(クライアント,c_setup ) */
/*      hostname : サーバ名                     */
/*      port : ポート番号                       */
/*      return : ソケットのディスクリプタ       */
/************************************************/
int c_setup(char *hostname, in_port_t port)
//int c_setup(char *hostname, int port)    // コンパイラのバージョンによっては,
                                           // 上の行の代わりにこの行を使用
{
	int client_socket = -1;   // 送受信用のソケット
/*
		サーバ名からサーバ情報を得る
*/
	hostent *server_inf1;   // サーバ情報1
	if ((server_inf1 = gethostbyname(hostname)) == NULL)
		printf("***error***  サーバ名が不適当です\n");
/*
		サーバ情報をsockaddr_in構造体に設定
*/
	else {
		sockaddr_in server_inf2;   // サーバ情報2
		memset((char *)&server_inf2, 0, sizeof(server_inf2));
		server_inf2.sin_family      = AF_INET;   // アドレスファミリー
		server_inf2.sin_port        = htons(port);   // htons(2バイト)
                                                     //   整数の上位及び下位バイトの指定
		memcpy((char *)&server_inf2.sin_addr.s_addr, server_inf1->h_addr,
               server_inf1->h_length);
/*
		ソケットを作成
*/
		if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
			printf("***error***  socketを作成できませんでした\n");
/*
		サーバへ接続
*/
		else {
			if (connect(client_socket, (sockaddr *)&server_inf2, sizeof(server_inf2)) == -1)
				client_socket = -1;
		}
	}
/*
		ディスクリプタを返す
*/
	return client_socket;
}

/***************************************/
/* 会話のための準備( prepare )       */
/*      soc : ソケットのディスクリプタ */
/*      return : select用初期化マスク  */
/***************************************/
fd_set prepare(int soc)
{
	fd_set mask;
/*
		マスクの準備
*/
	FD_ZERO(&mask);
	FD_SET(0, &mask);   // ディスクリプタ0(キーボード)をセット
	FD_SET(soc, &mask);   // ソケットのディスクリプタをセット

	return mask;
}

/**************************************/
/* 会話( chat )                     */
/*      fd : ソケットのディスクリプタ */
/*      buf : バッファ                */
/*      size : バッファサイズ         */
/*      mask : select用初期化マスク   */
/**************************************/
void chat(int fd, char *buf, int size, fd_set mask)
{
	int sw = 1;

	while (sw > 0) {
					// マスクの初期化
		fd_set now = mask;
		select(fd+1, (fd_set*)&now, NULL, NULL, NULL);
					// キーボードからの入力チェック
		if (FD_ISSET(0, &now)) {
			gets(buf);
			int n = strlen(buf);
			if (n > 0)
				write(fd, buf, n);   // 送信
		}
					// ソケット内のデータを調べる
		if (FD_ISSET(fd, &now)) {
			int n  = read(fd, buf, size);
			buf[n] = '\0';
			if (strcmp(buf, "Server.quit") == 0) {
				printf("チャットルームを閉じます!\n");
				close(fd);
				sw = 0;
			}
			else if (strcmp(buf, "quit") == 0) {
				close(fd);
				sw = 0;
			}
			else
				write(1, buf, n);
		}
	}
}

/*****************************************/
/* チャットルーム(クライアント,main ) */
/*      coded by Y.Suganuma              */
/*****************************************/
int main(int argc, char *argv[])
{
/*
		サーバ名
*/
	char hostname[50];   // ホスト名
	if (argc != 2) {
		printf("***error***  サーバ名を入力してください\n");
		exit(1);
    }
	else
		strcpy(hostname, argv[1]);
/*
		初期設定
*/
	int client_socket;   // 送受信用のソケット
	if ((client_socket = c_setup(hostname, PORT)) == -1) {
		printf("***error***  接続できませんでした\n");
		exit(1);
	}
/*
		参加登録
*/
	char buf[BUF_LEN];   // 送受信用バッファ
	int n = read(client_socket, buf, BUF_LEN);   // 参加者名の入力を促すメッセージ
	write(1, buf, n);
	buf[n] = '\0';

	if (strstr(buf, "満員ですので,後ほどご利用下さい") != NULL)
		close(client_socket);

	else {
		gets(buf);   // 参加者名の入力
		write(client_socket, buf, strlen(buf)+1);
		n = read(client_socket, buf, BUF_LEN);   // 「しばらくお待ち下さい」
		write(1, buf, n);
					// サーバとクライアントが会話を行うための準備(マスクの設定)
		fd_set mask = prepare(client_socket);   // 相手を選択する(select)ための初期化マスク
					// チャットの実行
		chat(client_socket, buf, BUF_LEN, mask);
	}

	return 0;
}
		
  接続までは,基本的に,プログラム例 18.2 と同じです.接続後,クライアント側には,サーバ側またはキーボードから情報が入ってきますので,どちらからの情報かを識別する必要があります.そのために,prepare 関数内で,キーボード及びソケットからの情報を受け取れるように設定します.

  チャットは,chat 関数内で行われます.select 関数(プログラム内で使用されている FD_ZERO,FD_SET,FD_ISSET,FD_CLR などに関しては,この関数の説明を参照)によって,どこからの情報かを調べます.キーボードから入力された場合は,入力されたデータをそのままサーバに送ります.チャットルームから出るために quit を入力した場合も,そのままサーバに転送します.また,ソケットを介したデータの場合は,受け取ったデータをそのまま画面に表示します.ただし,終了メッセージの場合( quit を含む)は,接続を閉じます.

  次に,サーバ側のプログラムを見て下さい.

#include <stdio.h>
#include <stdlib.h>   // atoi等
#include <string.h>   // 文字列操作,memset,FD_ZERO等
#include <sys/socket.h>   // ソケット(AF_INET, select等)
#include <netinet/in.h>    // in_port_t, sockaddr_in等
#include <sys/types.h>   // fd_set
#include <unistd.h>   // read, write, open, close

#define BUF_LEN_TMP 562   // バッファの大きさ
#define PORT 50000   // ポート番号
#define BUF_LEN 512   // バッファの大きさ

/**************/
/* 参加者情報 */
/**************/
class Member {
	public:
		int state;   // 回線の状態(1 : 開, 0 : 閉)
		int fd;   // ソケットディスクリプタ
		char name[50];   // 参加者名
		Member();   // コンストラクタ
};

/********************************/
/* クラスMemberのコンストラクタ */
/********************************/
Member::Member()
{
	state = 0;
}

/******************************************/
/* ソケットの初期設定(サーバ,s_setup ) */
/*      port : ポート番号                 */
/*      w_no : 待ち受けの最大数           */
/*      return : リスニングソケット       */
/******************************************/
int s_setup(in_port_t port, int w_no)
//int s_setup(int port, int w_no)
	// コンパイラのバージョンによっては,上の行の代わりにこの行を使用
{
/*
		サーバ情報をsockaddr_in構造体に設定
*/
	sockaddr_in server_inf;   // サーバ情報
	memset((char *)&server_inf, 0, sizeof(server_inf));
	server_inf.sin_family      = AF_INET;   // アドレスファミリー
	server_inf.sin_addr.s_addr = htonl(INADDR_ANY);   // IPアドレス
	server_inf.sin_port        = htons(port);   // htonl(4バイト),htons(2バイト)
                                                //   整数の上位及び下位バイトの指定
/*
		リスニングソケットを作成
*/
	int listening_socket = -1;   // リスニングソケットソケット
	if ((listening_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		printf("***error***  接続できませんでした\n");
/*
		サーバのアドレスをソケットに設定する
*/
	else {
	    if (bind(listening_socket, (sockaddr *)&server_inf, sizeof(server_inf)) < 0)
			printf("***error***  設定に失敗しました\n");
/*
		待ち受ける(待機状態の準備)(第2引数は,待ち受けの数)
*/
		else {
			if (listen(listening_socket, w_no) == -1 )
				printf("***error***  待ち受けに失敗しました\n");
		}
	}

	return listening_socket;
}

/************************************/
/* 参加者の登録と初期設定( init ) */
/*      soc : 接続済みソケット      */
/*      w_no : 待ち受けの最大数     */
/*      mask : select用初期化マスク */
/*      no : 現在の接続数           */
/*      mem : 接続登録簿            */
/*      return : 現在の接続数       */
/************************************/
int init(int soc, int w_no, fd_set *mask, int no, Member **mem)
{
					// 満員
	if (no == w_no) {
		write(soc, "満員ですので,後ほどご利用下さい\n", 33);
		close(soc);
	}
					// 参加できる
	else {
		int i1, sw = 0;
		for (i1 = 0; i1 < w_no && sw == 0; i1++) {
			if (mem[i1]->state == 0) {
						// ディスクリプタの登録
				mem[i1]->state = 1;
				mem[i1]->fd    = soc;
				no++;
						// マスクの設定
				FD_SET(mem[i1]->fd, mask);
						// 参加者名
				write(soc, "名前を入力して下さい ", 21);
				read(soc, mem[i1]->name, 50);
				strcat(mem[i1]->name, ": ");
						// 開始OK
				write(soc, "しばらくお待ち下さい\n", 21);
				write(soc, "チャット開始OKです!\n", 23);
				sw = 1;
			}
		}
	}

	return no;
}

/**********************************************/
/* チャットの実行( chat)                    */
/*      w_no : 待ち受けの最大数               */
/*      max_fd : ソケットの最大ディスクリプタ */
/*      buf : バッファ                        */
/*      size : バッファサイズ                 */
/*      mask : select用初期化マスク           */
/*      no : 現在の接続数                     */
/*      mem : 各ソケットの状態                */
/*      listening_socket : リスニングソケット */
/**********************************************/
int chat(int w_no, int *max_fd, char *buf, int size, fd_set *mask,
         int no, Member **mem, int listening_socket)
{
					// マスクの初期化と事象の選択
	fd_set now = *mask;
	select((*max_fd)+1, (fd_set *)&now, NULL, NULL, NULL);
					// 新規のクライアント登録
	if (FD_ISSET(listening_socket, &now)) {
		int connected_socket;   // 接続済みソケット
		if ((connected_socket = accept(listening_socket, NULL, NULL)) != -1) {
			int k = init(connected_socket, w_no, mask, no, mem);
			if (k > no) {
				no = k;
				if (connected_socket > *max_fd)
					*max_fd = connected_socket;
			}
		}
		else {
			printf("***error***  クライアントとの接続に失敗しました\n");
			exit(1);
		}
	}
					// チャットの終了チェック
	int sw = 1;
	if (FD_ISSET(0, &now)) {   // キーボードからの入力チェック
		gets(buf);
		if (strcmp(buf, "quit") == 0) {
			int i1;
			for (i1 = 0; i1 < w_no; i1++) {
				if (mem[i1]->state > 0)
					write(mem[i1]->fd, "Server.quit", 11);
			}
			for (i1 = 0; i1 < w_no; i1++) {
				if (mem[i1]->state > 0)
					close(mem[i1]->fd);
			}
			for (i1 = 0; i1 < w_no; i1++)
				delete [] mem[i1];
			delete [] mem;
			sw = 0;
			no = -1;
		}
	}
					// 各ソケットを調べる
	if (sw > 0) {
		int i1;
		for (i1 = 0; i1 < w_no; i1++) {
			if (mem[i1]->state > 0 && FD_ISSET(mem[i1]->fd, &now)) {
				int n  = read(mem[i1]->fd, buf, size);
				buf[n] = '\0';
						// クライアントの終了
				if (strcmp(buf, "quit") == 0) {
					write(mem[i1]->fd, "また参加して下さい!\n", 21);
					write(mem[i1]->fd, "quit", 4);
					FD_CLR(mem[i1]->fd, mask);
					close(mem[i1]->fd);
					mem[i1]->state = 0;
					*max_fd        = -1;
					int i2;
					for (i2 = 0; i2 < w_no; i2++) {
						if (mem[i2]->state > 0 && mem[i2]->fd > *max_fd)
							*max_fd = mem[i2]->fd;
					}
					no--;
				}
						// チャットの続行
				else {
					int len = strlen(mem[i1]->name);
					char str[BUF_LEN_TMP];
					strcpy(str, mem[i1]->name);
					strcat(str, buf);
					strcat(str, "\n");
					int i2;
					for (i2 = 0; i2 < w_no; i2++) {
						if (mem[i2]->state > 0)
							write(mem[i2]->fd, str, n+len+1);
					}
				}
			}
		}
	}

	return no;
}

/***********************************/
/* チャットルーム(サーバ,main ) */
/*      coded by Y.Suganuma        */
/***********************************/
int main(int argc, char *argv[])
{
/*
		最大参加者数
*/
	int max = 0;
	if (argc != 2) {
		printf("***error***  参加者数を入力してください\n");
		exit(1);
    }
	else
		max = atoi(argv[1]);
/*
		リスニングソケットの準備
*/
	int listening_socket;   // リスニングソケットソケット
	if ((listening_socket = s_setup(PORT, max)) == -1) {
		printf("***error***  リスニング接続に失敗しました\n");
		exit(1);
	}
/*
		参加登録簿とマスクの準備
*/
	Member **mem = new Member * [max];
	int i1;
	for (i1 = 0; i1 < max; i1++)
		mem[i1] = new Member;

	int max_fd = listening_socket;   // 接続済みソケットの最大ディスクリプタ
	fd_set mask;
	FD_ZERO(&mask);
	FD_SET(0, &mask);   // ディスクリプタ0(キーボード)をセット
	FD_SET(listening_socket, &mask);   // リスニングソケットをセット
/*
		チャットの実行
*/
	printf("Ready to Chat\n");

	int sw = 1;
	while (sw > 0) {
		char buf[BUF_LEN];   // バッファ
		int no = chat(max, &max_fd, buf, BUF_LEN, &mask, no, mem, listening_socket);
		if (no < 0) {
			sw = 0;
			close(listening_socket);
		}
	}

	return 0;
}
		
  リスニングソケットを作成するまでは,チャットルームへの最大参加者数を入力する点以外は,基本的に,プログラム例 18.2 と同じです.次に,参加者登録簿を作成した後,キーボードからの入力とソケットからのデータとを識別するために使用するマスクを設定します.

  クライアント用のプログラムと同様,チャットは,chat 関数内で実行されます.select 関数によって,どこからの情報かを調べます.サーバの場合,以下に示す 3 つの場合が存在します.一つは,リスニングソケットからのデータの場合です.この場合は,accept 関数によって接続済みソケットを作成します.もし,チャットルームが満員の場合は,メッセージを送信し,接続を閉じます.そうでない場合は,名前の入力を促すメッセージを送信した後,接続済みソケットを登録します.

  2 番目は,キーボードからの入力の場合です.このプログラムでは,チャットルームを閉じるための quit という入力しか認めていません.キーボードから quit が入力されると,チャットルームを閉じるメッセージをすべての参加者に送信した後,すべての接続済みソケットとリスニングソケットを閉じます.最後は,接続済みソケットからのデータの場合です.ソケットを介して受信したメッセージが quit であった場合は,対象となるクライアントとの接続を閉じ,参加者リストから外します.そうでない場合は,送られてきたメッセージを,全参加者に送信します.

情報学部 菅沼ホーム 全体目次 演習解答例 付録 索引