K3の住民

最近はレア社のゲームについては書いていませんが、一応レア社のゲームが一番好きな人です。『雪圀』で"圀"は"国"とほぼ同義ですが、国ではありませんって当たり前か(笑)。

【Cプログラミング】Cで行うソケットプログラミング(Linux編)

皆さん、こんにちは。雪圀です。
今回は、Cで行うソケットプログラミングを我流で解説しようと思います。


さて、何故作ろうかと思ったのかというと、調べても、動きが理解できるわけでは
ないってこと(関数一つ一つの解説をしているサイトが少なく、とりあえずこう
すれば動きますってのばかり)と、送信と受信を同時に行う双方向通信プログラムが
なかなか無い(僕の調べ方が悪いだけなんだろうけど)と思ったからです。

今回、TCPUDPのソケットプログラムの書き方を解説していく予定ですが、そもそも
TCPUDPの違いは何なのでしょうか?

TCPUDPの違い

TCPUDPの違いを一言で説明するならば、コネクション型であるか、そうで無い
コネクションレス)かの違いです。

コネクション型というのは、確立された通信を行える通信方式です。この通信方式
には、接続要求をするクライアントと、それを受け付けるサーバがあります。サーバ
から応答があって、かつ存在するもので、かつ受信可能である必要があり、この条件
を満たしていなければ、通信が開始されません。
これにより、通信の信頼性が保証されます。1対1の通信を行うときにお勧めです。

コネクションレス型は、コネクション型とは逆に確立せずに通信を行います。
つまり、受信側がどうなっていようとも、送信すればそれで終わり、という通信方式
です。1対多の通信を行うときにお勧めで、動画の配信はこのコネクションレス型が
使われています。

TCPがコネクション型で、UDPコネクションレス型です。
但し、ソケットプログラミングでは、TCPUDPのようにするのは多分無理ですが、
UDPTCPのように通信させることも可能です(UDPをそのように使うならTCP
使えよ、と思いますが)。

個人的プログラミング難易度としては、UDP < TCPという感じです。

ソケットプログラムを作る前に

先ずLinuxWindowsでは作り方が多少違います。流石にWindowsによる作り方も
書いていると、非常に長くなってしまうので、Linux編とWindows編で分けようと
思います(ついでにWindows編の投稿は未定にさせてください・・・すいません)。

Linuxではソケットプログラムを作るために以下のヘッダファイルをincludeする必要
があります。

・sys/types.h
・sys/socket.h
・netinet/in.h
・arpa/inet.h
・unistd.h
・(string.h)
・(errno.h)

string.hはmemset()関数(後述)を使うときには使いますが、使わないときには使わ
ないので()で閉じています。但し、受信のときには必要です。
errno.hはデバッグの際に非常に役立つヘッダファイルですので、これがあるだけで
デバッグが相当楽になります。「俺ソケットプログラミングで失敗しねえから!」
って人には要らないでしょう(突き放し)。
どういうわけかネット上ではarpa/inet.hとunistd.hとstring.hがincludeされていない
プログラムをよく見ますが、無いままソースだけコピペしてしまうとコンパイル
出来ないので、他のサイトを参考にする場合は、関数一つ一つを調べていき
ましょう。
後は必要に応じで、色々なヘッダファイルをincludeしていくって感じですかね。

エラー処理

では先ず一つ、重要なことをお伝えしておきましょう。
エラー処理は絶対に入れてください。これはプログラマとしての鉄則です。
何故エラー処理は必要なのか、と思われるかもしれませんが、生成したソケットは、
実質ファイルを開いたということと同義であり、エラーが起きてエラー処理を
入れる前にプログラムに何らかの障害があった場合、ソケットはずっと生成された
(=ファイルがずっと開いた)ままになるので、危険です
ファイル操作の場合、ファイルが使えなくなる危険性があることから、どのように
危険なのかはお察しください。
エラー処理の基本形を以下に示します。

 if(/* 関数 */ < 0) {
	// エラー出力 <- エラーを画面に表示する
	
	// ファイルのクローズ もしくは ソケットの破棄
	close(/* ファイルもしくはソケットのディスクリプタ */);
	
	return -1;
}

TCPソケットプログラム(サーバ)

基本的なサーバプログラムを以下に示します。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(void)
{
	struct sockaddr_in sv_addr, ce_addr;		// IPアドレス、ポート割り当て用
	int accept_sd, sr_sd;				// ソケット用のファイルディスクリプタ
	char buf[/* 任意 */];				// 受信用(256byteとか1024byteぐらいが妥当)
	
	// ソケット生成
	accept_sd = socket(AF_INET, SOCK_STREAM, 0);
	if(accept_sd < 0) {
		perror("socket");
		close(accept_sd);
		return -1;
	}
	
	// ソケットの設定
	sv_addr.sin_family = AF_INET;			// IPv4用に設定
	sv_addr.sin_port = htons(/* ポートは任意 */);	// ポートの割り当て
	sv_addr.sin_addr.s_addr = INADDR_ANY;		// IPアドレスの割り当て
	
	// ソケットに名前を付ける
	if( bind(accept_sd, (struct sockaddr *)&sv_addr, sizeof(sv_addr)) < 0 ) {
		perror("bind");
		close(accept_sd);
		return -1;
	}
	
	// クライアントからの接続要求の受け付ける回数を設定
	if( listen(accept_sd, /* 待つ回数は任意 */) < 0 ) {
		perror("listen");
		close(accept_sd);
		return -1;
	}
	
	// クライアントからの接続要求を受け付ける
	sr_sd = accept(accept_sd, (struct sockaddr *)&ce_addr, sizeof(ce_addr));
 	if(sr_sd < 0) {
		perror("accept");
		close(sr_sd);
		close(accept_sd);
		return -1;
	}
	
	// クライアントと送受信する <- ここに送受信処理が入る
	
	// ソケットを破棄する
	close(sr_sd);
	close(accept_sd);
	
	return 0;
}

何やら難しそうに見えますが、エラー処理を入れているからそうなのであって、実際
は関数を呼び出しまくってるだけです。

サーバプログラムでは以下の関数が使われています。

  • socket()

ソケットを生成する関数です。TCPの場合は第二引数にSOCK_STREAMが入ります。

  • htons()

short型の変数をビッグエンディアンに変換する関数です。これはネットワーク通信
がビッグエンディアンで行われるためです。
ビッグエンディアンって何?ってのは書くと少し長くなるので省略。

  • bind()

ソケットに名前を付けます。生成したソケットにポートとIPアドレスを関連付けする
ことをソケットに名前を付けると言います。
ここで覚えておいていただきたいのが、割り当てるIPアドレスです。
このプログラムではIPアドレスはINADDR_ANYとなっています。これはどのアドレス
から通信してきてもいいよってことです。IPを指定したい場合、サーバのIPアドレス
にする、ということを覚えておいてください。クライアントのIPアドレスにすると
bind()関数にエラーが発生します。

  • listen()

クライアントからの接続要求を受け付ける前に設定する関数です。第二引数では接続
要求を受け付ける回数を指定することが出来ます。
ネットワークに詳しい人であればその回数分pingを行う、というイメージで良いと
思います。

  • accept()

接続要求を受け付ける関数です。これが成功したら、クライアントとの接続が完了
した、ということになります。
因みに、accept_sdとsr_sdという変数が宣言されていたと思いますが、accept_sdは
接続要求を受け付ける用ファイルディスクリプタ、sr_sdは送受信用ファイルディス
クリプタです。サーバプログラムではソケット用ファイルディスクリプタが2つ必要
なので覚えておいてください。

  • close()

ソケットを破棄します。ソケットを生成するということは、ファイルを開いたと
いうことです。つまり、ファイルは閉じる必要があるということからソケットも
破棄する必要があると言えます。

TCPソケットプログラム(クライアント)

基本的なクライアントプログラムを以下に示します。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[])
{
	struct sockaddr_in addr;	// IPアドレス、ポート割り当て用
	int sr_sd;			// ソケット用のファイルディスクリプタ
	char buf[/* 任意 */];		// 受信用(256byteとか1024byteぐらいが妥当)
	
	// コマンドラインが3つ未満の場合、エラー
	if (argc < 3) {
		fprintf(stderr, "usage: ./%s <address> <port>\n", argv[0]);
		return -1;
	}
	
	// ソケット生成
	sr_sd = socket(AF_INET, SOCK_STREAM, 0);
	if(sr_sd < 0) {
		perror("socket");
		close(sr_sd);
		return -1;
	}
	
	// ソケットの設定
	addr.sin_family = AF_INET;	// IPv4用に設定
	addr.sin_port = htons(argv[2]);	// ポートの割り当て
	addr.sin_addr.s_addr = argv[1];	// IPアドレスの割り当て(サーバのIPアドレス)
	
	// サーバに接続する
	if( connect(sr_sd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("connect");
		close(sr_sd);
		return -1;
	}
	
	// サーバと送受信する <- ここに送受信処理が入る
	
	// ソケットを破棄する
	close(sr_sd);
	
	return 0;
}

さて、今まで書いてきた中で見慣れないものがあると思います。例えば、「main()
関数に引数がある!」とかね。これは後で説明します。
クライアントはサーバに比べて、やけに簡単になっていると思います。

  • socket()
  • htons()
  • close()

サーバプログラムと同じである為省略。

  • connect()

サーバと接続するための関数。つまり、サーバに接続要求を行いますので入れる
IPアドレスはサーバのIPアドレスになります。

TCPソケットプログラム(送受信)

コメントに「// ~と送受信する <- ここに送受信処理が入る」と書いていたと思い
ますが、そこに以下の3つの処理が入ります。

  • 送信

送信側で行う基本的な処理を以下に示します。

	// 文字列を送信する
	if( write(sr_sd, /* 送る文字列 */, /* 送る文字列のbyte数 */) < 0 ) {
		perror("write");
		
		// 破棄するソケットはサーバの場合は2つ、クライアントの場合は1つ
		
		return -1;
	}

破棄するソケットはサーバとクライアントで違います。サーバは2つ生成してる
から2つ破棄しなければなりません。クライアントは1つだけ生成しているので
1つ破棄すれば良いです。
送信をする場合はソケットはファイルと同じなのでwrite()関数を使います。
今回は使っていませんが、変数を使う場合はmemset()関数を使うよう心がけ
ましょう。

  • 受信

受信側で行う基本的な処理を以下に示します。

	// 文字列を受信する
	memset(buf, 0, sizeof(buf));
	if( read(sr_sd, buf, sizeof(buf)) < 0 ) {
		perror("read");
		
		// 破棄するソケットはサーバの場合は2つ、クライアントの場合は1つ
		
		return -1;
	}
	
	printf("recv : %s\n", buf);

破棄するソケットは送信と同じ。
受信をする場合はソケットはファイルと同じなのでread()関数を使います。但し、
変数に不定値が入っているかもしれないので、read()の前にmemset()関数を使って
初期化しています。

  • 双方向

送信→受信の順で行う基本的な処理を以下に示します。

	// 文字列を送信する
	if( write(sr_sd, /* 送る文字列 */, /* 送る文字列のbyte数 */) < 0 ) {
		perror("write");
		
		// 破棄するソケットはサーバの場合は2つ、クライアントの場合は1つ
		
		return -1;
	}
	
	// 文字列を受信する
	memset(buf, 0, sizeof(buf));
	if( read(sr_sd, buf, sizeof(buf)) < 0 ) {
		perror("read");
		
		// 破棄するソケットはサーバの場合は2つ、クライアントの場合は1つ
		
		return -1;
	}
	
	printf("recv : %s\n", buf);

受信→送信の順で行う基本的な処理を以下に示します。

	// 文字列を受信する
	memset(buf, 0, sizeof(buf));
	if( read(sr_sd, buf, sizeof(buf)) < 0 ) {
		perror("read");
		
		// 破棄するソケットはサーバの場合は2つ、クライアントの場合は1つ
		
		return -1;
	}
	
	printf("recv : %s\n", buf);
	
	// 文字列を送信する
	if( write(sr_sd, /* 送る文字列 */, /* 送る文字列のbyte数 */) < 0 ) {
		perror("write");
		
		// 破棄するソケットはサーバの場合は2つ、クライアントの場合は1つ
		
		return -1;
	}

write()→read()とread()→write()の順で並べただけです。
それ以外は送信、受信と同じです。

UDPソケットプログラム(送信)

UDPソケットプログラムはTCPに比べたら簡単です。だって送ったり受けたり
するだけだもの。
というわけで、基本的なUDPソケットプログラムの送信側を以下に示します。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char *argv[])
{
	struct sockaddr_in addr;	// IPアドレス、ポート割り当て用
	int sd;				// ソケット用のファイルディスクリプタ
	
	// コマンドラインが3つ未満の場合、エラー
	if (argc < 3) {
		fprintf(stderr, "usage: ./%s <address> <port>\n", argv[0]);
		return -1;
	}
	
	// ソケット生成
	sd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sd < 0) {
		perror("socket");
		close(sd);
		return -1;
	}
	
	// ソケットの設定
	addr.sin_family = AF_INET;	// IPv4用に設定
	addr.sin_port = htons(argv[2]);	// ポートの割り当て
	addr.sin_addr.s_addr = argv[1];	// IPアドレスの割り当て(受信側のIPアドレス)
	
	// 文字列を送信する
	if( sendto(sd, /* 送る文字列 */, /* 送る文字列のbyte数 */, 0, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("sendto");
		close(sd);
		return -1;
	}
	
	// ソケットを破棄する
	close(sd);
	
	return 0;
}

UDPの場合、送信するか、受信するかの処理をすればいいだけなので、送信のときは
TCPのクライアントプログラムのように書いて、sendto()関数を使うだけ!
簡単ですね。

  • socket()

UDPの場合、第二引数にSOCK_DGRAMを入れます。

  • htons()
  • close()

TCPサーバプログラムと全く同じなので省略。

UDPソケットプログラム(受信)

これもTCPに比べたら簡単ですが、送信側に比べると少し手間が必要です。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(void)
{
	struct sockaddr_in addr;	// IPアドレス、ポート割り当て用
	int sd;				// ソケット用のファイルディスクリプタ
	char buf[/* 任意 */];		// 受信用(256byteとか1024byteぐらいが妥当)
	
	// ソケット生成
	sd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sd < 0) {
		perror("socket");
		close(sd);
		return -1;
	}
	
	// ソケットの設定
	addr.sin_family = AF_INET;			// IPv4用に設定
	addr.sin_port = htons(/* ポートは任意 */);	// ポートの割り当て
	addr.sin_addr.s_addr = INADDR_ANY;		// IPアドレスの割り当て
	
	// ソケットに名前を付ける
	if( bind(sd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("bind");
		close(sd);
		return -1;
	}
	
	// 文字列を受信する
	memset(buf, 0, sizeof(buf));
	if( recv(sd, buf, sizeof(buf), 0) < 0 ) {
		perror("recv");
		close(sd);
		return -1;
	}
	
	// ソケットを破棄する
	close(sd);
	
	return 0;
}

受信側の場合はサーバプログラムのようにIPアドレスを指定し、bind()、recv()関数を
使います。

  • socket()

UDP送信プログラムと全く同じなので省略。

  • htons()
  • bind()
  • close()

TCPサーバプログラムと全く同じなので省略。

UDPソケットプログラム(双方向)

送信→受信はTCPに比べ、簡単ではありますが少し注意が必要です。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[])
{
	struct sockaddr_in addr;			// IPアドレス、ポート割り当て用
	int send_sd, recv_sd;				// ソケット用のファイルディスクリプタ
	char buf[/* 任意 */];				// 受信用(256byteとか1024byteぐらいが妥当)
	
	// コマンドラインが3つ未満の場合、エラー
	if (argc < 3) {
		fprintf(stderr, "usage: ./%s <address> <port>\n", argv[0]);
		return -1;
	}
	
	// 送信用ソケット生成
	send_sd = socket(AF_INET, SOCK_DGRAM, 0);
	if(send_sd < 0) {
		perror("socket");
		close(send_sd);
		return -1;
	}
	
	// 受信用ソケット生成
	recv_sd = socket(AF_INET, SOCK_DGRAM, 0);
	if(recv_sd < 0) {
		perror("socket");
		close(recv_sd);
		return -1;
	}
	
	// ソケットの設定(受信用)
	addr.sin_family = AF_INET;			// IPv4用に設定
	addr.sin_port = htons(argv[2]);			// ポートの割り当て
	addr.sin_addr.s_addr = INADDR_ANY;		// IPアドレスの割り当て
	
	// ソケットに名前を付ける
	if( bind(recv_sd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("bind");
		close(recv_sd);
		return -1;
	}
	
	// ソケットの設定(送信用)
	addr.sin_addr.s_addr = argv[1];			// IPアドレスの割り当て(受信側のIPアドレス)
	
	// 文字列を送信する
	if( sendto(send_sd, /* 送る文字列 */, /* 送る文字列のbyte数 */, 0, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("sendto");
		close(send_sd);
		return -1;
	}
	
	// 文字列を受信する
	memset(buf, 0, sizeof(buf));
	if( recv(recv_sd, buf, sizeof(buf), 0) < 0 ) {
		perror("recv");
		close(recv_sd);
		return -1;
	}
	
	// ソケットを破棄する
	close(send_sd);
	close(recv_sd);
	
	return 0;
}

僕的に注意しなければならないと思ったのは、ソケット用ファイルディスクリプタ
とbind()です。
sendto()はソケットに名前を付ける必要が無い為、設定が送信側の設定であればどこ
でも良いわけですが、bind()はソケットに名前を付ける必要があります。要は同じ
ソケット用ファイルディスクリプタを使ってsendto()を使った後にIPアドレスだけを
変えて、bind()を使うとエラーになる、ということが言いたいのです。
だから、送信→受信の際には、ファイル用ディスクリプタとbind()には気を付け
ましょう。

次に受信→送信です。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[])
{
	struct sockaddr_in addr;		// IPアドレス、ポート割り当て用
	int sd;					// ソケット用のファイルディスクリプタ
	char buf[/* 任意 */];			// 受信用(256byteとか1024byteぐらいが妥当)
	
	// コマンドラインが3つ未満の場合、エラー
	if (argc < 3) {
		fprintf(stderr, "usage: ./%s <address> <port>\n", argv[0]);
		return -1;
	}
	
	// ソケット生成
	sd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sd < 0) {
		perror("socket");
		close(sd);
		return -1;
	}
	
	// ソケットの設定(受信用)
	addr.sin_family = AF_INET;		// IPv4用に設定
	addr.sin_port = htons(argv[2]);		// ポートの割り当て
	addr.sin_addr.s_addr = INADDR_ANY;	// IPアドレスの割り当て
	
	// ソケットに名前を付ける
	if( bind(sd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("bind");
		close(sd);
		return -1;
	}
	
	// 文字列を受信する
	memset(buf, 0, sizeof(buf));
	if( recv(sd, buf, sizeof(buf), 0) < 0 ) {
		perror("recv");
		close(sd);
		return -1;
	}
	
	// ソケットの設定(送信用)
	addr.sin_addr.s_addr = argv[1];	// IPアドレスの割り当て(受信側のIPアドレス)
	
	// 文字列を送信する
	if( sendto(sd, /* 送る文字列 */, /* 送る文字列のbyte数 */, 0, (struct sockaddr *)&addr, sizeof(addr)) < 0 ) {
		perror("sendto");
		close(sd);
		return -1;
	}
	
	// ソケットを破棄する
	close(sd);
	
	return 0;
}

送信する際にはソケットに名前を付ける必要が無い為、受信→送信は送信→受信より
かは難易度は低いでしょう。

コマンドライン引数

TCPクライアントプログラムやUDPの単方向送信、双方向通信プログラムでmain()関数
が以下のように記述されていたと思います。

int main(int argc, char *argv[])
{

}

この引数ですが、これはmain()関数限定で使われる引数で、コマンドライン引数
言います。これはプログラムを実行するときに複数のコマンドラインを扱えるように
する引数です。
例えば、Linuxのcdコマンドで/homeに移動する際以下のように書くと思います。

cd /home

これをCのコマンドライン引数に当てはめると、「cd」がargv[0]で、「/home」が
argv[1]になります。
argcはコマンドラインの数を示します。これによりコマンドライン数が足りなかった
場合にエラー処理を入れることが出来ます。そのときに使い方を書いてあげるのが
ユーザへの配慮に徹底したプログラムであると言えます。
今回のTCPクライアント、UDPの送信時に使われたプログラムでは、argv[1]をIP
アドレス、argv[2]をポートとしています。


以上です。もの凄く長くなってしまいましたが、ソケットプログラム自体サンプルに
しても長くなってしまう為仕方ないと言えるでしょう。

今回はこれくらいにしときます。