目次
4 -- send() 関数とrecv() 関数の共通オプション
1 -- I/O 多重化に基づくサーバー
マルチプロセス サーバーには次のような欠点があります: 複数のクライアントが接続要求を開始すると、クライアントの要求をそれぞれ処理するために複数のプロセスが作成されます。複数のプロセスを作成すると、多くの場合、莫大なコストがかかります。
サーバー側の I/O 多重化によりプロセス数を削減できるため、クライアントがいくつ接続されても、サービスを提供するプロセスは 1 つだけです。
2--select() 関数
select()関数は、複数のファイル記述子をまとめて統合監視することができます。監視ファイル記述子は、監視ソケットとみなすことができます。複数のファイル記述子を収集する場合、受信、送信、例外の3 つの状況に従って区別する必要があります。
select() は、配列変数 fd_set を介して監視操作を実行しますが、fd_set 内のファイル記述子 (インデックス) に対応するビット (値) が 1 に設定されている場合、そのファイル記述子が監視対象であることを示します。
// 对 fd_set 数组的常用操作
FD_ZERO(fd_set* fd_set); // 将 fd_set 变量的所有位初始化为0
FD_SET(int fd, fd_set* fdset); // 在参数 fdset 指向的变量中注册文件描述符fd的信息
FD_CLR(int fd, fd_set* fdset); // 从参数 fdset 指向的变量中消除文件描述符fd的信息
FD_ISSET(int fd, fd_set* fdset); // 若参数 fdset 指向的变量中包含文件描述符fd的信息,则返回true
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
// 成功时返回大于 0 的值,值为发生事件的文件描述符数;失败时返回 -1;超时返回 0
// maxfd 表示监视对象文件描述符的数量
// readset 表示将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
// writeset 表示将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
// exceptset 表示将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值
// timeout 表示调用 select() 函数后,为防止陷入无限阻塞的状态,传递超时信息
select()関数は、監視対象のファイルディスクリプタが変化した場合のみリターンします。変化がない場合はブロッキング状態になります。タイムアウトイベントを指定することで、無限ブロッキングを防ぐことができます。ファイルディスクリプタが変化しなくても、タイムアウトが超過 イベントを指定すると関数から返され、戻り値は 0 になります。
select() 関数が呼び出されると、変更されたファイル記述子を除いて、元の値 1 を持つすべてのビットが 0 に変更されるため、値が 1 のままのファイル記述子が変更された ことがわかります。
// gcc select.c -o select
// ./select
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char* argv[]){
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads); // 初始化fd_set变量
FD_SET(0, &reads); // 将文件描述符 0 对应的位设置为1,表示监视标准输入(文件描述符0对应标准输入stdin)
while(1){
temps = reads; // 记录初始值,新循环时重新初始化为初始值
timeout.tv_sec = 5; // 超时时间设置为 5s
timeout.tv_usec = 0;
result = select(1, &temps, 0, 0, &timeout); // 5s内监视是否有标准输入时间发生
if(result == -1){
puts("select() error!");
break;
}
else if(result == 0){ // 返回值为0表示超时
puts("Time-out!");
}
else{
if(FD_ISSET(0, &temps)){ // 验证是否是标准输入发生了变化,打印标准输入的内容
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0;
printf("message from console: %s", buf);
}
}
}
return 0;
}
3 -- I/O 多重化に基づくエコー サーバー
// gcc echo_selectserv.c -o echo_selectserv
// ./echo_selectserv 9190
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(char *buf){
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if(argc != 2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1){
error_handling("bind() error");
}
if(listen(serv_sock, 5) == -1){
error_handling("listen() error");
}
FD_ZERO(&reads); // 初始化fd_set变量
FD_SET(serv_sock, &reads); // 监视 serv_sock
fd_max = serv_sock;
while(1){
cpy_reads = reads; // 记录初始值
timeout.tv_sec = 5; // 设置超时时间
timeout.tv_usec = 5000;
if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1){
break;
}
if(fd_num = 0) continue; // 判断是否是超时
// 真的有事件发生,执行以下代码
for(i = 0; i < fd_max + 1; i++){
if(FD_ISSET(i, &cpy_reads)){ // 查找发生状态变化的文件描述符
if(i == serv_sock){ // 服务器端socket有变化,执行受理连接请求
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads); // 将客户端socket注册到fd_set变量中
if(fd_max < clnt_sock){
fd_max = clnt_sock;
}
printf("connected client: %d \n", clnt_sock);
}
else{ // 不是服务器端socket发生变化,表明有要接收的数据
str_len = read(i, buf, BUF_SIZE);
if(str_len == 0){ // 接收的是 EOF,表明要断开连接
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else{ // 接收的是真实数据
write(i, buf, str_len); // 将接收到的数据返回客户端,实现回声功能
}
}
}
}
}
close(serv_sock);
return 0;
}
4 -- send() 関数とrecv() 関数の共通オプション
#include <sys/socket.h>
ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);
// flags 表示可选项信息
通常、send() と recv() のオプションのパラメータを 0 に設定しますが、実際には次のオプションがあります。
① MSG_OOB は帯域外データの送信 (送信、受信) に使用されることを意味します。
②MSG_PEEKは入力バッファ(recv)に受信データがあるか確認することを意味します。
③ MSG_DONTROUTE は、データ送信中にルーティングテーブルが参照されず、宛先(送信)がローカルネットワーク内にあることを意味します。
④ MSG_DONTWAIT は、I/O 関数呼び出し時にブロックしないことを意味し、ノンブロッキング I/O (send、recv) を使用するために使用されます。
⑤ MSG_WAITALL は、要求されたバイトがすべて受信されるまで関数が返らないようにすることを意味します (recv)
MSG_OOB は帯域外データの緊急メッセージの送信に使用され、オペレーティング システムが緊急メッセージを受信すると、SIGURG 信号を生成し、登録された信号処理関数を呼び出します。
// gcc oob_recv.c -o oob_recv
// ./oob_recv 9190
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define BUF_SIZE 30
int acpt_sock;
int recv_sock;
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void urg_handler(int signo){
int str_len;
char buf[BUF_SIZE];
str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB);
buf[str_len] = 0;
printf("Urgent message: %s \n", buf);
}
int main(int argc, char* argv[]){
struct sockaddr_in recv_adr, serv_adr;
int str_len, state;
socklen_t serv_adr_sz;
struct sigaction act;
char buf[BUF_SIZE];
if(argc != 2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = urg_handler; //设置信号的处理函数
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family = AF_INET;
recv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
recv_adr.sin_port = htons(atoi(argv[1]));
if(bind(acpt_sock, (struct sockaddr*) &recv_adr, sizeof(recv_adr)) == -1){
error_handling("bind() error");
}
listen(acpt_sock, 5);
serv_adr_sz = sizeof(serv_adr);
recv_sock = accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);
// getpid() 返回进程ID
// recv_sock发生SIGUGR信号,需要有确定的处理进程(假设创建了多个进程)来调用信号处理函数
// fcntl() 将 getpid() 返回的进程作为 SIGUGR 信号的处理进程
fcntl(recv_sock, F_SETOWN, getpid());
state = sigaction(SIGURG, &act, 0); // 发生SIGURG信号时,调用urg_handler()函数
while((str_len = recv(recv_sock, buf, sizeof(buf)-1, 0)) != 0){
if(str_len == -1){
continue;
}
buf[str_len] = 0;
puts(buf);
}
close(recv_sock);
close(acpt_sock);
return 0;
}
// gcc oob_send.c -o oob_send
// ./oob_send 127.0.0.1 9190
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[]){
int sock;
struct sockaddr_in recv_adr;
if(argc != 3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family = AF_INET;
recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
recv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1){
error_handling("connect() error!");
}
write(sock, "123", strlen("123"));
send(sock, "4", strlen("4"), MSG_OOB); // 紧急传输数据
write(sock, "567", strlen("567"));
send(sock, "890", strlen("890"), MSG_OOB); // 紧急传输数据
close(sock);
return 0;
}
MSG_PEEK オプションと MSG_DONTWAIT オプションを同時に設定すると、入力バッファに受信データがあるかどうかを確認できますが、同時に MSG_PEEK オプションが設定されているため、recv 関数を呼び出したときに入力バッファ内のデータが入力バッファ内のデータを読み取っても削除されません(つまり、次回読み込むときにも、前回読み込んだデータを読み込むことができます)。
// gcc peek_recv.c -o peek_recv
// ./peek_recv 9190
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]){
int acpt_sock, recv_sock;
struct sockaddr_in acpt_adr, recv_adr;
int str_len, state;
socklen_t recv_adr_sz;
char buf[BUF_SIZE];
if(argc != 2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&acpt_adr, 0, sizeof(acpt_adr));
acpt_adr.sin_family = AF_INET;
acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
acpt_adr.sin_port = htons(atoi(argv[1]));
if(bind(acpt_sock, (struct sockaddr*) &acpt_adr, sizeof(acpt_adr)) == -1){
error_handling("bind() error");
}
listen(acpt_sock, 5);
recv_adr_sz = sizeof(recv_adr);
recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);
while(1){
// 设置 MSG_PEEK|MSG_DONTWAIT 选项,即使不存在待读取的数据,也不会进入阻塞状态
// 假设存在待读取的数据,则读取且不删除输入缓冲的数据,因此下次仍可以读取
str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK|MSG_DONTWAIT);
if(str_len > 0){
break;
}
}
buf[str_len] = 0;
printf("Buffering %d bytes: %s \n", str_len, buf);
// 读取上一次留在输入缓冲的数据
str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
buf[str_len] = 0;
printf("Read again: %s \n", buf);
close(acpt_sock);
close(recv_sock);
return 0;
}
// gcc peek_send.c -o peek_send
// ./peek_send 127.0.0.1 9190
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[]){
int sock;
struct sockaddr_in recv_adr;
if(argc != 3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family = AF_INET;
recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
recv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1){
error_handling("connect() error!");
}
write(sock, "123", strlen("123"));
close(sock);
return 0;
}
5 -- readv() および writev() 関数
readv() 関数と writev() 関数は、データを読み取って送信する前にデータを統合します。つまり、writev() 関数は複数のバッファに散在するデータをまとめて送信でき、readv() 関数は複数のバッファからのデータをまとめて送信できます。 . 各バッファは個別に受信されます。
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
// 成功时返回发送的字节数,失败时返回 -1
// filedes 表示文件描述符
// iov 表示 iovec 结构体数组的地址值
// iovcnt 表示向第二个参数传递的数组长度
ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);
// 成功时返回接收的字节数,失败时返回 -1
// iovec结构体
struct iovec{
void* iov_base; // 缓冲地址
size_t iov_len; // 缓冲大小
}
コード例:
// gcc writev.c -o write
// ./write
#include <stdio.h>
#include <sys/uio.h>
int main(int argc, char* argv[]){
struct iovec vec[2];
char buf1[] = "ABCDEFG";
char buf2[] = "1234567";
int str_len;
vec[0].iov_base = buf1;
vec[0].iov_len = 3;
vec[1].iov_base = buf2;
vec[1].iov_len = 4;
str_len = writev(1, vec, 2); // 向文件描述符1写数据,即向标准输出写数据
puts("");
printf("Write bytes: %d \n", str_len);
return 0;
}
// gcc readv.c -o readv
// ./readv
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100
int main(int argc, char *argv[]){
struct iovec vec[2];
char buf1[BUF_SIZE] = {0,};
char buf2[BUF_SIZE] = {0,};
int str_len;
vec[0].iov_base = buf1;
vec[0].iov_len = 5; // 设置最多保存5个字节
vec[1].iov_base = buf2;
vec[1].iov_len = BUF_SIZE;
str_len = readv(0, vec, 2); // 向标准输入(文件描述符0)读数据
printf("Read bytes: %d \n", str_len);
printf("First message: %s \n", buf1);
printf("Second message: %s \n", buf2);
return 0;
}