程序员

服务器开发系列 1

2017-08-28  本文已影响214人  daydaygo

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, 这里简单解释一下 同步 / 异步:

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

协议

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

这里解释一下网络序:

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

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

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

protobuf

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

php 使用 protobuf 需要做的准备:

# 解压后, 进入 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]);
    }
});

需要注意:

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 需要使用的技术梳理了一遍, 下周就要开始写业务代码了, 期待最后的压测的结果.

上一篇下一篇

猜你喜欢

热点阅读