项目源码地址:https://github.com/zxf20180725/pygame-jxzj,求赞求星星~
1.前言
两个多月没更新了,这两个月经历了一些事情,让人挺难受的,不过人就是这样在这些经历中变得更加成熟。
好了,回到主题。这一章呢,我们需要动手封装一个非常非常简易的游戏服务端框架。前面我也说过,服务端的水很深,所以我只打算做一个基本能跑的服务端出来。这里就相当于带大家入个门吧,如果大家感兴趣的话,更全面的服务端知识还是得在其他地方系统的学习。
2.封装socket连接
阅读本章之前,本人强烈建议读者先看一遍完整的代码
在上一章中,服务端接收到新连接之后,就会把它存到全局变量g_conn_pool中。这次,我们把客户端socket连接封装成一个独立的类,把上一章的一些零散的操作都封装到一起。
class Connection:
"""
连接类,每个socket连接都是一个connection
"""
def __init__(self, socket, connections):
self.socket = socket
self.connections = connections
self.data_handler()
def data_handler(self):
# 给每个连接创建一个独立的线程进行管理
thread = Thread(target=self.recv_data)
thread.setDaemon(True)
thread.start()
def recv_data(self):
# 接收数据
try:
while True:
bytes = self.socket.recv(2048) # 我们这里只做一个简单的服务端框架,不去做分包处理。所以每个数据包不要大于2048
if len(bytes) == 0:
self.socket.close()
# 删除连接
self.connections.remove(self)
break
# 处理数据
self.deal_data(bytes)
except:
self.connections.remove(self)
Server.write_log('有用户接收数据异常,已强制下线,详细原因:\n' + traceback.format_exc())
def deal_data(self, bytes):
"""
处理客户端的数据,需要子类实现
"""
raise NotImplementedError
Connection封装了创建线程和处理数据的功能,但是处理数据的功能并没有具体实现,需要子类实现deal_data函数。
这么做的目的是为了这个简单的框架具有通用型。因为每个游戏处理数据的方式不同,如果我们这个框架要给别人用的话(也不可能有别人用啦,哈哈,主要是要有这个意识),别人可能有他自己的一套数据处理方式,所以不能给写死了,交给使用者自己实现才更灵活。
上面说了Connection是需要子类继承的,那么下面我们就实现一个简单的子类Player。
class Player(Connection):
"""
玩家类,我们的游戏中,每个连接都是一个Player对象
"""
def __init__(self, *args):
super().__init__(*args)
self.login_state = False # 登录状态
self.nickname = None # 昵称
self.x = None # 人物在地图上的坐标
self.y = None
def deal_data(self, bytes):
"""
处理服务端发送的数据
:param bytes:
:return:
"""
print('\n客户端消息:',bytes.decode('utf8'))
在构造方法中,我们调用了父类的构造方法,并且把外部的参数传给了父类的构造方法,*args就是父类的socket和connections参数。Player重写了父类的deal_data方法,功能很简单,就是输出一下客户端发来的消息。
3.服务端入口
在上面,我们封装了Connection类和Player类,现在我们就要用上它们。
现在整个程序还差一个启动入口,我们现在编写一个Server类,做为程序的入口。
class Server:
"""
服务端主类
"""
__user_cls = None
@staticmethod
def write_log(msg):
cur_time = datetime.datetime.now()
s = "[" + str(cur_time) + "]" + msg
print(s)
def __init__(self, ip, port):
self.connections = [] # 所有客户端连接
self.write_log('服务器启动中,请稍候...')
try:
self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 监听者,用于接收新的socket连接
self.listener.bind((ip, port)) # 绑定ip、端口
self.listener.listen(5) # 最大等待数
except:
self.write_log('服务器启动失败,请检查ip端口是否被占用。详细原因:\n' + traceback.format_exc())
if self.__user_cls is None:
self.write_log('服务器启动失败,未注册用户自定义类')
return
self.write_log('服务器启动成功:{}:{}'.format(ip,port))
while True:
client, _ = self.listener.accept() # 阻塞,等待客户端连接
user = self.__user_cls(client, self.connections)
self.connections.append(user)
self.write_log('有新连接进入,当前连接数:{}'.format(len(self.connections)))
@classmethod
def register_cls(cls, sub_cls):
"""
注册玩家的自定义类
"""
if not issubclass(sub_cls, Connection):
cls.write_log('注册用户自定义类失败,类型不匹配')
return
cls.__user_cls = sub_cls
write_log是对print的一个封装,就不多说了。
Server类有一个属性__user_cls,这个就保存着用户自己实现的Connection的子类,我们专门写了一个register_cls方法,用来给__user_cls赋值。这个方法可以直接当装饰器使用,就想这样:
@Server.register_cls
class Player(Connection):
"""
玩家类,我们的游戏中,每个连接都是一个Player对象
"""
# 具体代码省略
这样,我们的框架就知道了用户自定义的类是Player。
构造方法中,self.connections是用来保存所有客户端连接的。其中有一个while死循环,这是用来一直接收新的连接。
user = self.__user_cls(client, self.connections)
self.connections.append(user)
这两句是用来创建Player对象,并且保存到connections中的。
4.运行
我们先写一个非常简单的客户端,来看看我们服务端框架的效果
客户端代码:
import socket
s = socket.socket()
s.connect(('127.0.0.1', 6666))
s.send("你好呀,我是客户端".encode('utf8'))
input("")
先运行服务端,再运行客户端(可以多运行几个客户端)
运行效果: