C言語で学ぶソケットAPI入門 第1回 サーバ編の続編のクライアント編です。
前回はサーバのソフトを作成しましたので、今度はソケットAPIを使ったクライアントソフトを作成しながら異なるホスト間の通信の仕組みについてみていきます。
前回のサーバソフトはTCPプロトコルでしたので、今回のクライアントソフトもTCPプロトコルを使います。
クライアントプログラムもサーバプログラムも、やりとりするデータの構造や送受信の方法など似た部分はあるものの、異なるところがいくつかありますので、その部分に気をつけながら見ていきます。
###実行環境
今回はクライアントソフトなのでMac OS X上で作成し、実行しました。
Mac OS X 10.10.5です。コンパイラはgccです。Xcodeで導入しました。
ヘッダファイルのパスは私の環境では/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
という深い階層に格納されておりました。
##ソースコード
#include <stdio.h> //printf(), fprintf(), perror()
#include <sys/socket.h> //socket(), connect(), recv()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset()
#include <unistd.h> //close()
#define MSGSIZE 32
#define MAX_MSGSIZE 1024
#define BUFSIZE (MSGSIZE + 1)
int main(int argc, char* argv[]) {
int sock; //local socket descripter
struct sockaddr_in servSockAddr; //server internet socket address
unsigned short servPort; //server port number
char recvBuffer[BUFSIZE]; //receive temporary buffer
int byteRcvd, totalBytesRcvd; //received buffer size
if (argc != 3) {
fprintf(stderr, "argument count mismatch error.\n");
exit(EXIT_FAILURE);
}
memset(&servSockAddr, 0, sizeof(servSockAddr));
servSockAddr.sin_family = AF_INET;
if (inet_aton(argv[1], &servSockAddr.sin_addr) == 0) {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
if ((servPort = (unsigned short) atoi(argv[2])) == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
servSockAddr.sin_port = htons(servPort);
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
if (connect(sock, (struct sockaddr*) &servSockAddr, sizeof(servSockAddr)) < 0) {
perror("connect() failed.");
exit(EXIT_FAILURE);
}
printf("connect to %s\n", inet_ntoa(servSockAddr.sin_addr));
totalBytesRcvd = 0;
while (totalBytesRcvd < MAX_MSGSIZE) {
if ((byteRcvd = recv(sock, recvBuffer, MSGSIZE, 0)) > 0) {
recvBuffer[byteRcvd] = '\0';
printf("%s", recvBuffer);
totalBytesRcvd += byteRcvd;
} else if(byteRcvd == 0){
perror("ERR_EMPTY_RESPONSE");
exit(EXIT_FAILURE);
} else {
perror("recv() failed.");
exit(EXIT_FAILURE);
}
}
printf("\n");
close(sock);
return EXIT_SUCCESS;
}
###1〜6行目
必要なヘッダを読み込んでいます。サーバの時と同じものを読み込むだけで済みました。
右にコメントで何を使うために読み込んでいるか記述しています。
そこから更にincludeして使えるようになっているものもありますが、そこは省略します。
###8〜9行目
今回は不要ではあるのですが、次回あたりから必要になってくるので記号定数を定義しておきます。今後、記号定数は増えていくと思います。
###14〜18行目
必要なデータ型のメモリ領域を確保します。
ネットワークプログラミングでのデータ型についての定義は第1回で記述しましたので、そちらもあわせてご参照下さい。
###20〜23行目
引数のチェックです。
実行時に接続先ホストのIPアドレスをドット10進数表記(IPv4)の文字列で、ポート番号を任意の数字で指定できるようにしてあります。
サーバ編の場合はローカルホストに対してポート番号を紐付けるために指定していましたが、クライアント編の場合は、接続先となるリモートホストのIPアドレスとポート番号を指定します。
#25行目
サーバ編の時と同じように、sockaddr_in構造体の領域をゼロクリアしておきます。
サーバ編の時と違うのはこのsockaddr_in構造体には接続先ホストのアドレス情報が格納されるということです。
今回はローカルホスト用にsockaddr_in構造体を用意することはしません。
#26行目
サーバ編の時と同じように、sockaddr_in構造体のsin_familyにはインターネットアドレスファミリ(IPv4)であることを示すAF_INETを指定します。
#29行目から31行目
inet_aton関数を使い、ドット10進数表記(IPv4)の文字列をネットワークバイトオーダーのバイナリ表現に変換してsin_addrのフィールドに格納します。
サーバ編の時とは違い、ポインタを渡して格納していますが、それは単にそういう手法をとったというだけで、いくつか方法はあります。
初心者向けのサンプルではドット10進数表記(IPv4)の文字列を、バイナリ表現値に変換をかけて戻り値として返すinet_addr関数を用いてたりしますが、inet_addr関数ではエラー時の戻り値が-1(255.255.255.255)という有効なIPアドレスを指してしまい、エラー値としてはどうかなというところがあるので、今回はinet_aton関数を用いました。
#34行目から38行目
サーバ編と同じく引数のポート番号の数字をネットワークバイトオーダーのバイナリ表現に変換して、sockaddr_in構造体のsin_portに格納します。
#40行目から43行目
システムコールsocket()を用いて、ソケットの作成を依頼します。
各引数についてはサーバ編の時と同じですので、そちらをご確認下さい。
#45行目から48行目
ここがサーバ編のプログラムと最も大きく違うところです。
クライアントではconnect()システムコールを呼び出し、サーバプログラムとの接続を確立します。
第1引数にさきほど作成したソケットを識別するソケットディスクリプタを、第2引数にサーバのIPアドレスとポート番号を含むsockaddr_in構造体を、第3引数にそのサイズを指定します。
サーバ編のプログラムと同じですが、ソケットAPIは汎用的なAPIですのでsockaddr_in構造体のポインタは汎用的なデータ型であるsockaddr構造体のポインタにキャストされています。
ここで一つ疑問が浮かんだ方がいるかもしれません。
サーバ編の時と同じようにbind()システムコールを用いてローカルホストのソケットとIPアドレスとポート番号を結びつける必要はないのか?と。
その通りなんです。
接続を開始する立場であるクライアントであっても通信を行うためにはソケットにアドレス情報が結びついている必要があります。
実はクライアントがconnect()システムコールを呼び出すと、ソケット構造体には接続先のアドレス情報とともに、ローカルホストのローカルIPアドレスと開いているポート番号の値が自動的に設定されます。
connect()の前に、サーバと同じようにbind()システムコールを呼び出せば、明示的にローカルアドレスの情報をソケットに結びつけることができます。ただ、通常はこの指定は必要ないと言えます。
何故なら、クライアントはサーバとの接続を開始するにあたって、サーバのアドレス情報をあらかじめ知っておく必要があるのに対して、サーバはクライアントのアドレス情報をあらかじめ知っている必要はないからです。
connect()が正常に終わり制御が復帰すると、3ウェイハンドシェイクが無事完了したことになりますので、サーバ編の時にも利用したnetstat
を使って、状態を確認することができます。
サーバ編で作成したプログラムを走らせ(ただclose()を呼び出さないように変更する)、クライアント環境で./a.out xxx.xxx.xxx.xxx 8080
(xxx.xxx.xxx.xxxはサーバのIPアドレス)を実行し、別の端末でnetstat -t
を実行するとtcp4 0 0 192.168.xxx.xxx.65486 xxx.xxx.xxx.xxx.8080 ESTABLISHED
のようなメッセージの表示が確認できます。
65486というポート番号はconnect()時に、OSによって開いているポートから自動的に割り当てられた、クライアントホストのポート番号です。
#50行目
connect()を用いて無事接続が確立したら、サーバと接続が確立したことを意味するので、それを示すメッセージを表示しています。
#52行目から66行目
次回あたりのコードを少し先行して書いてしまっていますが、今注目すべきなのは54行目の(byteRcvd = recv(sock, recvBuffer, MSGSIZE, 0)
と58行目のbyteRcvd == 0
の箇所です。
recv()は受信バッファキューに溜まっているバイト列をユーザープロセスに取り込むシステムコールです。受信バッファはnetstat
ではRecv-Qとして確認することができます。
recvBufferは受信したバイト列が格納されるメモリ領域の先頭アドレスを意味し、MSGSIZEは取得するサイズを指定します。通常、受信バイト列にはNULL文字は含まれていないはずなので文字列出力関数などを使用する場合はNULL文字を取得バイト列の末尾に追加します。
第4引数に0を指定していますが、これはrecvの動作を変更するためのフラグです。0は受信可能になるまでプログラムの動作をブロックするというデフォルトの挙動を意味します。
返り値は受信したバイト数ですが、これが0の場合は、通信先のプログラムがTCPのコネクションを切断したことを意味します。
前回のサーバ編のプログラムを確認してほしいのですが、あのプログラムはacceptしてクライアントとサーバ間でデータが送受信可能になった後に、すぐに接続を切断しています。
ですので、今回のプログラムと前回のプログラムをクライアントとサーバでそれぞれ実行すると、クライアント側はERR_EMPTY_RESPONSE
というメッセージを表示して終了します。
このメッセージは私が前回、Chromeをクライアントソフトとして利用した際、サーバから接続が切断された時に表示されたエラーメッセージにちなんでいます。
今回はTCPクライアントソフトの作成を行い、前回のサーバプログラムと通信可能な状態にすることができました。
いったりきたりで申し訳ないですが、次回はサーバのソフトを拡張し、クライアントに対して意味のあるメッセージの送信、そしてトータルのボリューム次第ですがそれを受信するクライアントソフトの拡張を行いたいと思います。
###参考書籍
- TCP/IPによるネットワーク構築〈Vol.1〉原理・プロトコル・アーキテクチャ
- 基礎からわかるTCP/IP ネットワーク実験プログラミング―Linux/FreeBSD対応
- TCP/IPソケットプログラミング C言語編
- 詳解 Linuxカーネル 第3版
###参考書籍