Musoucrow' BLOG


  • Home

  • Categories

  • Archives

  • Tags

  • Search

游戏测试同步于Android设备的解决方案

Posted on 2017-12-07 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  最近开发中的游戏需要在Android手机上进行测试,但采用USB数据线连接主机进行文件替换的更新实在是太麻烦了。遂考虑整个远程更新的解决方案,在研究的过程中排除了开发APP(耗费时长)与内置热更新(LuaSocket遇到点问题),最终选择了现在的在手机上安装Git客户端的方案。

详解

  首先我选用了PocketGit作为Android的Git客户端,它可以满足我的基本需求。(感谢Hs的提供)但是仅仅如此是不够的,事实上不可能直接使用工程所用的Git仓库作为测试使用,毕竟测试的提交是十分频繁的。于是我首先考虑到了在工程仓库上创建测试分支,但是这也不够舒适,因为各分支的文件情况是独立的,到了特定时刻还得考虑合并的事。且测试分支的提交也会影响节点视图的观赏性,所以我选择了专门开一个测试仓库,通过编写Python脚本以一键同步相关文件并commit,十分方便。

脚本的实现

  以下为程序源码:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import os
import time
import shutil
import zipfile
def sync(path_a, path_b):
is_file_a = os.path.isfile(path_a)
is_file_b = os.path.isfile(path_b)
is_dir_a = os.path.isdir(path_a)
is_dir_b = os.path.isdir(path_b)
if is_file_a:
if is_dir_b:
shutil.rmtree(path_b, True)
if is_file_b:
time_a = os.stat(path_a).st_mtime
time_b = os.stat(path_b).st_mtime
if time_a != time_b:
shutil.copy2(path_a, path_b)
else:
shutil.copy2(path_a, path_b)
elif is_dir_a:
if is_file_b:
os.remove(path_b)
if is_dir_b:
list_a = os.listdir(path_a)
list_b = os.listdir(path_b)
list_merger = list(set(list_a + list_b))
for path in list_merger:
sync(path_a + "/" + path, path_b + "/" + path)
else:
shutil.copytree(path_a, path_b)
else:
if is_dir_b:
shutil.rmtree(path_b, True)
elif is_file_b:
os.remove(path_b)
def make_archive(path):
zip_file = zipfile.ZipFile(path, "w")
zip_file.write("conf.lua")
zip_file.write("main.lua")
zip_file.write("path.lua")
for root, dirs, files in os.walk("asset/font"):
for f in files:
zip_file.write(root + "/" + f)
zip_file.close()
sync("asset", "build/asset")
sync("config", "build/config")
sync("source", "build/source")
make_archive("build/game.love")
now_time = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(time.time()))
os.system(
"""
cd build
git add .
git commit -m %s
""" % now_time
)

  脚本的实现思路为将工程仓库下的三个文件夹与对应的测试仓库里的文件夹做对比,若工程仓库存在文件而测试仓库不存在,则复制之。若存在,则对比两者的最后修改日期,不一致则复制。(这里原本是考虑使用MD5检测文件完整性的,不过这将会影响到效率,发现在合理使用的前提下,只检测最后修改日期也无妨,遂采用)若出现测试仓库存在而工程仓库不存在的文件/文件夹,则删除之。
  然后便是对某些关键文件进行打包为.love文件存放到测试仓库,它是LÖVE的可执行文件,但实际上只是个.zip文件。最后便是对测试仓库进行commit。

后记

  config然后如这般做好配置,便可在Android设备上克隆主机的测试仓库了(以局域网的形式)。config当然还得确保主机开启了ssh,在macOS下开启ssh的方式为:System Preferences → Sharing → Remote Login。

关于C语言函数strtok引发的思考

Posted on 2017-10-13 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  近期遇到个C语言的课题作业,要求完成parse功能(以空格、回车、TAB为分割符分割字符串,输出结果且返回数组。)该功能涉及到strtok函数的一些问题,特此开贴记录。

详解

  以下为程序源码:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct ListNode {
char * value;
struct ListNode * next;
};
char ** parse(char * line) {
if (line == NULL) {
return NULL;
}
static char delim[] = " \t\n"; /* SPACE or TAB or NL */
int count = 0;
char * token;
char ** newArgv;
char str[strlen(line)];
strcpy(str, line);
token = strtok(str, delim);
if (token == NULL) {
return NULL;
}
struct ListNode * head = (struct ListNode *)malloc(sizeof(struct ListNode));
struct ListNode * cur = head;
cur->value = token;
count ++;
while (1) {
token = strtok(NULL, delim);
if (token == NULL) {
break;
}
cur->next = (struct ListNode *)malloc(sizeof(struct ListNode));
cur = cur->next;
cur->value = token;
count ++;
}
newArgv = (char **)malloc((count + 1) * sizeof(char *));
cur = head;
for (int i = 0; i < count; i++) {
newArgv[i] = (char *)malloc(strlen(cur->value) * sizeof(char));
strcpy(newArgv[i], cur->value);
printf("[%d] : %s\n", i, cur->value);
free(cur);
cur = cur->next;
}
newArgv[count] = NULL; //tail
return newArgv;
}
int main() {
char ** argv = parse("system program");
return 0;
}

第一个问题

  首先第一个问题便是这里:

1
2
3
4
char str[strlen(line)];
strcpy(str, line);
token = strtok(str, delim);

  最初尝试直接把parse函数的参数line直接作为strtok函数的第一参数填入,结果不行。查阅文档后发现strtok的声明为:

1
2
3
4
//param: str -- 要被分解成一组小字符串的字符串。
//param: delim -- 包含分隔符的 C 字符串。
//return: 该函数返回被分解的最后一个子字符串,如果没有可检索的字符串,则返回一个空指针。
char *strtok(char * str, const char * delim);

  可以发现,第一参数char * str要求的并非const,而我在调用时填入的参数为‘system program’,这种字符串数据是作为‘const char[]’保存在字符串常量区的,故不符合参数需求。需要重新申请一片栈空间复制line的内容再作为参数填入。

第二个问题

  由此衍生的第二个问题便是:为何要为newArgv[i]申请新的空间,而非newArgv[i] = cur->value;?

1
2
newArgv[i] = (char *)malloc(strlen(cur->value) * sizeof(char));
strcpy(newArgv[i], cur->value);

  这一点的原因主要是 strtok返回的字符串其实并非新的副本,而是从str上截取的一部分而已。 而cur->value便是来自于str,且str是拥有生命周期的栈数据,而如果将这样的部分保存在newArgv后返回到外部,便会因为生命周期问题,导致数据被回收。这将会产生很可怕的后果。所以必须申请新的空间,形成复制。

后记

  没有垃圾回收的C/C++,编程时必须对内存的分配和流向必须要有十分清晰的认识,不然就很容易发生内存泄漏和野指针现象。慎之、慎之。

帧同步的初步探究

Posted on 2017-08-26 | In Network | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  在阅读这篇文章之前,你需要了解一下何为帧同步。关于帧同步的实现尝试,其实近年来一直都有不间断的尝试,不过大多浅尝辄止,这次总算是一次较为完整的实现了。接下来便对这次实现介绍一二。

详解

  本次项目使用的开发引擎为LÖVE,项目地址在此。以下是运行演示(外网联机测试也通过了):1
  由于我并没有什么服务端开发的相关经验,所以只是使用了个简单的UDP网络库——ENet。客户端与服务端共同处于一个项目下,非常的浅薄,以下是服务端与客户端的代码展示。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
--server.lua
local Enet = require ("enet")
local Lib = require("lib")
local host = Enet.host_create("localhost:6789")
local peerMap = {}
local dataMap = {}
local inputMap = {}
local playList = {}
local playFrame = 0
local frame = 0
local userCount = 0
print("start")
while (true) do
local event = host:service(17)
while (event) do
local type, ip, data = Lib.Recv(event)
if (type == "input") then
inputMap[ip] = data
elseif (type == "connect") then
event.peer:timeout(10, 3000, 5000)
local data = {ip = ip, x = math.random(0, 800), y = math.random(0, 600)}
for k, v in pairs(peerMap) do
Lib.Send(v, "addNewUser", data)
end
peerMap[ip] = event.peer
dataMap[ip] = data
inputMap[ip] = {}
userCount = userCount + 1
Lib.Send(event.peer, "loginSuccess", {ip = ip, users = dataMap, playList = playList})
print("connect", ip)
elseif (type == "disconnect") then
peerMap[ip] = nil
dataMap[ip] = nil
inputMap[ip] = nil
userCount = userCount - 1
for k, v in pairs(peerMap) do
Lib.Send(v, "delUser", ip)
end
event.peer:disconnect(-1)
print("disconnect", ip)
end
event = host:service()
end
if (userCount > 0) then
frame = frame + 1
if (frame % 3 == 0) then
playFrame = playFrame + 1
print(playFrame)
for k, v in pairs(peerMap) do
Lib.Send(v, "play", inputMap)
end
playList[#playList + 1] = Lib.Clone(inputMap)
end
end
end
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
--main.lua
local Actor = require("actor")
local Lib = require("lib")
local Enet = require ("enet")
local userList = {}
local userMap = {}
local input = {}
local playList = {}
local playFrame = 0
local perdt = 17
local timer = 0
local frame = 0
local player
local fps = 0
local timer2 = 0
local host = Enet.host_create()
local server = host:connect("localhost:6789")
local function NewActor(x, y, ip)
local actor = Actor.New(x, y, ip)
userMap[ip] = actor
userList[#userList + 1] = actor
return actor
end
local function Update()
if ((frame + 1) % 3 == 0 and #playList == 0) then
return
end
frame = frame + 1
fps = fps + 1
if (frame % 3 == 0) then
for k, v in pairs(playList[1]) do
if (userMap[k]) then
userMap[k].input = v
end
end
playFrame = playFrame + 1
table.remove(playList, 1)
end
for n=1, #userList do
userList[n]:Update()
end
end
function love.load()
local event, type, ip, data
repeat
event = host:service()
if (event) then
type, ip, data = Lib.Recv(event)
end
until event ~= nil and type == "loginSuccess"
for k, v in pairs(data.users) do
local actor = NewActor(v.x, v.y, v.ip)
if (v.ip == data.ip) then
player = actor
print("loginSuccess", v.ip)
end
end
playList = data.playList
while (#playList > 0) do
Update()
end
end
function love.update(dt)
local event = host:service()
while event do
local type, ip, data = Lib.Recv(event)
if (type == "play") then
playList[#playList + 1] = data
elseif (type == "addNewUser") then
NewActor(data.x, data.y, data.ip)
elseif (type == "delUser") then
userMap[data] = nil
for n=#userList, 1, -1 do
if (userList[n].ip == data) then
table.remove(userList, n)
end
end
end
event = host:service()
end
dt = math.floor(dt * 1000)
timer = timer + dt
while (timer >= perdt) do
--print(#playList)
if ((frame + 1) % 3 == 0 and #playList > 1) then
while (#playList > 0) do
Update()
end
else
Update()
end
timer = timer - perdt
end
timer2 = timer2 + dt
if (timer2 >= 1000) then
--print(timer2, fps)
fps = 0
timer2 = timer2 - 1000
end
end
function love.draw()
for n=1, #userList do
userList[n]:Draw()
end
love.graphics.print(playFrame, 5, 5)
end
function love.keypressed(key)
if (key == "up" or key == "down" or key == "left" or key == "right") then
input[key] = 1
Lib.Send(server, "input", input)
end
end
function love.keyreleased(key)
if (key == "up" or key == "down" or key == "left" or key == "right") then
input[key] = 0
Lib.Send(server, "input", input)
end
end

  从代码中可以看出,以上实现便是Skywind所说的乐观帧锁定。事实上我认为传统的帧同步(Lockstep)并不适合网络游戏,甚至只是一种早期的理论模型。在实际应用还是要以乐观帧锁定为准。关于乐观帧锁定的实现原理我便不再复述,只说说实际开发中遇到的一些问题。

Fixed Update

  很抱歉我找不到这个词对应的中文词汇,直译过来的意思便是“固定的更新”。在实际应用的含义为,每隔一段时间便会Update一次,如果遇到卡住之类导致积累了很长时间的行为,则会根据时间一次性进行多次Update。也就是这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--Fixed Update(main.lua)
timer = timer + dt
while (timer >= perdt) do
if ((frame + 1) % 3 == 0 and #playList > 1) then
while (#playList > 0) do
Update()
end
else
Update()
end
timer = timer - perdt
end

  使用了Fixed Update后,你的游戏进度便由时间牢牢把控住,这样便能摆脱帧率的影响。在业务层也不再需要使用DT(Delta Time)了,而是采用一个固定的值(譬如1 / 60)。这个概念很重要,因为过快与过慢的帧率都会与同步不那么搭调。

收发频率

  按照Skywind的说法是服务端每秒20-50次向所有客户端发送同步包。这里我们需要理清乐观帧锁定的本质:就是服务端每隔一段时间发送同步包,然后客户端每隔一段时间接收并应用之,如果在那段时间内没有收到,就持续等待。所以我们必须设置好合适的时间,令服务端和客户端之间配合无间。
  在我的设计里,为服务端和客户端都设置了帧(Frame)的概念,每Update一次即是一帧,每次Update的间隔时间为17毫秒,这个数字是根据(1/60)秒取整得出。每隔三帧服务端便会发送同步包,而客户端则是每帧都会接收,每隔三帧便会应用之。我称这种每隔三帧的帧为同步帧。
  衡量设置是否良好,主要看每帧接收的同步包的数量,以及每秒帧数的多少。正常情况下,每帧接收的同步包的数量应是0-1,如果超过这个数值,证明服务端或客户端其中一方的频率太快或太慢。至于每秒帧数的多少,则能看出发送频率是否过剩,正常情况下在60左右即可,这和FPS的要求是类似的。

1
local event = host:service(17) --服务端每隔17毫秒接收一次封包

收包Q&A

  • 关于收包的问题,有个最明显的问题便是为何不是在同步帧时进行收包,而是每帧都尝试收包?
    • 这是因为收包的内容不仅仅是关于帧同步,还可能有其他东西。其次便是先收后收并不影响什么,以及每隔三帧才进行一次收包恐怕会卡。
  • 关于一次性收到多个同步包的情况时,该怎么办?
    • 遇到收到多个同步包的情况,说明客户端失联了一段时间,这时候便需要一次性进行多次Update以迅速追回进度。
  • 既然确定遇到多个同步包时需要一次性追回进度,那为何不选择在接收后立即执行,而要等到同步帧?
    • 因为基本上不会遇到这种情况,能遇到多个同步包的情况,一般客户端已经在等待了。换做客户端卡住这种情况的话,根据Fixed Update的规则,本来就会立即赶到同步帧的。
  • 为何等待同步包的代码并非是阻塞式的,而是每帧去判定一下?
    • 因为如果是阻塞式的话,会使得DT变得很大,影响Fixed Update。

输入应用

  关于输入应用方面,首先要明确一点:客户端每次按下/释放按键,就会改写输入表然后发送之。服务端收到后便会改写在服务端的对应输入表。这种模式在高延迟的表现便是呈现出某些按键因为一直按下而导致一些鬼畜操作,这点算是可以接受的。
  另外注意不要贪图方便每次只发送当前修改的按键,这样会失去输入的稳定性,一旦发生丢包之类的,会产生键盘失灵的感觉。
  在每次同步帧时便会根据同步包的内容更新所有玩家的输入表,从而改变每个联机角色的操作。这个输入表会一直应用到下次同步帧之前,从这点来看帧同步也是一种回合制。

1
2
3
4
5
6
7
8
9
10
11
12
--根据同步包的内容更新所有玩家的输入表(main.lua)
if (frame % 3 == 0) then
for k, v in pairs(playList[1]) do
if (userMap[k]) then
userMap[k].input = v
end
end
playFrame = playFrame + 1
table.remove(playList, 1)
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--客户端每次按下/释放按键,就会改写输入表然后发送之(main.lua)
function love.keypressed(key)
if (key == "up" or key == "down" or key == "left" or key == "right") then
input[key] = 1
Lib.Send(server, "input", input)
end
end
function love.keyreleased(key)
if (key == "up" or key == "down" or key == "left" or key == "right") then
input[key] = 0
Lib.Send(server, "input", input)
end
end

断线重连/中途加入

  想要做到这两点,便需要做到在服务端保存每一份同步包,这样服务端只需要记录每个玩家的初始数据,在新玩家加入游戏时,首先发送每个玩家的初始数据给新玩家同步,然后再把所有同步包打包发送给新玩家,让新玩家一次性Update,即可完成中途加入。当然不要忘记给现有玩家发送新玩家的数据。

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
--新玩家同步(main.lua)
local event, type, ip, data
repeat
event = host:service()
if (event) then
type, ip, data = Lib.Recv(event)
end
until event ~= nil and type == "loginSuccess"
for k, v in pairs(data.users) do
local actor = NewActor(v.x, v.y, v.ip)
if (v.ip == data.ip) then
player = actor
print("loginSuccess", v.ip)
end
end
playList = data.playList
while (#playList > 0) do
Update()
end

浮点数

  根据网上的信息看来,浮点数因为在不同环境的实现有所偏差,所以可能会导致不一致的问题,这样便会产生蝴蝶效应,最终导致同步失效。目前我使用LuaJIT在Windows(x64)、Ubuntu(x64)、macOS、Android(红米Note4)、iOS测试来看,浮点数在「输出」的场合下并无不同。当然只是输出,并不能代表真实数据的情况。以及我的测试范本并不算多,对于浮点数的问题还不敢保证。所以我选择在业务层放弃使用浮点数,相关数据都事先进行转换,到最后需要浮点数的对接场合再进行转换。当然我并不敢保证这种做法的可行性,真正成熟的做法应该是使用定点数,但我暂时并未这么做。

1
dt = math.floor(dt * 1000) -- 浮点数转换为整数(毫秒)

后记

  以上便是本次我对帧同步的初步探究,它注定是不成熟的,需要经过实践的检验,接下来我会考虑将其接入到一些项目中。有相关经验的朋友欢迎前来讨论。

DNF的SPK文件解析笔记

Posted on 2017-07-21 | In Analysis | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  SPK文件是DNF的一种更新时所用的压缩包(国服除外),在更新时会从服务器下载这种SPK文件然后于本地解压。有时候为了获得更快的速度,以及突破墙的限制,我们通过获取到更新服务器的网址直接下载。但是此刻对于如何将SPK文件解压便是个问题了,本文遂由此而生。

详解

  SPK文件的本质上是将原文件按照一定规则进行分割成多个片段,然后对这些片段使用bzip2算法进行压缩。以下是文件结构图:1

  由于信息的不对称,导致不少地方是处于盲区的,不过这并不影响获取到关键数据。这种被压缩的片段开头标识为”BZh91AY&SY”,所以只需要以此为关键字进行分割就能得出关键数据了,不过实际操作时发现除了压缩数据之外还有非压缩数据,并且会用一段48字节的数据进行分隔。且拥有非压缩数据的片段结尾也会有一段意味不明的数据,好在它们都有对应的开头标识,进行分割即可。
  在得出压缩数据后使用bzip2算法进行解压缩,然后将所有解压后的数据与非压缩数据按顺序进行拼接,每个片段如此类推最后总体拼接起来即可得到完整的原文件。本次解析使用的环境是Python3.6,以下是过程代码:

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
import sys
import bz2
import struct
HEADER = b"BZh91AY&SY"
MARK = b"\x00\x00\x00\x00\x00\x10\x0e\x00\xff\xff\xff\xff\xff\xef\xf1\xff"
# 00 00 00 00 00 10 0E 00 FF FF FF FF FF EF F1 FF
TAIL = b"\x01\x00\x00\x00" # 01 00 00 00
def decompress_spk(path):
f = open(path, "rb")
f.read(4) # unknown, all files are same.
f.read(260) # total 260 bits, contain name and buffer.
f.read(4) # unknown, all files are same.
struct.unpack('i', f.read(4))[0] # decompress_size
content = f.read()
parts = content.split(HEADER)
f.close()
new_content = b""
for n in range(1, len(parts)):
lst = parts[n].split(MARK)
length = len(lst)
new_content = new_content + bz2.decompress(HEADER + lst[0])
if length > 1:
for m in range(1, length - 1):
new_content = new_content + lst[m][32:]
pos = lst[length - 1].rfind(TAIL)
new_content = new_content + lst[length - 1][32:pos]
f2 = open(path[:-4], "wb")
f2.write(new_content)
f2.close()
if __name__ == "__main__" and len(sys.argv) > 1:
decompress_spk(sys.argv[1])

后记

  事实上这种SPK的压缩机制效果根本是微乎其微,我完全不明白Neople这么做的用意。以及片段的划分机制和诸多盲区都没搞懂,看来我得考虑学下逆向方面的知识了。不过无论如何,最终要达到的目标还是做到了。

关于Socket.listen方法的一点体悟

Posted on 2017-07-16 | In Network | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  最近在接触Socket的的时候,关于其中的listen方法感到不解,于是对其进行了一番研究,得出了一点体悟,特此记录。

详解

  让我们先来看看listen方法在Python3.6文档说明:

1
2
3
4
5
6
7
socket.listen([backlog])
Enable a server to accept connections. If backlog is specified, it must be at least 0 (if it is lower, it is set to 0); it specifies the number of unaccepted connections that the system will allow before refusing new connections. If not specified, a default reasonable value is chosen.
启用服务器以接受连接。如果指定backlog,则必须至少为0(如果低于0,则设置为0);它指定系统在拒绝新连接之前将允许的未接受连接的数量。如果未指定,则选择默认的合理值。
Changed in version 3.5: The backlog parameter is now optional.
在版本3.5中已更改: backlog参数现在是可选的。

  起初我看了这说明想当然的以为是可以接入的Client上限,不过实践过后发现并非如此。在网上找的解答基本上就是文档所言的复述,后来请教了专业人士后,方知这涉及到Socket的底层知识。
  在了解listen方法之前,首先我们需要了解connect方法和accept方法,以下是文档说明:

1
2
3
4
5
6
7
8
9
10
socket.connect(address)
Connect to a remote socket at address. (The format of address depends on the address family — see above.)
在地址连接到远程套接字。(地址的格式取决于地址系列 - 请参见上文)
If the connection is interrupted by a signal, the method waits until the connection completes, or raise a socket.timeout on timeout, if the signal handler doesn’t raise an exception and the socket is blocking or has a timeout. For non-blocking sockets, the method raises an InterruptedError exception if the connection is interrupted by a signal (or the exception raised by the signal handler).
如果连接被信号中断,则该方法等待直到连接完成,或者如果信号处理程序没有引发异常并且套接字正在阻塞或者已经阻塞,则在超时时引入socket.timeout超时。对于非阻塞套接字,如果连接被信号中断(或由信号处理程序引发的异常),则该方法引发InterruptedError异常。
Changed in version 3.5: The method now waits until the connection completes instead of raising an InterruptedError exception if the connection is interrupted by a signal, the signal handler doesn’t raise an exception and the socket is blocking or has a timeout (see the PEP 475 for the rationale).
在版本3.5中已更改:该方法现在等待直到连接完成,而不是提高InterruptedError异常,如果连接被信号中断,信号处理程序不引发异常,套接字阻塞或超时(参见 PEP 475)。
1
2
3
4
5
6
7
8
9
10
11
12
13
socket.accept()
Accept a connection. The socket must be bound to an address and listening for connections. The return value is a pair (conn, address) where conn is a new socket object usable to send and receive data on the connection, and address is the address bound to the socket on the other end of the connection.
接收一个连接.该socket 必须要绑定一个地址和监听连接.返回值是一对(conn, 地址)其中conn是新 t4 > socket对象可用于在连接上发送和接收数据,address是连接另一端的套接字的地址。
The newly created socket is non-inheritable.
新创建的套接字non-inheritable。
Changed in version 3.4: The socket is now non-inheritable.
在版本3.4中更改:套接字现在是不可继承的。
Changed in version 3.5: If the system call is interrupted and the signal handler does not raise an exception, the method now retries the system call instead of raising an InterruptedError exception (see PEP 475 for the rationale).
在版本3.5中更改:如果系统调用中断并且信号处理程序没有引发异常,则此方法现在重试系统调用,而不是引发InterruptedError异常 PEP 475)。

  相比listen方法,它俩就好理解多了,一个是Client用于连接Server的方法,一个是Server用于接收Client的连接申请的方法。
  但事实上accept方法一次只能接收一个Client的连接申请,而Client则是多个的,这样Socket会设计一个队列来存储Client的连接申请则是理所当然的。于是accept便从这个队列里提取首位成员处理即可。以下是示意图:1

  如此便很清晰了,backlog参数的含义便是这个队列的最大值,也就是同时受理连接申请的最大值。关于backlog该设置为多少,从Skynet得到的参考为32。如果满了便需要Client重新connect。以上listen方法之谜便解开了。

后记

  不得不说网络编程的水真的很深,我也得买点书充充电了,目前的目标为啃下谢希仁著的《计算机网络》。另外推荐《TCP/IP详解》及《UNIX网络编程》。

关于MVC框架的一点体悟

Posted on 2017-06-16 | In ProgramDesign | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  MVC框架相信大家都有所耳闻,不过想必没有深入接触的话,大多是知其然而不知其所以然。而本人几乎没有GUI软件的开发经验,只是走马观花般的碰过几下.NET的Winform之类,所以之前自然也是如此。
  而近日帮助友人使用C++/CLI完成一款篮球比赛记录软件,在思考要如何搭建架构时很自然的选择了将核心和界面分离的设计,在完成后我感觉Winform把自动生成的代码和人为实现的事件接口放一起很是冗杂。于是选择用Python重新实现一次,随着把界面数据和事件接口的分离,赫然发现:这不就是传说中的MVC框架么?directory

详解

  当然有一点是需要明确的:我实现的这套「MVC框架」并不算标准,并且为了便利性还牺牲了一点解耦性,以下仅供参考。
  relationship
  以上的core, interface, design分别是三个模块,代表着MVC框架下的model, controller, view。箭头表明了引用关系(例:interface调用了core的功能)。
  如图所示,interface和design都引用了core,而core并没有引用其他模块。由此可见,core是完全独立于界面的,无论是怎样的界面,只要对接好core就没有问题,这正是「业务和界面分离」。
  而interface和design,实际上是可以解耦的。不过这需要令design使用一些手段监听core的数据是否发生了变化,我认为这样的实现颇为复杂,况且interface和design本为一一对应的关系(interface是界面事件,design是界面数据),所以也不必苛求解耦。遂选择由interface通知design去同步core的数据。

演示

  本演示使用Python3编写,界面使用了Pyqt5,理解这个演示你需要了解Python以及Qt。项目地址:MVCShow.git
  演示的内容十分简单,仅是一个窗口包含了一个文本框和按钮而已。通过点击按钮来改变文本框的内容。

  • 点击前:before
  • 点击后:after
  • 流程图:progress
  • 代码展示:
1
2
3
4
5
6
7
8
9
10
11
12
# core
__value = "123"
def set_value(value):
global __value
__value = value
def get_value():
return __value
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
# design
import core
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow
class EntryDesign(QMainWindow):
def __init__(self):
super(EntryDesign, self).__init__()
self.setObjectName("MVCShow")
self.resize(400, 300)
self.pushButton = QtWidgets.QPushButton(self)
self.pushButton.setGeometry(QtCore.QRect(150, 210, 113, 32))
self.pushButton.setObjectName("pushButton")
self.lineEdit = QtWidgets.QLineEdit(self)
self.lineEdit.setGeometry(QtCore.QRect(150, 110, 113, 21))
self.lineEdit.setObjectName("lineEdit")
self.retranslateUi()
QtCore.QMetaObject.connectSlotsByName(self)
def retranslateUi(self):
_translate = QtCore.QCoreApplication.translate
self.setWindowTitle(_translate("MVCShow", "MVCShow"))
self.pushButton.setText(_translate("MVCShow", "click"))
def refresh(self):
self.lineEdit.setText(core.get_value())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# interface
import core
from design.entry import EntryDesign
class Entry(EntryDesign):
def __init__(self, ):
super(Entry, self).__init__()
self.pushButton.clicked.connect(self.__pushButton_clicked)
def showEvent(self, event):
self.refresh()
def __pushButton_clicked(self):
core.set_value("456")
self.refresh()

后记

  框架是死的,思想是活的。在实际开发中,并不需要教科书般地去套用模板,事实上大家实现的MVC框架也不尽相同。而我认为MVC框架的核心思想便是「界面与业务分离,数据与逻辑分离」。只要把握好这个核心思想去做即可。对此有所心得者,欢迎前来探讨,提供更佳的思路。

关于像素PNG图片在游戏引擎缩放后出现毛边的解决方案

Posted on 2017-05-01 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

  今日遇到了一个很值得记录的问题:对于像素PNG图片在游戏引擎里缩放后,便会在图片边缘出现很奇怪的毛边。类似这样:1

  经过研究发现,似乎与文件格式无关。我还尝试将不会出现毛边的像素图混搭一块,结果反而导致靠近毛边的地方会被感染:2

  当然这个问题在Unity下是可以通过设置解决的:3

  不过并非所有游戏引擎都能这么做的,所以还需要从文件根本上解决。经过一段无心插柳的尝试,发现解决方案为使用PS的文件→存储为Web所用格式:4

  关键便在于将杂边设置为“无”,保存后再拿去测试,问题解决:5

  虽然到头来具体的原理还是没有弄明白,但好歹也算是解决了。如果有人明白具体原理,还请不吝赐教。至于这么做是否有损尚未可知,但从肉眼来看是不成问题的。以及这个问题目前只发现出现在像素图上,带半透明的特效图并无此问题。

观察者模式的一种实现——Caller

Posted on 2017-04-21 | In ProgramDesign | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  在程序设计的时候,观察者模式是一种应用广泛的设计模式,它是一种模块间的通信方式,对于程序的解耦性有所帮助。本文便提供了一种实现思路,也即是说,你需要先去了解观察者模式才方便阅读本文。
  以下代码演示将使用Lua语言,接下来的内容对阅读者的Lua水平有一定的要求。如果你不知道在Lua中class的实现方式,可参考这篇文章。

实现

  首先构造Caller的结构关系,如图所示:relationship

  由此可见,Caller包含一个结构体——Listener。接下来是代码实现:

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
-- Caller.lua
local Class = require("class")
local Caller = Class()
function Caller:Ctor()
self.listenerList = {}
end
function Caller:AddListener(obj, func)
table.insert(self.listenerList, {obj = obj, func = func}) -- Create Listener.
end
function Caller:DelListener(obj, func)
for n=#self.listenerList, 1, -1 do
if (self.listenerList[n].obj == obj and self.listenerList[n].func == func) then
table.remove(self.listenerList, n)
return true
end
end
return false
end
function Caller:Call(...)
for n=1, #self.listenerList do
self.listenerList[n].func(self.listenerList[n].obj, ...)
end
end
return Caller

应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- A.lua
local Class = require("class")
local Caller = require("caller")
local B = require("b")
local A = Class()
function A:Ctor()
self.caller = Caller.New()
self.b = B.New(self.caller)
self.value = 0
end
function A:SetValue(value)
self.value = value
self.caller:Call(value)
end
return A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- B.lua
local Class = require("class")
local B = Class()
function B:Ctor(caller)
caller:AddListener(self, self.Print)
end
function B:Print(...)
print(...)
end
return B

  如上所示,在调用A:SetValue(value)时会连带调用B:Print(…),如此只需要通过操作Caller就能达到不同对象之间通信的效果。并且也无须将业务写死在A:SetValue(value)中,业务将集中存放在Caller中。

疑难解答

  问:Caller的缺点是什么?
  答:Caller的缺点的很明显,需要将Caller对象直传到其他模块处,这样并不是很安全(别处也可以做Caller的一切行为)。当然这个问题也很好解决,只需要采用Event机制对其进行封装即可。代码如下:

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
-- A.lua
local Class = require("class")
local Caller = require("caller")
local B = require("b")
local A = Class()
function A:Ctor()
self.event = function(...) self:OnEvent(...) end
self.caller = Caller.New()
self.b = B.New()
self.value = 0
end
function A:SetValue(value)
self.value = value
self.caller:Call(value)
end
function A:OnEvent(type, ...)
if (type == "AddListener") then
return self.caller:AddListener(...)
end
return "No Event"
end
return A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- B.lua
local Class = require("class")
local B = Class()
function B:Ctor(upperEvent)
upperEvent("AddListener", self, self.Print)
end
function B:Print(...)
print(...)
end
return B

  问:Caller在功能上有何可改进之处?
  答:在实际开发中,Caller的需求数量一般不止一个,届时可以考虑用List或Map对Caller进行管理。也可以考虑直接让Caller具备管理多个ListenerList,代码如下:

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
48
-- Caller.lua
local Class = require("class")
local Caller = Class()
function Caller:Ctor()
self.listenerListMap = {}
end
function Caller:AddListener(type, obj, func)
if (not self.listenerListMap[type]) then
self.listenerListMap[type] = {}
end
table.insert(self.listenerListMap[type], {obj = obj, func = func}) -- Create Listener.
end
function Caller:DelListener(type, obj, func)
if (not self.listenerListMap[type]) then
return false
end
local listenerList = self.listenerListMap[type]
for n=#listenerList, 1, -1 do
if (listenerList[n].func == func and listenerList[n].obj == obj) then
table.remove(listenerList, n)
return true
end
end
return false
end
function Caller:Call(type, ...)
if (not self.listenerListMap[type]) then
return
end
local listenerList = self.listenerListMap[type]
for n=#listenerList, 1, -1 do
listenerList[n].func(listenerList[n].obj, ...)
end
end
return Caller

后记

  以上内容仅为提供一个思路,它或许并非最完善的,也不一定能适用于所有编程语言中,对此有所心得者,欢迎前来探讨,提供更佳的思路。

浅谈对象之间通信的解决方案——Event机制

Posted on 2017-04-19 | In ProgramDesign | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  在程序设计的时候,不同对象与模块间总是不可避免会发生相互调用的情况,如果选择将对象互相作为参数传入给对方使用,那么这种现象一般被称为耦合,这样实际上就让两个部分连在了一块。当然这样子实际上并没有什么问题,只要这符合你的设计预期。只是一旦开发规模增大、开发人员增多、耦合程度加剧的话,程序的维护成本也便会随之剧增。往往会出现某个模块在另一个模块处被做了一些修改而不自知,以及在脚本语言的情况下,没有private保护的对象到了他处等同彻底的暴露,随便的修改的话,封装性也随之不存。那么由此看来,必须拥有一套对象之间通信的解决方案了,本文便是提供了一种思路供给参考。
  以下代码演示将使用Lua语言,接下来的内容对阅读者的Lua水平有一定的要求。如果你不知道在Lua中class的实现方式,可参考这篇文章。

使用机制之前的做法

  首先我们演示一下不用Event机制之前的做法,也就是一般人会用的做法。

1
2
3
4
5
6
7
8
9
10
11
12
-- A.lua
local Class = require("class")
local A = Class()
local B = require("b")
function A:Ctor()
self.value = 1
self.b = B.New(self)
end
return A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- B.lua
local Class = require("class")
local B = Class()
function B:Ctor(a)
self.a = a
end
function B:Print()
print(self.a.value)
end
return B

使用机制之后的做法

  由上可以看出,这种直传对象的做法其实是十分危险的,它很容易破坏封装性,并且会达到「你中有我,我中有你」的效果,并且这还是在上下级的情况下。接下来便演示下使用了Event机制后的做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- A.lua
local Class = require("class")
local A = Class()
local B = require("b")
function A:Ctor()
self.value = 1
self.event = function(self, ...) self:OnEvent(...) end
self.b = B.New(self.event)
end
function A:OnEvent(type, ...)
if (type == "GetValue") then
return self.value
end
return "No Event"
end
return A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- B.lua
local Class = require("class")
local B = Class()
function B:Ctor(upperEvent)
self.upperEvent = upperEvent
end
function B:Print()
print(self.upperEvent("GetValue"))
end
return B

  通过对比可以看出,使用了Event机制后,不仅保证了封装性,而且还隔离了A对象的实现,换句话说,B对象的upperEvent已经不仅限于A了,只要是提供了一致的Event接口即可,并且A对象还能清除的知道自己对外究竟提供了什么接口,可维护性也随之提高了。
  当然代价还是有的,那便是调用Event时的函数调用次数比传统方式有所增加,但我认为这是值得的。当然这只是一种思路,也未尝不可优化。

疑难解答

  问:关于两个平级对象需要相互调用要怎么做?
  答:参考以下例子:

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
-- A.lua
local Class = require("class")
local A = Class()
local B = require("b")
local C = require("c")
function A:Ctor()
self.value = 1
self.event = function(self, ...) self:OnEvent(...) end
self.b = B.New(self.event)
self.c = C.New(self.event)
end
function A:OnEvent(type, ...)
if (type == "GetValue") then
return self.b:GetValue()
elseif (type == "SetTag") then
return self.c:SetValue(...)
end
return "No Event"
end
return A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- B.lua
local Class = require("class")
local B = Class()
function B:Ctor(upperEvent)
self.upperEvent = upperEvent
self.value = 1
self.upperEvent("SetTag", "123")
end
function B:GetValue()
return self.value
end
return B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- C.lua
local Class = require("class")
local C = Class()
function C:Ctor(upperEvent)
self.upperEvent = upperEvent
self.tag = ""
end
function C:SetTag(tag)
self.tag = tag
end
function C:Print()
print(self.upperEvent("GetValue"))
end
return C

  由此可见,平级对象的相互调用,只需要统一由上级管理好需要调用的接口,然后平级对象从上级Event中获取即可。

  问:一个对象可以拥有多个OnEvent()么?也就是针对不同对象派出不同的Event。
  答:一般来说不推荐这么做,因为这只会使得维护成本提升。当然你很清楚自己的需求以及这么做的代价的话,但试无妨。

  问:使用Event机制后有效的保证了封装性,但是对于上级管理下级的时候并不存在这种封装性的保护,那么该怎么办呢?
  答:这个在脚本语言里是一个没办法的事,原则上最好是不要对外直接暴露变量,只使用函数。哪怕这会带来更高的性能代价,对于维护性而言也是值得的,当然这个问题或许也可以通过特殊的手段解决(参考Lua元表的内容)。

  问:是不是严格意义所有情况下都不应该直传对象而采用Event呢?
  答:这样显然是不现实的,比如某些类的业务本身就需要获取到对象本身(如容器),以及Event本身也是有性能代价以及构建成本的,不可能面面俱到。在某些你认为必要且可掌控的情况下,直传对象也并非不可以。毕竟解耦的目的也是为了提高可维护性,只要你觉得这样做是可以接受的,那么便可以了。

后记

  以上内容仅为提供一个思路,它或许并非最完善的,也不一定能适用于所有编程语言中,对此有所心得者,欢迎前来探讨,提供更佳的思路。

更换Homebrew的更新源

Posted on 2017-03-29 | In Teach | | Visitors

  欢迎参与讨论,转载请注明出处。

前言

  更换Homebrew的更新源的教程,在网上数不胜数,然内容大多大同小异且述之不详,且未提及版本上的差异。故作此文,以正视听。
  在阅读此文之前,你需要了解Homebrew和Git并安装了它们。并且对于Homebrew官方更新源的速度赶到不满且不打算利用其它手段解决(如VPN),或者看了其它文章感到不求甚解,那么此文对你而言是有价值的。

更新源的机制

  Homebrew的更新源由三部分组成:本体(brew.git)、核心(homebrew-core.git)以及二进制预编译包(homebrew-bottles)。
  在很多教程中,只会提及到更换本体,而未涉及到核心与二进制预编译包的更换。这样实际上效果是不完全的(尽管这样也无法做到完全,毕竟有一些软件包的地址是不被收录的,只能从它们提供的链接处下载)。
  从.git的后缀名可以看出,Homebrew的更新源是以Git仓库的形式存在的,这便是为什么需要用到Git的原因。也正是如此,使得可以对其进行克隆,成为新源。

更新源的选择

  默认官方的更新源都是存放在GitHub上的,这也是中国大陆用户访问缓慢的原因,一般来说我们会更倾向选择国内提供的更新源,在此推荐中国科大以及清华大学提供的更新源,因为它们能够完整以上源组成的三个部分。并且在此感谢他们为大家提供的服务。   

替换更新源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 替换brew.git:
$ cd "$(brew --repo)"
# 中国科大:
$ git remote set-url origin https://mirrors.ustc.edu.cn/brew.git
# 清华大学:
$ git remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git
# 替换homebrew-core.git:
$ cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
# 中国科大:
$ git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git
# 清华大学:
$ git remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git
# 替换homebrew-bottles:
# 中国科大:
$ echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.bash_profile
$ source ~/.bash_profile
# 清华大学:
$ echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles' >> ~/.bash_profile
$ source ~/.bash_profile
# 应用生效:
$ brew update

  以上在中国科大和清华大学任选其一即可,在使用其他源的时候,最好先尝试访问其链接看看是否健在,并且因为历史原因,最初的brew.git是叫homebrew.git的,而现在部分更新源早已随官方更名,所以切记要验证。
  并且没有严格规定必须三个组成部分必须是来自同一提供,可随性发挥。
  且Homebrew在早期版本中更新源的是在/usr/local目录下的,而现在是在/usr/local/Homebrew,不过应该都是可以使用"$(brew --repo)"来自动指向目录的,所以无需理会。
  如果你之前折腾过不少导致你的Homebrew有点问题,那么可以尝试使用如下方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 诊断Homebrew的问题:
$ brew doctor
# 重置brew.git设置:
$ cd "$(brew --repo)"
$ git fetch
$ git reset --hard origin/master
# homebrew-core.git同理:
$ cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
$ git fetch
$ git reset --hard origin/master
# 应用生效:
$ brew update

重置更新源

  所谓有进则有退,在某些时候也有换回官方源的需求。

1
2
3
4
5
6
7
# 重置brew.git:
$ cd "$(brew --repo)"
$ git remote set-url origin https://github.com/Homebrew/brew.git
# 重置homebrew-core.git:
$ cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
$ git remote set-url origin https://github.com/Homebrew/homebrew-core.git

  至于homebrew-bottles,本质上作为一个环境变量的存在,之前的命令也只是将其写入到/usr/.bash_profile中,并且只是在文件尾部添加一行。所以之前的命令不推荐重复执行,在未掌握相关命令技巧的前提下,我推荐直接去修改.bash_profile文件:bash_profile
  当然这里的主题是重置更新源,所以我们直接选择删除环境变量HOMEBREW_BOTTLE_DOMAIN,使其成为默认值即可。
  当然,最后不要忘记$ brew update进行应用。

后记

  在完成更新源的更换后,我们可以使用$ brew upgrade将现有的软件进行更新至最新版本,这样便能很直接的看出速度上的变化了。最后不要忘记$ brew cleanup将旧有的软件安装包进行清理。

1…456
Musoucrow

Musoucrow

53 posts
7 categories
37 tags
RSS
Coding Github
© 2022 Musoucrow
Powered by Hexo
Theme - NexT.Mist