使用Python中的Socket编程实现Unity的聊天室功能

最近开始使用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’才结束, 从而引发传输堵塞的问题.

后续实现过程中可能还会踩不少坑, 如果有值得记录的再来更新叭~

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注