IO多路复用之select服务端程序简单实现

select函数

原型: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

  • 参数1:所监听的所有文件描述符中,最大的文件描述符+1
  • 参数2/3/4:读/写/异常事件集合
  • 参数5:超时时间
  • 返回值:成功,所监听的所有监听集合中触发所监听事件的总数

相关操作函数:

  1. FD_ZERO(fd_set* set),将监听集合置空,用于初始化。
  2. FD_CLR(int fd, fd_set* set),将fd从集合中清除出去。
  3. FD_ISSET(int fd, fd_set* set),判断fd是否在监听集合中。
  4. FD_SET(int fd, fd_set* set),将fd设置到监听集合中去。

C实现

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <sys/un.h>
#include <arpa/inet.h>  //inet_ntop的头文件

#define SERV_ADDR "127.0.0.1"  //服务端IP
#define SERV_PORT 6666         //服务端端口
#define MAX_FD    1024         //最大文件句柄数

int main(void) {
	//lfd服务端文件句柄, clients,全部客户端文件句柄, cfd,某客户端句柄
	 int lfd, clients[MAX_FD], cfd;  
	 int nRet, i, len;
	 char buf[BUFSIZ]; //存储从客户端读取的字符串
	 char clie_ip[20]; //ip地址,字符串形式
	 int clie_port;    //端口号
	 struct sockaddr_in serv_addr, clie_addr;  //服务端和客户端地址
	 socklen_t clie_addr_len; //客户端地址长度
	 fd_set rset, allset; //监听集合位图
	 serv_addr.sin_family = AF_INET; //ipv4
	 inet_pton(AF_INET,SERV_ADDR, &serv_addr.sin_addr.s_addr); //转化字符串形式为网络字节序
	 serv_addr.sin_port = htons(SERV_PORT); //端口号,转化本机字节序为网络字节序
	 lfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字,返回服务端文件句柄
	 bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); //绑定服务端地址
	 listen(lfd, 128); //设置同时进行TCP链接的个数
	 FD_ZERO(&allset); //监听集合置空
	 FD_SET(lfd, &allset); //将服务端文件句柄加入到监听集合
	 for(i = 0; i < MAX_FD; ++i) { //将客户端文件句柄数组置为-1
		 clients[i] = -1;
	 }
	while(1) {
		 rset = allset; //设置读监听集合
		 nRet = select(MAX_FD, &rset, NULL, NULL, NULL); //调用select,此处由内核进行监听,nRet,产生的事件个数
		 if(FD_ISSET(lfd, &rset)){ //若服务端有读事件产生
			 clie_addr_len = sizeof(clie_addr);
			 cfd = accept(lfd, (struct sockaddr*)&clie_addr, &clie_addr_len); //建立TCP链接
			 for(i = 0; i < MAX_FD; ++i) { //设置cfd到客户端文件句柄数组
				 if(clients[i] == -1) {
					 clients[i] = cfd;
					 FD_SET(cfd, &allset);
					 inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip, clie_addr_len); //转化网络字节序为字符串clie_ip
					 clie_port = ntohs(clie_addr.sin_port);
					 printf("%s:%d connected!\n", clie_ip, clie_port);
					 break;
				 }
			 }
			 if(i == MAX_FD) {
				 printf("客户端超出最大数\n");
				 exit(1);
			 }
			 --nRet;
		 }
		 for(i = 0; i < MAX_FD && nRet > 0; ++i) {  //循环遍历客户端文件句柄数组
			 if(clients[i] ==  -1) continue;
			 if(FD_ISSET(clients[i], &rset)) { //客户端有读事件发生
				 --nRet;
				 //处理客户端请求
				 len = read(clients[i], &buf, sizeof(buf));
				 if(len <= 0) {
					 //客户端断开
					 FD_CLR(clients[i], &allset); //清空对应的监听事件
					 close(clients[i]);
					 clients[i] = -1;
					 printf("一个客户端断开连接!\n");
					 continue;
				 }
				 write(STDOUT_FILENO, buf, len);
				 write(clients[i], buf, len);
			 }
		 }
	}
	close(lfd);
	return 0;
}

    注意,上述程序是在linux环境下编译运行的c程序,没有写客户端程序,验证的方式可通过在新的终端下输入nc 127.0.0.1 6666命令来连接服务端。

总结

  • 每次select返回后,需要对全部文件描述进行遍历,即使设置了nRet参数,当最后一个文件描述符即1023有事件产生,那么就必须遍历整个数组。
  • 每次都要重新设置监听事件集合,本程序体现在将allset赋值给rset。
  • 调用select需要将监听事件集合拷贝到内核,当select返回后又需要拷贝回用户空间,开销较大。
  • select能够监听的事件个数有限,一般不超过1024。
发布了33 篇原创文章 · 获赞 6 · 访问量 551

猜你喜欢

转载自blog.csdn.net/weixin_43519984/article/details/105100661