0%

转做游戏服务器开发之后,基本上都在写游戏逻辑,关于redis的操作也已经有了现成的接口。今天提交了代码有点空闲时间打算学一学redis。

博主是按照网上的教程使用redis-cli去连接redis,但是之前本地的redis已经改过redis的默认配置文件,直接使用redis-cil发现并不能连接上。

查询资料后发现可以指定ip,端口,和密码如下。

** redis-cli -h xxx.xxx.xxx.xxx -p xxxx -a mima 例如(redis-cli -h 127.0.0.1 -p 12345 -a zheshimima)**

即可连接。作此记录。

笔者转行做游戏服务器开发有四个月了,现在公司用的是skynet框架,由于之前没做过服务器开发,现在还是处于边学习,边跟项目的情况。由于工作原因,网络这一块断断续续看了两个星期,一直没理解,今天再看的时候,终于懂了skynet监听网络端口的流程,也懂了,客户端是怎么连接的。果然应了那句话:欠了技术债,早晚要还的。(学习一定要专注啊!!!可以省好多时间。)

** **这张图是在网上找的,非常的清晰,要顺着标识看,并结合代码,就能大致了解这个过程。

1.在main中会newservice watchdog启动看门狗服务,lua start conf给看门狗发送lua消息start参数为conf。

2.在watchdog服务中会,newservice gate启动网关服务(实际上是通过gateserver.start启动),然后监听传入或者默认的端口。

3.当有新连接接入的时候,实际上是gate server监听到的。

4.gate server send2watchdog open,并传入参数。

5.在watchdog server中会创建一个agent(每个连接接入,创建一个agent),并传入相关的信息(gate,fd,watchdog)。

6.由于有watchdog传入的信息,agent可以call2gate server forward(用于打开fd,一定要打开,链接成功不代表马上可以读到数据,需要打开这个套接字,允许fd接收数据,这样才能接受到client传入的数据)。

7.当client send requste,首先到达gate。

8.gate server 的message方法中处理,转发给agent。

9.agent处理之后,在发回给client。

需要注意的是:

1.实际上client连接到的是gate server。

2.gate server把具体的client消息转发给agent由agent处理具体逻辑。

3.gate 与 watchdog 与 agent是相互配合的。

至于为什么要这么做,目前我也不是很清楚。。。

今天早上上班后,发现电脑的机械硬盘居然崩了,原来以为这种情况只会存在传说中,没想到自己真正碰到了。由于基本上所有的资料和开发环境都是存在机械硬盘上的,一切的一切只能重来,借此机会,记录一下自己走过的路。

** **使用的是:

VMware:Workstation 15 Pro https://www.7down.com/soft/310739.html

Ubuntu:18.04.3 https://ubuntu.com/download/desktop

这些下载好之后,便开始装svn:

** 1.sudo apt-get update**

** 2.sudo apt install subversion**

** 3.输入 svn help 查看是否安装成功 **

出现这个,则表示安装成功。

4.安装成功后,输入svn co + 你的地址,会提示你输入svn账号与密码。

前几天,在写游戏逻辑时,需要在不同的代码块中引用我写的逻辑模块(以下简称C模块)来对玩家数据进行修改。由于对skynet理解不够,多模块中调用自己写的逻辑时,例如:A模块调用C,B模块也调用C,我在C模块中多次对数据库进行了操作,以便可以让不同的模块能操作到同一组数据。

经理看了我的代码之后,对我进行了指导,作此记录:

1.每个agent都是一个独立的虚拟机。

2.在一个虚拟机中,引用同一块代码后,再次引用会直接返回,并引用到同一块代码。(A引用C,B引用C,最后会共享同一块C代码,所以在C中可以直接用变量存贮玩家数据,不用每次都从数据库中读取)

在做游戏服务器开发之前之前一直有疑问,服务器是干什么的?问了几位前辈,得到的答案大概都是:服务器就是一台电脑,你可以访问,然后做一些事情(我现在觉得这个答案是很精辟的)。这个答案对于之前的我来说,由于根本没接触过服务器,不能理解其中的含义。百度得到的答案也不是我想要的。

现在做游戏服务器开发两个月了,分享一下自己对游戏服务器的理解,希望能以另外的角度给想做游戏服务器开发的新人一些不同理解方向。如果有什么说的不对的地方,请见谅。

游戏服务器其实就是处理游戏逻辑的(这话说的,新手谁看的懂啊。 = =!)

举个大话例子:餐厅

** **将一个餐厅点菜比喻成一个游戏,桌子上有菜单,菜单上有:鱼香肉丝,清蒸牛肉,有一位客人看了菜单之后点了一道菜(鱼香肉丝)后,服务员将这道菜名告诉了厨房,厨房做好菜后递给服务员,最后服务员给你端上来了。

在这个游戏中,餐桌相当于游戏的客户端,厨房相当于游戏服务器,服务员相当于客户端与服务器的通信,客人相当于玩家

客户端:桌子上的菜单和上的菜(鱼香肉丝),这些都是客户端给玩家显示的。

服务器:当菜名到达了厨房之后的一系列操作都是服务器做的(厨房开始准备,切菜,炒菜,完成后,告诉服务员,让他把菜端出去)。

做这个游戏的服务器开发,就相当于,增加餐厅能提供的菜。比如餐厅要求增加一道番茄炒蛋的菜,你就要教会厨房怎么弄番茄炒蛋,确保经过你的调教之后,厨房在收到这个菜名时一定能做出这道菜,或者做不出菜的时候会给客户端提示(卖完了之类的)游戏就能更新,客户端就会在菜单上新增加一道番茄炒蛋的菜,客人就能点这道菜。

出现bug又是个什么情况呢?例如你在调教厨房做这道菜时,只教会了厨房做菜,没有做什么别的操作。当点了这道菜,然后番茄用完了,这时候厨房由于你只教厨房做菜,没告诉它出现这种情况怎么办,厨房就不知道怎么办(出现了bug),最后客人一直在等,最后却没有上这道菜。

举个实际游戏例子:简单描述斗地主的一个简单流程

当你(玩家2)的上家(玩家1)出了一个3,轮到你出牌,你手上有345JK。

客户端:1.显示三位玩家的牌,你的上家和下家的牌都是背对着你的。

** 2.显示你的手牌,供你选择。**

** 3.收到服务器发来的消息(玩家1出了3),显示给你看。**

这时候你点了一个3,然后点击出牌。客户端——->服务器,玩家2出个3。

服务器:1.收到客户端发来的消息(玩家2出3)。

** 2.判断你是否能出这张牌。**

** 3.将判断结果(不符合出牌规则,不许出)返回给客户端。**

这时候客户端收到消息

客户端:1.显示提示:你出的牌不服务规范。(这张牌一直出不去)

这时候你点了一个4,然后点击出牌。客户端——->服务器,玩家2出个4。

** 服务器:1.收到客户端发来的消息(玩家2出4)。**

** 2.判断你是否能出这张牌。**

** 3.将判断结果(可以出牌)返回给客户端。**

这时候客户端收到消息

客户端:1.你的手牌少了一张4。

** 2.牌桌上多了一张4。**

然后轮到下一家出牌。

在这些例子中,服务器做的事情,都是需要游戏服务器开发人员通过代码来实现的。回到我几位前辈对我的回答:服务器就是一台电脑(电脑是硬件服务器,写的游戏服务器是软件,需要有硬件载体),你可以访问(客户端连接服务器),然后做一些事情(玩家出了一张3,请服务器告诉我,可不可以出)。

以上就是对游戏服务器的理解,希望能给你提供一个理解游戏服务器是干什么的思路。如果有什么说的不对的地方,请指出,我会尽快修改。

之后的工作会使用Xshell,今天搞一个下来连接自己的虚拟机,发现连接失败:

猜想是可能是虚拟机或者Xshell部分没设置好

查询后通过以下方法解决,做此记录:

1.安装OpenSSH,执行sudo apt-get install openssh-server openssh-client命令。

2.执行netstat -tnl命令,查看22端口。

博主通过以上方法已可以接连上。

参考地址:https://blog.csdn.net/s243471087/article/details/80208985

协同程序(coroutine)简介

Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

线程和协同程序区别

1.线程可以同时运行,协同程序却需要彼此协作的运行。

2.在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

协同程序有点类似与,在等待同一个线程锁的几个线程。

方法概览

方法
描述
coroutine.create(func)

创建coroutine,该方法只创建,如需唤醒coroutine需配合resume方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<p>入参:是协程运行的函数;</p>

<p>返回:成功返回coroutine。</p>
</td>
</tr><tr><td style="width:225px;">coroutine.resume(co, val1, ...)</td>
<td style="width:624px;">
<p>唤醒coroutine,和create配合使用;</p>

<p>入参:可任意输入,但是第一个入参必须是coroutine.create()的返回值。</p>

<p> 1.如果是第一次唤醒,其余入参作为协程运行函数的入参。多余的参数被舍弃。</p>

<p> 2.如果是唤醒调用yield()挂起的协程,其他输入参数将作为yield()的返回值。</p>

<p> 除第一个入参外,如果入参个数少于yield()返回值个数,则执行失败,如果多于yield()返回</p>

<p> 值个数,则多余的参数被舍弃,执行成功;</p>

<p>返回:成功返回true与yield()的入参。失败返回错误提示。</p>
</td>
</tr><tr><td style="width:225px;">coroutine.yield(val1, ...)</td>
<td style="width:624px;">
<p>挂起coroutine,和resume配合使用能有很多有用的效果;</p>

<p>入参:可以任意输入,输入的入参将作为resume的返回值;</p>

<p>返回:resume()的入参作。</p>
</td>
</tr><tr><td style="width:225px;">coroutine.status(co)</td>
<td style="width:624px;">
<p>获取coroutine的状态;</p>

<p>入参:coroutine.create()的返回值;</p>

<p>返回:返回coroutine的状态有四种:dead,suspend,running,normal。</p>
</td>
</tr><tr><td style="width:225px;">coroutine.wrap(func)</td>
<td style="width:624px;">
<p>创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复;</p>

<p>入参:是协程运行的函数;</p>

<p>返回:成功返回一个函数。</p>
</td>
</tr><tr><td style="width:225px;">coroutine.running()</td>
<td style="width:624px;">返回:正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号</td>
</tr></tbody></table><h3>方法详解</h3>

1. coroutine.create(func)

创建一个主体函数为 func 的新协程。 func 必须是一个 Lua 的函数。 返回这个新协程,它是一个类型为 “thread” 的对象。不会启动该协程。

1
2
3
4
5
6
7
local co = coroutine.create(
function()
print("this is a coroutine")
return "coroutine return"
end)
print(co)
print(coroutine.resume(co))

输出:

2. coroutine.resume(co, val1, …)与coroutine.yield(val1, …)

coroutine.resume(co, val1, …),开始或唤醒协程co的运行。

如果第一次执行一个协程时,他会从协程函数开头处开始运行。val1,…这些值会以参数形式传入主体函数。
如果该协程被挂起,resume 会重新启动它; val1, … 这些参数会作为挂起点(yield)的返回值。
如果协程运行起来没有错误,将运行到协程挂起或协程结束, resume 返回 true 加上传给 yield 的所有值 (当协程挂起), 或是主体函数的所有返回值(当协程中止)。

coroutine.yield(val1, …),挂起正在调用的协程的执行。 传递给 yield 的参数都会转为 resume 的额外返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local co = coroutine.create(
function (input)
print("input : "..input)
local param1, param2 ,param3 = coroutine.yield("yield1", "learning1")
print("param1 is : " .. param1)
print("param2 is : " .. param2)
print("param3 is : " .. param3)
local param4, param5 = coroutine.yield("yield2", "learning2")
print("param4 is : " .. param4)
print("param5 is : " .. param5)
-- return 也会将结果返回给 resume
return "coroutine return" , 1+2
end)

--第一次执行,将参数传给input
print("first resume",coroutine.resume(co, "coroutine function"))
print("this is main chunk")
--第二次执行,将参数作为yield的返回值,传给param1 param2 param3
print("second resume",coroutine.resume(co, "param1", "param2", "param3"))
--第三次执行,将参数作为yield的返回值,传给param4 param5 多余的param6被舍弃
print("third resume",coroutine.resume(co, "param4", "param5", "param6"))

输出:

分析:

第一次调用resume,将协同程序唤醒,入参作为函数入参;
协同程序运行;
运行到yield语句;
yield挂起协同程序,第一次resume返回,,resume操作成功返回true,否则返回false;(注意:此处yield入参(yield1、learning1)是resume的返回值)
第二次调用resume,将协同程序唤醒,入参(param1, param2 ,param3)作为yield的返回值 ;
协同程序运行;
运行到yield语句;
yield挂起协同程序,第二次resume返回,,resume操作成功返回true,否则返回false;(注意:此处yield入参(yield2、learning2)是resume的返回值)

3. coroutine.status(co)

以字符串形式返回协程 co 的状态:

当协程正在运行(它就是调用 status 的那个) ,返回 “running”;
如果协程调用 yield 挂起或是还没有开始运行,返回 “suspended”;
如果协程是活动的,都并不在运行(即它正在延续其它协程),返回 “normal”;
如果协程运行完主体函数或因错误停止,返回 “dead”。

local co local co2 = coroutine.create(function() print(“3.”..coroutine.status(co)) end) co = coroutine.create( function () print(“2.”..coroutine.status(co)) coroutine.resume(co2) coroutine.yield() end)

print(“1.”..coroutine.status(co))
coroutine.resume(co)
print(“4.”..coroutine.status(co))
coroutine.resume(co)
print(“5.”..coroutine.status(co))

输出:

4. coroutine.wrap(func)

创建一个主体函数为 func 的新协程。func 必须是一个 Lua 的函数。返回一个函数,每次调用该函数都会延续该协程(不需要调用resume)。传给这个函数的参数都会作为 resume 的额外参数。和 resume 返回相同的值,只是没有第一个布尔量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local wrap = coroutine.wrap(
function (input)
print("input : "..input)
local param1, param2 ,param3 = coroutine.yield("yield1", "learning1")
print("param1 is : " .. param1)
print("param2 is : " .. param2)
print("param3 is : " .. param3)
local param4, param5 = coroutine.yield("yield2", "learning2")
print("param4 is : " .. param4)
print("param5 is : " .. param5)
-- return 也会将结果返回给 resume
return "coroutine return" , 1+2
end)

--第一次执行,将参数传给input
print("first resume", wrap("coroutine function"))
print("this is main chunk")
--第二次执行,将参数作为yield的返回值,传给param1 param2 param3
print("second resume", wrap("param1", "param2", "param3"))
--第三次执行,将参数作为yield的返回值,传给param4 param5 多余的param6被舍弃
print("third resume", wrap("param4", "param5", "param6"))

输出:

注:coroutine.wrap不是保护模式运行,如果发生任何错误,抛出这个错误。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local wrap = coroutine.wrap(
function (input)
print("input : "..input)
local param1, param2 ,param3 = coroutine.yield("yield1", "learning1")
print("param1 is : " .. param1)
print("param2 is : " .. param2)
print("param3 is : " .. param3)
local param4 = coroutine.yield("yield2", "learning2")
print("param4 is : " .. param4)
print("param5 is : " .. param5)
-- return 也会将结果返回给 resume
return "coroutine return" , 1+2
end)

--第一次执行,将参数传给input
print("first resume", wrap("coroutine function"))
print("this is main chunk")
--第二次执行,将参数作为yield的返回值,传给param1 param2 param3
print("second resume", wrap("param1", "param2", "param3"))
--第三次执行,将参数作为yield的返回值,传给param4 param5 多余的param6被舍弃
print("third resume", wrap("param4", "param5", "param6"))

输出:

5. coroutine.running()

返回当前的协程,如果实在主线程,则返回nil

1
2
3
4
5
6
7
8
local co = coroutine.create(
function ()
print(coroutine.running())
end)

print(coroutine.running())
coroutine.resume(co)
print(co)

输出:

什么是Lua元表:

原表可理解为“一个方法表(类似函数表)“,里面包含了一些解决方案。当一个table设置元表之后,相当于关联了这个方法表

setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
getmetatable(table): 返回对象的元表(metatable)。

实例:

1
2
3
mytable = {} ;                         -- 普通表
mymetatable = {} ; -- 元表
setmetatable(mytable,mymetatable); -- 把 mymetatable 设为 mytable 的元表

以下为返回对象元表:

1
getmetatable(mytable)                            -- 这回返回mymetatable

元表中有很多原方法,下面以**__index元方法**为例:

当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键。

例如:

1
2
3
4
house = {house_computer = "Macbook"};
company = {company_computer = "Acer"};
setmetatable(house, company); --把company设置为house的metatable
print(house.company_computer);

输出的结果是nil

把代码改为:

1
2
3
4
5
house = {house_computer = "Macbook"};
company = {company_computer = "Acer"};
company.__index = company; -- 把company的__index方法指向自己
setmetatable(house, company); --把company设置为house的metatable
print(house.company_computer);

输出的结果是Acer

在刚学习时,对**__index方法有所误解:如果house 的元表是company ,如果访问了一个house中不存在的成员,就会访问查找company中有没有这个成员。而这个理解实际上是错误的,即使将house的元表设置为company,而且company中也确实有这个成员,返回结果仍然会是nil,原因就是company的__index元方法没有赋值。之前有说过,原表类似于“方法表”,设置元表相当于关联了方法表,但是并不是在方法表里查找元素,而应该是调用方法表里相应的方法。__index就是定义了当表在查找相应的key值对应的value时,查找失败,应该怎么办。**

把代码改为:

1
2
3
4
5
6
7
house = {house_computer = "Macbook"};
company = {company_computer = "Acer"};
company.__index = function()
return "hello world!";
end
setmetatable(house, company); --把company设置为house的metatable
print(house.company_computer);

输出的结果是**hello world!**。

在上述例子中,访问house.company_computer时,house中没有company_computer这个成员,但Lua接着发现house有元表company,注意:此时,Lua并不是直接在company中找名为company_computer的成员,而是调用company的__index方法,如果__index方法为nil,则返回nil,如果是一个表,那么就到__index方法所指的这个表中查找名为company_computer的成员,于是,最终找到了company_computer成员。__index方法除了可以是一个表,还可以是一个函数,如果是一个函数,__index方法被调用时将返回该函数的返回值。

总结

Lua 查找一个表元素时的规则,其实就是如下 3 个步骤:

  • 1.在表中查找,如果找到,返回该元素,找不到则继续
  • 2.判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
  • 3.判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。

该部分内容来自作者寰子:https://blog.csdn.net/xocoder/article/details/9028347

注:别的元方法也一样,调用的是实际上是设置的元表的元方法。

在Lua中合理使用and,or,not可提高代码效率,减少代码量,增加可读性。

Lua逻辑运算符and,or,not规则如下:

设定 A 的值为 trueB 的值为 false

操作符
描述
实例
and
逻辑与操作符。 若 A 为 true,则返回 B;若A为false,则返回A。
(A and B) 为 false。

or

逻辑或操作符。 若 A 为 true,则返回 A,若A为false,则返回 B。
(A or B) 为 true。
not
逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。
not(A and B) 为 true。

优先级and>or,意味着当一行代码同时出现and与or的时候,先进行and操作。

1
A or B and C == A or (B and C)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print('---------------and--------------------')
print(true and true)
print(true and false)
print(false and true)
print(false and false)
print('---------------or---------------------')
print(true or true)
print(true or false)
print(false or true)
print(false or false)
print('--------------not---------------------')
print(not(true))
print(not(false))
print('------------and,or-------------------')
print(true or false and false)

输出:

lua三目运算符:

1
ret = a > b and a or b

a>b时:a>b and a or b—–>a or b—–>a

a<=b时:a>b and a or b —–>a>b or b—–>b

lua并不能完全实现三目运算符

三目运算的一般形式a ? b : c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<p>a = true,结果为b<br />
a = false,结果为c</p>
</li>
<li>
<p>对应Lua中的a and b or c</p>

<ul><li>b = true
<ul><li>a = true
<ul><li>a and b –> true</li>
<li>b or c –> b</li>
</ul></li>
<li>a = false
<ul><li>a and b –> false</li>
<li>b or c –> c</li>
</ul></li>
</ul></li>
<li>b = false
<ul><li>a = true
<ul><li>a and b –> false</li>
<li>b or c –> c</li>
</ul></li>
<li>a = false
<ul><li>a and b –> false</li>
<li>b or c –> c</li>
</ul></li>
</ul></li>
</ul></li>

由此可见,lua要想实现三目运算符要注意

注意:Lua中的and与or,和C/C++的与、或有所区别。不要混淆使用。

pairs遍历表中全部key,value

ipairs 这个迭代器只能遍历所有数组下标的值,这是前提,也是和 pairs 的最根本区别,也就是说如果 ipairs 在迭代过程中是会直接跳过所有手动设定key值的变量。

特别注意一点,和其他多数语言不同的地方是,迭代的下标是从1开始的。

pairs实例:

1
2
3
4
tab = {1, 2, 3, key1 = nil, key2 = "val2", nil,  "d"}
for k, v in pairs(tab) do
print(k,v)
end

以上实例结果:

注意:pairs遍历过程中元素出现的顺序可能是随机的,唯一能确定的是,每个元素只会出现一次。

ipairs实例:

1
2
3
4
5
6
7
8
9
tab = {1, 2, 3, key1 = nil, key2 = "val2", nil,  "d"}
for k, v in ipairs(tab) do
print(k,v)
end

tab = {1, 2, 3, key1 = nil, key2 = "val2", "d"}
for k, v in ipairs(tab) do
print(k,v)
end

以上实例结果:

ps:中间那道杠(——-)是分隔符,插入代码时不能选择LUA,选择了别的语言代替,上面实例代码中删除了打印分隔符,如果没删除,代码会变成纯白色,可读性不强。

总结:

1.pairs 能迭代所有键值对。

2.ipairs 可以想象成 int+pairs,只会迭代键为数字的键值对。

3.ipairs在迭代过程中如果遇到nil时会直接停止。