去年我在网上发了一个帖子《我用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',];publicfunctionunPackString(string$type,string$packet){returnunpack($this->packetString[$type],$packet)[1];}publicfunctionpackString(string$type,string$packet){returnpack($this->packetString[$type],$packet);}publicfunctionread(array$struct,string$packet):array{$data=[];foreach($structas$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]='';}休息;case'int8':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,1);}else{$数据[$k]=0;}休息;case'int16':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,2);}else{$数据[$k]=0;}休息;case'int32':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,4);}else{$数据[$k]=0;}休息;case'int64':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,8);}else{$数据[$k]=0;}休息;case'uint8':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,1);}else{$数据[$k]=0;}休息;case'uint16':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,2);}else{$数据[$k]=0;}休息;case'uint32':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,4);}else{$数据[$k]=0;}休息;case'uint64':if($packet){$data[$k]=$this->unPackString($v,$packet);$packet=substr($packet,8);}else{$数据[$k]=0;}休息;case'[]int8':如果($packet){$len=strlen($packet);$信息=[];对于($i=0;$i<$len;$i++){$info[]=$this->unPackString('int8',$packet);$packet=substr($packet,1);}$数据[$k]=$信息;}else{$数据[$k]=0;}休息;case'[]int32':if($packet){$len=4;$信息=[];对于($i=0;$i<$len;$i++){$info[]=$this->unPackString('int8',$packet);$packet=substr($packet,$len);}$数据[$k]=$信息;}else{$数据[$k]=0;}休息;}}返回$数据;}publicfunctionwrite(array$struct,array$packet){$data='';foreach($structas$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->写($v,$v1);}}else{$data.=$this->write($v,$packet[$k]);}}else{switch($v){case'string':$len=$this->packString($v,strlen($packet[$k]));$数据.=$len。$数据包[$k];休息;案例“布尔”:$数据包[$k]=$数据包[$k]?1:0;$data.=$this->packString($v,$packet[$k]);休息;case'[]int32':if(is_array($packet[$k])){foreach($packet[$k]as$k1=>$v1){$data.=$this->packString('int32',$v1);}}休息;case'[]uint8':if(is_array($packet[$k])){foreach($packet[$k]as$k1=>$v1){$data.=$this->packString('uint8',$v1);}}休息;case'[]string':if(is_array($packet[$k])){foreach($packet[$k]as$k1=>$v1){$len=$this->packString('string',strlen($v1));$数据.=$len。$v1;}}休息;默认值:$data.=$this->packString($v,$packet[$k]);休息;}}}}返回$数据;}}封装了11种数据格式,可以满足大部分游戏的打包和解包,下面看看它是如何使用的。比如先定义一个数据包的结构,然后根据字段对应的类型进行封装或者解包就可以得到数据。然后就可以写业务逻辑了~放出地址,详细代码已经开源了,有需要的童鞋可以试试,记得点个赞~求个赞,求个赞,求个赞:https://github.com/fan3750060/pmir2
