用php入门网络编程 
    
        2018-11-24
    
     
前言 随着工作年限的变长,干这行的紧迫感仍然和刚参加工作一样,毫无疑问作为一名服务端开发人员网络编程 是我下一步需要攻破的地方之一。
学习思路 以下是我对学习网络编程的一个简单的学习思路,之后我将会按照这个计划去逐步学习网络编程相关的知识。
step 1. 原生php实现TCP Server -> 原生php实现http协议 -> 掌握tcpdump的使用 -> 深刻理解tcp连接过程 step 2. 原生php实现多进程webserver     2.1 引入I/O多路复用     2.2 引入php协程(yield)     2.3 对比 I/O多路复用版本 和 协程版本的性能差异 step 3. 实现简单的go web框架 step 4. php c扩展实现简单的webserver 
 
为什么我会选择用php去学习网络编程?因为对于我来说,php算是最熟悉的,其次php相对来说简单些,同时php自身也有相应的函数支持。
我们今天先开始第一部分的学习。
step 1. 原生php实现TCP Server -> 原生php实现http协议 -> 掌握tcpdump的使用 -> 深刻理解tcp连接过程
 
正文 我们先简单回顾下php作为后端语言的常见的交互方式过程:
client –(protocol:http)–> nginx –(protocol:fastcgi)–> php-fpm –(interface:sapi)–> php
 
在这里nginx充当的web server和反向代理server的角色,把http协议转换成了fastcgi协议。看到这里有些小伙伴可能会说了:“如果php自己直接处理http请求,不就可以不用nginx&php-fpm了么?” 遗憾的是原生php木有实现http协议(是吧,欢迎纠错)。
然后可能又有小伙伴说:“原生php不是支持tcp协议么?nginx把http请求代理成tcp协议不就可以不用php-fpm了吗。” ,嗯,是的,没错。这位小伙伴的描述的交互过程如下:
client –(protocol:http)–> nginx –(protocol:tcp)–> php
 
这样看起来是没啥问题,很不错的想法,但是理论来说还是没有实现http协议,接收到的内容应该还是一坨字符串。我们马上来试一下:
step 1: 起一个nginx服务 step 2: php简单实现一个TCP server,简单的代码如下 <?php $server  = socket_create (AF_INET, SOCK_STREAM, SOL_TCP);socket_bind ($server , '127.0.0.1' , '8889' );socket_listen ($server );while  (true ) {    $client  = socket_accept ($server );     if  (! $client ) {         continue ;     }     $request  = socket_read ($client , 1024 );          var_dump ($request );     socket_close ($client ); } 
 
step 3: nginx 反向代理http请求到 上面的tcp server, 配置如下 upstream tcp_server {     ip_hash;     server 127.0.0.1:8889 max_fails=3 fail_timeout=5; } server {     listen       80;     server_name  test.local;     access_log  /tmp/logs/nginx/test.access.log  main;     location / {         proxy_set_header X-Forwarded-For $remote_addr;         proxy_set_header Host            $http_host;         proxy_pass http://tcp_server;     } } 
 
最后我们访问下http://test.local/?aaa=1/ 看下打印的结果和之前的推测一致:
string(127) "GET /?aaa=1 HTTP/1.0 X-Forwarded-For: 127.0.0.1 Host: test.local Connection: close User-Agent: curl/7.54.0 Accept: */* " 
 
所以我们就需要实现http协议,既然都实现了http协议,那就可以直接使用http作为web server了。
 client –(protocol:http)–> php
 
是吧!之后nginx的角色就是负载均衡,其实过分点你自己也可以用php做负载均衡。
原生php实现TCP Server 接着我们看看如何用php创建一个简单的TCP Server过程如下:
 
主要涉及的PHP函数如下:
socket_create socket_listen socket_accept socket_recv || socket_read socket_write socket_close 
 
代码:
<?php $server  = socket_create (AF_INET, SOCK_STREAM, SOL_TCP);socket_bind ($server , '127.0.0.1' , '8889' );socket_listen ($server );while  (true ) {         $client  = socket_accept ($server );     if  (! $client ) {         continue ;     }     $request  = socket_read ($client , 1024 );     socket_close ($client );     echo  socket_strerror (socket_last_error ($server )) . "\n" ; } 
 
命令行运行上述代码,然后用nc命令测试小tcp连接是否成功:
(tigerb) ➜  demo git:(master) ✗ nc -z -v 127.0.0.1 8889 found 0 associations found 1 connections:      1: flags=82<CONNECTED,PREFERRED>         outif lo0         src 127.0.0.1 port 60668         dst 127.0.0.1 port 8889         rank info not available         TCP aux info available Connection to 127.0.0.1 port 8889 [tcp/ddi-tcp-2] succeeded! 
 
没毛病,TCP Server起来了。
原生php实现HTTP协议 上面简单的TCP Server基本出来了,我们需要让php直接成为一个Web Server,想一想Web Server是基于HTTP协议的,HTTP协议又是基于TCP协议实现的。也就是说我们在上面的TCP Server基础上实现下HTTP协议即可。我们改进下流程图加入HTTP部分(橙黄色),如下
 
实现HTTP协议的过程其实就是:
能读懂发来请求的信息 
能返回给浏览器等客户端它们能懂的信息 
 
协议无非就是双方协定好的规范,一样在HTTP/1.1中 请求&响应的格式基本如下
请求:
<HTTP Method> <url> <HTTP Version> <KEY>:<VALUE>\r\n ... \r\n 
 
响应:
<HTTP Version> <HTTP Status> <HTTP Status Description> <KEY>:<VALUE>\r\n ... \r\n 
 
所以简单来说,我们的php代码只要按照上面的规范解析 和返回 出对应的内容即可,简单的代码例子如下:
class  HttpProtocol  {         public   $originRequestContentString  = '' ;          private  $originRequestContentList  = [];          private  $originRequestContentMap  = [];          private  $responseHead  = [         'http'          => 'HTTP/1.1 200 OK' ,         'content-type'  => 'Content-Type: text/html' ,         'server'        => 'Server: php/0.0.1' ,     ];          private  $responseBody  = '' ;          public   $responseData  = '' ;          public  function  request ($content  = ''  )      {        if  (empty ($content )) {                               }         $this ->originRequestContentList = explode ("\r\n" , $this ->originRequestContentString);         if  (empty ($this ->originRequestContentList)) {                      }         foreach  ($this ->originRequestContentList as  $k  => $v ) {             if  ($v  === '' ) {                                  continue ;             }             if  ($k  === 0 ) {                                  list ($http_method , $http_request_uri , $http_version ) = explode (' ' , $v );                 $this ->originRequestContentMap['Method' ] = $http_method ;                 $this ->originRequestContentMap['Request-Uri' ] = $http_request_uri ;                 $this ->originRequestContentMap['Version' ] = $http_version ;                 continue ;             }             list ($key , $val ) = explode (': ' , $v );             $this ->originRequestContentMap[$key ] = $val ;         }     }               public  function  response ($responseBody  )      {        $count  = count ($this ->responseHead);         $finalHead  = '' ;         foreach  ($this ->responseHead as  $v ) {             $finalHead  .= $v  . "\r\n" ;         }         $this ->responseData = $finalHead  . "\r\n"  . $responseBody ;     } } 
 
我们在socket_read后面插入代码即可
while  (true ) {         $client  = socket_accept ($server );     if  (! $client ) {         continue ;     }     $request  = socket_read ($client , 1024 );          $http  = new  HttpProtocol ;     $http ->originRequestContentString = $request ;     $http ->request ($request );     $http ->response ("Hello World" );     socket_write ($client , $http ->responseData);          socket_close ($client );     echo  socket_strerror (socket_last_error ($server )) . "\n" ; } 
 
最后访问http://127.0.0.1:8889/ 结果如下,或者浏览器打开页面即输出“Hello World”
(tigerb) ➜  demo git:(master) ✗ curl "http://127.0.0.1:8889/" -vv *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8889 (#0) > GET / HTTP/1.1 > Host: 127.0.0.1:8889 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/html < Server: php/0.0.1 * no chunk, no close, no size. Assume close to signal end < * Closing connection 0 Hello World%  
 
结语 至此我们用php就简单搭建出了一个web server,在这个基础上php就可以直接和客户端交互了。最后,我们将用这个简单的web server通过tcpdump抓包来分析tcp的连接过程。静待~