C语言select实现网络IO多路复用


摘要:这篇文章主讲IO多路复用,以及和阻塞/非阻塞IO对比,最后通过代码实例加深理解。
关键词:IO多路复用,阻塞/非阻塞IO。

1.IO多路复用

1.1 简介

1.I/O多路复用:允许同时对多个I/O进行控制,应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的。

2.基本常识
linux中每个进程最多可以打开1024个文件,最多有1024个文件描述符,文件描述符的特点:
2.1 非负整数;
2.2 从最小可用的数字来分配;
2.3 每个进程启动时默认打开0、1、2三个文件描述符。

3.注意
多路复用针对不止套接字fd,也针对普通的文件描述符fd。

4.步骤:
比较好的方法是使用IO多路复用,其基本思想是:
先构造一张又关描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行IO时函数才返回,函数返回时告诉进程那个描述符已就绪,可以进行IO操作。

1.2 优缺点

1.2.1 优点

节省CPU资源,高效利用进程和线程的资源。若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂。

1.2.2 缺点

当文件描述符很多的时候,多个文件之间需要获得CPU资源的的信号几乎短时间内让操作系统知道,select是对fd_set集合进行轮询,加上select一旦找到一个文件描述符进行操作,这个过程不能被打断,其他集合中的文件描述符只能进行等待。当集合中的文件描述符的数量足够多的时候,弊端也显而易见。(比如1万个文件描述符,其中10个在select函数中被筛出,0.1s后另外10个也有读写请求,假设前面10个文件处理完要1s,另外十个只能等待0.9s进行下一次的集合重整。)

1.3 阻塞/非阻塞IO

1.3.1阻塞I/O

大部分程序使用的都是阻塞模式的I/O
缺省情况下,套接字建立后所处于的模式就是阻塞I/O模式
读操作:read,recv,recvfrom
写操作:write,send
其他操作:accept,connect
读阻塞
进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发送阻塞它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据,如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。

写阻塞
在写操作时发送阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。
这时,写操作不进行任何拷贝工作,将发送阻塞。
一但发送缓冲区内有足够多的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。
UDP不用等待,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。

1.3.2 非阻塞IO

可防止进程阻塞在I/O操作上,需要轮询(最常见的就是我们第一次玩单片机搞流水灯)。
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核"当我请求的I/O操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停的测试是否一个文件描述符有数据可读(polling)。
应用程序不停的polling内核来检查是否I/O操作已经就绪,这将是一个极浪费CPU资源的操作。
这种模式使用中不普遍。
1.fcntl()函数
当你一开始建立一个套接字描述符的时候,系统内核 将其设置为阻塞IO模式。可以使用函数fcntl()设置一个套接字的标志位为O_NOBLOCK来实现非阻塞。

int fcntl(int fd, int cmd, long arg) 
int flag; 
flag = fcntl(sockfd,F_GETFL,0) 
flag |= O_NOBLOCK; 
fcntl(sockfd,F_SETFL,flag); 

2.ioctl()函数

int b_on = 1; 
ioctl(sock_fd, FIONBIO,&b_on); 

2. 函数介绍

2.1 fd_set集合编辑函数

void FD_zero(fd_set* fdset)  //对集合清零
void FD_set(int fd,  fd_set * fdset) //把fd加入集合
void FD_CLR(int fd,fd_set *fdset) //从集合中清除fd
int FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fd_set中
	return: fd存在返回1,不存在返回0

2.1 select函数

select:
可以同时监视多个文件描述符,当其中任何一个文件描述符有可读、可写或异常状态变化时,select函数会返回,告知进程哪些文件描述符可以进行相应的 I/O 操作。但select有一些限制,比如文件描述符数量有限制等。
poll:
与select类似,但在一些方面进行了改进,如没有文件描述符数量的严格限制。(使用链表储存fd)
epoll:
是一种高效的 I/O 多路复用机制。它通过事件触发的方式,当文件描述符状态发生变化时,内核会主动通知进程,避免了像select和poll那样的轮询操作,从而大大提高了性能。

int select(int nfds, fd_set*readfds, fd_set*writefds,fd_set*exceptfds,struct timeval *timeout);
nfds: 最大文件描述符+1
readfds: 读fd集合
writefds: 写fd集合
exceptfds: 异常fd集合
timeout: 阻塞时间,总时间:tv_sec+tv_usec
	假如tv_sec = 2, tv_usec = 100,最后时间2.1s,也就是说如果select阻塞在这,fd_set集合中的所有fd在这2.1s内没有请求活动,那select返回0退出阻塞,进行下一次集合重设。
		struct timeval{
			long tv_sec;	//秒
			long tv_usec;   //毫秒
		}
返回值:返回read,write,execpt三种集合的总活动fd数目,如果三种集合活动fd数目为0,正常返回0,错误返回-1,并且设置errno,fd集合在select之后只剩下活动的,timeout被重置。

3. 框架搭建

3.1框图

在这里插入图片描述

3.2 代码框架

3.2.1 client

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int main(int argc, char* argv[])
{
	//1.socket
	socket();
	...

	//IPV4网络结构体
	struct sockaddr_in XXX;
	...
	
	//2.connect
	connect();
	...

	//fd_set集合操作
	fd_set rset;    //集合变量
	struct timeval tout;  //时间结构体
	tout.tv_sec = XXX;
	tout.tv_usec = XXX;
	while(1)
	{
		FD_ZERO(&rset);  //reset rset
		FD_SET(0, &rset);   //append 0 to rset
		FD_SET(fd, &rset);   //adppend fd to rset
		int maxfd = fd;    //max fd
		select(maxfd+1, &rset, NULL, NULL, &tout);
		if(FD_ISSET(0, &rset)) //stdin have data
		{
			
		}

		if(FD_ISSET(fd, &rset)) //service have data
		{
			
		}	
	}

	close(fd);
	return 0;

}

3.2.2 server

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>

void *pthread_handle(void *arg);
int main()
{

	//1.socket
	socket();
	...
	
	//IPV4网络结构体初始化
	struct sockaddr_in XXX;
	...
	
	//2.bind
	bind();
	...
	
	//3.listen
	listen();
	...
	
	while(1)
	{
		//4.accept
		accept();
		...	
		//5.创建线程和处理函数
		pthread_create();
		...
	}
	close(sockfd);
	return 0;

}

void *pthread_handle(void *arg)
{
	//数据处理
	...
	read();
	...
	write();
	...
	}
}

4. 实例

4.1 client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#define SERV_RESP_STR "SERVICE:"
#define QUIT_FLAG "QUIT"
#define BUF_SIZE 1024
#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.119.141"
int main(int argc, char* argv[])
{
	if(argc<3)
	{
		exit(1);
	}
	int fd;
	struct sockaddr_in sin;
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
		perror("socket");
		exit(1);
	}

	bzero(&sin, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(atoi(argv[2]));
	sin.sin_addr.s_addr = inet_addr(argv[1]);
	int ret;
	ret = connect(fd, (struct sockaddr*)&sin, sizeof(sin));
	if(ret < 0)
	{
		perror("connect");
		exit(1);
	}
	char writeBuf[BUF_SIZE];
	char readBuf[BUF_SIZE];
	fd_set rset;
	struct timeval tout;
	tout.tv_sec = 5;
	tout.tv_usec = 0;
	//int ret = 0;
	while(1)
	{
		FD_ZERO(&rset);  // reset rset data
		FD_SET(0, &rset);
		FD_SET(fd, &rset);

		int maxfd = fd;
		select(maxfd+1, &rset, NULL, NULL, &tout);
		if(FD_ISSET(0, &rset)) //stdin have data
		{
			bzero(writeBuf, BUF_SIZE);
			do
			{
				ret = read(0, writeBuf, BUF_SIZE-1);		
			}while(ret<1);
			if(ret<0)
			{
				perror("read");
				break;
			}
			else if(ret== 0)
			{
				printf("no data\n");
				continue;
			}
			if(write(fd, writeBuf, strlen(writeBuf))<0)
			{
				perror("write");
				continue;
			}
			if(!strncasecmp(writeBuf, QUIT_FLAG, strlen(QUIT_FLAG)))
			{
				printf("client exited\n");
				break;
			}
		}

		if(FD_ISSET(fd, &rset)) //service have data
		{
			bzero(writeBuf, BUF_SIZE);

			do
			{
				ret = read(fd, writeBuf, BUF_SIZE-1);
			}while(ret<1);
			if(ret<0)
			{
				perror("read");
				continue;
			}
			else if(!ret) continue;
			printf("service said: %s\n", writeBuf);
			if(!strncasecmp(writeBuf, QUIT_FLAG, strlen(QUIT_FLAG)))
			{
				printf("client exit\n");
				break;
			}
		}	
	}

	close(fd);
	return 0;

}

4.2 server.c

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>

#define QUIT_FLAG "quit"
#define BUF_SIZE 256
#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.119.152"
#define SERV_RESP "SERV:"
void *pthread_handle(void *arg);
int main()
{
	int sockfd;
	//1.socket
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd==-1)
	{
		perror("socket");
		return -1;
	}
	//2.bind
	//2.1 handling protocol
	struct sockaddr_in sin,cin;
	bzero(&sin, sizeof(sin));
	bzero(&cin, sizeof(cin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_PORT);
	sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);

	if(bind(sockfd,(struct sockaddr*)&sin, sizeof(sin))==-1)
	{
		perror("bind");
		return -2;
	}
	//3.listen
	if(listen(sockfd, 5)==-1)
	{
		perror("listen");
		return -3;
	}
	int newfd = 0;
	pthread_t pthid;
	socklen_t addrlen = sizeof(cin);
	while(1)
	{
		//4.accept
		printf("connecting......\n");
		newfd = accept(sockfd, (struct sockaddr*)&cin, &addrlen);
		if(newfd==-1)
		{
			perror("accept");
			return -4;
		}
		pthread_create(&pthid, NULL, pthread_handle, (void *)&newfd);
		pthread_detach(pthid);
	}
	close(sockfd);
	return 0;

}

void *pthread_handle(void *arg)
{
	char readBuf[BUF_SIZE] = {0};
	char respBuf[BUF_SIZE] = {0};
	int ret = 0;
	int fd = *(int*)arg;
	while(1)
	{
		memset(readBuf, 0,BUF_SIZE);
		ret = read(fd, readBuf, BUF_SIZE); 	
		if(ret<0)
		{
			perror("read");
			break;
		}
		else if(!ret) continue;
		printf("the content of readBuf is %s\n", readBuf);
		if(!strncasecmp(readBuf, QUIT_FLAG, strlen(QUIT_FLAG)))
		{
			printf("the thread exited\n");
			break;
		}
		else
		{
			strncpy(respBuf, SERV_RESP, strlen(SERV_RESP));
		        strcat(respBuf, readBuf);
			write(fd, respBuf, BUF_SIZE);
			memset(respBuf, 0, BUF_SIZE);
		}
	}
}

4.3 终端结果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值