服务器开发系列 1

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    保川阅读 5,941评论 1 13
  • 前言 上回我们简单介绍了一下TCP Server的工作方式以及如何用Swoole实现一个简单的TCP Server...
    零一间阅读 1,207评论 0 2
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,182评论 11 349
  • title: 服务器开发系列 2date: 2017-9-13 11:27:46 经过 3 周的疯狂加班后, 服务...
    daydaygo阅读 669评论 0 4
  • 真的是很厌烦实习学校的安排 学校就不能派出一两个老师来监考吗 实习生的课就能说不上就不上吗 有没有尊重过别人的劳动...
    因为我是成凤i阅读 192评论 0 0