思路
lfd = socket(); //创建套接字
bind(); //绑定地址结构
listen(); //设置监听上限
————
fd_set rset; //创建读(r)监听集合
fd_set allset;//区分读监听集合和后添加进去的总的监听集合
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)) //lfd有响应表示有新连接,给新cfd
{
//1在表示有客户端进行连接了
cfd = accept();//建立连接,返回用于通信的文件描述符
FD_SET(cfd, &allset);//添加到监听通信描述符集合中
}
for(i = lfd+1; i<= 最大文件描述符; i++)//挨个去对比,谁发生事件了(读写了
{
if(FD_ISSET(i, &rset);) //有读写操作
{
read();
toupper();
write;
}
continue;
}
}
}
代码实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arap/inet.h>
#include<ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int listenfd;//socket
struct sockaddr_in serv_addr;//bind
int nready;//select返回的发生事件的值
int connfd;//accept的返回值
struct sockaddr_in clie_addr;//accept的第二参数
socklen_t clie_addr_len;//accept的第三参数,注意类型
char buf[BUFSIZ];//读写需要的缓冲区
int i,j;//找集合中事件时以及读写时
int n;//读写的长度
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htol(INADDR_ANY);
serv_addr.sin_port = htos(SERV_PORT);
listenfd = Socket(AF_INET, SOCK_STREAM,0);//①socket,lfd接收
//设置端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//②bind,参数2为结构体地址,所以先定义再清空最后赋值去绑定
Bind(listenfd, (struct sockaddr*)serv_addr, sizeof(serv_addr));
//③listen
Listen(listen,128);
//----------
//----------用select去监听
//①构造文件描述符集合,以及维护一个最大文件描述符号用于方便循环找事件
fd_set rset, allset;//一开始主要是读(r)集合
macfd = listenfd;
FD_ZERO(&allset);//集合清空
FD_SET(listenfd, &allset);//首先往集合中将lfd加入
while(1)
{
rset = allset;//每次循环时都重新设置select监控信号集
nready = select(maxfd+1, &rset, NULL,NULL,NULL);//select去监听(工作),根据返回值nready知道有几个事件,rset是传入传出参数也可以利用函数去知道谁发生了事件
if(nready < 0)//返回值-1,出错
perr_exit("select error");
if(FD_ISSET(listenfd, &rset))//listenfd在事件中,表示有新的客户端链接请求
{
//有新的请求,就给一个cfd去连接
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len);
//并将新的cfd也加入到监听集合中
FD_SET(connfd, &allset);
if(maxfd<connfd)
maxfd = connfd;//维护最大文件描述符号
if(0 == --nready)//表示只有一个监听文件描述符有事件,那后续的循环就不用了
continue;
}
//去检测哪个clients有数据就绪,要做读写
for(i = listenfd+1; i<= maxfd; i++)
{
if(FD_ISSET(i, &rset))//在集合中表示有事件,就读进来
{
if((n = Read(i, buf, sizeof(buf))) == 0)
{//表示读到最后,可理解为客户端client关闭链接
close(i);//则服务器也可以关闭链接
FD_CLR(i, &allset);//并从集合中删除
}else if(n > 0)//还在读
{
for(j = 0; j < n; j++)
{
buf[j] = toupper(buf[j]);
}
Write(i, buf, n);
}
}
}
}
Close(listenfd);
return 0;
}
select优缺点
以上述代码的缺点:
①万一监听的是lfd(3号),C3(6号)以及CN(1023号),那么虽然只监听了3个却还是轮询了1024个,效率就很低
====> 解决办法:自定义一个数组👇,即当检测满足条件的id时,需要自己添加业务逻辑提高效率,提高编码难度。但是性能不比poll、epoll低
②文件符集合最大上限是1024,所以我们客户端数目也是有限的,最大也就1024个
优点:唯一一个可以跨平台完成文件描述符监听
添加一个自定义数组提高效率(进阶版xX👇
定义一个client数组
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>
#include "wrap.h"
#definr SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, maxi;
int nready;
int client[FD_SETSIZE];//自定义client数组,防止遍历1024个文件描述符,FD_SETSIZE默认值为1024
int maxfd, listenfd,connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];//INET_ADDRSTRLEN默认值为16
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset;//rset读事件文件描述符集合,allset用来暂缓
listenfd = Socket(AF_INET, SOCK_STREAM,0);
//端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET,SO_REUSEADDR,&opt, sizeof(opt));
//bind之前地址结构清零
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr*)serv_addr, sizeof(serv_addr));
Listen(listenfd,128);
maxfd = listenfd;//起初listenfd即为最大文件描述符
maxi = -1;//将来用作client[]的下标,初始值指向0个元素之前下标位置
for(i = 0; i<FD_SETSIZE; i++)
{
client[i] = -1;//用-1初始化client[]
}
FD_ZERO(&allset);//清空文件描述符
FD_SET(listenfd, &allset);//将监听lfd加入
while(1)
{
rset = allset;//⭐每次循环时都重新设置select监控信号集
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready < 0)
perr_exit("select error");
if(FD_ISSET(listenfd, &rset))//lfd有事件,表示有新的客户端链接请求
{
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len);
printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));//地址结构转换的函数要再去理解记忆一下⭐
//把这个新的cfd加入到数组中
for(i = 0; i<FD_SETSIZE; i++)
{
if(client[i] < 0)//第一个空位就填上去
{
client[i] = connfd;
break;//很秒,跳出这个循环,下面的i就都是这个i
}
}
if(i == FD_SETSIZE)//达到select能监控的文件个数上限1024
{
fputs("too many clients\n", stderr);
exit(1);
}
//也要把这个新的cfd加入到监控集合中
FD_SET(connfd, &allset);
if(connfd > maxfd)
maxfd = connfd;
if(i > maxi)
maxi = i;//保证maxi存的总是client[]最后一个元素的下标
if(--nready == 0)//表示刚刚就只有lfd工作了,那accept给出cfd就ok
continue;
//还有别的cfd有请求
for(i = 0; i <= maxi; i++)//依次检测哪个clients有数据就绪
{
if((sockfd = client[i]) <0 )//没动静不是它,下一个
continue;
//这个client[i]有事件,则👇
if((n = Read(sockfd, buf, sizeof(buf))) == 0)
{
//当client关闭链接时,服务器端也关闭对应链接
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;//别忘了数组
}else if(n>0)//读写
{
for(j = 0; j<n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if(--nready == 0)//没有别的cfd有请求了,退出找对比找cfd的循环
break;
}
}
}
Close(listenfd);
return 0;
}