前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >扯点儿高性能(一):CGI篇【搞附近】

扯点儿高性能(一):CGI篇【搞附近】

作者头像
老李秀
发布2019-11-13 16:22:45
7670
发布2019-11-13 16:22:45
举报

首先得说明下CGI和高性能没有半毛钱关系,甚至是低性能的代名词。

CGI是什么

CGI是一种协议,并不是一种具体的代码程序。上古时代的PHP程序就是靠CGI协议与HTTP服务器比如Apache协作完成。最开始那会儿Web站点的出现一般都是纯静态货色,只要你精通HTML和PS然后你就能配合Apache什么的就能搞出一个炫酷狂拽屌炸天的网站。

然而总有刁民想整幺蛾子:他们想整动态数据

但是这件事情让Apache来做总归是不合理的。从工程角度来讲,叫做耦合太严重;从UNIX哲学角度来讲,软件功能要精悍专一。Apache服务器就应该老老实实做好http,动态数据的读取应该交给其他程序来做。所以CGI就应运而生,全称叫做Common Gateway Interface。除了HTML和CSS以及jQuery外的任何一门语言都可以用来编写CGI程序,PHP、Python、Perl都可以的。


CGI粗暴流程

http服务器和cgi程序相互进行友好数据磋商一共就三个套路:

  • 环境变量
  • 标准输入
  • 标准输出

其中http服务器向cgi程序传输数据,是通过环境变量和标准输入。比如php里我们常见的$_SERVER['REQUEST_METHOD']等就是通过环境变量传递的,又或者说POST方法的PO过去的数据一般说来是通过标准输入向cgi写入。当cgi程序完成了CURD工作后处理好的数据需要返回给http服务器,此时则是通过cgi向标准输出中写数据完成。考虑到一般情况下http服务器的标准输入已经重定向到了cgi程序,所以cgi程序里直接echo、print_r等等就相当于直接将数据写入到了标准输出。

每当有HTTP请求打到http服务器上时候,服务器程序要做的标准流程就是fork出一个子进程,然后该子进程去exec写好的cgi程序。我听行业大佬们叫这个流程为fork-and-execute。毫无疑问,这就是传说中“低性能”代表操作。fork为宝贵系统资源,一次fork操作都是需要一些吃奶力气的,更可怕的时候如果有10000个http请求,就需要fork 10000次,你们感受下。

为了让我们广大泥腿子们内心找到灵魂归宿和熟悉的味道配方,为了更好的以熟悉的面孔向大家展示CGI协议的具体内容,我决定从ietf的CGI协议标准里给大家找一些老面孔混个脸熟,你们感受一下:

原来PHP里$_SERVER全局变量的数据都是来自于这里... ...


Ctrl + C && Ctrl + V

有道是老话说的好

下面我们【粗暴地模拟】一下上古时代的基于CGI协议的Web开发是什么感受。首先我用上古语言C语言手写了一个【能用】的服务器,然后我们在服务器收到请求的时候fork一个子进程,在子进程中调用php-cgi程序(此处注意!php-cgi是fastcgi协议的实现)。我先把基于C语言的服务器代码贴一下,里面包含大量注释,一般人都能看懂,尽管该段程序可能充斥着内存泄漏、野指针到处飞、随时出现core dump,但是大概率情况下还是能用的。在后面的日子里,这坨烂代码将会伴随着我们逐渐演化为可能是【高性能】的服务器软件。

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUFFER_SIZE 4096
extern char ** environ;
int main( int argc, char * argv[] ) {
  //for ( int index = 0; environ[ index ] != NULL; index++ ) {
    //printf( "%s\n", environ[ index ] );
  //}
  if ( argc < 3 ) {
    printf( "usage : ./server 0.0.0.0 6666\n" );
    exit( -1 );
  }
  const char * ip_string_p = argv[ 1 ];
  int port_int    = atoi( argv[ 2 ] );
  int backlog_int = 10;
  int common_ret_int;
  int listen_socket_fd;
  int client_socket_fd;
  struct sockaddr_in socket_base_struct;
  struct sockaddr_in client_base_struct;
  int client_struct_length_int;
  int socket_opt_address_reuse_int = 1;
  listen_socket_fd = socket( PF_INET, SOCK_STREAM, 0 );
  if ( listen_socket_fd < 0 ) {
    exit( -1 );
  }
  setsockopt( listen_socket_fd, SOL_SOCKET, SO_REUSEADDR, &socket_opt_address_reuse_int, sizeof( socket_opt_address_reuse_int ) );
  // 创建socket struct结构体并清空其中内存的数据
  bzero( &socket_base_struct, sizeof( socket_base_struct ) );
  socket_base_struct.sin_family = PF_INET;
  // 将PORT转换成big-endian的PORT
  socket_base_struct.sin_port   = htons( port_int );
  // 将IP地址转换为big-endian的IP地址
  inet_pton( PF_INET, ip_string_p, &socket_base_struct.sin_addr );
  // 将分配好的address struct绑定好创建的listen socket上去
  common_ret_int = bind( listen_socket_fd, ( struct sockaddr * )&socket_base_struct, sizeof( socket_base_struct ) );
  if ( common_ret_int < 0 ) {
    exit( -1 );
  }
  // 开始监听listen socket
  common_ret_int = listen( listen_socket_fd, backlog_int );
  if ( common_ret_int < 0 ) {
    exit( -1 );
  }
  client_struct_length_int = sizeof( client_base_struct );
  // 让服务器陷入无限循环中
  while ( 1 ) {
    client_socket_fd = accept( listen_socket_fd, ( struct sockaddr * )&client_base_struct, &client_struct_length_int );
    if ( client_socket_fd < 0 ) {
      exit( -1 );
    }  
    // fork一下,子进程去调用处理 php-cgi 程序
    pid_t pid;
    pid = fork();
    if ( 0 == pid ) {
      // 别废话那么多,先能用再说
      char buf[ BUFFER_SIZE ];
      char content[ BUFFER_SIZE ];
      char * http_state_line_string_p;
      char * http_method_string_p;
      char * http_query_string_p;
      char * http_version_string_p;
      FILE * file_fd;
      recv( client_socket_fd, content, BUFFER_SIZE - 1, 0 );
      /*
       此处顺带为了让泥腿子们了解HTTP协议,我直接把http协议传输过来的数据
       全部打印出来,你们感受一下传说中HTTP协议load的数据是长什么样子的.
       一般说来,http服务器要做的就是解析这段http数据,解析成标准格式供我们
       使用。
       */
      printf( "这就是传说中的HTTP协议的具体数据内容:\n" );
      printf( "%s\n", content );
      printf( "传说中HTTP协议数据内容已经OVER\n" );
      /*
       下面四行代码,是将HTTP数据中第一行:状态请求行 截取出来后开始解析
       - GET则是PHP中常见的$_SERVER['http_method']
       - /?username=xiaodushe则为QUERY_STRING
       - HTTP/1.1则为http协议版本
       这三项内容在php中都保存在了$_SERVER中..如果我没记错的话
       strtok()是C语言函数中的一个奇葩......
       */
      http_state_line_string_p = strtok( content, "\r\n" );
      http_method_string_p     = strtok( http_state_line_string_p, " " );
      http_query_string_p      = strtok( NULL, " " );
      http_version_string_p    = strtok( NULL, " " );
      // 就先码死这个php-cgi程序吧,理论上cgi程序应该根据请求路径不同加载不同的cgi程序...
      // 这个。。。先将就一下,码死成一个固定的cgi,能用就行..
      // 我们将get参数通过设置环境变量传递给php-cgi程序
      setenv( "QUERY_STRING", http_query_string_p, 1 );
      setenv( "HTTP_METHOD", http_method_string_p, 1 );
      setenv( "HTTP_VERSION", http_version_string_p, 1 );
      // 从php-cgi拿回来数据...
      FILE * fp = popen( "./test.php", "r" );
      // 下面是按照http协议标准手工构造http数据返回给客户端
      // 如果你不按照下面标准进行构造,客户端一般会返回一些提示,比如
      // curl会返回:curl: (52) Empty reply from server
      char html_entity[ BUFFER_SIZE ];
      char html_body_content[ BUFFER_SIZE ];
      fread( html_body_content, sizeof( char ), sizeof( html_body_content ), fp );
      char html_response_template[] = "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\nContent-Length: %d\r\nHttp-Server:ti-server\r\n\r\n%s";
      sprintf( html_entity, html_response_template, strlen( html_body_content ), html_body_content );
      send( client_socket_fd, html_entity, sizeof( html_entity ), 0 );
      // 关闭与客户端的连接.
      close( client_socket_fd );
      exit( -1 );
    }
  }
  // 关闭socket
  close( listen_socket_fd );
  return 0;
} 

上面的程序保存为server.c,请在Linux下输入如下命令编译一下,反正能用:

代码语言:javascript
复制
gcc server.c -o server  // 编译
./server 0.0.0.0 6666   // 表示在6666端口上启动该服务器

与server.c同级目录下新建一个test.php文件,内容如下:

代码语言:javascript
复制
#! /usr/bin/php-cgi
<?php
echo 'http版本:'.$_SERVER['HTTP_VERSION'].PHP_EOL;
echo 'http方法:'.$_SERVER['HTTP_METHOD'].PHP_EOL;
echo 'query-string:'.$_SERVER['QUERY_STRING'].PHP_EOL;
echo "hello,xiaodushe~".PHP_EOL; 

上述demo代码已经上传到github,地址为:

https://github.com/elarity/wechat-official-accounts-demo-code

好了,一切就绪,我们使用curl充当浏览器访问一下服务器。我这里服务器打印日志和curl客户端打印的日志分别如下图所示,你们感受一下:

服务器端的日志数据

curl客户端的日志数据

好了,这就是一个典型的极其粗暴的CGI程序流程。其中一些细节并不完全遵守CGI标准流程,重在参与重在参与。。。

遥想泥腿子之王Lerdorf当年,拖鞋裤衩,抠脚趾头间PHP CGI码网站...

说来有点儿意思,我当初刚接触PHP那会儿还是用的APACHE服务器,这APACHE最初和PHP就有两种友好的数据洽谈方式:

  • 本文中的CGI方式
  • 赫赫有名的PHP_MOD方式(模块方式)
  • FAST-CGI方式

由于PHP_MOD方式性能明显是要比CGI方式好不少的,所以默认情况下APACHE是用PHP_MOD方式。有兴趣的同学可以扒一个APACHE然后配置一波儿试试看,还是那句话:反正能用。


文章附录以及关键字:

  • CGI
  • CGI协议标准地址:https://tools.ietf.org/html/rfc3875
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
NAT 网关
NAT 网关(NAT Gateway)提供 IP 地址转换服务,为腾讯云内资源提供高性能的 Internet 访问服务。通过 NAT 网关,在腾讯云上的资源可以更安全的访问 Internet,保护私有网络信息不直接暴露公网;您也可以通过 NAT 网关实现海量的公网访问,最大支持1000万以上的并发连接数;NAT 网关还支持 IP 级流量管控,可实时查看流量数据,帮助您快速定位异常流量,排查网络故障。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档