項目源碼地址: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("")
先運行服務端,再運行客戶端(可以多運行幾個客戶端)
運行效果: