socket多路复用应用
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
功能:轮循等待的方式,从多个文件描述符中获取状态变化后的情况
readfds :包含所有可能因状态变成可读而触发select()函数返回的文件描述符
writefds :包含所有可能因状态变成可写而触发select()函数返回的文件描述符
exceptfds :包含所有可能因状态发生特殊异常(如带外数据到来)而触发select()函数返回的文件描述符
针对文件描述符集合的操作如下:
#define FD_SET(fd, fdsetp) //把fd添加到fdsetp中 #define FD_CLR(fd, fdsetp) //从fdsetp中删除fd #define FD_ISSET(fd, fdsetp) //检测fdsetp中的fd是否出现异常 #define FD_ZERO(fdsetp) //初始化fdsetp为空
参数1:限制上面要检测的文件描述符的范围,范围在0到最大文件描述符值之间
最后一个参数:表示阻塞超时时限
struct timeval { long tv_sec; long tv_usec; };
返回值:函数错误,返回-1; 超时返回0,将时间结构体清空为0;有文件需要处理,返回相应的文件描述符,在文件描述符集合中清除不需要处理的文件描述符
例子:
1.检测某个socket是否可读
fd_set rdfds; //声明一个fd_set集合来保存要检测的socket struct timeval tv; //保存时间 int ret; //保存返回值 FD_ZERO(&rdfds); //集合清零 FD_SET(socket, &rdfds); //把要检测的文件描述符加入集合 tv.tv_sec = 1; tv.tv_usec = 500; //设置select等待的最大时间为1s+500ms ret = select(socket + 1, &rdfds, NULL, NULL, &tv); //检测集合中是否有可读信息 if(ret < 0) //出错 perror("select"); else if(ret == 0) //超时 printf("超时 "); else //有状态变化 { printf("ret = %d ", ret); //判断socket是否变成可读 if(FD_ISSET(socket, &rdfds)) { recv(...); //读取 } }
2.检测用户键盘输入。需要把标准输入文件描述符0放入select检测
FD_ZERO(&rdfds); FD_SET(0, &rdfds); tv.tv_sec = 1; tv.tv_usec = 500; ret = select(1, &rdfds, NULL, NULL, &tv); if(ret < 0) //出错 perror("select"); else if(ret == 0) //超时 printf("超时 "); else //有输入 scanf("%s", buf);
int pselect (int __nfds, fd_set *__restrict __readfds, fd_set *__restrict __writefds, fd_set *__restrict __exceptfds, const struct timespec *__restrict __timeout, const __sigset_t *__restrict __sigmask)
该函数与select()函数功能几乎相同,只是时间精度更高,同时设置了阻塞的信号集合。
时间的结构体声明如下:
struct timespec{ long ts_sec; long ts_nsec; //ns };
poll与ppoll函数
可以实现比select/pselect函数更强大的功能,更细粒的等待时间
int poll (struct pollfd *fds, nfds_t nfds, int timeout)
int ppoll (struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts, const sigset_t *sigmask)
其中文件描述符的结构体定义为:
struct pollfd{ int fd; //文件描述符 short events; //请求事件 short revents; //返回的事件 };
请求或返回的事件类型如下:
ppoll()函数也可以在阻塞过程中屏蔽某些信号,而且timeout上ppoll的精度更高。
调用
ready = ppoll(&fds, nfds, timeout_ts, &sigmask);
相当于调用
sigset_t origmask; int timeout; timeout = (timeout_ts == NULL) ? -1 : (timeout_ts.tv_sec * 1000 + timeout_ts.tv_nesc / 1000000); sigprocmask(SIG_SETMASK, &sigmask, &origmask); ready = poll(&fds, nfds, timeout); sigprocmask(SIG_SETMASK, &origmask, NULL);
示例
基于多路复用的服务器客户端聊天程序
这个的效果是目前为止最好的,可以实现一对多的通信。消息也比较清晰。
服务器端
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<netinet/in.h> #include<sys/socket.h> #include<sys/wait.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/time.h> #define MAXBUF 1024 int main(int argc, char * argv[]) { int sockfd, new_fd; socklen_t len; struct sockaddr_in my_addr, their_addr; unsigned int myport, lisnum; char buf[MAXBUF + 1]; fd_set rfds; //文件描述符集合 struct timeval tv; int retval, maxfd = -1; if(argv[2]) myport = atoi(argv[2]); //参数2为端口号 else myport = 7838; //默认端口号 if(argv[3]) lisnum = atoi(argv[3]); //命令行第3个参数为listen队列大小 即可以等待多少个客户端 else lisnum = 2; //创建socket对象 ipv4 TCP 默认协议 if((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(EXIT_FAILURE); } bzero(&my_addr, sizeof(my_addr)); my_addr.sin_family = PF_INET; my_addr.sin_port = htons(myport); if(argv[1]) my_addr.sin_addr.s_addr = inet_addr(argv[1]); //参数1为IP地址 else my_addr.sin_addr.s_addr = INADDR_ANY; //绑定地址信息 if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(EXIT_FAILURE); } //服务器监听网络 if(listen(sockfd, lisnum) == -1) { perror("listen"); exit(EXIT_FAILURE); } while(1) { printf(" ---------wait for new connect "); len = sizeof(struct sockaddr); //接收客户端连接 if((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &len)) == -1) { perror("accept"); exit(EXIT_FAILURE); } else { //打印连接信息 printf("server: got connection from %s, port %d, socket %d ", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd); while(1) { FD_ZERO(&rfds); FD_SET(0, &rfds); FD_SET(new_fd, &rfds); maxfd = new_fd; //只有两个文件描述符0和new_fd,最大值为sockfd tv.tv_sec = 1; tv.tv_usec = 0; //多路复用 retval = select(maxfd + 1, &rfds, NULL, NULL, &tv); if(retval == -1) //函数出错 { perror("select"); exit(EXIT_FAILURE); } else if(retval == 0) //超时 { continue; } else { //检测是否为标准输入引起异常 if(FD_ISSET(0, &rfds)) { bzero(buf, MAXBUF + 1); fgets(buf, MAXBUF, stdin); //从标准输入读数据 if(!strncasecmp(buf, "quit", 4)) //如果quit退出 { printf("i will quit! "); break; } //将数据发送给客户端 len = send(new_fd, buf, strlen(buf) - 1, 0); //为什么要-1 ?? if(len > 0) printf("send successful, %d byte send! ", len); else { printf("send failure!"); break; } } //如果是当前sockfd引起的异常 if(FD_ISSET(new_fd, &rfds)) { bzero(buf, MAXBUF + 1); //从中读取数据 len = recv(new_fd, buf, MAXBUF, 0); if(len > 0) printf("recv success :'%s', %dbyte recv ", buf, len); else if(len == 0) { printf("the other one end quit "); break; } } } } } close(new_fd); printf("need other connect (no->quit)"); //是否需要等待其他客户端连接 fflush(stdout); //刷新标准输出 bzero(buf, MAXBUF + 1); fgets(buf, MAXBUF, stdin); if(!strncasecmp(buf, "no", 2)) { printf("quit! "); break; } } close(sockfd); return 0; }
客户端
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<netinet/in.h> #include<sys/socket.h> #include<sys/wait.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/time.h> #define MAXBUF 1024 int main(int argc, char **argv) { int sockfd, len; struct sockaddr_in dest; char buffer[MAXBUF + 1]; fd_set rfds; struct timeval tv; int retval, maxfd = -1; if(argc != 3) { printf("argv format errno, pls: %s IP port ", argv[0]); exit(0); } //创建socket if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket"); exit(EXIT_FAILURE); } bzero(&dest, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(atoi(argv[2])); //参数2为端口号 if(inet_aton(argv[1], (struct in_addr *)&dest.sin_addr.s_addr) == 0) //参数1为IP地址 { perror(argv[1]); exit(EXIT_FAILURE); } //发起连接 if(connect(sockfd, (struct sockaddr *)&dest, sizeof(dest)) != 0) { perror("Connect"); exit(EXIT_FAILURE); } printf(" get ready pls chat "); while(1) { FD_ZERO(&rfds); FD_SET(0, &rfds); FD_SET(sockfd, &rfds); maxfd = sockfd; tv.tv_sec = 1; tv.tv_usec = 0; //多路复用 retval = select(maxfd + 1, &rfds, NULL, NULL, &tv); if(retval == -1) { printf("select %s", strerror(errno)); break; } else if(retval == 0) //超时 continue; else { if(FD_ISSET(sockfd, &rfds)) { bzero(buffer, MAXBUF + 1); len = recv(sockfd, buffer, MAXBUF, 0); if(len > 0) { printf("recv message:'%s', %dbyte recv ", buffer, len); } else if(len < 0) { printf("message recv failure "); } else { printf("the other quit, quit "); break; } } if(FD_ISSET(0, &rfds)) { bzero(buffer, MAXBUF + 1); fgets(buffer, MAXBUF, stdin); if(!strncasecmp(buffer, "quit", 4)) { printf("i will quit! "); break; } len = send(sockfd, buffer, strlen(buffer) - 1, 0); if(len > 0) printf("send successful, %d byte send! ", len); else printf("send failure!"); } } } close(sockfd); return 0; }
服务器效果
客户端1效果
客户端2效果