去年在网上发了一篇《我用php构建了魔兽世界服务器,只为证明php是世界上最好的语言》的帖子反响还不错,github骗了不少赞。年初由于疫情在家里待了很久,无聊之中又开了一个新项目准备继续骗赞~哈哈哈
说起传奇,8090的童鞋估计都有所接触,这可是比魔兽世界还早的网络游戏,记得那时候中国区开服,网吧里基本上都是玩传奇的童鞋,楼主那时候还小,一个叔叔带着我玩了一段时间,每次玩到7级就换号,没钱去充值,哈哈哈,但是感觉也很有乐趣~
随着年纪越来越大,工作越来越忙,玩游戏的时间几乎没有,偶尔心血来潮去玩玩,感觉也是物是人非,玩着玩着也没什么意思了,但是依旧喜欢游戏,那就干脆自己写点游戏过过瘾吧
言归正传,看看怎么用php实现传奇服务端吧
我们选用的扩展依然为Swoole,框架使用Hyperf做底层,这个框架不错,很多东西不用自己再去写了,比如连接池、依赖注入容器、注解、AOP面向切面、基于 PSR-15 的中间件、自定义进程等等
相比于之前的魔兽世界模拟器用户验证服务及游戏服务,本次传奇模拟服务端将多个Worker进程作为网关服务,然后将数据包转发到游戏进程(其实就是一个自定义进程),在多进程模式下存在进程内存隔离,不同进程之间无法同步
对应的解决方案就是使用外部存储服务:
数据库,如:MySQL、MongoDB
缓存服务器,如:Redis、Memcache
磁盘文件,多进程并发读写时需要加锁。
普通的数据库和磁盘文件操作,存在较多 IO 等待时间。
Redis 内存数据库,读写速度非常快,但是有 TCP 连接等问题,性能也不是最高的。
/dev/shm 内存文件系统,读写操作全部在内存中完成,无 IO 消耗,性能极高,但是数据不是格式化的,还有数据同步的问题。
最后还要扩展内置的Swoole\Table,但是这种方案是需要先设置内存大小和字段类型不够灵活。
最后经过各种折腾决定使用自定义进程多协程的方案,网关可以开任意个进程处理并发业务,自定义进程去解决核心的数据业务,单进程多协程解决内存隔离问题同时也满足了性能要求~
接下来看看TCP数据的解包与封包
传奇使用BinaryReader来进行数据包的封包与解包,但php好像没有相关的类,但可以用pack/unpack函数去封装
'c',
'int8' => 'c',
'int16' => 's',
'int32' => 'l',
'int64' => 'q',
'uint8' => 'C',
'uint16' => 'v',
'uint32' => 'V',
'uint64' => 'P',
'bool' => 'c',
'float32' => 'f',
];
public function unPackString(string $type, string $packet)
{
return unpack($this->packetString[$type], $packet)[1];
}
public function packString(string $type, string $packet)
{
return pack($this->packetString[$type], $packet);
}
public function read(array $struct, string $packet): array
{
$data = [];
foreach ($struct as $k => $v) {
switch ($v) {
case 'string':
if ($packet) {
$len = $this->unPackString($v, $packet);
$data[$k] = substr($packet, 1, $len);
$packet = substr($packet, $len + 1);
} else {
$data[$k] = '';
}
break;
case 'int8':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 1);
} else {
$data[$k] = 0;
}
break;
case 'int16':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 2);
} else {
$data[$k] = 0;
}
break;
case 'int32':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 4);
} else {
$data[$k] = 0;
}
break;
case 'int64':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 8);
} else {
$data[$k] = 0;
}
break;
case 'uint8':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 1);
} else {
$data[$k] = 0;
}
break;
case 'uint16':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 2);
} else {
$data[$k] = 0;
}
break;
case 'uint32':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 4);
} else {
$data[$k] = 0;
}
break;
case 'uint64':
if ($packet) {
$data[$k] = $this->unPackString($v, $packet);
$packet = substr($packet, 8);
} else {
$data[$k] = 0;
}
break;
case '[]int8':
if ($packet) {
$len = strlen($packet);
$info = [];
for ($i = 0; $i < $len; $i++) {
$info[] = $this->unPackString('int8', $packet);
$packet = substr($packet, 1);
}
$data[$k] = $info;
} else {
$data[$k] = 0;
}
break;
case '[]int32':
if ($packet) {
$len = 4;
$info = [];
for ($i = 0; $i < $len; $i++) {
$info[] = $this->unPackString('int8', $packet);
$packet = substr($packet, $len);
}
$data[$k] = $info;
} else {
$data[$k] = 0;
}
break;
}
}
return $data;
}
public function write(array $struct, array $packet)
{
$data = '';
foreach ($struct as $k => $v) {
if (isset($packet[$k]) && $packet[$k] !== null) {
if (is_array($v)) {
if (!empty($packet[$k][0]) && is_array($packet[$k][0])) {
foreach ($packet[$k] as $k1 => $v1) {
$data .= $this->write($v, $v1);
}
} else {
$data .= $this->write($v, $packet[$k]);
}
} else {
switch ($v) {
case 'string':
$len = $this->packString($v, strlen($packet[$k]));
$data .= $len . $packet[$k];
break;
case 'bool':
$packet[$k] = $packet[$k] ? 1 : 0;
$data .= $this->packString($v, $packet[$k]);
break;
case '[]int32':
if (is_array($packet[$k])) {
foreach ($packet[$k] as $k1 => $v1) {
$data .= $this->packString('int32', $v1);
}
}
break;
case '[]uint8':
if (is_array($packet[$k])) {
foreach ($packet[$k] as $k1 => $v1) {
$data .= $this->packString('uint8', $v1);
}
}
break;
case '[]string':
if (is_array($packet[$k])) {
foreach ($packet[$k] as $k1 => $v1) {
$len = $this->packString('string', strlen($v1));
$data .= $len . $v1;
}
}
break;
default:
$data .= $this->packString($v, $packet[$k]);
break;
}
}
}
}
return $data;
}
}
封装了11中数据格式,可以满足大部分游戏的封包及解包了,看看是如何使用的
比如先定义好一个数据包的结构,根据字段对应的类型进行封包或解包就能获取数据
到此就可以去写业务逻辑了~
放出地址,详细代码已经开源,有需要的童鞋可以去试试,记得点赞哈~求赞求赞求赞:https://github.com/fan3750060/pmir2