1. select模型
2. select()函数
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
fd_set类型
readfds和writefds, exceptfds的类型都是fd_set,那么fd_set类型是什么呢?
- fd_set类型本质是一个位图,位图的位置 表示
相对应的文件描述符,内容表示该文件描述符是否有效,1代表该位置的文件描述符有效,0则表示该位置的文件描述符无效 - 如果将文件描述符2,3设置位图当中,则位图表示的是为1100
- fd_set的上限是1024个文件描述符
readfds
- readfds是 等待读事件的文件描述符集合,.如果不关心读事件(缓冲区有数据),则可以传NULL值
- 应用进程和内核都可以设置readfds,应用进程设置readfds是为了通知内核去等待readfds中的文件描述符的读事件.而
内核设置readfds是为了告诉应用进程哪些读事件生效
writefds
与readfds类似,writefds是等待写事件(缓冲区中是否有空间)的集合,如果不关心写事件,则可以传值NULL
exceptfds
如果内核等待相应的文件描述符发生异常,则将失败的文件描述符设置进exceptfds中,如果不关心错误事件,可以传值NULL
timeout
设置select在内核中阻塞的时间,如果想要设置为非阻塞,则设置为NULL。如果想让select阻塞5秒,则将创建一个struct timeval time={5,0};
其中struct timeval的结构体类型是:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值
- 如果没有文件描述符就绪就返回0;
- 如果调用失败返回-1;
- 如果在timeout时间内,readfds中有事件发生,则返回timeout剩下的时间
3. select的工作流程
应用进程和内核都需要从readfds和writefds获取信息,其中,内核需要从readfds和writefds知道哪些文件描述符需要等待,应用进程需要从readfds和writefds中知道哪些文件描述符的事件就绪
select采用水平触发的方式,如果报告了fd后事件没有被处理或数据没有被全部读取,那么下次select时会再次报告该fd。
select 模型是一种多路复用 I/O 模型,它可以同时监视多个文件描述符,等待其中任意一个文件描述符上有事件发生,从而实现 I/O 多路复用。select 模型的原理如下:
调用 select 函数,将要监视的文件描述符集合传递给它。
select 函数将进程挂起,等待文件描述符集合中的任意一个文件描述符上有事件发生。
当文件描述符集合中的任意一个文件描述符上有事件发生时,select 函数返回,并将有事件发生的文件描述符集合返回给进程。
进程遍历有事件发生的文件描述符集合,对每个文件描述符进行相应的操作,如读、写等
4. 缺陷
- 由于fd_set的上限是1024,所以select能等待的读事件的文件描述符和写事件的文件描述是有上限的,如果作为一个大型服务器,能够同时链接的客户端是远远不够的
- 内核每一次等待文件描述符 都会重新扫描所有readfds或者writefds中的所有文件描述符,如果有较多的文件描述符,则会影响效率
5. 源码
client.cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
if (argc!=3)
{
printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n"); return -1;
}
// 第1步:创建客户端的socket。
int sockfd;
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
// 第2步:向服务器发起连接请求。
struct hostent* h;
if ( (h = gethostbyname(argv[1])) == 0 ) // 指定服务端的ip地址
{ printf("gethostbyname failed.\n"); close(sockfd); return -1; }
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) // 向服务端发起连接清求。
{ perror("connect"); close(sockfd); return -1; }
char buffer[1024];
// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
for (int ii=0; ii<5; ii++)
{
int iret;
memset(buffer, 0, sizeof(buffer));
printf("Please input:");
scanf("%s", buffer);
//sprintf(buffer, "这是第%d个消息, 编号%03d。", ii+1, ii+1);
if ( (iret=send(sockfd, buffer, strlen(buffer), 0))<=0) // 向服务端发送请求报文。
{ perror("send"); break; }
printf("发送: %s\n",buffer);
memset(buffer, 0, sizeof(buffer));
if ( (iret=recv(sockfd, buffer, sizeof(buffer),0)) <= 0) // 接收服务端的回应报文。
{
printf("iret=%d\n", iret); break;
}
printf("接收: %s\n", buffer);
sleep(1);
}
// 第4步:关闭socket,释放资源。
close(sockfd);
}
tcpselect.cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>
// 初始化服务端的监听端口。
int initserver(int port);
int main(int argc,char *argv[])
{
if (argc != 2)
{
printf("usage: ./tcpselect port\n"); return -1;
}
// 初始化服务端用于监听的socket。
int listensock = initserver(atoi(argv[1]));
printf("listensock=%d\n",listensock);
if (listensock < 0)
{
printf("initserver() failed.\n"); return -1;
}
fd_set readfdset; // 读事件的集合,包括监听socket和客户端连接上来的socket。
int maxfd; // readfdset中socket的最大值。 maxfd是集合中socket中最大的值
// 初始化结构体,把listensock添加到集合中。
FD_ZERO(&readfdset);
FD_SET(listensock, &readfdset);
maxfd = listensock;
while (1)
{
// 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。
fd_set tmpfdset = readfdset;
struct timeval time = {5,0};
int infds = select(maxfd+1, &tmpfdset, NULL, NULL, &time); //select会改变tmpfdset集合
//printf("select infds=%d\n",infds);//select会阻塞在这里,
// 返回失败。
if (infds < 0)
{
printf("select() failed.\n");
perror("select()");
break;
}
// 超时,在本程序中,select函数最后一个参数为空,不存在超时的情况,但以下代码还是留着。
if (infds == 0)
{
printf("select() timeout.\n"); continue;
}
// 检查有事情发生的socket,包括监听和客户端连接的socket。
// 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。
for (int eventfd=0; eventfd <= maxfd; eventfd++)
{
if (FD_ISSET(eventfd, &tmpfdset) <= 0) continue;
if (eventfd == listensock)
{
// 如果发生事件的是listensock,表示有新的客户端连上来
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock, (struct sockaddr*)&client, &len);
if (clientsock < 0)
{
printf("accept() failed.\n"); continue;
}
printf ("client(socket=%d) connected ok.\n", clientsock);
// 把新的客户端socket加入集合
FD_SET(clientsock, &readfdset);
if (maxfd < clientsock) maxfd = clientsock;//更新maxfd
continue;
}
else
{
// 客户端有数据过来或客户端的socket连接被断开
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
// 读取客户端的数据。 通过 iSzie 的值判断客户端是否断开连接 iSize <= 0
ssize_t isize=read(eventfd, buffer, sizeof(buffer));
// 发生了错误或socket被对方关闭
if (isize <= 0)
{
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket。
FD_CLR(eventfd, &readfdset); // 从集合中移去客户端的socket。
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
if (eventfd == maxfd)
{
for (int ii=maxfd; ii>0; ii--)
{
if (FD_ISSET(ii, &readfdset))
{
maxfd = ii; break;
}
}
printf("maxfd=%d\n", maxfd);
}
continue;
}
printf("recv(eventfd = %d, size=%d):%s\n",eventfd, isize, buffer);
// 把收到的报文发回给客户端。
write(eventfd, buffer, strlen(buffer));
}
}
}
return 0;
}
// 初始化服务端的监听端口。
int initserver(int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
printf("socket() failed.\n"); return -1;
}
// Linux如下 解决重复开启程序时,报端口占用问题
int opt = 1; unsigned int len = sizeof(opt);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
if (bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 )
{
printf("bind() failed.\n"); close(sock); return -1;
}
if (listen(sock,5) != 0 )
{
printf("listen() failed.\n"); close(sock); return -1;
}
return sock;
}