2020/2/22 Linux Socket编程 高级篇——IO多路复用

前面介绍的函数,如:recv、send、read、write都是阻塞性函数,如果资源没有准备好,调用该函数的进程将进入阻塞状态。

本节将介绍两种多路复用的解决方案。


1 IO多路复用(多路转换)

(1)利用fcntl函数实现(非阻塞方式)

fcntl用来操作文件描述符(如套接字,套接字是抽象出来的概念,本质上也是文件描述符)的一些函数,

参数是fd(套接字描述符)和cmd(操作)。

主控线程:产生fd,维护一个fd的动态数组

子线程:遍历动态数组中所有fd,并通过这写fd和对应的客户端进行双向通信(采用非阻塞read/write)


动态数组+fcntl()实现IO多路复用

源代码

vector_fd.h

#ifndef __VECTOR_H__
#define __VECTOR_H__

typedef struct{
	int		*fd;
	int		counter;
	int		max_counter;
}VectorFD;

extern VectorFD* create_vector_fd(void);//创建数组
extern void 	 destroy_vector_fd(VectorFD *);//销毁数组
extern int  	 get_fd(VectorFD *, int index);//根据下标获得某个套接字描述符
extern void 	 remove_fd(VectorFD *, int fd);//移除某个套接字描述符
extern void 	 add_fd(VectorFD *, int fd);//增加某个套接字描述符

#endif

 vector_fd.c

#include <malloc.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>
#include "vector_fd.h"

static void encapacity(VectorFD *vfd)
{
	if(vfd->counter >= vfd->max_counter){
		int *fds = (int*)calloc(vfd->counter + 5, sizeof(int));
		assert(vfd != NULL);
		memcpy(fds, vfd->fd, sizeof(int) * vfd->counter);
		free(vfd->fd);
		vfd->fd = fds;
		vfd->max_counter += 5;
	}
}

static int indexof(VectorFD *vfd, int fd)
{
	int i = 0;
	for(; i < vfd->counter; i++)
	{
		if(vfd->fd[i] == fd) return i;	
	}
	return -1;
}

VectorFD* create_vector_fd(void)
{
	VectorFD *vfd = (VectorFD*)calloc(1,sizeof(VectorFD));//创建一个VectorFD结构体,对结构体成员fd赋为一个动态数组
	assert(vfd != NULL);
	vfd->fd = (int*)calloc(5, sizeof(int));//初始动态数组中可以存放5个socket描述符
	assert(vfd->fd != NULL);
	vfd->counter = 0;
	vfd->max_counter = 0;
	return vfd;
}

void 	 destroy_vector_fd(VectorFD *vfd)
{
	assert(vfd != NULL);
	free(vfd->fd);
	free(vfd);
}

int  	 get_fd(VectorFD *vfd, int index)
{
	assert(vfd != NULL);
	if(index < 0 || index > vfd->counter-1)
		return 0;
	return vfd->fd[index];
}

void 	 remove_fd(VectorFD *vfd, int fd)
{
	assert(vfd != NULL);
	int index = indexof(vfd, fd);
	if(index == -1) return;
	int i = index;
	for(; i < vfd->counter-1; i++){
		vfd->fd[i] = vfd->fd[i+1];
	}
	vfd->counter--;
}

void 	 add_fd(VectorFD *vfd, int fd)
{
	assert(vfd != NULL);
	encapacity(vfd);//扩展动态数组
	vfd->fd[vfd->counter++] = fd;
	
}

编译运行:编译此文件需要包含-Iinclude——增加头文件的搜索路径

gcc -o obj/vector_fd.o -Iinclude -c src/vector_fd.c 


在TCP多线程基础上进行修改,改多线程的阻塞性读写为非阻塞性——

1) 不再采用自定义发送接收message的协议,只采用标准的read/write

2) 包含#include "vector_fd.h"头文件

#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <signal.h>
#include <time.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/wait.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include "vector_fd.h"

//#define INADDR_ANY (uint32_t)0x00000000
VectorFD *vfd;

int sockfd;

void sig_handler(int signo)
{
	if(signo == SIGINT){
		printf("server close\n");
		/*step 6 close socket*/
		close(sockfd);
		//销毁动态数组
		destroy_vector_fd(vfd);
		exit(1);//0 or 1 all can close
	}
}

/*
fd对应于某个连接的客户端
和某一个连接的客户端进行双向通信(非阻塞方式)
*/
void do_service(int fd)
{
	char buff[512];
	memset(buff, 0, sizeof(buff));
	//因为采用非阻塞方式,若读不到数据直接返回,直接服务下一个客户端,所以不用判断size小于0的情况
	size_t size = read(fd, buff, sizeof(buff));
	if(size == 0){//客户端已经关闭连接
        //不要用标准IO来输出,因为标准IO带缓存功能可能会出现问题,建议用内核提供的read/write函数往终端输出信息
		char info[] = "client closed";
		write(STDOUT_FILENO, info, sizeof(info)); 
		//从动态数组中删除对应的fd
		remove_fd(vfd, fd);
		//关闭对应客户端的socket
		close(fd);
	}
	else if(size > 0){	
		write(STDOUT_FILENO, buff, sizeof(buff));
		if(write(fd, buff, size) < 0){//客户端关闭连接,产生EPIPE,跳出进程
			if(errno == EPIPE){
				perror("write error");
				remove_fd(vfd, fd);
				close(fd);
			}
		}
	}
	
}

void out_addr(struct sockaddr_in *clientaddr)
{
	char ip[16];
	memset(ip, 0, sizeof(ip));
	int port = ntohs(clientaddr->sin_port);
	inet_ntop(AF_INET, &clientaddr->sin_addr.s_addr, ip, sizeof(ip));
	printf("%s(%d) connected!\n", ip, port);
}

//定义线程运行函数,子线程不断地遍历动态数组,然后与客户端进行非阻塞读写操作
void* th_fn(void *arg)
{
	int i;
	while(1){
		i = 0;
		//遍历动态数组中的socket描述符
		for(; i < vfd->counter; i++){
			do_service(get_fd(vfd, i));
		}
	}

	return (void*)0;
}


int main(int argc, char *argv[])
{
	if(argc < 2){
		printf("usage: %s #port\n", argv[0]);//打印程序的名字(包括路径)
		exit(1);
    }
	if(signal(SIGINT, sig_handler) == SIG_ERR){//SIGINT:ctrl+C can end ; signal function get SIG_ERR if error
		perror("signal sigint error");         //perror function is defined in <stdlib.h>,we can see" str: error message"
		exit(1);
	}
	

	/*step 1 creat socket
	 *socket is a struct in kernel
	 *AF_INET: IPV4
	 *SOCK_STREAM: tcp protocol(udp: SOCK_DGRAM)
	 **/
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0){
		perror("socket error");
		exit(1);
	}

	/*step 2 bind()
	 *bound with socket and address(ip\port\intnet type)
     *sockaddr_in is special net_struct for internet */
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));  //clear
	serveraddr.sin_family = AF_INET;	 	     //IPV4(Host byte order is ok)
	serveraddr.sin_port   = htons(atoi(argv[1]));//port from terminal(atoi:string to int)(htons:Host to Network byte order 16bit)
	serveraddr.sin_addr.s_addr = INADDR_ANY;     //htons("192.168.0.10")
	if(bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0){//cast as common address type : sockaddr
		perror("bind error");
		exit(1);
 	}

	/*step 3 listen()
	 *tell system to accept connecting request (in server port)
	 *put connecting request to queue*  (10 is the length of queue)
	 */
	if(listen(sockfd, 10) < 0){
		perror("listen error");
		exit(1);	
	}
	
	/*step 4 accept()
	 *get a connection and return the new socket file descriptor(sockfd)
	 *This sockfd(client's fd) is different from the sockfd in step 1(server's fd)
	 */
	

	//创建放置套接字描述符fd的动态数组
	vfd = create_vector_fd();
	//设置线程的分离属性
	pthread_t th;
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

	int err;
	if((err = pthread_create(&th, &attr, th_fn, (void*)0)) != 0){
		perror("pthread create error");
		exit(1);
	}
	pthread_attr_destroy(&attr);

	/* 1)主控线程负责去调用accept获得客户端的连接,将新的socket描述符放置到动态数组中
	 * 2)启动的子线程负责遍历动态数组中socket描述符并和对应的客户端进行双向通信(采用非阻塞读写)	
   	 */
	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);//地址结构体,用于放置客户端地址信息

	while(1){
		int fd = accept(sockfd,	(struct sockaddr*)&clientaddr, &len);
		if(fd < 0) {
 			perror("accept error");
			continue;
		}
		out_addr(&clientaddr);
		//将读写修改为非阻塞方式 fcntl
		int val;
		fcntl(fd, F_GETFL, &val);//通过F_GETFL宏命令获得原来套接字对应的状态标志,放到对应的val里
		val |= O_NONBLOCK;
		fcntl(fd, F_SETFL, val);//非阻塞方式
		//将返回新的socket描述符加入到动态数组中
		add_fd(vfd, fd);


	}
	return 0;
}

编译运行:编译此文件需要包含-Iinclude——增加头文件的搜索路径

pthread是动态库,需要用-lpthread在链接阶段链接pthread库,所有的动态库都需要用-lxxx来引用

 gcc -o bin/echo_tcp_server_fcntl -Iinclude obj/vector_fd.o src/echo_tcp_server_fcntl.c -lpthread

通过这样的方式,可以实现IO多路复用,非阻塞方式的通信




(2)利用select函数实现

在src文件夹里对.h里的函数进行定义

#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
int select (int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回:准备就绪的描述符数,若超时则为0,若出错则为-1

struct timeval{//设置超时
     long tv_sec;//秒
     long tv_usec;//微秒
};

参数:

maxfdp1: 最大fd加1,在三个描述符集中找到最高描述符编号值+1, 作为函数的第一个参数

readfds、writefds、exceptfds(不常用):指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写和处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。

timeout:指定愿意等待的时间,有三种值:NULL(永远等待,直到捕捉到信号或文件描述符已经准备好);具体值(struct timeval类型的指针,等待timeout时间还没有文件描述符准备好,就立刻返回);0(从不等待,测试所有指定的描述符并立刻返回)

函数说明: 

select()用来等待文件描述符状态的改变。参数maxfdp1代表最大的文件描述符加1,参数readfds、writefds和exceptfds 称为描述符集,是用来回传该描述符的读,写或异常状况。

用户调用select()函数会产生阻塞,委托内核检查传进去的描述符集是否可以使用,返回的是准备就绪的描述符个数,超过规定的检查时间返回0,出错返回-1。

告诉内核:我们所关心的描述符;对于每个描述符所关心的条件(可读?可写?异常情况?);希望等待多长时间

select返回:已经准备好的描述符数量;哪个描述符已经准备好了条件;使用这种返回值,可以调用相应的IO函数,并确知函数不会阻塞

select函数根据希望进行的文件操作,对文件描述符分类处理,

文件描述符的处理主要涉及了四个宏函数:

FD_ZERO(fd_set *set);清楚一个文件描述符集

FD_SET(int fd,fd_set*set);将一个文件描述符加入到某描述符集中

FD_CLR(int fd,fd_set* set);将一个文件描述符从某个某描述符集中清除

FD_ISSET(int fd,fd_set *set);测试描述符集set中相关fd的位是否有变化

使用方法:

使用select函数前,要用FD_ZERO和FD_SET来初始化文件描述符集;

使用select函数时,可循环使用FD_ISSET测试描述符集

在执行完成对相关的文件描述符的操作后,要使用FD_CLD来清空描述符集。


动态数组+select()实现IO多路复用

源代码

#include ...

//#define INADDR_ANY (uint32_t)0x00000000
VectorFD *vfd;

int sockfd;

void sig_handler(int signo)
{}

/*
fd对应于某个连接的客户端
和某一个连接的客户端进行双向通信(非阻塞方式)
*/
void do_service(int fd)
{
	char buff[512];
	memset(buff, 0, sizeof(buff));
	//因为采用非阻塞方式,若读不到数据直接返回,直接服务下一个客户端,所以不用判断size小于0的情况
	size_t size = read(fd, buff, sizeof(buff));
	if(size == 0){//客户端已经关闭连接
		char info[] = "client closed";
		write(STDOUT_FILENO, info, sizeof(info));
		//从动态数组中删除对应的fd
		remove_fd(vfd, fd);
		//关闭对应客户端的socket
		close(fd);
	}
	else if(size > 0){	
		printf("%s\n", buff);
		if(write(fd, buff, size) < 0){//客户端关闭连接,产生EPIPE,跳出进程
			if(errno == EPIPE){
				perror("write error");
				remove_fd(vfd, fd);
				close(fd);
			}
		}
	}
	
}

void out_addr(struct sockaddr_in *clientaddr)
{}

//遍历出动态数组中所有的描述符,并加入到描述符集set中
//同时此函数返回动态数组中最大的描述符标号
int add_set(fd_set *set)
{
	FD_ZERO(set);
	int max_fd = vfd->fd[0];
	int i = 0;
	for(; i < vfd->counter; i++){
		int fd = get_fd(vfd, i);
		if(fd > max_fd) max_fd = fd;
		FD_SET(fd, set);//将fd加入到描述符集中
	}
	return max_fd;
}

//定义线程运行函数,子线程不断地遍历动态数组,然后与客户端进行非阻塞读写操作
void* th_fn(void *arg)
{
	struct timeval t;//超时时间 2s
	t.tv_sec = 2;
	t.tv_usec = 0;
	int n = 0;
	int maxfd;
	fd_set set;//描述符集
	maxfd = add_set(&set);
	//调用select函数会阻塞,委托内核检查传入的描述符是否准备好,若有则返回准备好的描述符数量,超时返回0
	//第一个参数是描述符的范围(最大描述符+1)
	while((n = select(maxfd+1, &set, NULL, NULL, &t)) >= 0){
		if(n > 0){
			//检测哪些描述符准备好,并和准备好的描述符对应客户端进行双向通信
			int i = 0;
			for(; i < vfd->counter; i++){
				int fd = get_fd(vfd, i);
				if(FD_ISSET(fd, &set))
					do_service(fd);
			}
		}
		//重新设置时间,清空描述符集
		t.tv_sec = 2;
		t.tv_usec = 0;
		//重新遍历动态数组中最新描述符放置到描述符集中
		maxfd = add_set(&set);
	}	

	return (void*)0;
}


int main(int argc, char *argv[])
{
	...

	/* 1)主控线程负责去调用accept获得客户端的连接,将新的socket描述符放置到动态数组中
	 * 2) a) 启动的子线程调用select函数委托内核去检查传入到select中的描述符是否准备好,并和对应的客户端进行双向通信(非阻塞)\
     *    b) 利用FD_ISSET来找出准备好的那些描述符,并和对应的客户端进行双向通信
   	 */
	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);//地址结构体,用于放置客户端地址信息

	while(1){
		int fd = accept(sockfd,	(struct sockaddr*)&clientaddr, &len);
		if(fd < 0) {
 			perror("accept error");
			continue;
		}
		out_addr(&clientaddr);
		
		//将返回新的socket描述符加入到动态数组中
		add_fd(vfd, fd);
	}
	return 0;
}

编译运行:编译此文件需要包含-Iinclude——增加头文件的搜索路径

pthread是动态库,需要用-lpthread在链接阶段链接pthread库,所有的动态库都需要用-lxxx来引用

 gcc -o bin/echo_tcp_server_select -Iinclude obj/vector_fd.o src/echo_tcp_server_select.c -lpthread
通过这样的方式,也可以实现IO多路复用,非阻塞方式的通信


运行结果

猜你喜欢

转载自blog.csdn.net/Xinyue_Lu/article/details/104445455
今日推荐