Simple Chat Application for Python

一、知识点介绍:

asyncore 、asynchat模块使用

由于 Python 是一门带 GIL 的语言,所以在 Python 中使用多线程处理IO操作过多的任务并不是很好的选择。同时聊天服务器将同多个 socket 进行通信,所以我们可以基于 asyncore 模块实现聊天服务器。aysncore 模块是一个异步的 socket 处理器,通过使用该模块将大大简化异步编程的难度。asynchat 模块在 asyncore 模块的基础上做了进一步封装,简化了基于文本协议的通信任务的开发难度。

  • 异步socket处理器-asyncore

python 3.6 版后已移除: 请使用asyncio模块替代

asyncore模块提供了以异步的方式写入套接字服务的客户端和服务器的基础架构。
模块主要包括:

asyncore.loop(…)  #用于循环监听网络事件。loop()函数负责检测一个字典,字典中保存dispatcher的实例,这个字典被称为channel。
asyncore.dispatcher.__init__(self)  #一个底层套接字对象的简单封装,相当于一个socket对象。此类有少数由异步循环调用的,用来事件处理的函数。

dispatcher类中的writable()和readable()在检测到一个socket可以写入或者数据到达的时候被调用,并返回一个bool值,决定是否调用handle_read或者handle_write,也就是说,一旦检测到可读或可写,就调用handle_read/handle_write。打开asyncore.py可以看到,dispatcher类中定义的方法writable和readable的定义相当的简单:

asyncore.dispatcher_with_send类  #一个 dispatcher的子类,添加了简单的缓冲输出能力,对简单的客户端很有用。

每次创建一个dispatcher对象,都可以看做我们需要处理的一个socket(可以TCP也可以是UDP,甚至是一些不常用的),都会把自己加入到一个默认的dict里面去(当然也可以自己指定channel)。当对象被加入到channel中的时候,socket的行为都已经被定义好,程序只需要调用loop(),一切功能就实现了。

  • 异步 socket 指令/响应 处理器-asynchat
    此模块建立在asyncore模块的基础上,简化了异步客户端和服务器,并使处理元素由任意字符串终止或长度可变的协议变得更加容易。 asynchat定义了子类的抽象类async_chat,提供了collect_incoming_data()和found_terminator()方法的实现。它使用与asyncore相同的异步循环,并且两种通道类型asyncore.dispatcher和asynchat.async_chat可以在通道中自由混合。通常,asyncore.dispatcher服务器通道在接收到传入的连接请求时会生成新的asynchat.async_chat通道对象。
asynchat.async_chat.__init__(self, sock) #此类是asyncore.dispatcher的抽象子类。一般使用其collect_incoming_data()和found_terminator()方法。
async_chat.collect_incoming_data()  #接收数据。
async_chat.found_terminator() #当输入数据流符合由 set_terminator() 设置的终止条件时被调用。
async_chat.set_terminator()  #设置终止条件。
async_chat.push()  #向通道压入数据以确保其传输。

二、代码实现

服务器端

1.服务器类

这里我们首先需要一个聊天服务器类,通过继承 asyncore 的 dispatcher 类来实现

import asynchat
import asyncore


# 定义端口
PORT = 6666

# 定义结束异常类
class EndSession(Exception):
    pass


class ChatServer(asyncore.dispatcher):
    """
    聊天服务器
    """

    def __init__(self, port):
        asyncore.dispatcher.__init__(self)
        # 创建socket
        self.create_socket()
        # 设置 socket 为可重用
        self.set_reuse_addr()
        # 监听端口
        self.bind(('0.0.0.0', port))
        self.listen(5)
        self.users = {}
        self.main_room = ChatRoom(self)

    def handle_accept(self):
        conn, addr = self.accept()
        ChatSession(self, conn)

2.会话类

有了服务器类还需要能维护每个用户的连接会话,这里通过继承 asynchat 的 async_chat 类来实现。

class ChatSession(asynchat.async_chat):
    """
    负责和客户端通信
    """

    def __init__(self, server, sock):
        asynchat.async_chat.__init__(self, sock)
        self.server = server
        self.set_terminator(b'\n')
        self.data = []
        self.name = None
        self.enter(LoginRoom(server))

    def enter(self, room):
        # 从当前房间移除自身,然后添加到指定房间
        try:
            cur = self.room
        except AttributeError:
            pass
        else:
            cur.remove(self)
        self.room = room
        room.add(self)

    def collect_incoming_data(self, data):
        # 接收客户端的数据
        self.data.append(data.decode("utf-8"))

    def found_terminator(self):
        # 当客户端的一条数据结束时的处理
        line = ''.join(self.data)
        self.data = []
        try:
            self.room.handle(self, line.encode("utf-8"))
        # 退出聊天室的处理
        except EndSession:
            self.handle_close()

    def handle_close(self):
        # 当 session 关闭时,将进入 LogoutRoom
        asynchat.async_chat.handle_close(self)
        self.enter(LogoutRoom(self.server))

3.协议命令解释器

我们需要实现协议命令的相应方法,具体来说就是处理用户登录,退出,发消息,查询在线用户的代码。

class CommandHandler:
    """
    命令处理类
    """

    def unknown(self, session, cmd):
        # 响应未知命令
        # 通过 asynchat.async_chat.push 方法发送消息
        session.push(('Unknown command {} \n'.format(cmd)).encode("utf-8"))

    def handle(self, session, line):
        line = line.decode()
        # 命令处理
        if not line.strip():
            return
        parts = line.split(' ', 1)
        cmd = parts[0]
        try:
            line = parts[1].strip()
        except IndexError:
            line = ''
        # 通过协议代码执行相应的方法
        method = getattr(self, 'do_' + cmd, None)
        try:
            method(session, line)
        except TypeError:
            self.unknown(session, cmd)

4. 聊天室

接下来就需要实现聊天室的房间了,这里我们定义了三种房间,分别是用户刚登录时的房间、聊天的房间和退出登录的房间,这三种房间都继承自 CommandHandler,代码如下:

class Room(CommandHandler):
    """
    包含多个用户的环境,负责基本的命令处理和广播
    """

    def __init__(self, server):
        self.server = server
        self.sessions = []

    def add(self, session):
        # 一个用户进入房间
        self.sessions.append(session)

    def remove(self, session):
        # 一个用户离开房间
        self.sessions.remove(session)

    def broadcast(self, line):
        # 向所有的用户发送指定消息
        # 使用 asynchat.asyn_chat.push 方法发送数据
        for session in self.sessions:
            session.push(line)

    def do_logout(self, session, line):
        # 退出房间
        raise EndSession


class LoginRoom(Room):
    """
    处理登录用户
    """

    def add(self, session):
        # 用户连接成功的回应
        Room.add(self, session)
        # 使用 asynchat.asyn_chat.push 方法发送数据
        session.push(b'Connect Success')

    def do_login(self, session, line):
        # 用户登录逻辑
        name = line.strip()
        # 获取用户名称
        if not name:
            session.push(b'UserName Empty')
        # 检查是否有同名用户
        elif name in self.server.users:
            session.push(b'UserName Exist')
        # 用户名检查成功后,进入主聊天室
        else:
            session.name = name
            session.enter(self.server.main_room)


class LogoutRoom(Room):
    """
    处理退出用户
    """

    def add(self, session):
        # 从服务器中移除
        try:
            del self.server.users[session.name]
        except KeyError:
            pass


class ChatRoom(Room):
    """
    聊天用的房间
    """

    def add(self, session):
        # 广播新用户进入
        session.push(b'Login Success')
        self.broadcast((session.name + ' has entered the room.\n').encode("utf-8"))
        self.server.users[session.name] = session
        Room.add(self, session)

    def remove(self, session):
        # 广播用户离开
        Room.remove(self, session)
        self.broadcast((session.name + ' has left the room.\n').encode("utf-8"))

    def do_say(self, session, line):
        # 客户端发送消息
        self.broadcast((session.name + ': ' + line + '\n').encode("utf-8"))

    def do_look(self, session, line):
        # 查看在线用户
        session.push(b'Online Users:\n')
        for other in self.sessions:
            session.push((other.name + '\n').encode("utf-8"))

if __name__ == '__main__':

    s = ChatServer(PORT)
    try:
        print("chat server run at '0.0.0.0{0}'".format(PORT))
        asyncore.loop()
    except KeyboardInterrupt:
        print("chat server exit")

客户端

1.登录窗口

完成了服务器端后,就需要实现客户端了。客户端将基于 wxPython 模块实现。wxPython 模块是 wxWidgets GUI 工具的 Python 绑定。所以通过 wxPython 模块我们就可以实现 GUI 编程了。同时我们的聊天协议基于文本,所以客户端和服务器之间的通信将基于 telnetlib 模块实现。
登录窗口通过继承 wx.Frame 类来实现,编写 client.py 文件,代码如下:

import wx
import telnetlib
from time import sleep
import _thread as thread

class LoginFrame(wx.Frame):
    """
    登录窗口
    """
    def __init__(self, parent, id, title, size):
        # 初始化,添加控件并绑定事件
        wx.Frame.__init__(self, parent, id, title)
        self.SetSize(size)
        self.Center()
        self.serverAddressLabel = wx.StaticText(self, label="Server Address", pos=(10, 50), size=(120, 25))
        self.userNameLabel = wx.StaticText(self, label="UserName", pos=(40, 100), size=(120, 25))
        self.serverAddress = wx.TextCtrl(self, pos=(120, 47), size=(150, 25))
        self.userName = wx.TextCtrl(self, pos=(120, 97), size=(150, 25))
        self.loginButton = wx.Button(self, label='Login', pos=(80, 145), size=(130, 30))
        # 绑定登录方法
        self.loginButton.Bind(wx.EVT_BUTTON, self.login)
        self.Show()

    def login(self, event):
        # 登录处理
        try:
            serverAddress = self.serverAddress.GetLineText(0).split(':')
            con.open(serverAddress[0], port=int(serverAddress[1]), timeout=10)
            response = con.read_some()
            if response != b'Connect Success':
                self.showDialog('Error', 'Connect Fail!', (200, 100))
                return
            con.write(('login ' + str(self.userName.GetLineText(0)) + '\n').encode("utf-8"))
            response = con.read_some()
            if response == b'UserName Empty':
                self.showDialog('Error', 'UserName Empty!', (200, 100))
            elif response == b'UserName Exist':
                self.showDialog('Error', 'UserName Exist!', (200, 100))
            else:
                self.Close()
                ChatFrame(None, 2, title='H4ck3R Chat Client', size=(500, 400))
        except Exception:
            self.showDialog('Error', 'Connect Fail!', (95, 20))

    def showDialog(self, title, content, size):
        # 显示错误信息对话框
        dialog = wx.Dialog(self, title=title, size=size)
        dialog.Center()
        wx.StaticText(dialog, label=content)
        dialog.ShowModal()

2.聊天窗口

聊天窗口中最主要的就是向服务器发消息并接受服务器的消息,这里通过子线程来接收消息,继续在 client.py 文件中定义,代码如下:

class ChatFrame(wx.Frame):
    """
    聊天窗口
    """

    def __init__(self, parent, id, title, size):
        # 初始化,添加控件并绑定事件
        wx.Frame.__init__(self, parent, id, title)
        self.SetSize(size)
        self.Center()
        self.chatFrame = wx.TextCtrl(self, pos=(5, 5), size=(490, 310), style=wx.TE_MULTILINE | wx.TE_READONLY)
        self.message = wx.TextCtrl(self, pos=(5, 320), size=(300, 25))
        self.sendButton = wx.Button(self, label="Send", pos=(310, 320), size=(58, 25))
        self.usersButton = wx.Button(self, label="Users", pos=(373, 320), size=(58, 25))
        self.closeButton = wx.Button(self, label="Close", pos=(436, 320), size=(58, 25))
        # 发送按钮绑定发送消息方法
        self.sendButton.Bind(wx.EVT_BUTTON, self.send)
        # Users按钮绑定获取在线用户数量方法
        self.usersButton.Bind(wx.EVT_BUTTON, self.lookUsers)
        # 关闭按钮绑定关闭方法
        self.closeButton.Bind(wx.EVT_BUTTON, self.close)
        thread.start_new_thread(self.receive, ())
        self.Show()

    def send(self, event):
        # 发送消息
        message = str(self.message.GetLineText(0)).strip()
        if message != '':
            con.write(('say ' + message + '\n').encode("utf-8"))
            self.message.Clear()

    def lookUsers(self, event):
        # 查看当前在线用户
        con.write(b'look\n')

    def close(self, event):
        # 关闭窗口
        con.write(b'logout\n')
        con.close()
        self.Close()

    def receive(self):
        # 接受服务器的消息
        while True:
            sleep(0.6)
            result = con.read_very_eager()
            if result != '':
                self.chatFrame.AppendText(result)

if __name__ == '__main__':
    app = wx.App()
    con = telnetlib.Telnet()
    LoginFrame(None, -1, title="Login", size=(320, 250))
    app.MainLoop()

三、运行

  1. 首先,我们执行 server.py ,如下图所示:

  2. 这时,我们再另一台机器(我的是虚拟机)打开一个终端,执行 client.py 文件,输入服务端的地址、端口,及自己在聊天室的ID,点击 Login ,即可进入

此时,点击Users,将显示处于聊天室的当前用户:

  1. 同样,在另外一台机器中重复上述操作:

以下是模拟的两个用户的对话(当然,也可以更多用户),但只要在线的用户都可以收到对话消息。此时,再次点击Users,显示处于聊天室的两个用户:

posted @ 2020-02-14 23:52  zha0gongz1  阅读(373)  评论(0编辑  收藏  举报