最近开始使用Python中的Socket编程着手实现Unity的聊天室功能, 但之前自己的服务端编程经验基本为0(虽然之前使用过Unity的Network插件, 但该插件仅适用于局域网, 且使用过程中还是存在着较多的bug), 所以还是走得比较坎坷~
参考材料
1. Python的非阻塞式(non-blocking)socket通訊程式
2. [JAVA]Socket中BufferedReader.readLine()的阻塞特性导致的数据无法多次发送的问题
3. Python中send()和sendall()的区别
目前的主要工作都集中在服务端搭建上, 核心代码为
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import socket
import string
from client import Client
class Server:
clientList = []
def broadcastMessage(self, message):
'''
广播消息
@param message: 待广播的消息
@return: void
'''
for client in self.clientList:
if client != None and client.isConnected:
client.sendMessage(message)
def main(self, host, port, backlog, size):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))
s.setblocking(False)
s.settimeout(0.5)
s.listen(backlog)
print "Server running on port " + str(port)
while True:
try:
client, address = s.accept()
print 'Client ' + str(len(self.clientList) + 1) + ' connects.\n'
client.sendall('欢迎你, 玩家' + str(len(self.clientList) + 1) + '!\n')
self.clientList.append(Client(client, size))
except:
pass
for client in self.clientList:
if client != None:
if client.isConnected:
if client.isSend:
self.broadcastMessage(client.data)
else:
index = self.clientList.index(client)
self.clientList[index] = None
print 'Client ' + str(index + 1) + ' goes away.\n'
其中使用的Client类的定义为
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import socket
import string
import threading
class Client:
client = None
isConnected = True
isSend = False
data = ''
def __init__(self, s, size):
self.client = s
self.client.setblocking(False)
self.client.settimeout(0.5)
self.isConnected = True
t = threading.Thread(target = self.receiveMessage, args = [size])
t.start()
def receiveMessage(self, size):
while True:
if not self.isConnected:
break
try:
# 接收消息
self.data = self.recv(size).rstrip('\r\n')
if self.data == b"":
self.isConnected = False
self.isSend = False
break
else:
self.sendMessage('你刚刚说: ' + self.data)
# 广播消息
self.isSend = True
except:
self.isSend = False
self.close()
def sendMessage(self, message):
self.client.sendall(message + '\n')
def recv(self, size):
return self.client.recv(size)
def getsockname(self):
return self.client.getsockname()
def close(self):
self.client.close()
其中, 值得注意的有以下几点.
1. 针对多客户端情形需要作如下设置,
s.setblocking(False) s.settimeout(0.5)
通过setblocking方法将服务端socket设置为非阻塞模式, 同时设置超时时间为0.5秒, 对于这个超时时间我的简单理解为每隔0.5秒执行一次while循环中的代码, 而非之前的每一帧都在执行, 那样我觉得是比较消耗计算机性能的.
2. 当仅是作出了1中的改动, 而没有进行后续处理的话, 容易出现BlockingIOError(阻塞IO错误), 这是因为程序原本应该停在s.accept()那一行, 等到有客户端连线再自动继续执行后面的代码, 由于服务端socket被设定成以非阻塞方式执行, 导致程序沒有停止. 在沒有客户端连线的情況下, 强制执行accept(), 就會出現BlockingIOError.
解决的方法是用try…except包装可能会出错的叙述. 客户端socket的recv(接收消息)方法也會造成阻塞, 当客户端socket改成非阻塞方式之後, 在尚未收到客户端socket的消息时强制执行recv将造成错误, 因此也要用try…except包裝.
3. threading.Thread方法创建一个新线程用于监听客户端socket的消息, 需要注意的是, 尽量不要在多个线程监听客户端socket的消息, 否则容易出错.
4. 发送消息时使用的是sendall方法而非send方法, sendall是对send的包装, 完成了用户需要手动完成的部分, 它会自动判断每次发送的内容量, 然后从总内容中删除已发送的部分, 将剩下的继续传给send进行发送.
5. 一个困扰我最久的问题还是一个阻塞问题, 经排查, 是Unity端脚本的StreamReader.ReadLine()读取socket字节流时出现了问题, 若服务端在向客户端发送消息时没有以’\n’结尾, 则StreamReader.ReadLine()会一直等待’\n’才结束, 从而引发传输堵塞的问题.
后续实现过程中可能还会踩不少坑, 如果有值得记录的再来更新叭~