根据网络视频整理:
问题1:socket编程中,为何select放在while(1)中?
因为select需要将监听到多个客户端分别 fd = accept()一下,然后再进行读写判断。
select()在服务器端和客户端的区别:
服务器端多了一个以读的形式监听“建立连接的”lfd。(lfd = socket();)其他读写的fds(fd = accept();)
客户端中只有监听读、写的连接。
多路IO转接,通过内核来监听多个客户端的连接,监听对端发送数据,代替accept函数的监听阻塞功能了。
故:
1)不需要阻塞等待连接(accept不会阻塞了)。
2)不需要阻塞去读对端(read不会阻塞了),这样server.c就可以解放去做其他事情了。
内核一旦给返回了,一定是事件发生了。
服务器通过accept监听客户端1,首先是建立连接的的请求,然后是读写请求;
服务器通过accept监听客户端2,首先是建立连接的的请求,然后是读写请求;
但是会在select()这里进行阻塞,select()函数是在accept()函数前面的,帮忙解决了accept的阻塞,read函数的阻塞。
listenfd也应该在读事件中(服务器监听客户端,相当于读操作),listenfd只需要一个就行了,每accept()一次,会多一个客户端的fd
内核监听是通过select函数来监听的。
#include <sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数解释:
nfds:监听的文件描述符(fd)中,最大的那个+1;(多个fd,多路复用的体现)+1的实质是指监听了前n+1个描述符0,1,2,3
readfds:文件描述符可读事件,数据类型是个位图,本质上是一个long数组的各个位。
writefds:文件描述符可写事件
exceptfds:文件描述符异常事件
timeout:阻塞时间,如果监听的事件在timeout时间内都没有来,则停止阻塞,则NULL,则select永久等待。
readfds\writefds\exceptfds都是传入传出参数
struct timeval{ //都是相对时间
long tv_sec; //秒
long tv_usec; //微秒
};
timeout的值决定了select函数的返回状态:
1)传入NULL,就将select函数置于阻塞状态,一定要等到监视的文件描述符有变化才会返回
2)将时间设为0秒0毫秒,就成为一个非阻塞的函数,不管文件描述符有无变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值。
3)设为大于0的值,即为等待的超时时间,即在timeout时间内阻塞,如为超时,且有事件到来则返回,否则超时后不管怎么一定返回,返回值同2)
(ps:select是通过timeout值来避免阻塞的吧。。。,只要不设为NULL,就不会阻塞)
返回值:
1)如果成功,则返回的是所监听的所有监听集合满足条件的总数:是读+写+异常的和
2)等待超时,没有可读写的文件,则返回0
3)失败,则返回-1,并设置errno
void FD_ZERO(fd_set *set);//将set清空为0//位操作 0000 0000//每次都要清零
void FD_SET(int fd,fd_set *set);//将fd设置到set集合中,即将文件描述符中fd的位置置1
int FD_ISSET(int fd,fd_set *set);//判断fd是否在集合中//返回1,满足条件;返回0,不满足条件
void FD_CLR(int fd,fd_set *set);//将fd在set中清除,即将描述符在位图中的对应位置置0,对端关闭了,将描述符清除。
fd_set readfds;
FD_ZERO(&readfsd);
FD_SET(lfd,&readfsd);//监听连接文件描述符lfd,相当于读事件。
FD_SET(fd1,&readfsd);//监听fd1的读事件
FD_SET(fd2,&readfsd);//监听fd2的读事件
FD_SET(fd3,&readfsd);//监听fd3的读事件
n = select(fds+1,&readfds,NULL,NULL,NULL);//readfds是传入传出参数,有事件发生则该值发生变化
FD_ISSET(fd1,&readfsd);
accept....
readfds、writefds、exceptfds是传入传出参数,如上,当传入到select后,readfds传入的有3值,假如此时只有fd1和fd2中有读事件发生,则select函数返回,则readfds中传出的是2值
readfds传入 | fd3 | fd2 | fd1 | lfd | ||
位图 | ... | 0 | 1 | 1 | 1 | 1 |
readfds传出 | fd3 | fd2 | fd1 | lfd | ||
位图 | ... | 0 | 0 | 1 | 1 | 1 |
传入的时候位图全是1,传出的时候,没有读事件发生的置0了。
所以,如上述程序,readfds传入前位图中有4个1,假设lfd,fd1和fd2有读事件了,则select返回值n=3.位图中fd3被置0了。因此会使用数组来存储要监听的文件描述符。
文件描述符上限是1024,所以select同时监听的文件描述符最大是1024个。
一般用的时候,会定义数组来存储所监听的fd,否则,select去找的话,会从3遍历到1023,浪费时间(0.1.2都被占用)。
因为 readfds、writefds、exceptfds是传入传出参数,所以要对上一次的这三个值进行保存,否则这次就会被改写了。即,监听集合和满足条件的集合是一个集合,为了防止下次监听的集合被改写,所以先对原先的集合进行保存。
总结:
1)select函数及以上的代码写好后,就完成了内核监听注册;
2)然后通过FD_ISSET(fd1,&readfsd);判断到底是哪个fd的事件是否发生。
3) 在设置timeout参数为NULL的情况下,select函数若返回,一定是有监听事件满足了。
具体步骤:
1)FD_set(lfd,&rset); maxfd=lfd;
2)select(madfd+1,&rset,,)//只是在监听lfd
3)判断FD_ISSET(lfd),//进行判断是否有连接,为1表明有客户端在请求连接
4)接着调用confd = accept(),返回一个新的文件描述符,服务器与客户端建立连接。
5)FD_SET(confd,&rset);if(confd>maxfd) maxfd = confd;
6)selcet(maxfd+1,&rset,,,);
7)FD_ISSET(confd,&rset),为1,则read.......
#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[])
{
int i, j, n, maxi;
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define 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));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
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); /* 构造select监控文件描述符集 */
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)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
if (--nready == 0)
continue;
}
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
Close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */
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)
break; /* 跳出for, 但还在while中 */
}
}
}
Close(listenfd);
return 0;
}