本文参考自徐晓鑫《后台开发》,记录之。
一、为什么要使用非阻塞I/O之select
初学socket的人可能不爱用select写程序,而习惯诸如connect、accept、recv/recvfrom这样的阻塞程序。
当让服务器同时为多个客户端提供一问一答服务时,很多程序员采用多线程/进程模型来解决。但是若同时响应成百上千的连接请求,无论是多进程还是多线程都会严重占据系统资源降低系统对外响应的效率。(“线程池”旨在降低创建和销毁线程的频率,“连接池”旨在尽量重用已有连接,二者都需要考虑面临的响应规模,即池的大小是有限的)。
高级程序员使用select就可以完成非阻塞方式工作的程序,它能够监视被监测文件描述符的变化情况。
使用select的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多CPU资源,同时能为多客户端提供服务。当然select也有缺点如下:
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
select支持的文件描述符数量太小了,默认是1024
后面学习poll、epoll就是解决这个问题的,这个后面会了解到。
二、slect函数原型
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
具体解释select的参数:
maxfdp是一个整数值,集合中所有文件描述符的范围,即所有文件描述符的最大值加1。
fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0;若发生错误返回负值。
fd_set *writefds是指向fd_set结构的指针,主要关心文件的写变化,即是否可写。
fd_set *errorfds用来监视文件错误异常
返回值:
正值表示准备就绪的描述符数, 0表示等待超时,负值表示select出错
三、使用select函数循环读取键盘输入
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int keyboard;
int ret,i;
char c;
fd_set readfd;
struct timeval timeout;
keyboard = open("/dev/tty",O_RDONLY |O_NONBLOCK);
assert(keyboard>0);
while(1)
{
timeout.tv_sec = 5;
timeout.tv_usec = 0;
FD_ZERO(&readfd);
FD_SET(keyboard,&readfd);
ret = select(keyboard+1,&readfd,NULL,NULL,&timeout);
if(ret == -1)
perror("select error\n");
else if (ret) {
if(FD_ISSET(keyboard,&readfd)) {
i = read(keyboard,&c,1);
if('\n'== c)
continue;
printf("The input is %c\n",c);
if('q'==c)
break;
}
}
else if (ret ==0)
printf("time out\n");
}
return 0;
}
只要发现键盘输入字符,程序就输出对应字符。若超过5s不输入,打印time out。
四、使用select函数提高服务器处理能力
服务器端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEFAULT_PORT 6666
int main( int argc, char ** argv){
int serverfd,acceptfd; /* 监听socket: serverfd,数据传输socket: acceptfd */
struct sockaddr_in my_addr; /* 本机地址信息 */
struct sockaddr_in their_addr; /* 客户地址信息 */
unsigned int sin_size, myport=6666, lisnum=10;
if ((serverfd = socket(AF_INET , SOCK_STREAM, 0)) == -1) {
perror("socket" );
return -1;
}
printf("socket ok \n");
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(DEFAULT_PORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero), 0);
if (bind(serverfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr )) == -1) {
perror("bind" );
return -2;
}
printf("bind ok \n");
if (listen(serverfd, lisnum) == -1) {
perror("listen" );
return -3;
}
printf("listen ok \n");
fd_set client_fdset; /*监控文件描述符集合*/
int maxsock; /*监控文件描述符中最大的文件号*/
struct timeval tv; /*超时返回时间*/
int client_sockfd[5]; /*存放活动的sockfd*/
bzero((void*)client_sockfd,sizeof(client_sockfd));
int conn_amount = 0; /*用来记录描述符数量*/
maxsock = serverfd;
char buffer[1024];
int ret=0;
/*不断的查看是否有新的client连接;已连接的client是否有发送消息过来*/
while(1){
/*初始化文件描述符号到集合*/
FD_ZERO(&client_fdset);
/*加入服务器描述符*/
FD_SET(serverfd,&client_fdset);
/*设置超时时间*/
tv.tv_sec = 30; /*30秒*/
tv.tv_usec = 0;
/*把活动的句柄加入到文件描述符中*/
for(int i = 0; i < 5; ++i){
/*程序中Listen中参数设为5,故i必须小于5*/
if(client_sockfd[i] != 0){
FD_SET(client_sockfd[i], &client_fdset);
}
}
/*printf("put sockfd in fdset!\n");*/
/*select函数,根据返回值判断程序是否有异常*/
ret = select(maxsock+1, &client_fdset, NULL, NULL, &tv);
if(ret < 0){
perror("select error!\n");
break;
} else if(ret == 0){
printf("timeout!\n");
continue;
}
/*轮询各个(已连接上的client的)文件描述符有无可读(接收)数据,有就输出,没有或者异常时,关闭相应的client连接,并在集合里清理掉*/
for(int i = 0; i < conn_amount; ++i){
/*FD_ISSET检查client_sockfd是否可读写,>0可读写*/
if(FD_ISSET(client_sockfd[i], &client_fdset)){
printf("start recv from client[%d]:\n",i);
ret = recv(client_sockfd[i], buffer, 1024, 0);
if(ret <= 0){
printf("client[%d] close\n", i);
close(client_sockfd[i]);
FD_CLR(client_sockfd[i], &client_fdset);
client_sockfd[i] = 0;
}
else{
printf("recv from client[%d] :%s\n", i, buffer);
}
}
}
/*检查是否有新的连接,如果有,接收连接加入到client_sockfd中*/
if(FD_ISSET(serverfd, &client_fdset))
{
/*接受连接*/
struct sockaddr_in client_addr;
size_t size = sizeof(struct sockaddr_in);
int sock_client = accept(serverfd, (struct sockaddr*)(&client_addr), (unsigned int*)(&size));
if(sock_client < 0){
perror("accept error!\n");
continue;
}
/*把连接加入到文件描述符集合中*/
if(conn_amount < 5)
{
client_sockfd[conn_amount++] = sock_client;
bzero(buffer,1024);
strcpy(buffer, "this is server! welcome!\n");
send(sock_client, buffer, 1024, 0);
printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
bzero(buffer,sizeof(buffer));
ret = recv(sock_client, buffer, 1024, 0);
if(ret < 0){
perror("recv error!\n");
close(serverfd);
return -1;
}
printf("recv : %s\n",buffer);
//更新maxsock,因为下一次进入while循环调用时,需要传当前最大的fd值+1给select函数
if(sock_client > maxsock){
maxsock = sock_client;
}
else{
printf("max connections!!!quit!!\n");
break;
}
}
}
}
//最后,把已连接上的clent的fd和server自身的fd都关闭
for(int i = 0; i < 5; ++i){
if(client_sockfd[i] != 0){
close(client_sockfd[i]);
}
}
close(serverfd);
return 0;
}
客户端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEFAULT_PORT 6666
int main( int argc, char * argv[]){
int connfd = 0;
int cLen = 0;
struct sockaddr_in client;
if(argc < 2){
printf(" Uasge: clientent [server IP address]\n");
return -1;
}
client.sin_family = AF_INET;
client.sin_port = htons(DEFAULT_PORT);
client.sin_addr.s_addr = inet_addr(argv[1]);
connfd = socket(AF_INET, SOCK_STREAM, 0);
if(connfd < 0){
perror("socket" );
return -1;
}
if(connect(connfd, (struct sockaddr*)&client, sizeof(client)) < 0){
perror("connect" );
return -1;
}
char buffer[1024];
bzero(buffer,sizeof(buffer));
recv(connfd, buffer, 1024, 0);
printf("recv : %s\n", buffer);
bzero(buffer,sizeof(buffer));
strcpy(buffer,"this is client!\n");
send(connfd, buffer, 1024, 0);
while(1){
bzero(buffer,sizeof(buffer));
scanf("%s",buffer);
int p = strlen(buffer);
buffer[p] = '\0';
send(connfd, buffer, 1024, 0);
printf("i have send buffer\n");
}
close(connfd);
return 0;
}
验证:
客户端1:
客户端2: