美文网首页程序员
服务器开发系列 1

服务器开发系列 1

作者: daydaygo | 来源:发表于2017-08-28 04:06 被阅读214次

    title: 服务器开发系列 1
    date: 2017-8-28 01:18:14

    算是第一次在实际项目中写 tcp server, 确实有些吃力, 不过投入产出还不错, Mark 一下, 和大家一起学习.

    用 php 来写服务器, swoole 当然是首选. 当然, swoole 是将网络底层都打包好了, 应用层的服务治理发现 / 分布式 / 框架 等等, 还是需要自己基于 swoole 来写了. 不过 swoole 现在的生态链很好, 开源项目也多. 至于之前一直被诟病的文档, rango 在 php 开发者大会 上说过今年 swoole 的开发工作, 其中一部分就是文档. 所以, 期待 swoole 越来越好.

    入门例子

    $serv = new swoole_server("127.0.0.1", 9501); // 绑定的本地 ip, 所以只能本地访问
    $serv->set(array(
        'worker_num' => 8,
        'daemonize' => true, // 后台服务, 测试时设置成 false, 方便查看打印的信息
    ));
    $serv->on('connect', function ($serv, $fd){
        echo "Client:Connect.\n";
    });
    $serv->on('receive', function ($serv, $fd, $from_id, $data) {
        $serv->send($fd, 'Swoole: '.$data);
        $serv->close($fd);
    });
    $serv->on('close', function ($serv, $fd) {
        echo "Client: Close.\n";
    });
    $serv->start();
    

    上面的代码就是一个 异步 的 tcp server, 这里简单解释一下 同步 / 异步:

    • 同步代码, 会同步阻塞, 使用进程模型, 一个进程只能处理一个请求, 依赖进程多少来来处理并发, 但进程越多, 进程间切换开销会越来越大
    • 异步代码, 使用事件驱动, 所有地方都要改为异步代码, 当某个请求进入等待后, 进程并不会等在这里, 而是切换去处理就绪请求, 所以只需要设置为 1-4 倍的 cpu 核数即可

    补充一个知识点: php 中回调的 4 种写法 -- 闭包(也叫 匿名函数) / 函数 / 类方法 / 类静态方法

    协议

    为什么要使用协议(protocol)? 因为 tcp 协议是流式的, 可能多次信息合并到一个包, 也可能一个信息分多个包传输, 所以应用层就需要自定义协议, 进行「分包」「合包」, 来确定数据的边界(即确定一次消息). 常用的自定义协议有 2 种: EOF 协议 和 固定包头协议.

    • EOF 协议: 固定结尾符

    读取消息, 直到遇到自定义结尾符. 优点是简单, 但是需要保证发送的消息中不包含「结尾符」, 否则就被拆开了.

    swoole 中要设置 EOF 协议非常轻松:

    $serv->set([
        'open_eof_split' => true,   // 开启EOF检测
        'package_eof' => '/r/n' ,   // 设置EOF标记
    ]);
    

    实际中可能并不常使用, 但是在测试时非常有用, 方便直接用 telnet 连接服务器进行调试.

    • 固定包头协议: 先读取固定包头, 获取包体大小信息, 然后读取这样大小的数据, 就是包体了

    swoole 中设置固定包头协议, 也非常轻松:

    $serv->set([
       'open_length_check'     => 1,       // 开启协议解析
       'package_length_type'   => 'N',     // 长度字段的类型
       'package_length_offset' => 0,       // 第N个字节是包长度的值
       'package_body_offset'   => 4,       // 第N个字节开始计算长度
       'package_max_length'    => 2000000, // 协议最大长度
    ]);
    

    package_length_type 这里使用的 N, 代表 4 字节 uint 型网络序. 对应的类型, 可以参考 php manual - pack().

    这里解释一下网络序:

    • 最小的计算机单位是 bit, 即 , 只能表示 0 和 1, 实际使用时, 最小的单位是 byte, 即 字节(8 bit)
    • 对于多字节数据, 按照字节进行划分, 就出现了一个排序的问题, 即高位放在前面还是后面的问题, 于是就产生了大端序和小端序
    • 究竟使用的大端序还是小端序, 不同的机器是不一样的, 这个就叫做机器序
    • 不统一当然不行了, 网络传输的数据就不一致了, 所以出现了网络序, 统一使用 大端序 来传输数据

    固定包头还有一个玩法, 前 4 字节用来表示消息编号, 然后再用 4 字节来表示数据包大小, 而这只需要修改一下 package_length_offsetpackage_max_length 参数即可

    比较简单的做法 固定包头 + json 包体

    当然, 还可以指定更加复杂的协议, 不过本质上都抛不开这 2 种方式, 比如 http 协议中会有 content-lentth, 还有我之前所在的游戏公司, 使用 固定整形签名 + 校验和 + 包体大小 + 包 作为协议.

    protobuf

    有 Google 当爹, 这个我就不过多介绍了. 我这里说明一下 php 如何快速入门 protobuf.

    • protobuf 现在已经支持 php 了(我大 php 在服务器领域还是后劲十足的)
    • protobuf 由 2 部分组成, protoc(protobuf compile), 用来将 proto 文件, 编译输出为不同语言可以使用的文件; protobuf runtime, 用来执行这些文件

    php 使用 protobuf 需要做的准备:

    • 到官网下载 protoc 的可执行文件, 安装到系统中
    • 下载 protobuf 扩展, 即 protobuf runtime for php
    # 解压后, 进入 protoc 的可执行文件的文件夹
    cp bin/* /usr/local/bin/ # 复制 protoc 文件
    cp -r include/* /usr/local/include/
    
    # 使用 pecl 安装 protobuf 扩展
    pecl install protobuf
    pecl install protobuf-xxx # 指定不同斑斑
    pecl install localfile # 使用本地文件安装
    

    好了, 接下来定义我们的 protobuf 消息. protobuf 的消息类型(数据结构)很少, 扫一下 官方文档 即可:

    # game.proto 文件
    syntax = "proto3";
    
    package game.protobuf;
    
    message Auth {
        uint32 msgType = 1;
        int64 uid = 2;
        string token = 3;
        uint32 roomId = 4; // 认证并尝试上机
    };
    
    # 生成 php 可以使用的文件
    protoc --php_out=build/ game.proto
    
    # 复制 build/ 文件夹中的内容, 加入到我们的项目中, 修改 composer.json, 实现自动加载
    {
     "autoload": {
        "psr-4": {
          "Game\\Protobuf\\": "protobuf/Game/Protobuf",
          "GPBMetadata\\": "protobuf/GPBMetadata"
        }
      }
    }
    composer dumpautoload
    

    好了, 来一发:

    $auth = new Auth();
    $auth->setMsgType(1);
    $auth->setUid(1);
    $auth->setToken('daydaygo');
    $auth->serializeToString();
    
    $auth = new  \Game\Protobuf\Auth();
    $auth->mergeFromString($data);
    echo $auth->getToken();
    

    PS: 折腾了 protobuf 很久, 一个重要的原因的就是没有好好的阅读官方文档, 采取直接百度「php protobuf」这样的方式直接找「实战」, 但是, 理解相关的概念更重要
    PPS: 把文档里面下载 protoc 文件的文件名看错了, 然后一直跑不起来, 浪费了好长时间

    tcp auth

    需要做一个简单的认证: tcp 连接后, 客户端必须先发送 Auth 消息来认证, 认证不过就会断开连接. 这个需求的难点在于: 怎么确定用户是第一次给你发消息?

    和负责另一个 tcp server 的同事(java)讨论, 他那边将每个 连接 都抽象成了对象, 有私有变量 _auth 来表示是否认证. 虽然是放在对象里, 其实本质还是使用内存保存了当前连接的状态的. 既然如此, 我也可以直接申请内存来保存这个状态.

    于是, swoole_table 参上:

    $swooleTable = new swoole_table('100000'); // 最多 10w 同时连接
    $swooleTable->column('auth', swoole_table::TYPE_INT, '1'); // 判断是否 auth
    $swooleTable->create();
    $swooleTable->set(1, ['auth' => 1]);
    var_dump($swooleTable->get(2, 'auth'));
    
    $serv->on('receive', function (swoole_server $serv, $fd, $from_id, $data) use ($swooleTable) {
        // 协议解析
       $data = decode($data);
       $auth = new  \Game\Protobuf\Auth();
       $response = 'your token: '. $auth->getToken();
       $auth->mergeFromString($data);
       $serv->send($fd, encode($response));
    
        // auth 后
        if ($swooleTable->get($fd, 'auth')) {
            echo $data, "\n";
            return;
        }
        echo "$data\n";
        $arr = explode(':', $data);
        var_dump($arr);
        if ($arr[0] !== 'token') {
            echo "need auth $fd\n";
            $serv->close($fd);
        } else {
            echo "auth ok $fd $data";
            $swooleTable->set($fd, ['auth' => 1]);
        }
    });
    

    需要注意:

    • swoole_table 中, row 用来代表数据个数, column 用来表示数据可以具有的属性;
    • swoole_table 运行后不能再次动态分配

    PS: 欢迎大大们提供更好的方案, 总感觉这个一杯茶熬出来的方案不是最常用的

    mq

    有 2 个 tcp server 需要进行数据交互, 考虑到解耦, 于是引入了 mq(message queue , 消息队列). 实际场景中, mq 中的数据量不大, 所以直接使用 redis 的 pub/sub, 当然, 最终还是要做压测的, 以压测数据为准. 目前我们看靠了这篇 blog redis的pub/sub性能测试.

    // pub.php
    $redis = new \Redis();
    $res = $redis->connect('127.0.0.1', 6379);
    $res = $redis->publish('test','hello,world'); // channel + msg
    
    // sub.php
    $redis = new \Redis();
    $res = $redis->pconnect('127.0.0.1', 6379,0);
    $redis->subscribe(['test'], 'callback');
    
    // 回调函数,这里写处理逻辑
    function callback($instance, $channelName, $message) {
     echo $channelName, "==>", $message,PHP_EOL;
    }
    

    好了, 这周主要把这次 tcp server 需要使用的技术梳理了一遍, 下周就要开始写业务代码了, 期待最后的压测的结果.

    相关文章

      网友评论

        本文标题:服务器开发系列 1

        本文链接:https://www.haomeiwen.com/subject/vjkwdxtx.html