一. 多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
如上图的select函数,假设c1客服端想连接服务器(老板)端,会先找select函数中的监听套接字,然后select函数告诉服务器有客户端连接 ,服务器调用accept函数连接,并返回cfd1套接字给select函数继续监听,如果c1有数据写入,select函数会通知服务器端再调用read函数。常用的多路函数有select
二. select函数
1.select函数简介
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
(1)nfds:监听的所有文件描述符中,最大文件描述符+1
(2)readfds: 读(事件 )文件描述符监听集合。传入、传出参数
传入的是你监听的 传出的是实际发送数据的
注意:客户端向服务器端写数据 意思就是服务端要做读事件。
(3)writefds:写事件 文件描述符监听集合。 传入、传出参数 NULL大部分用
(4)exceptfds:异常事件 文件描述符监听集合 传入、传出参数 NULL大部分用
(5)timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
>0: 所有监听集合(3个)中, 满足对应事件的总数。
0:没有满足监听条件的文件描述符
-1: errno
传入传出参数是指传入的是select函数需要监听的客户端文件描述,传出的是实际发送数据的,满足的话返回如下图所示,传入是监督 的数据,传出是视实际产生数据交互的fd
2.select相关函数简介
可以看到上图,我们需要标记这些监听的文件描述符。这里采用位图来对这些监听的文件描述符进行记录。下面介绍一下select要用到的函数的具体含义:
位图操作函数:
void FD_CLR(int fd, fd_set *set) 把某一个fd清除出去
int FD_ISSET(int fd, fd_set *set) 判定某个fd是否在位图中
void FD_SET(int fd, fd_set *set) 把某一个fd添加到位图
void FD_ZERO(fd_set *set) 位图所有二进制位置零
select多路IO转接原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
函数的具体实现:
第一个:void FD_ZERO(fd_set *set);
功能:清空一个文件描述符集合。这个文件描述符集合用来存储客服端fd。
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);
端口可以重复使用。设置端口复用 setsockopt()函数:SO_REUSERADDR选项
函数原型:
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
头文件:
#include <sys/types.h>
#include <sys/socket.h>
参数:
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层。
1)SOL_SOCKET:通用套接字选项.
optname:需要访问的选项名。
SO_REUSERADDR 允许重用本地地址和端口 int
optval:对于setsockopt(),指向包含新选项值的缓冲。
设置端口复用只要这样设置:int opt = 1; // 设置端口复用。
optlen:对于setsockopt(),现选项的长度。
返回值:
成功返回0,
失败返回-1,
errno被设为以下的某个值
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字
使用示例:端口复用就这样用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
3.select设计思路
listend函数只有有客服端连接时才会返回数据。服务端才会有读事件发生
思路分析:
1.创建套接字
int maxfd = 0;
lfd = socket();
maxfd = lfd;
2.绑定地址结构
bind();
3.设置监听上限
listen();
4. 位图的初始化设置
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
5.循环调用select函数监听
while(1) {
5.1 保存监听集合
rset = allset;
5.2 监听文件描述符集合对应事件。
ret = select(lfd+1, &rset, NULL, NULL, NULL);
if(ret > 0)
{
有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset))
{ //看lfd还在不在 &rset中,注意这个 &rset 之前的 &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();
}
}
}
4.代码实现
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, nready;
int maxfd = 0;
int listenfd, connfd;
char buf[BUFSIZ];
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
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);
/*定义两个位图:rset读事件描述符集合 allset用来暂存*/
fd_set rset, allset;
maxfd = listenfd;
FD_ZERO(&allset);
/*将监听文件描述符,添加到监听集合,
*构造了select函数初始的监控文件描述符集合*/
FD_SET(listenfd, &allset);
/*调用select函数循环监听是否有客户端连接*/
while(1)
{
/*每次循环都重新设置select监控信号集*/
rset = allset;
/*第一次进入循环,maxfd为listenfd,监听read集合中只有listenfd*/
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
{
perr_exit("select error");
}
/*如果有客户端连接,listenfd在返回的rest文件描述符集合中*/
/*rest传入的是需要监听的集合,listenfd文件描述符在监听集合中
*如果有客户端则连接,则监听的listenfd会被返回*/
if (FD_ISSET(listenfd, &rset))
{
//有客户端连接,调用accept函数,accept不会阻塞
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
/*将新连接的文件描述符,添加到文件描述符集合allset中*/
FD_SET(connfd, &allset);
/*切换最大文件描述符的值*/
if(maxfd < connfd)
{
maxfd = connfd;
}
/*如果只有listenfd有事件,后面for调用read读取客户端的操作就不需要了
* 1.第一次有客户端连接的时候只有listenfd
2.监听的集合中只有连接请求,都没有数据传向服务器的情况*/
if(0 == --nready)
{
/*退出循环,nready = 1:listenfd*/
continue;
}
/*遍历所有的监听的文件描述符集合,检测哪个client有数据就绪*/
for(i = listenfd+1; i<=maxfd; i++)
{
//判读哪个client有数据就绪
if(FD_ISSET(i, &rset))
{
/*当服务器端client关闭连接是,服务器也关闭对于的连接*/
if ((n = Read(i, buf, sizeof(buf))) == 0)
{
Close(i);
/* 解除select对此文件描述符的监控 */
FD_CLR(i, &allset);
}
/*处理读取的数据*/
else if (n > 0)
{
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(i, buf, n);
}
}
}
}
}
return 0;
}
编译运行,结果如下:
5. select优缺点
缺点: 监听上限受文件描述符限制。 最大 1024,检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips