Linux下select的用法--实现一个简单的回射服务器程序
1、先看man手册
SYNOPSIS
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
// 关于第5个参数
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
2. 函数说明:可以同时监控多个文件描述符是否发生了读写或者异常。(有点像windows下的waitformultipleobjects,可以同时等待多个事件)
参数说明:
1)nfds:要监控的文件描述符的最大值加1,这个值不能错。
2)readfds:指向fd_set的指针。这是一个集合,专门用于监视读取数据的。所有需要监控读取数据的描述符都需要放进这个集合中。比如你需要监控4描述符的读取数据,就把4放进这个集合之中。
3)writefds:同上,这里是专门监视写的集合
4)exceptfds:同上,这里是专门监视异常的集合
5)timeout:超时。指向的timeval 结构体。
如果参数设为NULL,则select是阻塞的。
如果不为空,则表示超时时间(当结构体里面的成员都设为0时,表示不阻塞,立即返回)。
重要说明:
第2-4个参数是输入输出参数,select返回时会将哪个文件描述符放到这个集合中。
比如我们监控了fd=5的描述符的读取数据操作,当发生了读取操作时,select则会返回,通过第二个参数可以获取5发生了读操作。用FD_ISSET();实现。
返回值:
<0:表示出错
>0:等到了我们需要的事件
=0:表示超时了
3.fd_set说明
这是一个文件描述符的集合。
有几个宏可以对这个集合进行操作。
void FD_CLR(int fd, fd_set *set); // 将集合中指定的fd移除
int FD_ISSET(int fd, fd_set *set); // 判断fd是否在set集合中
void FD_SET(int fd, fd_set *set); // 将fd插入集合中
void FD_ZERO(fd_set *set); // 将集合里面的数据清空
4.实例1:这个例子用来监控标准输入:
#include<sys/select.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include<stdio.h> int fun() { int ret = 0, nready = 0; fd_set rset; FD_ZERO(&rset); // 清空操作 int fd_stdin = fileno(stdin); int maxfd = fd_stdin; char readbuf[1024] = {0}; struct timeval tv; tv.tv_sec = 10; tv.tv_usec = 0; while(1) { FD_SET(fd_stdin, &rset); // 需要重新设置,因为select每次返回会改变里面的值 //nready = select(maxfd+1, &rset, NULL, NULL, &tv);// 有超时 nready = select(maxfd+1, &rset, NULL, NULL, NULL); if(nready <= 0) { //printf(); break; } if(FD_ISSET(fd_stdin, &rset)) // rset也是输出参数,这里判断是否是fd_stdin发生了读操作 { int i = read(fd_stdin, readbuf, sizeof(readbuf)); //printf("recv STDIN read:%d\n", i); if(i > 0) { printf("STDIN buf:%s\n", readbuf); } memset(readbuf, 0, sizeof(readbuf)); } } printf("fun() ---, nready = %d\n", nready); } int main() { printf("stdin:%d, stdout:%d, stderr:%d\n", fileno(stdin), fileno(stdout), fileno(stderr)); // 这个是其他知识 fun(); }
5.实例2(回射服务器):
我实现了一个socket服务器和客户端程序。server可以直接在终端中发送数据给client。client可以在终端中显示,也可以用标准输入直接发送回去。
所以在client的程序中,用select同时监控server的socket和标准输入,当有任意一个发生读取数据时都进行处理。
server.c 接收client的连接,并且收到client的数据原封不动的发送回去。可以接收多个client的连接。能同时处理多个client发送的数据。
先看server.c:
#include<sys/types.h> #include<sys/socket.h> #include<sys/select.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<stdio.h> #include<string.h> #include<errno.h> #define CLIENTCOUNT 100 int main(int argc, char **argv) { int listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd < 0) { perror("socket"); return -1; } unsigned short sport = 8080; if(argc == 2) { sport = atoi(argv[1]); } struct sockaddr_in addr; addr.sin_family = AF_INET; printf("port = %d\n", sport); addr.sin_port = htons(sport); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return -2; } if(listen(listenfd, 20) < 0) { perror("listen"); return -3; } struct sockaddr_in connaddr; int len = sizeof(connaddr); int i = 0, ret = 0; int client[CLIENTCOUNT]; for(i = 0; i<CLIENTCOUNT; i++) // 将连接上client的描述符放到一个数组里,这里对数组进行初始化。-1表示空闲 client[i] = -1; fd_set rset; fd_set allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(listenfd, &allset); int maxfd = listenfd; int nready = 0; char buf[1024] = {0}; /* 这里很巧妙的是用到了两个fd_set。因为rset会发生改变。我们管理处理好allset就好了。 每次循环开始都将allset赋值给rset。 当有client连接时,加入到allset中。有client关闭时就从allset中移除 */ while(1) { rset = allset; nready = select(maxfd+1, &rset, NULL, NULL, NULL); if(nready == -1) { perror("select"); return -3; } if(nready == 0) continue; if(FD_ISSET(listenfd, &rset)) // 这里用来监听 监听套接字,就是listenfd { int conn = accept(listenfd, (struct sockaddr*)&connaddr, &len); if(conn < 0) { perror("accept"); return -4; } char strip[64] = {0}; char *ip = inet_ntoa(connaddr.sin_addr); strcpy(strip, ip); printf("new client connect, conn:%d,ip:%s, port:%d\n", conn, strip,ntohs(connaddr.sin_port)); FD_SET(conn, &allset); // 将连接上的套接字加入到allset中去 if(maxfd < conn) // update maxfd maxfd = conn; int i = 0; for(i = 0; i<CLIENTCOUNT; i++) { if(client[i] == -1) { client[i] = conn; // 将conn存进client数组中取去 break; } } if(i == CLIENTCOUNT) { printf("to many client connect\n"); exit(0); } if(--nready <= 0) continue; } for(i = 0; i < CLIENTCOUNT; i++) // 这里处理客户端发送过来的数据,逐个处理所有的套接字 { if(client[i] == -1) continue; if(FD_ISSET(client[i], &rset)) { ret = read(client[i], buf, sizeof(buf)); if(ret == -1) { perror("read"); return -4; } else if(ret == 0) // 表示client关闭了 { printf("client close remove:%d\n", client[i]); FD_CLR(client[i], &allset); // 在allset中移除它 close(client[i]); // 同时也要关闭 client[i] = -1; // 要在这里移除,这步非常重要。 } // fputs(buf, stdout); printf("client%d:%s\n", client[i], buf); write(client[i], buf, sizeof(buf)); // 发送数据给client memset(buf, 0, sizeof(buf)); if(--nready <= 0) continue; } } } close(listenfd); return 0; }
关于while(1)循环里面的理解:
1)先将listenfd加入监听队列
2)如果有client连接,就将连接套接字加入allset
3)然后重新监听rset(allset会赋值给它)。这样就相当于监听了client和listenfd
4)如果select又返回,若是listenfd,则重复上面的步骤
5)如果是client发来了数据,read返回不为0则打印出来,再发送回去。若read返回0,则表示client关闭了。就把对应文件描述符从allset中移除。同时关闭它。也要更新那个数组client。如果不更新for循环会出错
接着来看客户端:
client.c:
#include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<sys/select.h> #include<stdlib.h> #include<stdio.h> #include<string.h> void select_test(int conn) { int ret = 0; fd_set rset; FD_ZERO(&rset); int nready; int maxfd = conn; int fd_stdin = fileno(stdin); if(fd_stdin > maxfd) { maxfd = fd_stdin; } int len = 0; char readbuf[1024] = {0}; char writebuf[1024] = {0}; while(1) { FD_ZERO(&rset); FD_SET(fd_stdin, &rset); FD_SET(conn, &rset); nready = select(maxfd+1, &rset, NULL, NULL, NULL); // 同时监听标准输入和服务器。 if(nready == -1) { perror("select"); exit(0); } else if(nready == 0) { continue; } if(FD_ISSET(conn, &rset)) // 服务器来了数据 { ret = read(conn, readbuf, sizeof(readbuf)); if(ret == 0) { printf("server close1\n"); break; } else if(-1 == ret) { perror("read1"); break; } fputs(readbuf, stdout); memset(readbuf, 0, sizeof(readbuf)); } if(FD_ISSET(fd_stdin, &rset)) // 标准输入来了数据就发送给server { read(fd_stdin, writebuf, sizeof(writebuf)); len = strlen(writebuf); ret = write(conn, writebuf, len); if(ret == 0) { printf("server close3\n"); break; } else if(-1 == ret) { perror("write"); break; } memset(writebuf, 0, sizeof(writebuf)); } } } int sockfd = 0; int main(int argc, char **argv) { sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("socket"); return -1; } unsigned short sport = 8080; if(argc == 2) { sport = atoi(argv[1]); } struct sockaddr_in addr; addr.sin_family = AF_INET; printf("port = %d\n", sport); addr.sin_port = htons(sport); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("connect"); return -2; } struct sockaddr_in addr2; socklen_t len = sizeof(addr2); if(getpeername(sockfd, (struct sockaddr*)&addr2, &len) < 0) { perror("getsockname"); return -3; } printf("Server: port:%d, ip:%s\n", ntohs(addr2.sin_port), inet_ntoa(addr2.sin_addr)); select_test(sockfd); close(sockfd); return 0; }
运行:
先运行服务器程序,再启动客户端程序,通过标准输入给server发送数据。server会回射回来。
这个服务器可以同时处理多个客户端的数据。
6. 用select可以实现在单进程中同时处理多个文件描述符的事件。
7.读、写、异常事件发生的条件
可读:
1)套接口缓冲区有数据可读
2)连接的读一半关闭,即接收到FIN段,读操作返回0
3)如果是监听套接口,已完成连接队列不为空时
4)套接口上发生了一个错误待处理,错误可以同getsetopt指定SO_ERROR选项来获取
可写:
1)套接口发送缓冲区有空间容纳数据
2)连接的写一半关闭,即接收到RET段,再次调用write操作
3)套接口上发生了一个错误待处理,错误可以同getsetopt指定SO_ERROR选项来获取
异常:
1)套接口存在带外数据