C++网络编程进阶篇_IO多路复用

文章目录

        • 网络编程绕不开的`C10k`问题
        • 解决方案

之前写过一篇C++关于网络编程入门的博文:
socket网络编程入门

主要是介绍C++网络编程的API接口的使用,该博文中的例子对socket
调用流程是最简单且最基本的,它只能实现一对一通信,因为它使用的是同步阻塞的方式。

现代网络编程都需要考虑到并发,也就是一对多的通信状态,
继续使用之前一对一的通信模型是行不通的,只有通过改进网络I/O模型来实现。


网络编程绕不开的C10k问题

C++网络编程进阶篇_IO多路复用_第1张图片

提出该问题的作者是Dan KegelWinetricks的作者,博客原文
《The C10K problem》。
相关的资源拓展:
高性能网络编程经典:《The C10K problem(英文)》[附件下载]

原文介绍了一些I/O框架,其中libevent是目前后端开发较常用的网络库,
本人曾经也在项目中使用过该库,是实现多并发较理想的解决方案之一。

C10k问题的本质是尽可能的减少网络程序并发状态下的服务器资源消耗,
比如避免或减少频繁的创建和销毁进程和线程,针对此问题的解决方案有使用线程池来管理线程资源;
避免或减少频繁的数据拷贝,引申出了零拷贝的解决方案。
总的来说解决C10k问题的关键就是尽可能减少这些CPU资源消耗!


解决方案

这里我想通过逐一介绍不同的网络I/O模型来引出现阶段网络编程处理多并发场景较理想的解决方案。
以最基本的实现一对一通信模型的socket代码为例展开修改,
这里仅以tcp服务端代码举例,客户端使用其它软件连接:

/* 同步阻塞基本socket服务端模型 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_LENGTH   128

int main() {    
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  //
	if (listenfd == -1) {
		return -1;
	}
			
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);
	
	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
			return -2;
	}
	listen(listenfd, 10);
	
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
	int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
	
	unsigned char buffer[BUFFER_LENGTH] = {0};
	int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
	printf("buffer : %s, ret: %d\n", buffer, ret);
	
	ret = send(clientfd, buffer, ret, 0);
	
	close(clientfd);
	return 0;
}
  1. 使用循环的方式处理多个socket连接

传统的I/O网络模型就是阻塞式I/O模型,在 socket网络编程入门
这篇文章中的例子就是该模型。刚才已经在上文说过这类模型不适合直接应用于多并发场景下的网络编程。
上述的代码很简单,问题也很明显:

  • 可以连接多个客户端,但是第二个客户端不能正常处理发送和接受数据;
  • 第一个客户端断开之后,服务端程序也直接退出。

接下来我们使用循环来处理上面的问题,
当然像下面这样直接将接受和发送的代码放入循环是不能解决根本问题的:

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

#define BUFFER_LENGTH   128

int main() {

// block
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);  //
        if (listenfd == -1) return -1;
// listenfd
        struct sockaddr_in servaddr;
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(9999);

        if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
				return -2;
        }
        listen(listenfd, 10);

        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);

        while (1) {
                unsigned char buffer[BUFFER_LENGTH] = {0};
                int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
                if (ret == 0) {
                        close(clientfd);
                        break;
                }
                printf("buffer : %s, ret: %d\n", buffer, ret);

                ret = send(clientfd, buffer, ret, 0);
        }

        return 0;
}


这里推荐阅读多线程模型
结合下面的代码一起理解。这里直接跳过了多进程模型是因为进程占用的资源较大,
而且进程的善后工作没做好会产生僵尸进程,这类进程越多就会逐渐耗尽我们的系统资源。
所以采用轻量级的多线程模型,是现阶段较好的解决方案。
首先将处理接受和发送数据的代码放入到线程中,用多线程模型来处理socket连接的并发场景:

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

#define BUFFER_LENGTH   128
void *routine(void *arg) {
        int clientfd = *(int *)arg;
        printf("listen --> clientfd: %d\r\n",clientfd);
        while (1) {
                unsigned char buffer[BUFFER_LENGTH] = {0};
                int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
                if (ret == 0) {
                        printf("close clientfd: %d\r\n",clientfd);
                        close(clientfd);
                        break;
                }

                printf("buffer : %s, ret: %d clientfd: %d\r\n", buffer, ret,clientfd);

                ret = send(clientfd, buffer, ret, 0); //
        }
}

int main(){
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);  //
        if (listenfd == -1) return -1;

        struct sockaddr_in servaddr;
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(9999);

        if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
                return -2;
        }

        listen(listenfd, 10);
        while (1) {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);

                pthread_t threadid;
                pthread_create(&threadid, NULL, routine, &clientfd);

        }
        return 0;
}

程序运行图示:
C++网络编程进阶篇_IO多路复用_第2张图片

  1. 运用到select模型来处理多个socket连接

使用多线程循环处理socket的结果似乎已经能满足一对多的想法了,但它仍然存在一些问题。
比如一个线程对应一个socket请求,造成服务器资源的消耗较大,
频繁的创建和销毁线程对系统也是一个不小的开销,当然我们可以使用线程池的方式来避免线程的频繁创建和销毁,
但最关键的一点是,多线程模型还不能解决C10k问题。

多线程循环处理socketselect的区别是,selcet对线程进行了复用,也是I/O多路复用模型的优势,
系统不需要频繁的创建和销毁线程,一个线程管理所有socket
它相对于多线程模型的优势是同时能处理更多的socket连接:

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

#define BUFFER_LENGTH 128

int main() {

	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  
	if (listenfd == -1) return -1;

	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
		return -2;
	}

	listen(listenfd, 10);

	fd_set rfds, wfds, rset, wset;

	FD_ZERO(&rfds);
	FD_SET(listenfd, &rfds);
	FD_ZERO(&wfds);

	int maxfd = listenfd;

	unsigned char buffer[BUFFER_LENGTH] = {0}; // 0 
	int ret = 0;

	while (1) {
		/* 每次调用之前都要初始化fd_set结构体 */
		rset = rfds;
		wset = wfds;

		int nready = select(maxfd+1, &rset, &wset, NULL, NULL);
		if (FD_ISSET(listenfd, &rset)) {
			printf("listenfd --> %d\r\n",listenfd);

			struct sockaddr_in client;
			socklen_t len = sizeof(client);
			int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
			
			FD_SET(clientfd, &rfds);

			if (clientfd > maxfd) maxfd = clientfd;
		} 
		
		int i = 0;
		for (i = listenfd+1; i <= maxfd;i ++) {
			if (FD_ISSET(i, &rset)) { //
				memset(buffer, 0, sizeof(buffer));	
				ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if (ret == 0) {
                    printf("close clientfd: %d\r\n",i);
					close(i);
					FD_CLR(i, &rfds);
				} else if (ret > 0) {
					printf("buffer : %s, ret: %d, clientfd: %d\r\n", buffer, ret,i);
					FD_SET(i, &wfds);
				}

				
			} else if (FD_ISSET(i, &wset)) {
				ret = send(i, buffer, ret, 0); // 
				FD_CLR(i, &wfds); //
				FD_SET(i, &rfds);
			}
		}
	}
	return 0;
}

程序运行图示:
C++网络编程进阶篇_IO多路复用_第3张图片

  1. 使用poll模型来处理多个socket连接

上面的select模型的优势很明显,实现了I/O多路复用,但问题也不少,
比如监视文件句柄的数量上存在上限;每次调用之前都要重新初始化fd_set结构体;需要通过遍历数组的方式来监视fd的状态,存在不必要的消耗。

poll模型主要解决了前面两个问题,它将select使用的三个fd_set结构体参数修改成了struct pollfd *类型的数组的形式,原理还是和select模型一样的,只是理论上解决了文件句柄数量的上限和避免了避免重复初始化的问题,遍历数组监视fd状态的效率依旧存在。

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

#define MAX_FD  1024
#define BUFFER_LENGTH 128

int main(){
    //创建一个侦听socket
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  
	if (listenfd == -1) return -1;
	
	//默认使用阻塞模式
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
        printf("bind error.");
		return -2;
	}
    //启动侦听
    if (-1 == listen(listenfd, SOMAXCONN)){
        printf("listen error.");
        close(listenfd);
        return -3;
    }
	
	struct pollfd  fds[MAX_FD] = {0};
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;	
	
	int cur_max_fd = listenfd;
	/* 初始化poll数组 */
	int i = 0;
	for(i=1; i < MAX_FD; i++){
		fds[i].fd = -1;
	}

	unsigned char buffer[BUFFER_LENGTH] = {0}; // 0 
	int ret = 0;
	
	while(1){
		int nready = poll(fds, cur_max_fd+1, -1);
		if (nready < 0){
            perror("poll error\r\n");
            return -4;
        }
		
		if(fds[0].revents & POLLIN){
			struct sockaddr_in client;
			socklen_t len = sizeof(client);
			int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
			fds[clientfd].fd = clientfd;//将客户端socket加入到集合中
            fds[clientfd].events = POLLIN;
			
			printf("listen --> \r\n");
			if (clientfd > cur_max_fd) {
				cur_max_fd = clientfd;
			}
			if (--nready == 0) continue;
		}
		
		for(i=listenfd+1; i<= cur_max_fd; i++){
			if(fds[i].revents & POLLIN){
				ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if(ret > 0){
					buffer[ret] = '\0';
					printf("buffer : %s, ret: %d, clientfd: %d\r\n", buffer, ret,i);

					send(i, buffer, ret, 0);
				}else if(ret == 0){
					fds[i].fd = -1;
                    printf("close clientfd: %d\r\n",i);
		            close(i);
				}
				if (--nready == 0) break;
			}
		}
	}
	close(listenfd);
	return 0;
}
  1. epoll

epoll使用了红黑树的数据结构来监视所有fd的状态变化,它只返回状态有变化的fd
而不像select/poll那样通过轮询扫描的方式去遍历整个fd数组。
它利用三个关键的apifd进行管理,先用epoll_create创建一个 epoll 对象 epfd
再通过 epoll_ctl 将需要监视的 socket 添加到 epfd 中,最后调用 epoll_wait 等待数据。
在客户端请求的fd数量达到10k时,epoll的性能优于select/poll,这样的设计解决了C10k问题:

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

#define MAX_FD  1024
#define BUFFER_LENGTH 128

int main(){
    //创建一个侦听socket
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(-1 == listenfd) return -1;
	
	//默认使用阻塞模式
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
        printf("bind error.");
		return -2;
	}
    //启动侦听
    if (-1 == listen(listenfd, SOMAXCONN)){
        printf("listen error.");
        close(listenfd);
        return -3;
    }
	
	int epfd = epoll_create(1); //int size

	struct epoll_event events[MAX_FD] = {0};
	struct epoll_event ev;

	ev.events = EPOLLIN;
	ev.data.fd = listenfd;

	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

	unsigned char buffer[BUFFER_LENGTH] = {0}; // 0 
	int ret = 0;
	while (1) {
		int nready = epoll_wait(epfd, events, MAX_FD, 5);
		if (nready == -1) continue;

		int i = 0;
		for (i = 0;i < nready;i ++) {
			int clientfd =  events[i].data.fd;
			if (clientfd == listenfd) {

				struct sockaddr_in client;
			    socklen_t len = sizeof(client);
				int connfd = accept(listenfd, (struct sockaddr *)&client, &len);

				printf("listen --> \r\n");
				ev.events = EPOLLIN;
				ev.data.fd = connfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

			} else if (events[i].events & EPOLLIN) {

				ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
		        if (ret > 0) {
		            buffer[ret] = '\0';
		            //printf("recv msg from client: %s\n", buffer);
					printf("buffer : %s, ret: %d, clientfd: %d\r\n", buffer, ret,clientfd);

					send(clientfd, buffer, ret, 0);
		        } else if (ret == 0) { //
					ev.events = EPOLLIN;
					ev.data.fd = clientfd;

					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
                    printf("close clientfd: %d\r\n",clientfd);

		            close(clientfd);
		        }
			}
		}
	}
    close(listenfd);
	return 0;
}

引用列表

《深入理解计算机系统》- 第三部分 程序间的交流和通信
深入理解LinuxIO复用之epoll
深入了解epoll模型(特别详细)
《图解系统》- 九、网络系统 -小林coding
《高性能网络编程》- 即时通讯网博客

你可能感兴趣的:(网络编程,网络,c++)