天堂开发笔记(一)
开发日志
(一)游戏服务器主体框架
最初想法是使用gevent.server
的StreamServer
,采用coroutine
的方式来替代源码的多线程方式,但是本身对python语言不太熟悉,怕影响后期的调试,所以暂时还是和源码保持一致使用原始的多线程。相关文件:Server.py、GameServer.py、ClientThead.py
SocketServer的使用
源码多线程的python版本
import SocketServer
from Config import Config
from server.ClientThread import ClientThread
class ClientHandler(SocketServer.BaseRequestHandler):
def handle(self):
t = ClientThread(self.request)
t.start()
t.join()
server = SocketServer.TCPServer((Config.get('server', 'GameserverHostname'), Config.getint('server', 'GameserverPort')), ClientHandler)
server.serve_forever()
StreamServer的使用
coroutine版本
# -*- coding: utf-8 -*-
from gevent.server import StreamServer
def read_message(socket):
socket.send('ok')
def handle(socket, address):
# 处理单个客户端连接
t = gevent.spawn(read_message, socket)
t.join()
pass
server = StreamServer(('127.0.0.1', 5900), handle)
server.serve_forever()
(二)解析配置文件
游戏中必不可少的就是参数的设置,所以接下来就是配置文件的解析,python提供了现成的ConfigParser
模块,而且文档里有详细的使用教程。相关文件:Config.py
(三)日志的记录
初期使用print
是比较快捷的方式,但是后期改造麻烦,所以直接使用python自带的logging
模块来代替print
,只要在初始化的时候设置好logging
模块的level
就可以了。相关文件:无
(四)通信数据的加密解密
基本上是直接把源码由java改成python,使用python的struct
模块可以快速实现各类数据类型到字节流的转换。相关文件:Cipher.py
struct的使用
# struct的pack生成的是字符流,通过bytearray可以转换成字节流
dk = bytearray(struct.pack('<L', 1L))
# struct的pack返回的是tuple
mask, = struct.unpack('<L', buf[0:4])
# 转换字符串为字节流
bytearray("silvermagic", 'utf-8')
Format | C Type | Python type | Standard size | Notes |
---|---|---|---|---|
x |
pad byte | no value | ||
c |
char |
string of length 1 | 1 | |
b |
signed char |
integer | 1 | (3) |
B |
unsigned char |
integer | 1 | (3) |
? |
_Bool |
bool | 1 | (1) |
h |
short |
integer | 2 | (3) |
H |
unsigned short |
integer | 2 | (3) |
i |
int |
integer | 4 | (3) |
I |
unsigned int |
integer | 4 | (3) |
l |
long |
integer | 4 | (3) |
L |
unsigned long |
integer | 4 | (3) |
q |
long long |
integer | 8 | (2), (3) |
Q |
unsigned long long |
integer | 8 | (2), (3) |
f |
float |
float | 4 | (4) |
d |
double |
float | 8 | (4) |
s |
char[] |
string | ||
p |
char[] |
string | ||
P |
void * |
integer | (5), (3) |
(五)网络数据的传输
主要是格式转换的问题,仔细分析源码,发现传输使用的是小端模式,配合struct
模块还是很容易将数据转换成传输的字节流的,为了方便调试,我们还需要实现下字节流的打印。相关文件:ByteArrayUtil.py
# 编码并发送数据
data = struct.pack('<HBI', bogus, Opcodes.S_OPCODE_INITPACKET, key) + _FIRST_PACKET
self._csocket.sendall(data)
# 接收并解码数据
data = bytearray(self._csocket.recv(1024))
length, = struct.unpack('<h', data[0:2])
(六)客户端数据包的解码和服务器数据包的编码
客户端数据包的解码主要是:从接收的字节流中按照约定的顺序,依次从中读取各种数据类型;服务器数据包的编码主要是:按照约定的顺序,将各种数据类型依次转换成字节流。相关文件:ServerBasePacket.py、ClientBasePacket.py、PacketHandler.py
# 字符串的编码
"".encode(Config.get('server', 'ClientLanguageCode')) + b'\0'
# 字符串的解码
ret = b"".decode(Config.get('server', 'ClientLanguageCode'))
ret = ret[:ret.index(b'\0')]
(七)服务端版本验证
在源码中添加打印日志,方便和我们python生成或接收的数据包进行对比。
对源码的修改
# 源码中添加对初始包的打印
byte first[] = {(byte) (Bogus & 0xFF), (byte) (Bogus >> 8 & 0xFF), Opcodes.S_OPCODE_INITPACKET, (byte) (key & 0xFF), (byte) (key >> 8 & 0xFF), (byte) (key >> 16 & 0xFF), (byte) (key >> 24 & 0xFF)};
System.out.println("[Init C]:" + new ByteArrayUtil(first).dumpToString());
# 源码中添加对接收的数据包的打印
_log.info("[C]\n" + new ByteArrayUtil(data).dumpToString());
python代码的修改
# 对初始包的打印
logging.debug('\n' + ByteArrayUtil.dumpToString(bytearray(data)))
# 对接收的数据包的打印
logging.debug('\n' + ByteArrayUtil.dumpToString(bytearray(data)))
通过运行源码发现,当客户端打开登入界面时,服务器会发送一个初始包,然后断开连接
接着当我们点击进入服务器后,客户端会向客户端发送一个版本验证包
Nov 09, 2016 3:45:03 PM l1j.server.server.ClientThread run
INFO: [C]
0000: 36 33 00 a8 03 00 00 00 d4 b0 01 00 63..........
然后服务端要响应这个数据包,并向客户端发送S_ServerVersion
数据包,完成验证后客户端就可以进入到账号登入界面了。相关文件:S_ServerVersion.py、S_ServerMessage.py、C_ServerVersion.py、
(八)用户账户登入验证
运行源码发现用户登入时会发送如下数据包C_OPCODE_LOGINPACKET = 115; // 請求登錄伺服器
Nov 11, 2016 3:12:48 PM l1j.server.server.ClientThread run
INFO: [Recv C]
0000: 73 46 77 64 73 73 67 00 66 77 64 73 73 67 00 7f sFwdssg.fwdssg.
0010: 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 1f 00 00 00 00 ............
[Send C]:
0000: 76 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 v...............
[Send C]:
0000: 24 5c 66 43 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d $\fC============
0010: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ================
0020: 3d 3d 3d 3d 0a 5c 66 4d a1 f1 a1 f1 5c 66 33 bb ====.\fM....\f3.
0030: b6 d3 ad c0 b4 b5 bd cc ec cc c3 b5 c4 ca c0 bd ................
0040: e7 5c 66 4d a1 f1 a1 f1 20 20 0a 5c 66 43 3d 3d .\fM.... .\fC==
0050: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ================
0060: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 0a 5c ==============.\
0070: 66 33 20 78 78 cc ec cc c3 a3 ba 5c 66 3d cd f8 f3 xx......\f=..
0080: d6 b7 0a 5c 66 43 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ...\fC==========
0090: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ================
00a0: 3d 3d 3d 3d 3d 3d 0a 5c 66 4d 5b b7 c2 d5 fd ca ======.\fM[.....
00b0: d6 b5 e3 c4 dc c1 a6 c9 cf cf de 5d 5c 66 32 3a ...........]\f2:
00c0: 33 35 0a 5c 66 4d 5b cd f2 c4 dc d2 a9 b5 a5 cf 35.\fM[.........
00d0: ee c4 dc c1 a6 c9 cf cf de 5d 5c 66 32 3a 34 30 .........]\f2:40
00e0: 0a 5c 66 4d 5b cd f2 c4 dc d2 a9 cb ae ca b9 d3 .\fM[...........
00f0: c3 c9 cf cf de 5d 5c 66 32 3a 31 30 c6 bf 0a 5c .....]\f2:10...\
0100: 66 4d 5b d7 aa c9 fa ba f3 d1 aa c4 a7 c1 bf b1 fM[.............
0110: a3 c1 f4 5d 5c 66 32 3a 31 30 30 25 0a 5c 66 43 ...]\f2:100%.\fC
0120: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ================
0130: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ================
0140: 0a 5c 66 3a 5b be ad d1 e9 d6 b5 b1 b6 c2 ca 5d .\f:[..........]
0150: 5c 66 32 3a 32 30 30 30 b1 b6 0a 5c 66 3a 5b bd \f2:2000...\f:[.
0160: f0 c7 ae b1 b6 c2 ca 5d 5c 66 32 3a 31 30 30 b1 .......]\f2:100.
0170: b6 0a 5c 66 3a 5b ce ef c6 b7 b1 b6 c2 ca 5d 5c ..\f:[........]\
0180: 66 32 3a 31 b1 b6 0a 5c 66 3a 5b b3 e5 ce e4 c6 f2:1...\f:[.....
0190: f7 a1 a2 b7 c0 be df b3 c9 b9 a6 c2 ca 5d 5c 66 .............]\f
01a0: 32 3a 31 30 25 0a 5c 66 3a 5b ca f4 d0 d4 c7 bf 2:10%.\f:[......
01b0: bb af b3 c9 b9 a6 c2 ca 5d 5c 66 32 3a 31 30 25 ........]\f2:10%
01c0: 0a 5c 66 43 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d .\fC============
01d0: 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d ================
01e0: 3d 3d 3d 3d 0a 5c 66 4d b7 fe ce f1 c6 f7 33 d0 ====.\fM......3.
01f0: a1 ca b1 d7 d4 b6 af d6 d8 c6 f0 0a 5c 66 4d 48 ............\fMH
0200: 50 ba cd 4d 50 d3 d0 c9 cf cf de 20 20 0a 5c 66 P..MP...... .\f
0210: 4d 50 72 69 6e 63 65 3d cd f5 d7 e5 20 48 50 20 MPrince=.... HP
0220: 3d 20 31 34 30 30 30 20 4d 50 20 3d 20 38 30 30 = 14000 MP = 800
0230: 30 0a 5c 66 4d 4b 6e 69 67 68 74 3d f2 54 ca bf 0.\fMKnight=.T..
0240: 20 48 50 20 3d 20 32 30 30 30 30 20 4d 50 20 3d HP = 20000 MP =
0250: 20 36 30 30 30 0a 5c 66 4d 45 6c 66 3d d1 fd be 6000.\fMElf=...
0260: ab 20 48 50 20 3d 20 31 34 30 30 30 20 4d 50 20 . HP = 14000 MP
0270: 3d 20 39 30 30 30 0a 5c 66 4d 57 69 7a 61 72 64 = 9000.\fMWizard
0280: 3d b7 a8 8e 9f 20 48 50 20 3d 20 31 30 30 30 30 =.... HP = 10000
0290: 20 4d 50 20 3d 20 31 32 30 30 30 0a 5c 66 4d 44 MP = 12000.\fMD
02a0: 61 72 6b 65 6c 66 3d ba da b0 b5 d1 fd be ab 20 arkelf=........
02b0: 48 50 20 3d 20 31 34 30 30 30 20 4d 50 20 3d 20 HP = 14000 MP =
02c0: 39 30 30 30 0a 5c 66 4d 44 72 61 67 6f 6e 4b 6e 9000.\fMDragonKn
02d0: 69 67 68 74 3d fd 88 f2 54 ca bf 20 48 50 20 3d ight=...T.. HP =
02e0: 20 31 38 30 30 30 20 4d 50 20 3d 20 36 30 30 30 18000 MP = 6000
02f0: 0a 5c 66 4d 49 6c 6c 75 73 69 6f 6e 69 73 74 3d .\fMIllusionist=
0300: bb c3 d0 67 8e 9f 20 48 50 20 3d 20 31 31 30 30 ...g.. HP = 1100
0310: 30 20 4d 50 20 3d 20 31 31 30 30 30 0a 00 00 00 0 MP = 11000....
Nov 11, 2016 3:12:49 PM l1j.server.server.ClientThread run
INFO: [Recv C]
0000: 2a 00 00 00 *...
[Send C]:
0000: 0c 00 09 00
然后服务端要返回S_OPCODE_LOGINRESULT = 118 # 登入狀態
和S_OPCODE_COMMONNEWS = 36 # 公告視窗
数据包,客户端在收到数据包后显示如下
当用户单击确定后会向服务端发送C_OPCODE_COMMONCLICK = 42 # 請求下一步 (伺服器公告)
数据包,服务端要返回S_OPCODE_CHARAMOUNT = 12 # 角色列表
数据包作为响应,至此账户登入验证完成。账户的验证需要访问数据库,所以需要先实现对数据库的读取和修改,这边使用sqlalchemy
模块的orm
来实现此功能。
相关文件:S_LoginResult.py、C_AuthLogin.py、Datatables.py、Account.py、S_CommonNews.py、S_CharPacks.py、S_CharAmount.py、C_CommonClick.py
sqlalchemy的使用
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.automap import automap_base
Base = automap_base()
# engine, suppose it has two tables 'user' and 'address' set up
engine = create_engine("mysql+pymysql://root:root@localhost/l1jdb?charset=GBK")
SessionMaker = scoped_session(sessionmaker(bind=engine))
class Session():
def __enter__(self):
return SessionMaker()
def __exit__(self, exc_type, exc_val, exc_tb):
try:
if exc_type is None:
try:
SessionMaker.commit()
except:
SessionMaker.rollback()
raise
else:
SessionMaker.rollback()
finally:
SessionMaker.remove()
# reflect the tables
reload(sys)
sys.setdefaultencoding('utf-8')
Base.prepare(engine, reflect=True)
# mapped classes are now created with names by default
# matching that of the table name.
Accounts = Base.classes.accounts
# 插入数据
item = Accounts(login=account._name,
password=account._password,
lastactive=account._lastActive,
access_level=account._accessLevel,
ip=account._ip,
host=account._host,
banned=account._banned,
character_slot=account._characterSlot)
with Session() as session:
session.add(item)
# 获取数据
with Session() as session:
# 表的列名可以直接在查询中使用
item = session.query(Accounts).filter(Accounts.login == name).one_or_none()
if not item:
return account
account = Account()
account._name = item.login
account._password = item.password
account._lastActive = item.lastactive
account._accessLevel = item.access_level
account._ip = item.ip
account._host = item.host
account._banned = item.banned
account._characterSlot = item.character_slot
# 更新数据
with Session() as session:
session.query(Accounts).filter(Accounts.login == account._name).one_or_none().update({Accounts.character_slot : account._characterSlot})