Linux网络编程——I/O多路复用

本文介绍了UNIX/Linux下的四种I/O模型:阻塞I/O、非阻塞I/O、I/O多路复用和信号驱动I/O,并详细解释了I/O多路复用的三种机制:select、poll和epoll的工作原理及应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习笔记,小白可以相互学习,大佬看到能告诉咱理解不对的地方就好了。


I/O模型

在UNIX/Linux下主要有4种i/o模型:

1.阻塞i/o:最常用,最简单,效率最低。

大部分程序使用的就是阻塞i/o模式,缺省下,套接字建立后所处于的模式就是它。

2.非阻塞i/o:可以防止进程在i/o操作上,需要轮询。

用程序不停地来检查i/o操作是否就绪,这个是非常浪费cpu资源的操作。

可以使用fcntl()设置一个套接字的标志为O_NONBLOCK来实现非阻塞

int fcntl(int fd,int cmd,long arg)

int flag; flag = fcntl(sockfd,F_GETFL,0);//第二个参数是获得属性命令

flag |= O_NONBLOCK;  //设置非阻塞的属性

fcntl = (sockfd,F_GETFL,flag);  //再把非阻塞的属性赋给sockfd

3.i/o多路复用:允许同时对多个i/o进行控制

4.信号驱动i/o:一种异步通信模型


I/O多路复用

  应用程序中同时处理多路输入输出流,

若采用阻塞模式,将得不到预期的目的;

若采用非阻塞模式,对多个输入进行轮询,但又太浪费cpu时间。

若设置多个进程分别处理一条数据路,将产生进程间同步与通信问题,使程序变得复杂。

最好的方法是使用i/o多路复用。


i/o多路复用的基本思想是:

1. 将所有要处理的文件描述符存储到一张表当中;
2. 检测表当中有没有已经就绪的文件描述符,如果有返回就绪的文件描述符;
3. 轮询所有就绪的文件描述符
while(1) {
if(listenfd)
说明是新的客户端发起了连接请求;
if(connfd)
说明是已经连接的客户端发送了数据请求;
if(普通文件)
的到普通文件的数据;
if(0)
标准输入
}

根据处理细节的不同,分为三种类型:


select()机制:

1. 根据描述符处理事件不同,创建不同的表,例如将所有要读的文件描述符添加到一张读的表中
2. 检测表当中是否有就额绪的文件描述符,如果有返回有的状态,同时返回就绪的文件描述符
3. 轮询 :a找到那些文件描述符就绪;b处理数据

函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

功能:检测是否有文件描述符处于就绪状态,有返回。没有就一直阻塞。

参数:nfds:           最大文件描述符+1

  readfds:读的文件描述符表
  writefds: 写的文件描述符表
  exceptfds: 错误处理文件描述符表
  timeout: 超时时间;不设置用NULL,表示一直阻塞

返回:成功返回准备就绪的文件描述符的个数;失败返回-1;超时返回0;

宏函数:
         void FD_CLR(int fd, fd_set *set); //将fd从集合set当中清除;
         int  FD_ISSET(int fd, fd_set *set); //判断fd是否在集合set当中;
         void FD_SET(int fd, fd_set *set); //将fd添加到集合set当中;
         void FD_ZERO(fd_set *set); //清空集合set

int main()
{
    int ret;
    int nfds;
    char buf[256];
    struct timeval time;
    /*创建集合*/
    fd_set readfds;
    FD_ZERO(&readfds);
    /*添加所要处理的文件描述付*/
    FD_SET(0,&readfds);
    nfds = 1;
    memset(buf,0,sizeof(buf));
    /**如果不设置,表示select阻塞读集合,直到有文件描述府就绪,select函数返回*/
    /*设置之后,select函数阻塞3s,如果达到超时时间,改为非阻塞模式,不管有没有文件描述府,都返回*/

    while(1)
    {
        time.tv_sec = 3;
        time.tv_usec = 0;
        fd_set rfds = readfds;
        /*检测继续的文件描述府*/
        ret = select(nfds,&rfds,NULL,NULL,&time);
        if(-1 == ret)
        {
            perror("select");
            return -1;
        }
        else if(0 ==ret)
        {
            printf("time out....\n");
            continue;
        }
             
        int fd;
        for(fd = 0;fd < nfds; fd++)
        {

            if (FD_ISSET(fd,&rfds))
            {
                ret = read(0,buf,sizeof(buf));
                if(-1 == ret)
                {
                    perror("read");
                    return -1;
                }
                printf("buf: %s\n",buf);
            }
        }
        memset(buf,0,sizeof(buf));
    }
    return 0;
}



poll()机制:

1. 创建一个集合;添加(文件描述符及其所要处理的事件)
2. 检测集合当中是否有事件发生(是否有文件描述符准备就绪);如果有返回有的状态,同时返回就绪的文件描述符及事件;
3. 轮询 :a. 找所发生的事件;b. 处理数据

函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数功能:检测是否有文件描述符和事件处于就绪状态,有返回。没有一直阻塞。

参数: fds:事件和文件描述符的集合;
   struct pollfd {
              int   fd;               /* 文件描述符*/
                short events;     /* 请求的事件 */
                short revents;    /*返回事件 */
            };
nfds: 最大文件描述符+1;
timeout: 超时时间;ms级

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <poll.h>

int client(int connfd)
{
	int ret;
	char buf[256];
	memset(buf, 0, sizeof(buf));
	ret = read(connfd, buf, sizeof(buf));
	if (ret == -1) {
		perror("server->read");
		return -1;
	} else if (ret == 0) {
		close(connfd);
		return -1;
	}

	printf("buf : %s\n", buf);
	ret = write(connfd, buf, sizeof(buf));
	if (ret == -1) {
		perror("server->write");
		return -1;
	}
	return 0;
}

int main(int argc, char *argv[])
{
	int listenfd;
	int ret;
	socklen_t addrlen;
	int connfd;
	pid_t pid;
	char buf[256];
	struct sockaddr_in srvaddr;
	struct sockaddr_in cltaddr;

	/* 1. 创建服务器(创建一socket套接字);socket */
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (listenfd == -1) {
		perror("server->socket");
		return -1;
	}
	printf("create listenfd = %d success\n", listenfd);

	/* 2. 设置服务器的IP地址和端口号(将socket和服务器的IP地址和端口号进行绑定);bind */
	memset(&srvaddr, 0, sizeof(struct sockaddr_in));
	srvaddr.sin_family = AF_INET;
	srvaddr.sin_port = htons(9999);
	srvaddr.sin_addr.s_addr = inet_addr("192.168.2.100");
	ret = bind(listenfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr));

	printf("port : %d\n", ntohs(srvaddr.sin_port));
	if (ret == -1) {
		perror("server->bind");
		return -1;
	}
	printf("bind success !\n");

	/* 3. 启动监听(启动服务器);  listen */
	ret = listen(listenfd, 1024);
	if (ret == -1) {
		perror("server->listen");
		return -1;
	}
	printf("listen success !\n");

	/* 创建集合,并且将集合当中的每一个数组元素的fd成员赋值为-1;*/
	int nfds;
	int fd;
	int i;
	int j;
	struct pollfd fds[1024];
	for (i = 0; i < 1024; i++) {
		fds[i].fd = -1;
	}

	/* 添加所需要处理的事件和文件描述符 */
	fds[0].fd = listenfd;
	fds[0].events = POLLIN;
	nfds = listenfd+1;

	while(1) {
		/* 检测集合当中是否有继续的文件描述符 */
		ret = poll(fds, nfds, 5000);
		if (ret == -1) {
			perror("poll");
			return -1;
		} else if (ret == 0) {
			printf("timeout\n");
			continue;
		}

		/* 轮循 */
		for (i = 0; i < nfds; i++) {
			/* 判断是什么事件  */
			if (POLLIN == fds[i].revents) {
				/* 判断,寻找就绪的文件描述符 */
				if (fds[i].fd != -1) {
					fd = fds[i].fd;
					/* 如果是监听套接字listenfd,则建立连接 */
					if (fd == listenfd) {
						memset(&cltaddr, 0, sizeof(cltaddr));
						addrlen = sizeof(socklen_t);
						connfd = accept(listenfd, (struct sockaddr *)&cltaddr, &addrlen);
						if (connfd == -1) {
							perror("accept");
							return -1;
						}

						printf("connfd = %d\n", connfd);
						for (j = 0; j < 1024; j++) {
							if (fds[j].fd != -1) {
								continue;
							}
							fds[j].fd = connfd;
							fds[j].events = POLLIN;
#if 0
							if (nfds <= connfd) {
								nfds = connfd + 1;				
							}
#endif
							nfds = nfds <= connfd ? connfd+1 : nfds;
							break;
						}
					} else {
						ret = client(fd);
						if (ret == -1) {
							fds[i].fd = -1;
						}
					}
				}
			}
		}
	}
	close(listenfd);

	return 0;
}

epoll机制:
1. 创建一个集合,添加(文件描述符及其所要处理的事件);
2. 检测集合当中是否有事件发生(是否有文件描述符准备就绪);如果有返回有的状态,同时返回就绪的文件描述符及事件的集合;
3. 轮询 :a. 找所发生的事件;b. 处理数据;

int epoll_create(int size);
功能:创建一个epoll实例
参数: size:epoll实例所能处理的文件描述符的最大个数。而不是容纳的文件描述符的个数。
返回值:成功,返回一个非负的文件描述符。错误,返回- 1,并设置errno
  
  
  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:设置epoll实例(新增、修改、删除);
参数:epfd:epoll实例
op:执行的动作:
EPOLL_CTL_ADD (新增)
              EPOLL_CTL_MOD (修改)
           EPOLL_CTL_DEL (删除)
fd:文件描述符,
event:事件;
typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
            } epoll_data_t;

           struct epoll_event {
               uint32_t     events;      /* epoll的事件 */
               epoll_data_t data;        /* 用户数据变量 */
           };


       The events member is a bit set composed using the following available event types:

事件的成员是一个点集组成使用下面提供的事件类型:

       EPOLLIN

The associated file is available for read(2) operations.相关文件可供阅读(2)的操作。

        EPOLLOUT
              The associated file is available for write(2) operations.相关文件可供阅读(2)的操作。


int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:等待检测事件;
参数:
epfd:epoll实例;
events:事件集合(存储的是,准备就绪的文件描述符和对应事件集合);
maxevents:表示最大值。
timeout:超时时间;ms级;
返回值:
成功返回准备就绪的文件描述符的个数;失败返回-1;超时返回0;


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

int client(int connfd)
{
	int ret;
	char buf[256];
	memset(buf, 0, sizeof(buf));
	ret = read(connfd, buf, sizeof(buf));
	if (ret == -1) {
		perror("server->read");
		return -1;
	} else if (ret == 0) {
		return -1;
	}

	printf("buf : %s\n", buf);
	ret = write(connfd, buf, sizeof(buf));
	if (ret == -1) {
		perror("server->write");
		return -1;
	}
	return 0;
}

int main(int argc, char *argv[])
{
	int listenfd;
	int ret;
	socklen_t addrlen;
	int connfd;
	pid_t pid;
	char buf[256];
	struct sockaddr_in srvaddr;
	struct sockaddr_in cltaddr;

	/* 1. 创建服务器(创建一socket套接字);socket */
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (listenfd == -1) {
		perror("server->socket");
		return -1;
	}
	printf("create listenfd = %d success\n", listenfd);

	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	/* 2. 设置服务器的IP地址和端口号(将socket和服务器的IP地址和端口号进行绑定);bind */
	memset(&srvaddr, 0, sizeof(struct sockaddr_in));
	srvaddr.sin_family = AF_INET;
	srvaddr.sin_port = htons(9999);
	srvaddr.sin_addr.s_addr = inet_addr("192.168.2.100");
	ret = bind(listenfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr));

	printf("port : %d\n", ntohs(srvaddr.sin_port));
	if (ret == -1) {
		perror("server->bind");
		return -1;
	}
	printf("bind success !\n");

	/* 3. 启动监听(启动服务器);  listen */
	ret = listen(listenfd, 1024);
	if (ret == -1) {
		perror("server->listen");
		return -1;
	}
	printf("listen success !\n");

	/* 创建集合;*/
	int epfd;
	int i;
	int fd;

	epfd = epoll_create(1024);
	if (epfd == -1) {
		perror("epoll_create");
		return -1;
	}
	printf("epfd = %d\n", epfd);

	/* 添加所需要处理的事件和文件描述符 */
	struct epoll_event event;
	event.events = EPOLLIN;
	event.data.fd = listenfd;
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);
	if (ret == -1) {
		perror("epoll_ctl->EPOLL_CTL_ADD");
		return -1;
	}

	struct epoll_event events[1024];
	while(1) {
		/* 检测集合当中是否有继续的文件描述符 */
		ret = epoll_wait(epfd, events, 1024, 5000);
		if (ret == -1) {
			perror("poll");
			return -1;
		} else if (ret == 0) {
			printf("timeout\n");
			continue;
		}

		/* 轮循 */
		for (i = 0; i < ret; i++) {
			/* 判断是什么事件  */
			if (EPOLLIN == events[i].events) {
				fd = events[i].data.fd;
				/* 如果是监听套接字listenfd,则建立连接 */
				if (fd == listenfd) {
					memset(&cltaddr, 0, sizeof(cltaddr));
					addrlen = sizeof(socklen_t);
					connfd = accept(listenfd, (struct sockaddr *)&cltaddr, &addrlen);
					if (connfd == -1) {
						perror("accept");
						return -1;
					}

					printf("connfd = %d\n", connfd);
					event.events = EPOLLIN;
					event.data.fd = connfd;
					ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
					if (ret == -1) {
						perror("epoll_ctl->EPOLL_CTL_ADD");
						return -1;
					}
				} else {
					ret = client(fd);
					if (ret == -1) {
						ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
						if (ret == -1) {
							perror("epoll_ctl->EPOLL_CTL_DEL");
							return -1;
						}
						close(fd);
					}
				}
			}
		}
	}
	close(epfd);
	close(listenfd);

	return 0;
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值