ソケット通信(Linux)

目次

1.ソケット通信とは
 ソケットAPIを用いた異なるホスト間の通信のアプリケーションです。ソケットAPIはトランスポート層以下の機能を提供するプログラミングインターフェースでクライアント/サーバモデルの通信を行うために用いられます。

2.ソケットを用いたサーバ/クライアント間通信
 ソケットの種類として、TCPプロトコルを使うコネクション型のストリームソケットと、UDPプロトコルを使うコネクションレス型のデータグラムソケットがあります。
(1)ストリームソケットを使用したコネクション型の通信

(2)データグラムソケットを使用したコネクションレス型の通信

3.サンプルプログラム


3.1 コネクション型
 ストリームソケットを使用したコネクション型の通信のサンプルを作成します。
(1)サンプルの概要
 サーバはクライアントからの接続を待ち、クライアントがサーバに接続すると、サーバはクライアントからの送信電文をコンソールに表示する。サーバはクライアントに電文を送信して通信を終了する。クライアントはサーバからの電文をコンソールに表示して通信を終了する。

(2)ソースプログラム
 server.c

#include<sys/types.h>	//socket,bind,listen,accept,send
#include<sys/socket.h>	//socket,bind,listen,accept,send
#include<netinet/in.h>	//sockaddr_in
#include<netdb.h>	//hostent
#include<unistd.h>	//close
#include<stdio.h>	//perror,fdopen,fgets,
#include<stdlib.h>	//atoi,exit,
#include<string.h>	//bzero,strcmp
#include<errno.h>

char *resmsg="Hello to client from Server\r\n";

int main(int argc, char* argv[])
{
	FILE *fp;	//ファイルポインタ
	int s; //サーバ待ち受けのソケットディスクリプタ
	int ns; //クライアントから受入れるソケットディスクリプタ
	int port; //ポート番号
	struct sockaddr_in sin;	// ホストのIPアドレスなど情報を保持する構造体(※1)
	struct sockaddr_in fsin;	// クライアントの		〃
	char buf[128];	//受信バッファ128バイト
	
	socklen_t fromlen = sizeof(struct sockaddr_in);	//socklen_t型:unsigned int型の別名
	//コマンド引数が足りないときエラー
	if((argc <= 1) || ((port = atoi(argv[1])) == 0)) {	//文字ストリングから整数に変換
	  perror("no port number");	//エラーメッセージの出力
	  exit(EXIT_FAILURE);
	}
	//ソケットの作成(※2)
	if((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		perror("client socket()");	//エラーメッセージの出力
		exit(EXIT_FAILURE);
	}
	//バインド(※3)
	bzero(&sin, sizeof(sin));	//構造体sinを0で初期化
	sin.sin_family = AF_INET;	//アドレスファミリ  AF_INET:IPv4インターネットプロトコル
	sin.sin_port = htons(port);	//ポート番号をTCP/IPネットワークバイトオーダーに変換
	sin.sin_addr.s_addr = INADDR_ANY;	//任意のIPアドレスからも受付
	
	if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
		perror("server bind()");	//エラーメッセージの出力
		exit(EXIT_FAILURE);	
	}
	//リッスン(※4)
	if (listen(s, 128) == -1) {	//新規コネクションの最大数128
		perror("server listen()");	//エラーメッセージの出力
		exit(EXIT_FAILURE);	
	}
	//アクセプト(※5)
	//接続待ちソケットのキューから新規にソケットを生成
	if ((ns = accept(s, (struct sockaddr *)&fsin, &fromlen)) == -1) {
		perror("server accept()");	//エラーメッセージの出力
		exit(EXIT_FAILURE);
	}
	
	fp = fdopen(ns, "r");	//ファイルディスクリプターにストリームを結びつける(※6)
	
	//クライアントからデータを取得しコンソールに出力(※7)
	while (fgets(buf, sizeof(buf), fp) != NULL) {
		if(strcmp(buf,"\r\n") == 0) break;	//CR+LFで読み込み停止
		printf("%s", buf);
	}
    //クライアントに送信(※8)
	send(ns, resmsg, strlen(resmsg), 0);
	//ファイルディスクリプターをクローズ(※9)
	close(ns); // クライアント接続用
	close(s); // サーバ待ち受け用
	return 0;
}

(※1)sockaddr_in構造体
struct sockaddr_in {
u_char sin_len;
u_char sin_family; (アドレスファミリ.今回はAF_INETで固定)
u_short sin_port; (ポート番号)
struct in_addr sin_addr; (IPアドレス)
char sin_zero[8];
};
ポート番号やIPアドレスはネットワークバイトオーダー (big endian) になっていないといけない。このため、整数をネットワークバイトオーダーに変換するhtons関数を用いる。

(※2)socket()
通信のための端点 (endpoint) を作成する
(書式)
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
引数:
①domain
アドレスファミリーに指定できるプロトコル
AF_INET インターネットプロトコル
など
②type
通信方式 (semantics) を指定する。
定義されている型は現在以下
SOCK_STREAM:
順序性と信頼性があり、双方向の、接続された バイトストリーム (byte stream) を提供する。
帯域外 (out-of-band) データ転送メカニズムもサポートされる。
SOCK_DGRAM:
データグラム (コネクションレス、信頼性無し、固定最大長メッセージ) をサポートする。
など
③protocol
ソケットによって使用される固有のプロトコルを指定する。
与えられたプロトコルファミリーの種類ごとに一つのプロトコルのみをサポートする。その場合はprotocolに0を指定できる。
戻り値:ディスクリプタ
プログラムからファイルを操作する際、操作対象のファイルを識別・同定するために割り当てられる番号

(※3)bind()
ソケットが作成されたときは名前空間 (アドレスファミリー) に存在するがアドレスは割り当てられていない。bind() はファイルディスクリプターsockfdで参照されるソケットにaddrで指定されたアドレスを割り当てる。
(書式)
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
返り値:成功:0  失敗:-1
引数:
①int sockfd
②sockaddr *addr
③socklen_t addrlen

(※4)listen()
ソケットを待ち状態にする
(書式)
#include <sys/types.h>
#include <sys/socket.h>
int listen(int s, int backlog);
引数:
①s:
ソケットディスクリプタ
②backlog:
新規コネクションの最大数を指定する

(※5)accept()
クライアントからの接続を受け入れコネクションを確立する。この関数は、接続待ちソケットsocket宛ての保留状態の接続要求が入っているキューから先頭の接続要求を取り出し、接続済みソケットを新規に生成し、そのソケットを参照するための新しいファイルディスクリプターを返す。
(書式)
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
引数:
①s
ソケットディスクリプタ
②addr
宛先の情報を戻すsockaddr構造体
③addrlen
sockaddr構造体の大きさを指定する

(※6)fdopen()
既存のファイルディスクリプターにストリームを結びつける。ストリームの mode (“r”, “r+”, “w”, “w+”, “a”, “a+” のいずれか) はファイルディスクリプターのモードと互換のものでなければならない。

(※7)fgets()
streamから最大でsize-1個の文字を読み込み、sが指すバッファーに格納する。読み込みは EOF または改行文字を読み込んだ後で停止する。読み込まれた改行文字はバッファーに格納される。終端のヌルバイト (‘\0’) が一つバッファーの中の最後の文字の後に書き込まれる。
(書式)
#include <stdio.h>
int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
int getc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);
char *fgets(char *s, int size, FILE *stream);

(※8)send(), sendto(), sendmsg()
ソケットが接続された (connected) 状態にある場合(送信相手が決まっている)にソケットへメッセージを送る 。
(書式)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
引数:
①int sockfd
②const void
③*buf
④size_t len
⑤int flags

(※9)close()
ファイルディスクリプターをクローズする
(書式)
#include <unistd.h>
int close(int fd);int close(int fd);

 client.c

#include<sys/types.h>	//socket,send
#include<sys/socket.h>	//socket,send
#include<netinet/in.h>	//sockaddr_in
#include<netdb.h>	//hostent
#include<unistd.h>	//close
#include<stdio.h>	//perror,fdopen,fgets
#include<stdlib.h>	//atoi,exit
#include<string.h>	//bzero,memcpy
#include<errno.h>

#define NUMSTR  3

char *reqmsg[NUMSTR] = {	//ポインタ配列
  "Request from client\r\n",
  "Hello to Server\r\n",
  "\r\n"
};

int main(int argc, char* argv[])
{
	FILE *fp;	//ファイルポインタ
	char hostname[128];	//受信バッファ
	int s;	//サーバ接続のソケットディスクリプタ
	int port;//	ポート番号
	struct hostent *hp;	//hostent構造体(ホスト名など)(※1)
	struct sockaddr_in sin;	//接続先のIPアドレスやポート番号の情報を保持するための構造体
	char buf[128];	//受信バッファ
	
	//コマンド引数が足りないときエラー
	if(argc <= 2) {
	  perror("host and port required");	//エラーメッセージの出力
	  exit(EXIT_FAILURE);		
	}
	if((port = atoi(argv[2])) == 0) {	//文字ストリングから整数に変換
	  perror("no port");	//エラーメッセージの出力
	  exit(EXIT_FAILURE);
	}
	
		if ((hp = gethostbyname(argv[1])) == NULL) {	//ホスト名からIPアドレスを取得(※2)
		fprintf(stderr, "%s: unknown host.\n", hostname);
		exit(EXIT_FAILURE);
	}

	/* IPアドレス表示(確認用) start */
	// IPv4は4バイト
    if(hp->h_length != 4){
        printf("IPv6 address.");
        exit(EXIT_FAILURE);
    }

	for(int i=0; hp->h_addr_list[i]; i++){
        printf("IP address(%d)    = %d.%d.%d.%d\n" , i, 
                (unsigned char)*((hp->h_addr_list[i])) ,
                (unsigned char)*((hp->h_addr_list[i]) + 1) ,
                (unsigned char)*((hp->h_addr_list[i]) + 2) ,
                (unsigned char)*((hp->h_addr_list[i]) + 3)
        );
    }
	/* IPアドレス表示 end */

	//ソケット
	if((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		perror("client socket()");	//エラーメッセージの出力
		exit(EXIT_FAILURE);		
	}

	//コネクト(※3)
	bzero(&sin, sizeof(sin));	//構造体sinを0で初期化
	sin.sin_family = AF_INET;	//アドレスファミリ  AF_INET:IPv4インターネットプロトコル
	sin.sin_port = htons(port);	//ポート番号
	memcpy(&sin.sin_addr, hp->h_addr, hp-> h_length);	//IPアドレス(※4)
	
	if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
		perror("client connect()");	//エラーメッセージの出力
		exit(EXIT_FAILURE);	
	}
	
	fp = fdopen(s, "r");	//ファイルディスクリプターfdにストリームを結びつける

	// サーバにデータを送信
	for(int i = 0; i < NUMSTR; i++) {
	  send(s, reqmsg[i], strlen(reqmsg[i]), 0);
	}

	// サーバからのデータ受信待ち
	while (fgets(buf, sizeof(buf), fp) != NULL) {
	  printf("%s", buf);
	}
	
	close(s);	//ソケットディスクリプターをクローズ
	return 0;
}

(※1)hostent構造体
struct hostent {
char h_name; / ホストの正式名称 */
char *h_aliases; / 別名リスト / int h_addrtype; / ホストアドレスのタイプ (AF_INET6 など) / int h_length; / アドレスの長さ */
char *h_addr_list; / NULL で終わるアドレスのリスト */
};

(※2)gethostbyname()
ホスト名からIPアドレスを得る
(書式)
#include <netdb.h>
extern int h_errno;
struct hostent *gethostbyname(const char *name);

(※3)connect()
ソケットの接続を行う
(書式)
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
ファイルディスクリプターsockfdが参照しているソケットをaddrで指定されたアドレスに接続する
戻り値:成功:0、失敗:-1

(※4)memcpy()
メモリー領域srcの先頭nバイトをメモリー領域 dest にコピーする。
(書式)
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);

(3)コンパイルと実行

$ gcc server.c -o server
$ gcc client.c -o client
先にサーバアプリを起動する
$ ./server 1050(1024未満のポート番号は非rootユーザーは使用不可)

他の端末を開き、接続先サーバの受付ポートに接続
$ ./client localhost 105

サーバ側にはクライアントが送信したメッセージを表示し、クライアント側ではサーバが送信したメッセージが表示される。

3.2 コネクションレス型
 データグラムソケットを使用したコネクションレス型の通信のサンプルを作成します。
(1)サンプルの概要
 サーバはクライアントからの接続を待ち、クライアントがサーバに検索を要求すると、サーバは検索結果をクライアントに返す。クライアントは検索結果をコンソールに表示して通信を終了する。

(2)ソースプログラム
 server_cl.c

/* コネクションレス型のソケット通信(サーバ) */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>	//socket,bind,recvfrom,sendto
#include <sys/socket.h>	//socket,bind,recvfrom,sendto
#include <netinet/in.h>	//sockaddr_in
#include <netdb.h>	// gethostbyname() 
#include <errno.h>
#include <string.h>
#include <unistd.h>
#define  MAXHOSTNAME	64
#define  S_UDP_PORT	(u_short)5000  //本サーバのポート番号
#define  MAXKEYLEN	128
#define  MAXDATALEN	256

//プロトタイプ宣言
void db_search(int);//	検索

int main()
{
	int	s;	//ソケットディスクリプタ
	char	s_hostname[MAXHOSTNAME];	//ホスト名変数
	struct hostent	*hp;	//hostent構造体(ホスト名など)
	struct sockaddr_in	sin;	//ホストのIPアドレスなど情報を保持する構造体


	//IPアドレス求める
	gethostname(s_hostname, sizeof(s_hostname));
	hp = gethostbyname(s_hostname);
	
	// ソケットの作成
	if((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
		perror("socket");	//エラーメッセージの出力
		exit(EXIT_FAILURE);
	}

	// バインド
	bzero((char *)&sin, sizeof(sin));	//構造体sinを0で初期化
	sin.sin_family = AF_INET;	//アドレスファミリ  AF_INET:IPv4インターネットプロトコル
	sin.sin_port = htons(S_UDP_PORT);	//ポート番号
	bcopy((char *)hp->h_addr, (char *)&sin.sin_addr, hp->h_length);

	// ソケットへアドレス割り当て
	if(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
		perror("bind");	//エラーメッセージの出力
		exit(EXIT_FAILURE);
	}
	
	// クライアントからの検索要求受付
	db_search(s);

	return 0;
}

void db_search(int s)	//クライアントからの検索要求
{
	struct sockaddr_in	c_address;	//接続先のIPアドレスやポート番号の情報を保持するための構造体
	int	c_addrlen;
	char	key[MAXKEYLEN+1], data[MAXDATALEN+1];
	int	keylen, datalen;
	//ポインタ配列に文字列データを入れる
	static char *db[] = {"aoyama","aoyama@test.com","suzuki","suzuki@test.com",
                             "ando","ando@test.com",NULL};
	char	**dbp;

	while(1) {
		/* キーをソケッから読み込む */
		c_addrlen = sizeof(c_address);
		//ソケットから受信(※1)		
		if((keylen = recvfrom(s, key, MAXKEYLEN, 0, (struct sockaddr *)&c_address, &c_addrlen)) == -1) {
			perror("recvfrom");
			exit(EXIT_FAILURE);
		}
		key[keylen] = '\0';
		printf("Received key> %s\n",key);
		// キー検索
		dbp = db;
		while(*dbp) {	//ポインタ配列の繰り返し
			if(strcmp(key, *dbp) == 0) {
				strcpy(data, *(++dbp));	//keyが一致したらdataを取得
				break;
			}
			dbp += 2;//ポインタ配列を2つ(keyとdataの分)増やす
		}
		if(*dbp == NULL) strcpy(data, "No entry");
	
		// 検索したデータをソケットに書き込む(※2)
		datalen = strlen(data);
		if(sendto(s, data, datalen, 0, (struct sockaddr *)&c_address, c_addrlen) != datalen) {
			fprintf(stderr, "datagram error\n"); 
			exit(EXIT_FAILURE);
		}
		printf("Sent data> %s\n", data);
	}
}

(※1)recvfrom()
ソケットからメッセージを受け取る
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

(※2)sendto()
ソケットへメッセージを送る
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

 client_cl.c

/* コネクションレス型のソケット通信(クライアント)*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>	//socket,bind,recvfrom,sendto
#include <sys/socket.h> //socket,bind,recvfrom,sendto
#include <netinet/in.h> //sockaddr_in
#include <netdb.h>      //gethostbyname()
#include <errno.h>
#include <string.h>
#include <unistd.h>	//close
#define  MAXHOSTNAME    64
#define	 S_UDP_PORT	(u_short)5000 //サーバのポート番号
#define  MAXKEYLEN	128
#define	 MAXDATALEN	256

//プロトタイプ宣言
void remote_dbsearch(int, struct sockaddr_in*, int);//	リモート検索

int main()
{
	int	s;	//ソケットディスクリプタ
	char	s_hostname[MAXHOSTNAME];
	struct hostent	*hp;	//ホスト名変数
	struct sockaddr_in s_address;	//ホストのIPアドレスなど情報を保持する構造体
	struct sockaddr_in c_address;	//クライアントのIPアドレスなど情報を保持する構造体
	int	s_addrlen;

	// ホスト名入力
	printf("server host name?: "); scanf("%s",s_hostname);
	//IPアドレス求める
	if((hp = gethostbyname(s_hostname)) == NULL) {
		fprintf(stderr, "server host does not exists\n");
		exit(EXIT_FAILURE);
	}
	// ソケットの作成
    if((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
 	  	perror("socket");	//エラーメッセージの出力
       	exit(EXIT_FAILURE);
    }
	// サーバホストのアドレスなどの情報設定
    bzero((char *)&s_address, sizeof(s_address));	//構造体sinを0で初期化
    s_address.sin_family = AF_INET;	//アドレスファミリ  AF_INET:IPv4インターネットプロトコル
    s_address.sin_port = htons(S_UDP_PORT);	//ポート番号
    bcopy((char *)hp->h_addr, (char *)&s_address.sin_addr, hp->h_length);
	s_addrlen = sizeof(s_address);
	// クライアントのアドレスなどの情報設定
    bzero((char *)&c_address, sizeof(c_address));	//構造体sinを0で初期化
    c_address.sin_family = AF_INET;	//アドレスファミリ  AF_INET:IPv4インターネットプロトコル
    c_address.sin_port = htons(0);	//ポート番号
	c_address.sin_addr.s_addr = htonl(INADDR_ANY); // Internet
	// クライアントアドレスのソケッへの割り当て
	if(bind(s, (struct sockaddr *)&c_address, sizeof(c_address)) == -1) {
		perror("bind");	//エラーメッセージの出力
		exit(EXIT_FAILURE);
	}
	
	// データベース検索
	remote_dbsearch(s, &s_address, s_addrlen);
	close(s);
	return 0;
}	
//リモート検索
void remote_dbsearch(int s, struct sockaddr_in *s_addressp, int s_addrlen) // 検索
{
	char	key[MAXKEYLEN+1], data[MAXDATALEN+1];
	int	keylen, datalen;

	// キー値入力
	printf("key?: ");scanf("%s",key);
	// キー値をソケットに書き込む
	keylen = strlen(key);
	if(sendto(s, key, keylen, 0, (struct sockaddr *)s_addressp, s_addrlen) != keylen) {
		fprintf(stderr, "datagram error\n");
		exit(EXIT_FAILURE);
	}
	// 検索結果をソケットから読み込む
	if((datalen = recvfrom(s, data, MAXDATALEN, 0, NULL, &s_addrlen)) == -1) { 
		perror("recvfrom");	//エラーメッセージの出力
		exit(EXIT_FAILURE);
	}
	// データ表示
	data[datalen] = '\0';
	fputs("data: ",stdout);puts(data);
}

(3)コンパイルと実行

$ gcc server_cl.c -o server_cl
$ gcc client_cl.c -o client_cl
先にサーバアプリを起動する
$ ./server_cl 

他の端末を開き、接続先サーバの受付ポートに接続
$ ./client_cl
server host name?: localhost
key?: suzuki

サーバ側にはクライアントから受信したkey値と検索結果を表示し、クライアント側ではサーバから受信した検索結果を表示する。

The end