#network-programming http://tigerb.cn/tags/network-programming/
前言
随着工作年限的变长, 干这行的紧迫感仍然和刚参加工作一样,毫无疑问作为一名服务端开发人员网络编程是我下一步需要攻破的地方之一:
学习思路
以下是我对学习网络编程的一个简单的学习思路, 之后我将会按照这个计划去逐步学习网络编程相关的知识.
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
- $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
- socket_bind($socket, '127.0.0.1', '8889');
- socket_listen($socket);
- while (true) {
- // accept
- $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 代码只要按照上面的规范解析和返回出对应的内容即可, 简单的代码例子如下:
- /**
- * PHP 实现简单的 http 协议
- */
- class HttpProtocol
- {
- /**
- * 原始请求字符串
- *
- * @var string
- */
- public $originRequestContentString = '';
- /**
- * 原始请求字符串拆得的列表
- *
- * @var array
- */
- private $originRequestContentList = [];
- /**
- * 原始请求字符串拆得的键值对
- *
- * @var array
- */
- private $originRequestContentMap = [];
- /**
- * 定义响应头信息
- *
- * @var array
- */
- private $responseHead = [
- 'http' => 'HTTP/1.1 200 OK',
- 'content-type' => 'Content-Type: text/html',
- 'server' => 'Server: php/0.0.1',
- ];
- /**
- * 定义响应体信息
- *
- * @var string
- */
- private $responseBody = '';
- /**
- * 响应内容
- *
- * @var string
- */
- public $responseData = '';
- /**
- * 解析请求信息
- *
- * @param string $content
- * @return void
- */
- public function request($content = '')
- {
- if (empty($content)) {
- // exception
- }
- $this->originRequestContentList = explode("\r\n", $this->originRequestContentString);
- if (empty($this->originRequestContentList)) {
- // exception
- }
- foreach ($this->originRequestContentList as $k => $v) {
- if ($v === '') {
- // 过滤空
- continue;
- }
- if ($k === 0) {
- // 解析 http method/request_uri/version
- 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;
- }
- }
- /**
- * 组装响应内容
- *
- * @param [type] $responseBody
- * @return void
- */
- 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) {
- // accept
- $client = socket_accept($server);
- if (! $client) {
- continue;
- }
- $request = socket_read($client, 1024);
- /**
- * HTTP
- */
- $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 的连接过程. 静待~
来源: https://juejin.im/entry/5bfab6456fb9a049f818f4b6