接続待機状態に入る
前の章では、ソケットの作成方法と、IP アドレスとポートをソケットにバインドする方法を学習しました。次に、listen 関数を呼び出して接続待機状態に入る必要があります。listen 関数を呼び出した後でのみ、クライアントは connect 関数を呼び出すことができます。
#include<sys/socket.h>
int listen (int sock,int backlog);//成功时返回0,失败时返回-1
sock //希望进入等待连接请求状态的套接字文件描述符,传递的套接字参数称为服务器端监听套接字
backlog //连接请求等待队列的长度
まず、上記の名詞について説明しましょう。 1 つ目は、サーバー側のリスニング ソケットです。実際、これは簡単に理解できます。クライアントからの接続要求も、ネットワークから送られてくるデータの一種です。次のデータ送信の前に、この種のネットワーク データを受け入れるためのソケットが常に最初に用意されています。待機キューの長さについて説明すると、待機キューとはクライアントリクエストのキューであり、長さはこのキューに入れられるクライアントリクエストの最大数です。
クライアントがサーバーに「接続を開始してもいいですか?」と尋ねると、サーバー側のソケットは親切に答えます:「こんにちは! もちろんですが、システムがビジーです。待合室に行って番号を待ってください。準備ができたらすぐに接続が受け入れられます。」同時に、接続要求を待合室に送信してください。このゲートキーパー (サーバー側ソケット) は listen 関数を呼び出すことで生成でき、listen 関数の 2 番目のパラメーターで待機室のサイズが決まります。この待機室を接続要求待ちキューと呼び、サーバ側ソケットと接続要求待ちキューの準備が整い、接続要求を受け付けられる状態を接続要求待ち状態と呼びます。
クライアント接続リクエストを受け入れる
リクエストを受け入れた後、データ交換の状態に入ることができます。では、実際のデータ交換は何ですか? それはソケットではないでしょうか? そうすると、次のクライアント要求の受け入れも担当するため、データ交換をリッスンするソケット自体であってはなりません。この時点で別のソケットが必要になり、次の関数が独自にソケットを作成し、リクエストを開始したクライアントに接続します。
#include<sys/socket.h>
int accept(int sock,struct sockaddr* addr,socketlen_t *addrlen);
//成功时返回创建的套接字文件描述符,失败时返回-1
sock //服务器端监听套接字的文件描述符
addr //保存发起请求的客户端地址信息的变量地址值
addrlen //保存客户端地址长度,即第二个变量的长度
関数呼び出しが成功すると、accept 関数はデータ I/O 用のソケットを生成し、そのファイル記述子を返します。
TCP クライアントのデフォルトの関数呼び出し順序
サーバー側と比較すると、「接続要求」が異なります。これは、クライアントソケットの作成後にサーバー側に対して開始される接続要求です。これは、次の関数を呼び出すことで実行されます。
#include<sys/socket.h>
int connect (int sock,struct socketaddr* servaddr,socketlen_t addrlen);
//成功时返回0,失败时返回-1
sock //客户端套接字文件描述符
servaddr //保存目标服务器地址信息的变量地址值
addrlen //第二个结构体参数servadddr的变量地址长度(以字节为单位)
上記の関数は、次の条件のいずれかが発生する (関数呼び出しが完了する) まで戻りません。
ケース 1: サーバー側が接続要求を受け入れる
状況2:ネットワーク切断などの異常により接続要求が中断された場合
反復的なサーバー/クライアントを実装する
サーバー側/クライアント側のコードを実装する前は、接続の完了後に直接切断されました。この動作は明らかに不合理です。サーバーとして、なぜ 1 回だけ接続する必要があるのでしょうか? 次に実装する反復サーバー/クライアントは、実際には待機キュー内のクライアントを繰り返し受け入れているだけです。現時点で判明しているのは、サーバーは一度に 1 つのクライアントのみにサービスを提供できるということです。マルチプロセスとマルチスレッドについては次の章で説明します。
エコーサーバー/クライアントを反復処理する
プログラムの基本的な動作は次のとおりです。
□サーバーは同時に 1 つのクライアントにのみ接続し、エコー サービスを提供します。
□ サーバーは 5 つのクライアントに順番にサービスを提供し、終了します。
□クライアントは、ユーザーが入力した文字列を受信し、サーバーに送信します。
□サーバーは受信した文字列データ、つまり「エコー」をクライアントに送り返します。
□サーバーとクライアント間の文字列エコーは、クライアントが Q に入るまで実行されます。
以下はサーバー側のコードです
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#defind BUF_SIZE 1024
void error_handling(char*message);
int main(int argc,char* argv[]){
int serv_sock,clnt_sock;
char message[BUF_SIZE];
int str_len,i;
struct socketaddr_in serv_addr,clnt_addr;
socklen_t clnt_addr_sz;
if(argc!=2){
printf("Usage : %s <port>\n",argv[0]);
exit(1);
}
serv_sock=socket(PF_INET,SOCE_STREAM.0);
if(serv_sock==-1)error_handling("socket() argv[0]");
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
error_handling("bind() error");
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
clnt_addr_sz=sizeof(clnt_addr);
for(int i=0;i<5;++i){
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Conneted client %d \n",i+1);
while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)
write(clnt_sock,message,str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
voie error_handling(char*message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
クライアントコードをもう一度見てください
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#defind BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char*argv[]){
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_addr;
if(argc!=3){
printf("Usage : %s <IP> <port>\n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
error_handling("connect() error!");
else
puts("Connected.........");
while(1){
fputs("Input message(Q to quit):",stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"o\n"))
break;
write(sock,message,strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
close(sock);
}
return 0;
}
void error_handling(char *message)
fputs(message, stderr);
fputc('\n',stderr);
exit(1);
}
エコークライアントの問題
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len]=0;
printf("Message from server: %s",message);
上記のコードには、「読み取り関数と書き込み関数が呼び出されるたびに、実際の I/O 操作は文字列単位で実行される」という誤った仮定があります。もちろん、書き込み関数の呼び出しごとに文字列が渡されるため、この仮定はある程度合理的です
。しかし、第 2 章の「TCP にはデータ境界がない」という内容をまだ覚えていますか? また、サーバー側で次の状況を考慮する必要があります。「文字列が長すぎるため、2 つのパケットで送信する必要があります。」
サーバーは write 関数を 1 回呼び出してデータを転送したいと考えていますが、データが大きすぎる場合、オペレーティング システムはデータを複数のデータ パケットに分割してクライアントに送信することがあります。さらに、このプロセス中に、クライアントはすべてのデータ パケットを受信する前に読み取り関数を呼び出す場合があります。
「しかし、上記の例は機能しませんか?」
もちろん、エコー サーバー/クライアントは正しい結果を返します。しかし、それはただの幸運です!送受信するデータが小さく、動作環境が同じコンピュータまたは隣接する 2 台のコンピュータであるだけではエラーは発生しませんが、実際にはエラーが発生する可能性があります。
Windows ベースの実装
Windowsベースのエコーサーバー側
Linux プラットフォームの例を Windows プラットフォームの例に変換するには、次の 4 つの点に留意する必要があります。
□WSAStartup および WSACleanup 関数を通じてソケット関連ライブラリを初期化およびクリアします。
□ データ型と変数名を Windows スタイルに切り替えます。
□ データ送信では、read および write 関数の代わりに、recv および send 関数を使用します。
□ ソケットを閉じるときは、close 関数の代わりに closesocket 関数を使用します。