刚开始都是由server处理,不管是监听还是连接建立还是读写,那么能否让内核来帮忙处理一点呢
select、poll、epoll都属于多路I/O转换
select()函数
select(
_In_ int nfds, //在windows下面没有意义 在Unix衍生的操作系统中使用
_Inout_opt_ fd_set FAR * readfds, //_Inout_ 表示这个参数传入时是有意义的 传出时也是有意义的 指向检查可读性的套接字集合的可选的指针。表示客户端有读的socket客户端需求等待处理
_Inout_opt_ fd_set FAR * writefds, //指向检查可写性的套接字集合的可选的指针。表示客户端有写的socket客户端需求等待处理
_Inout_opt_ fd_set FAR * exceptfds, //指向检查错误的套接字集合的可选的指针。表示客户端有异常的Socket客户端的需求等待处理
_In_opt_ const struct timeval FAR * timeout //函数需要等待的最长时间,需要以TIMEVAL结构体格式提供此参数,对于阻塞操作,此参数为null。
);
第一个参数,如下图所示,前三个空间不用,然后有一个lfd,三个cdf,最后一个cfd在6位置上,因此是6+1=7
第2、3、4个参数分别对应要监听的读、写、异常集合,且是传入传出参数
将要监听读的socket放到读集合里,将要监听写的socket的放到读集合里,同理异常集合,然后在监听后返回需要读、写、异常的集合,因此传入的是需要监听的,传出的是实际有事件发生的
还有一个就是返回值,返回值便是这三个集合中有事件发生的总个数
第5个参数是指针类型的timeout,如果设置为NULL就会阻塞在那,如果是设置一个时间,那么它会等待这个时间,如果设置为0那么检查一下立马返回
// 这里的fd 实际使用都是以 句柄 传入
FD_ZERO(fd_set *fdset); // 清空
FD_SET(int fd, fd_set *fdset); // 将fd加入集合
FD_CLR(int fd, fd_set *fdset); // 将fd从集合中删除
FD_ISSET(int fd, fd_set *fdset); // 检测fd是否在set集合中,不在则返回0
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。
返回值: 在:1;不在:0;
FD_ISSET(4, &rset);
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout:
> 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
伪代码
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1) {
rset = allset; 保存监听集合
ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。
if(ret > 0) { 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。
cfd = accept(); 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); 添加到监听通信描述符集合中。
}
for (i = lfd+1; i <= 最大文件描述符; i++){
FD_ISSET(i, &rset) 有read、write事件
read()
小 -- 大
write();
}
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
//创建一个Listensocket
int listenfd = Socket(AF_INET,SOCK_STREAM,0);
//端口复用 避免2msl影响
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//绑定地址结构
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
Bind(listenfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
//设置监听上限
Listen(listenfd,128);
//设置set
//设置一个读集合,全部fd的集合
fd_set rset, allset;
//当前最大句柄
int maxfd = listenfd; //此时listenfd为3 0、1、2分别为读 写 异常
//清空allset
FD_ZERO(&allset);
FD_ZERO(&rset);
//将listenfd放到allset中
FD_SET(listenfd,&allset);
while(1){
//由于如果有新的连接建立只会放到allset中,因此更新要监听的读集合
rset = allset;
int nready = select(maxfd+1,&rset,NULL,NULL,NULL);
//如果select出错
if(nready < 0){
perr_exit("select error");
}
//新的连接请求
if(FD_ISSET(listenfd,&rset))
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int connfd = Accept(listenfd, (struct sockaddr *)&client_addr,&client_addr_len);
FD_SET(connfd, &allset);
if(maxfd < connfd)
maxfd = connfd;
if(nready==1) //如果仅有一个listenfd出现,那么就停止进行后面的工作
continue;
}
int i;
char buf[1024];
for(i = listenfd+1; i<=maxfd; i++){
//发现读事件
if(FD_ISSET(i, &rset)){
int n;
if(( n = Read(i, buf, sizeof(buf))) == 0){
Close(i);
FD_CLR(i, &allset);
}else if (n>0){
int j;
for (j=0; j<n; j++)
buf[j] = toupper(buf[j]);
Write(i, buf, n);
}
}
}
}
Close(listenfd);
return 0;
}
关于上面代码,在select有返回事件产生的时候也就是返回值大于0时
假设 如果此时返回值为2,并且此时集合中有1020个数,那么此时遍历就会从3遍历到1020个数,因此很费时,可以自定义一个数组来提高效率
select优缺点
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips