【从零开始学Skynet】实战篇《球球大作战》(五):gateway代码设计(上)

news2025/1/18 20:24:06

   

1、协议格式

       在写代码之前,我们要先了解什么是协议,协议就是 “客户端向服务端发起的登录请求”,那么登录请求是什么样子的呢?这得先从TCP数据流说起,客户端发起的请求,就是一些二进制数据。

(1)TCP粘包现象

        TCP协议是一种基于数据流的协议,举例来说,如果客户端分两次发送“1234”和“5678”这两条消息。服务端可能一次性接收到“12345678”;也可能先只收到“12”,过一会儿才收到“345678”。

游戏的网络模块需要实现数据切分的功能,具体有三种方法,如下表所示:

方法说明
长度信息法每个数据包前面加上长度信息,每次接收到数据后,先读取表示长度的字节,如果缓冲区的数据长度大于要去的字节数,则取出相应的字节,否则等待下一次接收(此为最常用的方法)
固定长度法每次都以相同的长度发送数据,假设规定每条信息的长度都为10个字符,那么“hello”、“12”这两条信息可以发送成“hello.....”、“12.......”,其中的“.”表示填充字符,只为凑数没有实际意义,接收方每次取10个字符,作为一条消息去处理
结束符号法规定一个结束符号,作为消息间的分隔符。假设规定结束符号为“$”,那么“hello”、“12”这两条信息都可以发送成“hello$”、“12$”。接收方不停地读取数据,知道“$”出现为止,并且使用“$”去分割消息。(该方法最简单直观,本项目都使用该方法)

(2)协议格式 

        本项目中都会使用字符串协议格式,每条消息由“\r\n”作为结束符,消息的各个参数用英文逗号分隔,如下图所示:

  • 参数 login:登录协议
  • 参数101:要登录的玩家id
  • 参数134:密码

后续会实现编码解码方法,让协议字符串与Lua表互相转换。

 2、连接类和玩家类

        gateway需要使用两个列表,一个用于保存客户端连接信息,另一个用于记录已登录的玩家信息。我们之前说的让gateway客户端agent关联起来,即是将“连接信息”“玩家信息”关联起来。

定义了connsplayers这两个表,以及conngateplayer这两个类,代码如下所示:

conns = {}   --[fd] = conn
players = {} --[playerid] = gateplayer

--连接类
function conn()
    local m = {
        fd = nil,
        playerid = nil,
    }
    return m
end

--玩家类
function gateplayer()
    local m = {
        playerid = nil,
        agent = nil,
        conn = nil,
    }
    return m
end

        在客户端进行连接后,程序会创建一个conn对象(稍后实现),gateway会以fd为索引把它存进conns表中。conn对象会保存连接的fd标识,但playerid属性为空。此时gateway可以通过conn对象找到连接标识fd,给客户端发送消息。如下图所示:

         当玩家成功登录时,程序会创建一个gateplayer对象,gateway会以玩家id为索引,将它存入players表中。gateplayer对象会保存playerid(玩家id)、agent(对应的代理服务id)和conn(对应的conn对象)。关联conngateplayer,即设置conn对象的playerid

 登录后,gateway可以做到双向查找:   

  • 若客户端发送了消息,可由底层Socket获取连接标识fdgateway则由fd索引到conn对象,再由playerid属性找到player对象,进而知道它的代理服务(agent)在哪里,并将消息转发给agent;
  • agent发来消息,只要附带着玩家idgateway即可由playerid索引到gateplayer对象,进而通过conn属性找到对应的连接及其fd,向对应客户端发送消息。

3、接收客户端连接

实现gateway处理客户端连接的功能。

(1)初始化监听

        在服务启动后,service模块会调用s.init方法,在里面编写功能。代码如下所示:

function s.init()
    local node = skynet.getenv("node")
    local nodecfg = runconfig[node]
    local port = nodecfg.gateway[s.id].port

    local listenfd = socket.listen("0.0.0.0", port)
    skynet.error("Listen socket :", "0.0.0.0", port)
    socket.start(listenfd , connect)
end

        先开启Socket监听,程序读取了我们之前编写的配置文件runconfig,找到该gateway的监听端口port,然后使用skynet.socket模块的listenstart方法开启监听。当有客户端连接时,start方法的回调函数connect(稍后实现)会被调用。

(2)客户端连接

        当客户端连接上时,gateway创建代表该连接的conn对象,并开启协程recv_loop(稍后实现)专接收该连接的数据。代码如下所示:

--有新连接时
local connect = function(fd, addr)
    print("connect from " .. addr .. " " .. fd)
	local c = conn()
    conns[fd] = c
    c.fd = fd
    skynet.fork(recv_loop, fd)
end
  • 参数fd:客户端连接的标识,这些参数是socket.start规定好的;
  • 参数addr:客户端连接的地址,如“127.0.0.1:60000”;
  • c:新创建的conn对象。

(3)接收客户端消息

        recv_loop负责接收客户端消息。其中参数fdskynet.fork传入,代表客户端的标识。

--每一条连接接收数据处理
--协议格式 cmd,arg1,arg2,...#
local recv_loop = function(fd)
    socket.start(fd)
    skynet.error("socket connected " ..fd)
    local readbuff = ""
    while true do
        local recvstr = socket.read(fd)
        if recvstr then
            readbuff = readbuff..recvstr
            readbuff = process_buff(fd, readbuff)
        else
            skynet.error("socket close " ..fd)
			disconnect(fd)
            socket.close(fd)
            return
        end
    end
end

这段代码可以分成四个部分:

1)初始化:使用socket.start开启连接,定义字符串缓冲区readbuff。为了处理TCP数据的粘包现象,我们把接收到的数据全部存入readbuff中。

2)循环:通过while true do ...end实现循环,该协程会一直循环。每次循环开始,就会由socket.read阻塞的读取连接数据。

 3)若有数据:若接收到数据,程序将数据拼接到readbuff后面,再调用process_buff(稍后实现)处理数据。process_buff会返回尚未处理的剩余数据。

4)若断开连接:若客户端断开连接,调用disconnect(稍后实现)处理断开事务,再调用socket.close关闭连接。

说明:通过拼接Lua字符串实现缓冲区是一种简单的做法,它可能带来GC(垃圾回收)的负担,后面我们会介绍更高效的方法。

        下图对前面写的代码做了一个总结:当客户端连接时,程序通过skynet.fork发起协程,协程recv_loop是个循环,每个协程都记录着连接fd和缓冲区readbuff。收到数据后,程序会调用process_buff处理缓冲区里的数据。

 

 4、处理客户端协议

        根据上面的代码,服务端接收到数据后,就会调用process_buff,并把对应连接的缓冲区传给它,process_buff会实现消息的切分工作。

举例:如果缓冲区readbuff的内容是“login,101,134\r\nwork\r\nwo”,那么process_buff会把它切分成“login,101,123”“work”这两条消息交由下一阶段的方法去处理,然后返回“wo”,供下一阶段的recv_loop处理。

process_buff的整个处理流程如下图所示:

  •  它先接收缓冲区数据(阶段①);
  • 然后按照分隔符\r\n切分数据,并将切分好的数据交由process_msg方法处理(②阶段)
  • 最后返回尚未处理的数据“wo”(阶段④,返回值会重新赋给readbuff)。
  • process_msg会解码协议,并将字符串转为Lua表(如把字符串“login,101,123”转成图中的msg表,阶段③)。

        process_buff方法如下代码所示。由于缓冲区readbuff可能包含多条消息,且process_buff主体是个循环结构,因此每次循环时都会使用string.match匹配一条消息,再调用下一阶段的process_msg(稍后实现)处理它。

local process_buff = function(fd, readbuff)
    while true do
        local msgstr, rest = string.match( readbuff, "(.-)\r\n(.*)")
        if msgstr then
            readbuff = rest
            process_msg(fd, msgstr)
        else
            return readbuff
        end
    end
end
  • fd:客户端连接的标识;
  • readbuff:接收数据的缓冲区;
  • msgstr和rest:根据正则表达式“(.-)\r\n(.*)”的规则,它们分别代表取出的第一条消息和剩余的部分。

举例:假如readbuff的内容是“login,101,134\r\nwork\r\nwo”,经过string.match语句匹配,msgstr的值为“login,101,134”rest的值为“work\r\nwo”;如果匹配不到数据,例如readbuff的内容是“wo”,那么经过string.match语句匹配后,msgstr为空值。

完整代码放在下一篇一起提交。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/414950.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV实例(六)行人检测

OpenCV实例(六)行人检测1.行人检测概述2.行人检测基础实现2.1基本流程2.2实现程序2.3参数优化3.完整行人检测程序作者:Xiou 1.行人检测概述 行人检测是目标检测的一个分支。目标检测的任务是从图像中识别出预定义类型目标,并确定…

【Python】json数据解析

目录 json文件数据解析 爬虫获取王者荣耀英雄信息json数据包并解析 爬虫获取抖音视频json数据包并解析 json文件数据解析 json字符串:通常类似python数据类型中的列表和字典的结合,也可能是单独的列表或者字典格式,通常可以通过json模块的…

亚马逊影响搜索排名的主要因素有哪些,使用测评做排名有哪些要求?

亚马逊产品的排名越高就意味着分配的流量越多而且带来更高的销量。那主要有哪些因素影响产品的排名呢? 1、产品销量 产品销量反映了该产品在同类产品中的销售情况,该数值会在产品Listing中展示,平台会每小时更新一次该排行榜。在平台算法看…

【Linux】线程控制分析:如何获取线程ID?线程如何自动回收?

Linux系统中, 线程是轻量级的进程. 我们已经介绍过了线程的相关概念, 见过了线程再Linux操作系统中的存在形式. 我们知道, 进程有自己相关控制接口, 等待、创建等 而线程作为轻量级的进程, 其实也是有控制接口的. 文章目录线程控制线程的创建与回收演示获取线程idpthread_sel…

用户管理系统-自动化测试

文章目录1. 思维导图编写 Web 自动化测试用例2. 创建测试项目3. 根据思维导图设计用户管理系统自动化测试用例3.1 准备工具类3.2 测试登录页面3.3 测试用户列表页3.4 测试添加用户页3.5 测试修改用户页3.6 未登录状态4. 自动化测试项目总结4.1 自动化测试项目实现步骤4.2 当前项…

图数据库驱动的基础设施运维实操

本文系图技术在大型、复杂基础设施之中 SRE/DevOps 的实践参考,并以 OpenStack 系统之上的图数据库增强的运维案例为例,揭示图数据库、图算法在智能运维上的应用。本文所有示例代码开源。 最近,有些尚未使用过图技术、DevOps/Infra 领域的工程…

除了Java,还可以培训学习哪些IT技术?

除了Java,还可以培训学习哪些IT技术?转行IT学Java似乎已经成为很多人的首选,原因无非是开发技术含量高、开发有前景、开发是一个互联网企业的核心岗位,最重要的是开发薪资待遇高。但其实只单纯因为薪资选择Java的话,小…

Flask数据迁移详细步骤

数据迁移详细步骤: 1. 安装好数据迁移的包 flask-sqlalchemy和flask-migrate Flask模型相关包安装 2. 在exts.py中初始化Migrate和SQLAlchemy 3. 在models中定义好模型 4. 在views.py中一定要导入models模块 from .models import * 5. 配置好数据库(sql…

MYSQL笔记01 数据库概述,SELECT语句,运算符,排序与分页,多表查询

数据库概述 为什么要使用数据库 持久化:把数据保存在可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以"固化",而持久化的实现过程大多通过各种关系数据…

详解 23 种设计模式(多图 + 代码)

创建型模式 创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类&a…

每68个孩子里,就有一个自闭症,“来自星星的孩子”,离我们很近

今年4月2日是世界上第14个“世界自闭症日”。自闭症儿童,又称“星星儿童”,形容他们像遥远的星星一样独自在夜空中闪耀。自闭症并不少见根据世卫组织的调查,世界上每160名儿童中就有一人患有自闭症。根据《中国自闭症(自闭症&…

【从零开始学Skynet】基础篇(四):网络模块常用API

游戏服务端要处理客户端请求,作为服务端引擎,网络编程也是Skynet的核心功能。1、学习网络模块 skynet.socket模块提供了网络编程的API,常用的API如下表所示:Lua API说明socket.listen(address ,port)监听一个端口,返回…

你需要知道的企业网页制作流程

企业网页制作是企业建立线上形象和宣传的重要手段之一,它不仅可以提高企业的品牌知名度,还可以扩大企业的影响力和拓展客户群。下面,我们将介绍一些企业网页制作的基本流程和技巧,并结合一个案例来详细解析。 企业网页制作的基本…

【DT】蒸脱机的结构和工作原理

DT蒸脱机的结构和工作原理什么是DTDT结构图工作过程什么是DT DT 蒸脱机(DesolventazationerToaster),根据英文名可以看出来,他的作用是脱溶、烘烤。用于蒸脱湿豆粕中的溶剂。 大豆油生产工艺有2种:压榨油的加工工艺是…

C++标准库--IO库(Primer C++ 第五版 · 阅读笔记)

C标准库--IO库(Primer C 第五版 阅读笔记)第8章 IO库8.1、IO类8.2、文件输入输出8.3、string流总结:第8章 IO库 8.1、IO类 为了支持这些不同种类的IO处理操作,在istream和ostream之外,标准库还定义了其他一些IO类型。 如下图分…

Java中的注解,自定义注解

文章目录1. 注解概述2. 注解与注释3. 注解的重要性4. 常见的Annotation作用4.1 生成文档相关的注解4.2 在编译时进行格式检查(JDK内置的三个基本注解)5. 元注解6. 自定义注解6.1 定义自定义注解6.2 使用自定义注解6.3 读取和处理自定义注解框架 注解 反射 设计模式 1. 注解概…

PC安装虚拟化平台趟坑记录

合肥先进光源永磁多极铁电机控制系统的规划 Zstack EPICS Archiver在小课题组的使用经验 神仙同学的永磁四极铁样铁已经开始加工了,过一个月左右就要回来了,电机控制部分交给留国做,调试的也差不多了。项目买过一台工控机,到时候…

STM32F407ZGT6实现OLED显示屏

1、调试工具 2、OLED简介 3、硬件电路(接线) 本文采用7脚,倘若采用4脚,资料代码啥的可以在江科大B站视频下载: 资料下载:https://pan.baidu.com/s/1SqKyKr5Fsl_9gBJi8aVxTw, 提取码:8kzh&#x…

日本首相会见奥特曼,考虑引入 ChatGPT 技术

文|小戏卖萌屋日本4月12日电,日本国第101任首相,日本自民党总裁岸田文雄4月10日于东京会见了奥特曼先生,二人就 ChatGPT 引入日本的可能性问题交换了意见并进行了深入的讨论。奥特曼先生表示,希望为日本人创造伟大的东…

NumPy 秘籍中文第二版:三、掌握常用函数

原文:NumPy Cookbook - Second Edition 协议:CC BY-NC-SA 4.0 译者:飞龙 在本章中,我们将介绍许多常用函数: sqrt(),log(),arange(),astype()和sum()ceil(),modf()&…