天堂开发笔记(一)

开发日志

(一)游戏服务器主体框架

最初想法是使用gevent.serverStreamServer,采用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})
posted @ 2017-10-14 12:12  银魔术师  阅读(1260)  评论(0编辑  收藏  举报