专栏首页可能是东半球最正规的API社区扯点儿高性能(二):服务器模型(上)

扯点儿高性能(二):服务器模型(上)

大家好,我是老李,我要开始了,你们准备好了吗。

首先是感谢后台一些友好关切的问候:一是我还活着呢,二是我没有太监,下面会继续持续更新。

其次是得解释下最近为啥搞得像太监了似的:一是主要是太忙不得不歇菜,二是尼古拉斯*永强忙着搞工业机器人也尥蹶子不写东西了,三是也没有投稿的了。

虽然冒着可以被老板看出工作是否饱和的巨大风险,但还是要说:公众号更新频率代表着我最近的工作状态与业余时间宽松程度。除了工作的事儿外,仅有的挤出来的业余时间都被我那个flag占据了不少。

什么flag?

我立了一个年底之前码一个山寨版本Redis的flag,既然是个flag,所以精力也偏重到这个flag上。当然了,在我自己看来这个flag一旦实现后公众号就可以跟着同步很多有价值的内容了,诸如标题类似于《老李手把手教你写Redis》亦或《不买书不看视频不学习也能精通Redis》亦或《我在研究Redis的日子里》...

既然时间偏重到了这个flag上,所以要给大家一个交代吧:目前项目放在github上,我正在封装epoll、select这些event-loop模块。具体地址我就先不说了,省的有人说我拿半成品骗star...

众所周知,作为一个名字叫做【高性能API】的正规公众号,本公众号有三个文章线:

  • 附近的人系列
  • 高性能系列
  • 杂乱非技术系列

其中这个高性能系列只写了一篇,而且是与高性能没有半毛钱关系的《扯点儿高性能(一):CGI篇》,今天我觉得有必要简单聊一下服务器模型。之前在写《连续研发【附近的人】---swoole love thrift 3000 ci》这篇文章的时候就屡次提到服务器模型概念,当时的场景上下文是这样的(冥冥之中,所有的文章之间其实都是相辅相成的):

如果你要从开头先要了解一个高并发网络软件比如Redis、比如Nginx,我个人认为首先要下手了解的就是服务器模型,有时候又叫服务器进程模型或者线程模型。今天我们先聊三种比较粗暴简单的服务器模型,在很久很久之前,这些服务器模型都是被应用过的,虽然现在看起来挺沙雕的但有时候还觉得挺好使的...

今天这个上篇我们就用世界上最好的语言来实现一个简单的http服务器来说明下三种服务器模型;下篇则引入一下select/epoll IO Multiplexing,你们简单感受一下。

希望后面我在其他文章里提到服务器进程/线程模型的时候,之前并不熟悉的诸位能够不再陌生。

我们要做的大概就是分两步,第一步是实现一个socket服务器,第二步是实现一个简单http协议解析器,然后一结合,完美~~~简单解释一下,http协议是在tcp协议的基础之上实现的,既然是要操控tcp了所以socket编程就一定躲不开了(不知道看这篇文章的泥腿子们有没有分不清tcp和socket区别的,如果分不开,就自己搜一下?)...

好了好了快别废话了,快闪开,赶紧进入CVS环节!


HTTP解析器

我认为你们还是有必要趁此正好了解一波儿HTTP协议的,看看HTTP协议的构成。我贴一下这个HTTP协议解析器的代码,简单粗暴地说就是用PHP来解析一大坨文本,将其解析到数组或者其他数据结构中去:

  • 首先是我写的
  • 其次这是我大概一年多以前写的
  • 然后是这是我参考WM写的
  • 继而是TA目前只能解析GET方法和PATHINFO
  • 最后是我只实现了简单HTTP协议
<?phpclass Http {    private static $method = array(        'GET',        'POST'    );    /*     * @desc : 解析收到的http数据 目前只支持post 和 get两个方法    */    public static function decode($rawData) {        //$request = new Request();        $server = [];         $header = [];         $get = [];         $post = [];         $files = [];         $rawContent = '';         // 将原始数据使用 PHP_EOL 分割,目前我还不知道使用PHP_EOL会不会有什么问题        // 所以,还得再分才行,在用"\r\n"将 请求行 和 请求头 分开        /*          0 => request-line\r\n        request-header        1 => request-body        */        $rawDataArr = explode("\r\n\r\n", $rawData, 2);         // 取出请求体        $rawRequestBody = isset( $rawDataArr[1] ) ? trim($rawDataArr[1]) : "" ;        // 取出 请求行 和 请求头        $requestHeader = explode("\r\n", $rawDataArr[0]);        $requestStartLine = $requestHeader[0];        unset($requestHeader[0]);        // 请求行,或者 我个人称为 请求起始行 request line        list($requestMethod, $requestUri, $httpVersion) = explode(' ', $requestStartLine);        if (!in_array($requestMethod, self::$method)) {            return $request;        }        // 初始化到系统常量中        $server['METHOD'] = trim($requestMethod);        // 在get方法中,可能存在如下方式 http://www.x.com/api?username=pangzi        if (false !== strpos($requestUri, '?')) {            list($pathInfo, $queryString) = explode('?', $requestUri);        } else {            $pathInfo = '';            $queryString = '';        }        $server['PATH_INFO'] = trim($pathInfo);        $server['QUERY_STRING'] = trim($queryString);        $server['HTTP_VERSION'] = trim($httpVersion);        // 首部,也就是http header        foreach ($requestHeader as $item) {            if (false !== strpos($item, ':')) {                list($key, $value) = explode(':', $item);                $key = strtoupper($key);                switch ($key) {                    case 'CONTENT-TYPE':                        if (!preg_match('/boundary="?(\S+)"?/', $value, $match)) {                            $header[$key] = trim($value);                        } else {                            //print_r( $match );                            $header[$key] = 'multipart/form-data';                            $boundary = '--' . trim($match[1]);                            //echo $boundary.PHP_EOL;                        }                        break;
                    default:                        $header[strtoupper(trim($key)) ] = trim($value);                        break;                }            }        }        // 主体 body,当然了在GET情况下直接忽略body体中的数据,但是需要解析query string        if ('' !== $queryString) {            // username=elarity&password=123454&option=rem            $getArr = explode('&', $queryString);            /*            [0] => username=elarity            [1] => password=123454            [2] => option=rem            */            foreach ($getArr as $getDataItem) {                list($queryKey, $queryValue) = explode('=', $getDataItem);                $get[$queryKey] = $queryValue;            }        }        // 在POST方法下,收集body信息,但是不能忽略queryString        if ('POST' === $requestMethod) {            // 判断 content-type            if ('application/x-www-form-urlencoded' == $header['CONTENT-TYPE']) {                // 数据样式案例:user=etc&password=12345                if ('' != $rawRequestBody) {                    $postKv = explode('&', $rawRequestBody);                    foreach ($postKv as $_item) {                        list($postKey, $postValue) = explode('=', $_item);                        $post[trim($postKey) ] = trim($postValue);                    }                }            } else if ('multipart/form-data' == $header['CONTENT-TYPE']) {                // form-data 中的数据是这样的                //print_r( explode( $boundary, $rawRequestBody ) );                $rawFormData = explode($boundary, $rawRequestBody);                if ('' == $rawFormData[0]) {                    unset($rawFormData[0]);                }                foreach ($rawFormData as $rawFormDataItem) {                    $rawFormDataItem = trim($rawFormDataItem);                    // rawFormDataItem 数据实例                    // 文件类型                    // Content-Disposition: form-data; name="testfile"; filename="111111111111111111.png"                    // Content-Type: image/png                    //                    // �PNG                    // !����4���VA��|��};pJ�&\�nome-ׄk�5��pM�&\��>GIׄk�5��pM�&\���O q��IEND�B`�                    // 普通类型                    // Content-Disposition: form-data; name="username"                    //                    // elarity                    // 如果是文件上传数据                    if (false !== strpos($rawFormDataItem, "filename")) {                        $rawFormDataArr = explode("\r\n", $rawFormDataItem);                        $fileKey = '';                        foreach ($rawFormDataArr as $__key => $__item) {                            if ('' !== trim($__item)) {                                if (preg_match('/name="(.*?)"; filename="(.*?)"$/', $__item, $match)) {                                    $fileKey = $match[1];                                    $files[$fileKey]['name'] = $match[2];                                } else if (false !== strpos(strtolower($__item) , "content-type")) {                                    list($contentTypeKey, $contentTypeValue) = explode(":", $__item);                                    $files[$fileKey]['type'] = trim($contentTypeValue);                                } else {                                    $files[$fileKey]['data'] = empty($files[$fileKey]['data']) ? '' : $files[$fileKey]['data'];                                    $files[$fileKey]['data'].= $__item;                                }                            }                        }                    }                    // 如果是表单数据                    else {                        $rawFormDataArr = explode("\r\n", $rawFormDataItem);                        if (3 === count($rawFormDataArr)) {                            preg_match('/name="(.*?)"/', $rawFormDataArr[0], $match);                            $post[$match[1]] = $rawFormDataArr[2];                        }                    }                }            }        }        $content['server'] = $server;        $content['header'] = $header;        $content['get']    = $get;        $content['post']   = $post;        $content['files']  = $files;        $content['rawContent'] = $rawRequestBody;        return $content;    }    /*     * @desc : 编码http数据 并返回    */    public static function encode($body="") {        $responseStartLine = "HTTP/1.1 200 OK\r\n";        $header = "";        $header = $header . "Server: Shadiao Http Server" . "\r\n";        $header = $header . "Content-Type: text/html\r\n";        $header = $header . "Content-Length: " . strlen($body) . "\r\n";        $header = $header . "Date: " . date("Y-m-d H:i:s") . "\r\n";        $header = $header . "\r\n";        $content = $responseStartLine . $header . $body;        return $content;    }}

第一种

这种是最粗暴也是最沙雕的模式,代码在下面你们先感受一下...

<?phprequire_once "http.php";echo PHP_EOL."A Sha.Diao PHP Http Server...".PHP_EOL;$s_host = '0.0.0.0';$i_port = 9999;// 创建一个tcp socket$i_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );// 将socket bind到IP:port上socket_bind( $i_listen_socket, $s_host, $i_port );// 开始监听socketsocket_listen( $i_listen_socket );// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上while( true ){  // 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的  // 所以你不用担心while循环会将机器拖垮,不会的   $i_connection_socket = socket_accept( $i_listen_socket );  // 接受http客户端发来的内容...  $s_receive_content = socket_read( $i_connection_socket, 8192, PHP_NORMAL_READ );  $a_content = http::decode( $s_receive_content );    // 解析http客户端发来的内容,并生成一个数组...  print_r( $a_content );  // 向客户端发送一个helloworld  $s_send_content = http::encode( "多冷的隆冬多冷的隆冬哒哒哒" );  socket_write( $i_connection_socket, $s_send_content, strlen( $s_send_content ) );  socket_close( $i_connection_socket );}// 关闭服务器...socket_close( $i_listen_socket );

上面文件保存成server.php,然后php server.php执行起来,至于客户端大家用curl直接临时客串一把就行了~~~我跑了一下你们感受一下:

这坨服务器代码反正能用,但是比较沙雕的地方在于在同一个时刻TA只能服务于同一个客户端,如果此时第二个客户端访问服务器了,那我只能祝第二个客户端幸福... ...


第二种

看到第一坨代码的缺陷,我们就改进一下:想想如何才能服务于多个客户,在假装了一番苦苦思索后我终于得出了答案---进程或线程(但是,很可惜PHP的多线程太废渣了,我说的就是它那个pthread扩展,所以我们就用进程来处理)。这个改进的大概意思就是如果有新的请求过来,程序就会fork出一个新的子进程来处理这个新的请求一直到会话完成,会话完成后子进程退出。当然了,如果在相对集中的时间前前后后来了5000个请求,那么很自然:也就会前前后后fork出5000个子进程来分别处理这5000个会话...嗯,别说处理会话了,光这5000个fork直接就能干挺服务器...

如果诸位对PHP-FPM配置熟悉的话,其中有一项配置叫做pm,TA的值有static、dynamic、ondemand三项。这个ondemand项,我抄袭一波儿配置文件里的英文解释:no children are created at startup. Children will be forked when new requests will connect,是不是有点儿类似?

<?phprequire_once "http.php";echo PHP_EOL."A Sha.Diao PHP Http Server...".PHP_EOL;$s_host = '0.0.0.0';$i_port = 9999;// 创建一个tcp socket$i_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );// 将socket bind到IP:port上socket_bind( $i_listen_socket, $s_host, $i_port );// 开始监听socketsocket_listen( $i_listen_socket );// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上while( true ){  // 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的  // 所以你不用担心while循环会将机器拖垮,不会的   $i_connection_socket = socket_accept( $i_listen_socket );  // 当accept了新的客户端连接后,就fork出一个子进程专门处理  $i_pid = pcntl_fork();  // 在子进程中处理当前连接的请求业务  if( 0 == $i_pid ){    // 接受http客户端发来的内容...    $s_receive_content = socket_read( $i_connection_socket, 8192, PHP_NORMAL_READ );    $a_content = http::decode( $s_receive_content );    // 解析http客户端发来的内容,并生成一个数组...    print_r( $a_content );    // 向客户端发送一个helloworld    $s_send_content = http::encode( "多冷的隆冬多冷的隆冬哒哒哒" );    socket_write( $i_connection_socket, $s_send_content, strlen( $s_send_content ) );    sleep( 5 );    socket_close( $i_connection_socket );    exit;  }}// 关闭服务器...socket_close( $listen_socket );

注意此处有个细节,就是fork的子进程中sleep了5秒钟,但是并不会影响多个客户调用,你可以用多个curl请求试试...这个,你们自己亲自试试?


第三种

考虑到第二坨代码也比较沙雕,我们再稍微做个改进:上来就fork出固定数量的进程,而且进程在处理完会话后并不退出,而是继续活着等待下一个会话。这样一来,你一听就感觉比第二种靠谱,是不是?有没有?

<?phprequire_once "http.php";echo PHP_EOL."A Sha.Diao PHP Http Server...".PHP_EOL;$s_host = '0.0.0.0';$i_port = 9999;// 创建一个tcp socket$i_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );// 将socket bind到IP:port上socket_bind( $i_listen_socket, $s_host, $i_port );// 开始监听socketsocket_listen( $i_listen_socket );cli_set_process_title( 'likedushe master process' );// 按照数量fork出固定个数子进程for( $i = 1; $i <= 10; $i++ ){  $i_pid = pcntl_fork();  if( 0 == $i_pid ){    cli_set_process_title( 'likedushe worker process' );    while( true ){      $i_connection_socket = socket_accept( $i_listen_socket );      // 接受http客户端发来的内容...      $s_receive_content = socket_read( $i_connection_socket, 8192, PHP_NORMAL_READ );      $a_content = http::decode( $s_receive_content );        // 解析http客户端发来的内容,并生成一个数组...      print_r( $a_content );      // 向客户端发送一个helloworld      $s_send_content = http::encode( "多冷的隆冬多冷的隆冬哒哒哒" );      socket_write( $i_connection_socket, $s_send_content, strlen( $s_send_content ) );      sleep( 5 );      socket_close( $i_connection_socket );    }     }}// 主进程不可以退出,代码演示比较粗暴,为了不保证退出直接走while循环,休眠一秒钟// 实际上,主进程真正该做的应该是收集子进程pid,监控各个子进程的状态等等while( true ){  sleep( 1 );}// 关闭服务器...socket_close( $i_listen_socket );

php server.php跑一下,这次你们留意到了么,我用cli_set_process_title为父进程和子进程设置了title,所以你们用ps -ef | grep process感受一下:

在这三种模式里,最后这个方式是最稳最好的方法,而且目前还有一些服务器软件采用的就是这种方式,你可以参考一下PHP-FPM的pm配置项中的static模式或者APACHE服务器的pre-fork模式,大致原理都是这样的...但是Nginx,不是的,Nginx不是这样的,Nginx是event-loop也就是传说中基于事件监听的异步非阻塞模式,也就是我们下一节要说的select/epoll IO Multiplexing...


本篇暂无参考文章,纯属自己瞎编...

本文分享自微信公众号 - 高性能API社区(high-performance-api)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 腾讯云服务器上部署LNMP环境

    最近在学Laravel,同参考文章,本来只是在虚拟机上运行,但现在正好因为手上有腾讯云服务器,所以就直接拿来部署Laravel。

    用户6468650
  • RESTful 架构基础

    REST(Representational State Transfer)架构风格是一种世界观,把信息提升为架构中的一等公民。通过 REST 可以实现系统的高性...

    zhisheng
  • PHP-fpm 远程代码执行漏洞(CVE-2019-11043)分析

    国外安全研究员 Andrew Danau在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常,疑似存在漏洞。

    知道创宇云安全
  • PHPstorm配置PHP环境

    缺MSVCR110.dll下载这个:Visual C++ Redistributable for Visual Studio 2012 Update 4 点我下...

    cherishspring
  • 腾讯云服务器搭建 WordPress站点『图文教程』

    WordPress 是一款常用的搭建个人博客网站软件,该软件使用 PHP 语言开发。您可通过在腾讯云服务器的简单操作部署 WordPress,发布个人博客。

    用户6559734
  • C# 如何获取Url的host以及是否是http

    参考资料:https://sites.google.com/site/netcorenote/asp-net-core/get-scheme-url-host

    跟着阿笨一起玩NET
  • 2016年系统架构师软考案例分析考点

    cwl_java
  • 令人惊叹的前端路由原理解析和实现方式

    ? 在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是...

    腾讯技术工程官方号
  • 相对路径和绝对路径

    根目录下有demo1和images/1.jpg,demo1下有index1.html文件和demo1.1文件夹。demo1.1下有index2.html和2.j...

    于小勇
  • 300万知乎用户数据如何大规模爬取?如何做数据分析?

    很早就有采集知乎用户数据的想法,要实现这个想法,需要写一个网络爬虫(Web Spider)。因为在学习 python,正好 python 写爬虫也是极好的选择,...

    机器学习AI算法工程

扫码关注云+社区

领取腾讯云代金券