从零开始自制实现WebServer(二)---- 勿在浮沙筑高层 摸谈初试进程/线程池与高效并发模型


全流程实现博客链接


从零开始自制实现C++ High-Performance WebServer 全流程记录(基于muduo网络库)


前引


话说上一章节谈到了 我们用了 单线程Epoll + non_blocking 做了个简易的echo server 我们还没有摸到多线程/多进程的坎呢

其实多线程 多进程 也无非就是 pthread_creater/fork而已 但是需要考虑的东西也就更多了 例如临界区/竞态条件了 race condition 考虑到这个 又要用一些进程间的通信原语去解决问题了 例如mutex cond/pipe sig shm 然后使用这些又需要注意很多地方 死锁/临界区 一些内存已经不存在而在其他线程中要被使用

上面也只不过是聊到了冰山一角 为了充分利用cpu中的多核 所以尽管困难重重 但是为了性能 这种任务还是交给感性的人来做是更好的 所以在这里个人认为 机器在短时间之内想要完全替代人类 还是不太可能的 毕竟像各种临界区 冲突情况 这么复杂的分析 我觉得也只有有经验的老程序员 或者真的认真思考过的 才能够比较好的写出 没有什么冲突的代码

好了 就先写到这里啦 我继续去看看后面的进/线程池代码源码 然后凭借着印象 自己一点一点鼓捣出来吧

刚刚忽然看到自己的第一篇这个系列博客的名字 万丈高楼平地起 哈哈 当时写的时候真的是随意写的
之后这第二篇 我是真的忽然想到一句 勿在浮沙筑高楼 因为第一篇我们只不是在为后面的一步步做基础而已 不断的熟悉各个I/O函数 不断地在一个雏形的模型上面不断打磨 最后才能出来项目的

结果没想到 这个居然是一句诗 哈哈 只能说 缘妙不可言啊 哈哈 可能上天都想让我知道这句话的重要性吧


(二)勿在浮沙筑高层 摸谈线程池与高效并发模型


刚刚仔细仔细的看了最后两个 一个是以process pool实现的 one loop per thread代码 然后又看了 一个是以thread pool实现的 reactor + work thread 用的工作队列 先到先得

其实最开始我是想在这篇就把 thread pool + one loop per thread简易版给实现出来 而且其实现在脑子里面非常有思路 刚刚仔细分析了代码一个多小时 感觉自己已经可以开始写了 思路大概如下 process pool 主进程给子进程们消息传递 采用pipe 而如果我们用 thread pool的话 就可以每个都用一个thread类 里面都有一个pthread_cond 条件变量 我们用这个通知 新进程来了

可是鉴于 我目前真的没有看到线程池 那种完整的代码 哪怕有思路 由于这一篇是打算直接实现出来 一个比较简易的HTTP或者CGI服务器 我怕实现出来了 如果有哪个地方有问题 我找不到对比代码得以验证 哪里出现了问题 所以呢 我还是选择退而求其次 先把process pool + one loop per thread 给实现出来吧 其实thread pool 和 process pool 就目前而言啊 我认为其实没啥差别 不过一个是fork + pipe 一个是用pthread_create + cond 之后都通过子进程/线程 来 accept 主进程/线程来 listen 有消息用round robin算法轮转 选择一个进程/线程来运行吗…

但是这里又有个问题 这里确实我还没有想到 进程池 子进程可以通过pipe来进行epoll_wait 那线程池的pthread_cond 怎么来弄呢 进程池可以统一事件源 那线程怎么处理呢 算了 这一篇就先从 process pool + one loop per thread 开整

手中有粮(心中有思路),自然不慌 哈哈 有思路那肯定实现起来也不会是很困难的事情啦


1、simple echo server 0.50 came out


1、一些在写代码时候的闲话

在经历了 几个小时(好吧 确实是几个小时 下午发现人有点顶不住了 回寝室睡觉去了) 自己的echo server 0.5是已经出世了 先说好 看分段标题都可以看得出来 这个版本是0.5 意味着是 简版的echo server而且里面也有一定的内存泄漏问题 因为用了singleton的进程模型 但是确实我也不知道怎么处理 这个static成员变量 而且第一本书中的源代码里面 也没有对这个全局变量进行处理(应该是)

而且在这之后 我也个人编写了一个简单的不能再简单的makefile 自己却发现出了各种大大小小的问题 后面自己才体会到 基础知识真的有多么多么重要 之前我还对编译链接的那本好书 不以为意 觉得在自己找到实习前就不看了 等找到实习了 之后工作完了 休息的时候看 现在看来 这本书需要阅读的优先级已经提高了

而且最恼火的是什么 就是我的makefile 之前加了依赖文件头文件 编译的话 要出现.h.gch的预编译头文件 每次编译都会出现 个人又觉得不太美观 然后就用隐式推导关系 让makefile自己去找 但是之后修改了头文件 竟然make显示没有改动 哎 估计是因为自己makefile才开始用 出现各种各样的问题是必然的

还有对gcc 尽管之前自己写tiny_os的时候 自己用了makefile gcc 但说句实话 确实那个时候只是认真的研究了os的代码 自己那个时候还不能接受makefile那种工具 自己的水平 对linux的认识理解 也不足以让我那个时候去学习 包括今天自己用gcc的时候 自己也对其中的参数 发生了各种各样的困扰 例如-c -o 什么含义 真的搞明白自己也是1个小时前的事情

所以现在也才明白 学的越多 写得越多 发现自己不会的越多 但是今天毕竟写了 自己收获也会满满 毕竟这个项目 是我之后打算一直迭代 也就是从雏形一直到后面的网络库 到高性能并发http 所以自己也没有焦急 毕竟所有东西也都得一步一步脚踏实地的走一遍

好了 扯远了 尽管书上有源代码 但是我写代码的习惯是 能在自己写的情况绝不看书 除非自己确实没有思路了 或者确实不会写了 对于一些细节的把控确实还不太清楚 自己再回去看看 然后再把书合上 自己写


上面那段话 是我在对我的echo server非常不满意的时候写的 按照原来的习惯 我应该是会把删掉了 但既然我想把这系列博客作为 我从零开始一步步实现的博客的话 我觉得就把上面的那段话留着吧

现在呢 我对我目前实现的echo server还算是觉得可以 但是呢 还是存在着些许的内存泄漏 所以第二本书个人认为是我后面要反复来看的 而且经过自己写了这个用进程池 + one loop per thread写的echo server后 我对我写过的I/O模型的理解也更深刻了 之前看第二本书里面 看到里面对每个模型的描述 什么银行在处理客人 那个时候对那些描述场景心里面一点都没有感觉 现在也再回头看看 心里面也有点感触

我发现这种模型 是不能有长时间I/O阻塞的 必须得用non-blocking 如果不是的话 当连接创建后 进入运行模板中后 那么就会出不去 所以在编写的用户处理类中 必须需要很快的操作 因为同一个进程池中的某一个进程 不仅仅处理一个用户 需要处理多个用户 如果一个进程处理过久 那么后面同一个进程的其他用户也就会产生等待 处理得越久 后面用户等待的越久 最不能够接受的是 用阻塞I/O 例如我现在写的echo server 如果每一个连接都用的阻塞I/O 那么只要产生8个进程 且大家都保持连接 那么进程池就一下子被占住了

昨晚思考的时候 就忽然发现非阻塞的 recv才是解决问题的办法 毕竟如果读不出来的话 recv返回是-1 我们检查一下errno 如果是EAGAIN 我们退出 等下次信号来了 我们再进来就好了 ^^

好了 那这段话就先写到这里 刚刚又想到这个echo server还有一些问题 没有处理一些信号处理 如果在调用recv的时候 信号中断了 而且信号没设置重入的话 那么就会丢失数据… 所以应该再去加一下信号处理的 但whatever 写出来了模板 自己有个心领神会就好了 至于细节 现在扣没有必要 之后正式进入写的时候 所有的细节都得自己一点一点处理 慢慢来

这里的退出处理还是比较糟糕的 尤其是主函数的close( listenfd ); 子进程由于fork之后也都能进来 都来关一次 而我对这个server跑起来之后 中断的处理方式是直接ctrl+c… 很暴力 所以就没有再改了… 总之这是一个错误的代码书写方式 还望请各位海涵


2、main.cc

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>

#include "processpool.h"
#include "echo.h"

int main( int argc , char* argv[] )
{
	if (argc <= 2)
	{
		printf( "Usage: %s ip_address portname\n", argv[0] );
		return 0;
	}

	const char* ip = argv[1];
	int port = atoi( argv[2] );
    
	int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
	assert( listenfd >= 1 );

	struct sockaddr_in address;
	memset( &address, 0, sizeof( address ) );
	address.sin_family = AF_INET;
	address.sin_port = htons( port );
	inet_pton( AF_INET, ip, &address.sin_addr );

	int ret = 0;
	ret = bind( listenfd, (struct sockaddr*)( &address ), 
				sizeof( address ) );
	assert( ret != -1 );

	ret = listen( listenfd, 5 );
	assert( ret != -1 );
	
	processpool<echo>* pool = processpool<echo>::create( listenfd, 8 );
	pool->run();

	close( listenfd );

	return 0;
}

2、processpoll.h

#ifndef __PROCESSPOLL_H_
#define __PROCESSPOLL_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>

class process
{
 public:
  int pid;              // 进程的pid
  int pipe[2];          // 父子通信管道 父 pipe[0] 子 pipe[1] socketpair双向通信
 
  process() : pid( -1 ), pipe{ 0, 0 } {}
};

template < typename T >
class processpool
{
 private:
  static const int MAX_EVENTS_NUMBER = 5;
  static const int MAX_USER_PER_PROCESS = 10000; 
  int idx;                  
  int listenfd;
  int epollfd;
  int max_processes_num;
  process* sub_processes;
  static processpool<T>* instance; 	// 涉及内存泄漏 由于是singleton模型

  processpool( int listenfd, int max_processes_num = 8 );
  ~processpool()
  {
	delete [] sub_processes;
  }

 public:
  static processpool<T>* create( int listenfd, int _max_processes_num = 8 )
  {
      if( instance == nullptr )
	  {
	      instance = new processpool<T>( listenfd, _max_processes_num );
		  return instance;
	  }

	  return instance;
  } 

  void run();
  void run_parent();
  void run_child();
  void setup_up_sig();
};

template < typename T>
processpool< T >* processpool< T > :: instance = nullptr;

template < typename T >
processpool<T>::processpool( int listenfd, int _max_processes_num ):
 						   idx( -1 ), listenfd( listenfd ), epollfd ( 0 ),
						   max_processes_num( _max_processes_num ), sub_processes( nullptr )
{
	sub_processes = new process [ max_processes_num ];
	
	for( int i = 0; i < max_processes_num; ++i )
	{
		socketpair( PF_UNIX, SOCK_STREAM, 0, sub_processes[i].pipe );
		sub_processes[i].pid = fork();
		
		if( sub_processes[i].pid > 0 )	// 父进程 关闭子进程方的pipe
		{
			close( sub_processes[i].pipe[1] );
			continue;
		}
		else
		{
			close( sub_processes[i].pipe[0] );
			idx = i;
			break;
		}
	}
}

static int set_non_blocking( int fd )
{
    int old_state = fcntl( fd, F_GETFL );
    int new_state = old_state | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_state );

    return old_state;
}

static void addfd( int epollfd , int fd )
{
    epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = fd;
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    set_non_blocking( fd );
}

static void removefd( int epollfd, int fd )
{
	epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, nullptr );
	close( fd );
}

template < typename T >
void processpool< T > :: run()
{
    if( idx == -1 )
	{
	    run_parent();
	}
	else
	{
	    run_child();
	}
}

template < typename T >
void processpool< T > :: setup_up_sig()
{
	epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
}

template < typename T >
void processpool< T > :: run_parent()
{
    epoll_event events[ MAX_EVENTS_NUMBER ];
	setup_up_sig();

    addfd( epollfd, listenfd );

	int pre_idx = 0;
	int has_new_cli = 1;
	int number = 0;
	while( 1 )
	{
		number = epoll_wait( epollfd , events, MAX_EVENTS_NUMBER, -1 );
		
		for( int i = 0; i < number; ++i )
		{
			int sockfd = events[i].data.fd;
			if( sockfd == listenfd ) 
			{
				int pos = pre_idx;
				do
				{
					pos = ( pos + 1 ) % max_processes_num;
				}
				while( sub_processes[pos].pid == -1 );
				pre_idx = pos;

				send( sub_processes[pos].pipe[0], ( void* )&has_new_cli, 
						  sizeof( has_new_cli ), 0 );
				printf( "parent processes has sent msg to %d child\n", pos );
			}	
		}
	}

	// close( pipe[0] );
}


template < typename T >
void processpool< T > :: run_child()
{
	epoll_event events[ MAX_EVENTS_NUMBER ];
	setup_up_sig();

	int pipefd = sub_processes[idx].pipe[1];
	addfd( epollfd, pipefd );
	T* users = new T [MAX_USER_PER_PROCESS];

	int number = 0;
	while( 1 )
	{
		number = epoll_wait( epollfd , events, MAX_EVENTS_NUMBER, -1 );
		for( int i = 0; i < number; ++i )
		{
			int sockfd = events[i].data.fd;
			if( sockfd == pipefd && ( events[i].events & EPOLLIN ) )
			{
				struct sockaddr_in client;
                socklen_t client_addrlength = sizeof( client );
                int connfd = accept( listenfd, ( struct sockaddr* )( &client ),
                                     &client_addrlength );
				addfd( epollfd, connfd );
				users[connfd].init( epollfd, connfd, client );
				printf( "child %d is addfding \n", idx );
				continue;
			}
			else if( events[i].events & EPOLLIN )
			{
				printf( "child %d has recv msg\n", idx );
				users[sockfd].process();
			}
		}
	}

	delete [] users;
	users = nullptr;

	close( epollfd );
	close( pipefd );
}

#endif

2、echo.h

#ifndef __ECHO_H_
#define __ECHO_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>

#include "processpool.h"

class echo
{
 private:
  static const int BUFFER_SIZE = 1024;
  static int epollfd;
  int sockfd;
  sockaddr_in client_addr;
  char buf[1024] = {0};

 public:
  echo()  {}
  ~echo() {}

  void init( int _epollfd, int _sockfd, const sockaddr_in& address )
  {
	epollfd = _epollfd;
	sockfd  = _sockfd;
	client_addr = address;
  } 

  void process()
  {
	  while( 1 )
	  {
		 memset( buf , 0, sizeof( buf ) );
	   	 int ret = recv( sockfd, buf, sizeof( buf ), 0 );
		 if( ret < 0 )
		 {
			if( errno == EAGAIN || errno == EWOULDBLOCK )
			{
				break;
			}
		 }
		 else if( ret == 0)
		 {
	  	 	removefd( epollfd, sockfd );
		 	break;
		 }
		 else
		 {
	  		send( sockfd, buf, sizeof( buf ), 0 );
		 }
	  }

	  return;
  }
};

int echo::epollfd = -1;

#endif

2、simple echo server 0.50 test


这里的话 我就没有写脚本的 个人初步手动的不断地用ctrl + alt + t 再复制粘贴nc 127.0.0.1 xxxx 的方式与服务器沟通 初步测试了一下 并发还算是有的 算了 下面还是自己再演示一下吧

这里再写一下哈只是为了心中有个概念 感受一下processpool + one loop per thread这种感觉 没写脚步 自己纯手动创建终端跟服务器通信

效果如下 大概是开了40多个终端 然后开启后每个连接都输入了一些话
设置的60秒自动关闭 最后那么recv msg 应该是60s 最后关闭发来的FIN 然后接收到了 之后调用recv 接收到0 然后退出任务了

初步来看基本完成了一个简简陋陋 四处充满着危机有bug的echo server 但毕竟我们只是标题都写了 摸谈的嘛 哈哈 心中有个概念 感觉还是非常好的 ^^

在这里插入图片描述


结束语


这一篇也就先写到这里 尽管写的只能说差强人意 但是还算是基本完成了自己想完成的事情 心中对进程/线程池有了个轮廓 对I/O高并发模型有了个概念 熟悉了一下gcc makefile 途中自己又去搜搜罗罗了一些资料 还算是正在稳步向上 哈哈

那不妨这篇就先写到这里 笔者去吃个午饭 回来想想下一步应该做什么 往什么方向探索 好了 各位下一篇再见了~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Love 6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值