多路IO
作者:互联网
1.高并发服务器实现方法
1.阻塞等待:进程开启线程去服务客户,每个线程服务一个客户,阻塞等待客户发送消息——消耗资源
2.非阻塞忙轮询:进程循环询问客户,问它上面是否有消息,有消息就进行处理。——消耗cpu
3.多路IO转接(多路IO复用): 内核监听多个文件描述符的属性(读写缓冲区)变化。如果某个文件描述符的读缓冲区变化了,这个时候就是可以读了,将这个事件告知应用层
三种多路IO
- select:windwos中常用,select可跨平台
- poll:很少用
- epoll:linux中常用
2.select函数
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能: 监听多个文件描述符的属性变化(读,写,异常)
在集合中进行增删改查等操作:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
参数:
nfds : 最大文件描述符+1。监听0~nfds的文件描述符
readfds : 需要监听的读的文件描述符存放集合。事件:数据到达了读缓冲区,需要读出来了,触发读事件
writefds :需要监听的写的文件描述符存放集合。事件:写缓存区满了,当写缓冲区有空余时,就触发可写事件。常设为NULL
exceptfds : 需要监听的异常的文件描述符存放集合。常设为NULL
timeout: 多长时间监听一次 固定的时间 NULL:永久监听
struct timeval {
long tv_sec; /* seconds */ 秒
long tv_usec; /* microseconds */微妙
};
返回值: 返回的是变化的文件描述符的个数
注意: 变化的文件描述符会存在监听的集合(readfds、writefds和exceptfds)中,未变化的文件描述符会从集合中删除
示例代码:
#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#include "wrap.h"
#include <sys/time.h>
#define PORT 8888
int main(int argc, char *argv[])
{
//创建套接字,绑定
int lfd = tcp4bind(PORT,NULL);
//监听
Listen(lfd,128);
int maxfd = lfd;//最大的文件描述符
fd_set oldset,rset;
FD_ZERO(&oldset);
FD_ZERO(&rset);
//将lfd添加到oldset集合中
FD_SET(lfd,&oldset);
while(1)
{
rset = oldset;//将oldset赋值给需要监听的集合rset
int n = select(maxfd+1,&rset,NULL,NULL,NULL);
if(n < 0)
{
perror("");
break;
}
else if(n == 0)
{
continue;//如果没有变化,重新监听
}
else//监听到了文件描述符的变化
{
//lfd变化 代表有新的连接到来
if( FD_ISSET(lfd,&rset)) // 变化的文件描述符都放在了rset集合中,如果lfd在rset中,就说明lfd变化了
{
struct sockaddr_in cliaddr;
socklen_t len =sizeof(cliaddr);
char ip[16]="";
//提取新的连接
int cfd = Accept(lfd,(struct sockaddr*)&cliaddr,&len);
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//将cfd添加至oldset集合中,以下次监听
FD_SET(cfd,&oldset);
//更新maxfd
if(cfd > maxfd)
maxfd = cfd;
//如果只有lfd变化,直接continue
if(--n == 0)
continue;
}
//cfd 遍历lfd之后的文件描述符是否在rset集合中,如果在则cfd变化
for(int i = lfd+1;i<=maxfd;i++) // 假设现在 4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了
// 本代码对于这种情况,还是遍历了4-1023
{
//如果i文件描述符在rset集合中
if(FD_ISSET(i,&rset))
{
char buf[1500]=""; // 因为网络中的最大传输单元为1500
int ret = Read(i,buf,sizeof(buf));
if(ret < 0)//出错,将cfd关闭,从oldset中删除cfd
{
perror("");
close(i);
FD_CLR(i,&oldset);
}
else if(ret == 0) // 客户关闭了连接
{
printf("client close\n");
close(i);
FD_CLR(i,&oldset);
}
else
{
printf("%s\n",buf);
Write(i,buf,ret);
}
}
}
}
}
return 0;
}
select优点: 跨平台
select缺点:
- 文件描述符数量限制在1024以下,需要修改内核的FD_SETSIZE参数,然后重新编译内核,才能修改1024
- 只是返回变化的文件描述符的个数,具体哪个那个变化需要遍历。当有大量的客户请求了连接但只有少量客户与服务器进行交互(即监听的文件描述符个数多,但是变化的文件描述符少),这种情况下,我们还是需要遍历全部的文件描述符才能找到变化的文件描述符。也就是说大量并发,少了活跃,select效率低。
- select对多个文件描述符进行监听是在内核中进行的,所以每次都需要将需要监听的文件描述集合由应用层符拷贝到内核
假设现在 4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了?——本代码修改后,可以解决。解决如下:
//进阶版select,通过数组防止遍历1024个描述符
#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 8888
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); //2 1--lfd 1--connfd
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;//数组内的文件描述符如果被释放有可能变成-1
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;
}
虽然本代码还是需要遍历,但是效率提高了一些。
3.poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能: 监听多个文件描述符的属性变化
参数:
fds : 监听的数组的首元素地址
nfds: 数组有效元素的最大下标+1
timeout : 超时时间 -1是永久监听 >=0 限时等待
数组元素:
struct pollfd {
int fd; /* file descriptor */ 需要监听的文件描述符
short events; /* requested events */需要监听文件描述符什么事件 EPOLLIN 读事件 EPOLLOUT写事件,其他事件可以通过man poll查看
short revents; /* returned events */ 返回监听到的事件 EPOLLIN 读事件 EPOLLOUT写事
};
poll相对与sellect的优缺点
优点:
- 没有文件描述符1024的限制
- 请求和返回是分离的【select中,readfds表示监听的事件集,而且将返回的有读事件的文件描述符也放在readfds中,这就叫做请求和返回是不分离的】
缺点和select一样: - 只是返回变化的文件描述符的个数,具体哪个那个变化需要遍历。当有大量的客户请求了连接但只有少量客户与服务器进行交互(即监听的文件描述符个数多,但是变化的文件描述符少),这种情况下,我们还是需要遍历全部的文件描述符才能找到变化的文件描述符。也就是说大量并发,少了活跃,poll效率低。
- poll对多个文件描述符进行监听是在内核中进行的,所以每次都需要将需要监听的文件描述集合由应用层符拷贝到内核
poll相对于select没有最大1024文件描述符限制,大小限制。但poll其实也是有最大文件描述符限制,如下:
ulimit -a # 查看最多打开多少个文件(open files)
ulimit -n 2048 # 将最多打开文件个数修改成2048
cat /proc/sys/fs/file-max # ulimit -n最大可修改的量
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include<netinet/in.h>
#include<poll.h>
#include<errno.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready; // 接收poll返回值,记录满足监听事件的fd个数
ssize_t n;
char buf[MAXLINE],str[INET_ADDRSTRLEN];
socklen_t clilen;
struct pollfd client[OPEN_MAX] ;//定义pol1的数组
struct sockaddr_in cliaddr,servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));//设置端口复用
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
Listen(listenfd,128);
client[0].fd = listenfd; /*要监听的第一一个文件描述符存入client[0]*/
client[0].events = POLLIN; /* listenfd监听普通读事件*/
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; // 用-1初始化client[]里剩下元素 0也是文件描述符,
maxi = 0; /* client[]数组有效元素中最大元素下标*/
for (;;) { // 等价与while循环
nready = poll(client, maxi + 1, -1); /* 阻塞监听是否有客户端链接请求*/
if (client[0].revents & POLLIN) { // listenfd有 读事件就绪。这里用&,而不用==
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 1; i < OPEN_MAX; i++)
if (client[i].fd < 0) {
client[i].fd = connfd; // 找到client []中空闲的位置,存放accept返回的connfd
break;
}
if (i == OPEN_MAX) /*达到了最大客户端数*/
perr_exit("too many clients");
client[i].events = POLLIN; /*设置刚刚返回的connfd, 监控读事件*/
if (i > maxi)
maxi = i; /*更新client[]中最大元素下标*/
if (--nready <= 0)
continue; /*没有更多就绪事件时,继续回到po11阻塞*/
}
for (i = 1; i <= maxi; i++) {
if ((sockfd = client[i].fd) < 0)
continue; //找到第一-个大于0的
if (client[i].revents & POLLIN) {
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) { /* 收到RST标志,代表对方想要关闭连接*/
printf("client[%d] aborted connection\n", i);
Close(sockfd);
client[i].fd = -1; // poll中不监控该文件描述符,直接置为-1即可,不用像se
}
else
perr_exit("read error");
}
else if (n == 0) { /*说明客户端先关闭链接*/
printf("client[%d] closed connection\n",i);
Close(sockfd);
client[i].fd = -1;
}
else {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
if (--nready <= 0)
break;
}
}
}
return 0;
}
标签:文件,多路,int,描述符,client,IO,include,监听 来源: https://www.cnblogs.com/codingbigdog/p/16255825.html