系统编程之多路复用

系统编程之多路IO复用

多路 io 复用的意义

多路 io 复用最大的意义就是将多个阻塞转换成一个阻塞

可以实现同时监测多个文件描述符的异动

包括 可写性 可读性 出错

TCP 的服务器的代码里就有多个阻塞造成程序执行不正常情况

在 tcp 的服务端里的套接字

在 TCP 的服务端代码里共有两类套接字

分别是用来处理客户端的链接请求的套接字

通过 socket 函数创建而来

还有专门用来跟客户端通讯的套接字

通过 accept 函数得到

这两类套接字对应的函数

read和accept 都会产生阻塞

并且这两个阻塞会互相影响

想要解决这个问题可以考虑将多个阻塞转换成一个阻塞

当 socket 函数创建的文件描述符产生了可读性表示有新的客户端来链接

这个时候再去调用 accept 函数

当用来通讯的套接字产生了可读性表示有客户端发送了通讯的消息

这个时候再去调用 read/recv 函数

这种解决的方法叫做多路 io 复用

共有三种手段

select

poll

epoll

SELECT 机制相关的基础知识

select 这种多路 io 复用的方法

是将所有的要监测的文件描述符放入到一张表里

当表里的文件描述符产生了异动之后

select 函数会马上解除阻塞,会将所有的没有产生异动的文件描述符清空

我们就可以通过特定的宏查看哪个文件描述符还在表里

找到文件描述符以后再根据文件描述符的不同采用不同的动作

select 机制相关的函数

多路 io 复用

函数的头文件

#include 

函数的原型

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

函数的参数

int nfds, 监测的文件描述符里最大的那个描述符+1

fd_set *readfds, 可读表

fd_set *writefds, 可写表

fd_set *exceptfds, 出错表

struct timeval *timeout 超时时间 NULL 表示永久等待

函数的返回值

失败返回 -1

超时返回 0

成功返回 产生异动的文件描述符的个数

操作表相关的宏定义

将文件描述符从表里删除

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);

PS:由于表里的文件描述符产生异动了以后会将没有产生异动的文件描述符清除,所以在使用 select 这种多路 io 复用的时候,一般是创建两个一样的表,表 一个用来备份, 一个用来使用

实例

#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int MAX,i;
int sfd,cfd,ret;
struct sockaddr_in seraddr;//服务端结构体
struct sockaddr_in cliaddr;//连接结构体
socklen_t len=sizeof(cliaddr);
fd_set list,lock;
char buf[100]={0};

int main()
{
    sfd=socket(AF_INET,SOCK_STREAM,0);//1.创建套接字
    if(sfd < 0)
    {
        perror("socket");
        return -1;
    }
    
    seraddr.sin_family=AF_INET;//地址类型IPV4
    seraddr.sin_port=htons(4444);//端口号//转换小端到大端
    seraddr.sin_addr.s_addr=inet_addr("192.168.39.201");//将小端IP转换为大端IP
    ret= bind(sfd, (struct sockaddr *)&seraddr,sizeof(seraddr));//2.绑定 ip 端口信息 服务端自身IP
    if(ret < 0)
    {
        perror("bind");
        return -1;
    }
    
    //3.监听网络
    ret = listen(sfd,10);
    if(ret < 0)
    {
        perror("listen");
        return -1;
    }
    MAX=sfd;//刚开始最大描述符为sfd
    FD_ZERO(&lock);//清空lock
    FD_SET(sfd,&lock);//把sfd写入lock
    
    while(1)
    {
        
        list=lock;
        select(MAX+1,&list,NULL,NULL,NULL);//阻塞进行
        for(i=sfd;i MAX)
                    {
                        MAX=cfd;
                    }
                }
                else//不是sfd 就是有人发信息 - read
                {
                    if(read(i,buf,100)==0)//读信息
                    {
                        printf("客户端下线\n");
                        FD_CLR(i,&lock);//清除文件描述符
                    }
                    else
                    {
                        printf("buf=%s\n",buf);//打印发送的数据
                        for(int j=sfd+1;j

现象

系统编程之多路复用_第1张图片

POLL 轮询相关基础知识

poll 轮询将所有的要监测的文件描述符放入到一个结构体数组里

这个结构体数组里存放的有:

要监测的文件描述符 要监测的事件的类型 监测到的事件的类型

struct pollfd {

int fd; 要监测的文件描述符

short events; 要监测的事件的类型

		POLLIN 		可读

		POLLOUT 	可写

		POLLERR 	出错

short revents; 监测到的事件的类型

};

当要监测的文件描述符产生了异动以后 系统会将产生的异动的类型

对应的宏填入到结构体里的 revents 这个成员里

我们就可以遍历整个的结构体数组 找到 revents 有变化的那个套接字

通过套接字的类型判断接下来要调用的函数

POLL 轮询相关的函数

函数的头文件

include 

函数的原型

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数的参数

struct pollfd *fds, 要监测的文件描述符存放的位置

nfds_t nfds, 要监测的文件描述符的个数

int timeout 超时时间 -1 表示永久等待

函数的返回值

失败返回 -1

超时返回 0

有异动产生 返回产生异动的文件描述符的个数

实例

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int i, j, count;
int sfd, cfd, ret;
struct sockaddr_in seraddr; // 定义服务器地址结构体
struct sockaddr_in cliaddr; // 定义客户端地址结构体
socklen_t len = sizeof(cliaddr); // 定义客户端地址结构体的长度
char buf[100] = {0}; // 定义缓冲区,用于存储接收的数据
struct pollfd myfd[50]; // 定义 pollfd 结构体数组,用于存储文件描述符和事件信息

int main()
{
    sfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
    if (sfd < 0)
    {
        perror("socket");
        return -1;
    }

    seraddr.sin_family = AF_INET; // 设置地址类型为 IPv4
    seraddr.sin_port = htons(4444); // 设置端口号为 4444
    seraddr.sin_addr.s_addr = inet_addr("192.168.39.201"); // 设置服务器 IP 地址
    ret = bind(sfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); // 绑定套接字到地址
    if (ret < 0)
    {
        perror("bind");
        return -1;
    }

    ret = listen(sfd, 10); // 设置套接字为监听状态
    if (ret < 0)
    {
        perror("listen");
        return -1;
    }

    myfd[0].fd = sfd; // 将服务器套接字加入 pollfd 数组
    myfd[0].events = POLLIN; // 设置监听事件为可读事件
    count = 1; // 初始化计数器

    while (1)
    {
        ret = poll(myfd, count, -1); // 调用 poll 函数,阻塞等待事件发生
        if (ret < 0) // 检查 poll 函数是否出错
        {
            perror("poll");
            continue;
        }

        for (i = 0; i < count; i++)
        {
            if (myfd[i].revents & POLLIN) // 检查是否有可读事件
            {
                myfd[i].revents = 0; // 清除事件标志
                if (myfd[i].fd == sfd) // 如果是服务器套接字
                {
                    cfd = accept(sfd, (struct sockaddr *)&cliaddr, &len); // 接受客户端连接
                    if (cfd < 0)
                    {
                        perror("accept");
                        continue;
                    }

                    printf("新的套接字产生: %d\n", cfd);

                    // 检查是否超出数组范围
                    if (count >= sizeof(myfd) / sizeof(myfd[0]))
                    {
                        printf("连接数达到上限,拒绝新的连接\n");
                        close(cfd); // 关闭新的连接
                        continue;
                    }

                    myfd[count].fd = cfd; // 将新连接的套接字加入 pollfd 数组
                    myfd[count].events = POLLIN; // 设置监听事件为可读事件
                    count++;
                }
                else // 如果是客户端套接字
                {
                    memset(buf, 0, sizeof(buf)); // 清零缓冲区
                    ret = read(myfd[i].fd, buf, sizeof(buf)); // 读取数据
                    if (ret == 0) // 客户端断开连接
                    {
                        printf("客户端下线:%d\n", myfd[i].fd);
                        close(myfd[i].fd); // 关闭文件描述符
                        for (j = i; j < count - 1; j++) // 移除该文件描述符
                        {
                            myfd[j] = myfd[j + 1];
                        }
                        count--; // 减少计数器
                    }
                    else if (ret < 0) // 读取失败
                    {
                        perror("read");
                        close(myfd[i].fd); // 关闭文件描述符
                        for (j = i; j < count - 1; j++) // 移除该文件描述符
                        {
                            myfd[j] = myfd[j + 1];
                        }
                        count--; // 减少计数器
                    }
                    else // 成功读取数据
                    {
                        printf("buf=%s\n", buf);
                    }
                }
            }
        }
    }

    return 0;
}

epoll 机制

epoll 相关说明

epoll 是 poll 和 select 的一个加强版

epoll 将所有的要监测的文件描述符放到一个文件里,当要监测的文件描述符产生异动以后会将所有的产生异动的文件描述符的信息放到一个结构体数组

epoll 相关的 api

创建 epoll

函数的功能

创建 epoll 的文件描述符

函数的头文件

#include 

函数的原型

int epoll_create(int size);

函数的参数参数

int size:大于 1 的值

函数的返回值

成功返回 epoll 的文件描述符

失败返回 -1

管理文件描述符

函数的头文件

    #include 

函数的原型

int epoll_ctl(int epfd, int op, int fd, struct	epoll_event *event);

函数的参数

int epfd, epoll 的文件描述符

int op, 要执行的操作

		EPOLL_CTL_ADD 添加文件描述符到 epoll

		EPOLL_CTL_DEL 删除文件描述符

int fd, 要操作的对象 文件描述符

struct epoll_event *event 文件描述符的信息结构体

函数的返回值

成功返回 0

失败返回 -1

轮询 epoll 里的文件描述符

函数的功能

轮询 epoll

函数的头文件

#include 

函数的原型

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

函数的参数

int epfd,

epoll 的文件描述符

struct epoll_event *events, 用来存放产生异动的文件描述符的结构体

int maxevents, 最大产生异动的文件描述符的个数

int timeout -1 永久等待

函数的返回值

失败返回 -1

超时返回 0

有异动返回产生异动的个数

实例

#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


int sfd,cfd,ret,epid,count,i;
struct sockaddr_in seraddr;// 定义服务器地址结构体
struct sockaddr_in cliaddr;// 定义客户端地址结构体
socklen_t len=sizeof(cliaddr);// 定义客户端地址结构体的长度
char buf[100]={0};// 定义缓冲区,用于存储接收的数据
struct epoll_event myev[10];// 定义epoll事件数组
struct epoll_event ev;// 定义epoll事件结构体
int main()
{
	sfd=socket(AF_INET,SOCK_STREAM,0);//1.创建套接字
	if(sfd < 0)
	{
		perror("socket");
		return -1;
	}
	
	seraddr.sin_family=AF_INET;//地址类型IPV4
	seraddr.sin_port=htons(4444);// 端口号为4444,转换为网络字节序
	seraddr.sin_addr.s_addr=inet_addr("192.168.39.201");//将小端IP转换为大端IP
	ret= bind(sfd, (struct sockaddr *)&seraddr,sizeof(seraddr));//2.绑定 ip 端口信息 服务端自身IP
	if(ret < 0)
	{
		perror("bind");
		return -1;
	}
	
	//3.监听网络
	ret = listen(sfd,10);
	if(ret < 0)
	{
		perror("listen");
		return -1;
	}
	epid = epoll_create(1);// 创建epoll实例
	ev.events = EPOLLIN;// 设置epoll事件类型为可读事件
	ev.data.fd=sfd;// 将监听套接字加入epoll
	epoll_ctl(epid,EPOLL_CTL_ADD,sfd,&ev);


	while(1)
	{
		count = epoll_wait(epid,myev,10,-1); // 等待epoll事件,最多处理10
		for(i=0;i

你可能感兴趣的:(网络)