Unix网络篇(2)使用select复用I/O

上一篇中使用fork来设计并发服务器。在本篇中,将介绍一种I/O复用手法,它就是select

1 服务端

1.1 fd_set集合与select阻塞

I/O复用的思想比较简单,就是将所有sockfd全部放在fd_set中统一管理。然后由一个叫select的函数监听事件发生情况。一旦fd_set中任何sockfd收到消息(被触发),都会解除select的阻塞状态。

此时,使用FD_ISSET来检查,到底是哪个sockfd收到了消息。

注意:select会破坏fd_set的值。每轮循环完成之后,一定要重新设置fd_set的值。

1.2 区分listenfdconnfd

一般服务端就两种sockfd。一种listenfd用来处理客户端的连接,另一种connfd用来与客户端通信。

1.3 maxfdFD_SETSIZE

FD_SETSIZE是一个定义在_fd_setsize.h中的宏,它的值为1024(Mac OS X)。fd_set虽然名字看起来像是集合。事实上,它就是一个int fd_set[1024]select第一个参数为maxfd,由它来指定fd_set中存放的sockfd的有效长度。举个例子,maxfd=10那就表示int fd_set[1024]数组的前10个是有效的。

fd_set的声明:

typedef	struct fd_set {
__int32_t fds_bits[__DARWIN_howmany(__DARWIN_FD_SETSIZE, __DARWIN_NFDBITS)];
} fd_set;

select的声明:

int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, struct timeval* tineout)

1.4 以下是select_server.c的源代码

#include <sys/socket.h> 
#include <netinet/in.h> //sockaddr_in
#include <unistd.h> //fork,read,write
#include <stdlib.h> //exit(0)
#include <errno.h>
#include <string.h> //bzero
#include <stdio.h>

const int SERV_PORT = 9527;
const int LISTENQ = 5;
const int MAXBUFSIZE = 1024;

int main(int argc, char* argv[]) {
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE]; //FD_SETSIZE被定义在_fd_setsize.h中,默认值为1024 (MaC OS X)
ssize_t n;
fd_set rset, allset;
char buf[MAXBUFSIZE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;

listenfd = socket(AF_INET, SOCK_STREAM, 0);

servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

if ( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0 )
exit_msg("bind error");

listen(listenfd, LISTENQ);

maxfd = listenfd;
maxi = -1;
for (i = 0; i < FD_SETSIZE; ++i)
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ; ) {
rset = allset; //每次务必重置
nready = select(maxfd+1, &rset, NULL, NULL, NULL);

if (FD_ISSET(listenfd, &rset)) { //a new client connection
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);

for (i = 0; i < FD_SETSIZE; ++i) {
if (client[i] < 0) {
client[i] = connfd;
break;
}
}
if (i == FD_SETSIZE)
exit_msg("too many clients");
FD_SET(connfd, &allset);
if (connfd > maxfd) maxfd = connfd;
if (i > maxi) maxi = i;
if (--nready <= 0) continue;
}
for (i = 0; i <= maxi; ++i) {
if ( (sockfd=client[i]) < 0 )
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = read(sockfd, buf, MAXBUFSIZE)) == 0 ) {
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
}
else {
writen(sockfd, buf, n); //DIY : 参见《Unix网络篇(1)一个典型的TCP Socket通信例子》
}
if (--nready <= 0)
break;
}
}
}

return 0;
}

2 客户端

客户端只是将上一篇中的str_cli函数稍加修改了一下,writen函数(后面多加了个n)是一个循环写函数避免缓冲区达到上限,导致write失败问题。

本来还想介绍一下,pselect的使用技巧,但是觉得也没有太大必要了。如果有需要用到更精确timeout(纳秒级别),或者是需要程序忽略某些信号,可以参考本文底部的参考文献。

以下是select_client.c源代码

#include <sys/socket.h> 
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //inet_pon
#include <unistd.h> //fork,read,write
#include <stdlib.h> //exit(0)
#include <errno.h>
#include <string.h> //bzero
#include <stdio.h>

const int SERV_PORT = 9527;
const int LISTENQ = 5;
const int MAXBUFSIZE = 1024;

void str_cli(FILE* fp, int sockfd) {
fd_set rset;
char buf[MAXBUFSIZE];
int maxfdp1, n, stdineof = 0;

FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);

if (FD_ISSET(sockfd, &rset)) {
if ( (n = read(sockfd, buf, MAXBUFSIZE)) == 0 ) {
if (stdineof == 1)
return;
else
exit_msg("str_cli:server terminated prematurely");
}
write(fileno(stdout), buf, n); //stdout使用write即可,不必writen
}
if (FD_ISSET(fileno(fp), &rset)) {
if ( (n = read(fileno(fp), buf, MAXBUFSIZE)) == 0 ) {
stdineof = 1;
shutdown(sockfd, SHUT_WR); //FIN
FD_CLR(fileno(fp), &rset);
continue;
}
writen(sockfd, buf, n); //DIY: 参见《Unix网络篇(1)一个典型的TCP Socket通信例子》
}
}
}

int main(int argc, char* argv[]) {
int sockfd;
struct sockaddr_in servaddr;

if (argc != 2)
exit_msg("argv != 2");

sockfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
exit_msg("conn err");//警告: connect请勿捕获EINTR

str_cli(stdin, sockfd);

return 0;
}

References: [1] W.Richard Stevens,《Unix网络编程 卷1:套接字联网API(第3版)》 [2] mumututu's growing,pselect 和 select