第十四章 项目案例《多人聊天室》

第十四章 项目案例《多人聊天室》

案例需求描述

多人聊天室比如微信群、QQ群等就属于多人聊天室项目。多人聊天室项目的特点就是可以拥有多个客户端,每个客户端都有自己的唯一的名称,而且当一个客户端发送数据到聊天室时,整个聊天室中所有成员都可以看到这条数据。

客户端可以有多个,但是服务器端只有一个。一个服务器要处理多个客户端之间的通信就需要使用到多线程。当一个客户端连接服务器成功后,服务器端就会开启一个线程与之通信。

在这里插入图片描述

这里使用第三方库wxPython来绘制聊天室界面。

wxPython:
是Python的第三方库(用于图形化界面的),代码实现基于C++的wxWidgets库封装,呈现的界面风格和系统本地风格一致。

安装方式:

pip install wxpython

使用pip命令安装wxpython:
在这里插入图片描述

项目功能实现分析

该项目代码功能实现:

  1. 启动服务器功能实现
  2. 客户端连接服务器
  3. 显示聊天信息
  4. 发送消息到聊天室
  5. 客户端断开连接
  6. 客户端重置
  7. 保存聊天记录
  8. 停止服务

使用wxPython绘制客户端界面

在这里插入图片描述
在使用wxPython去布局客户端界面时,是一层一层嵌套的,要把按钮放到可伸缩的网格布局中,可伸缩的网格布局又要放到盒子中,盒子放在面板上,面板放在窗体上。

客户端界面client.py:

# 注意:编码格式要加
# coding:utf-8
import wx  # 注意:导入wxPython用的是import wx


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    client = LxlClient('lxl')
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述

现在只显示了界面,具体的功能还未实现。

使用wxPython绘制服务器界面

在这里插入图片描述
在使用wxPython去布局服务器界面时,是一层一层嵌套的,要把按钮放到可伸缩的网格布局中,可伸缩的网格布局又要放到盒子中,盒子放在面板上,面板放在窗体上。

服务器界面server.py:

# 注意:编码格式要加
# coding:utf-8
import wx  # 注意:导入wxPython用的是import wx


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述
现在只显示了界面,具体的功能还未实现。

设置启动服务器的必要属性

server.py:

# 注意:编码格式要加
# coding:utf-8
import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

    def start_server(self, event):  # 绑定事件--->event
        print('启动服务的按钮被点击了')


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述

服务器端启动服务的功能实现

server.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

    def start_server(self, event):  # 绑定事件--->event
        # 判断服务器是否已经启动,只有服务器没有启动时才启动
        if not self.isOn:  # 等价于 self.isOn==False
            # 启动服务器
            self.isOn = True
            # 创建主线程对象,函数式创建主线程
            main_thread = threading.Thread(target=self.do_work)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            main_thread.daemon = True
            # 启动主线程
            main_thread.start()

    def do_work(self):
        # 判断isOn的值
        while self.isOn:
            # 接收客户端的连接请求
            session_socket, client_addr = self.server_socket.accept()
            # 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
            user_name = session_socket.recv(1024).decode('utf-8')

            # 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
            # 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
            session_thread = SessionThread(session_socket, user_name, self)
            # 将会话线程存储到字典中
            self.session_thread_dict[user_name] = session_thread
            # 启动会话线程
            session_thread.start()

        # 当self.isOn的值为False时,关闭socket对象
        self.server_socket.close()


# 服务器端会话线程的类
class SessionThread(threading.Thread):
    def __init__(self, client_socket, user_name, server):
        pass

    def run(self):
        pass


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

服务器端会话线程代码实现

server.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

    def start_server(self, event):  # 绑定事件--->event
        # 判断服务器是否已经启动,只有服务器没有启动时才启动
        if not self.isOn:  # 等价于 self.isOn==False
            # 启动服务器
            self.isOn = True
            # 创建主线程对象,函数式创建主线程
            main_thread = threading.Thread(target=self.do_work)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            main_thread.daemon = True
            # 启动主线程
            main_thread.start()

    def do_work(self):
        # 判断isOn的值
        while self.isOn:
            # 接收客户端的连接请求
            session_socket, client_addr = self.server_socket.accept()
            # 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
            user_name = session_socket.recv(1024).decode('utf-8')

            # 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
            # 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
            session_thread = SessionThread(session_socket, user_name, self)
            # 将会话线程存储到字典中
            self.session_thread_dict[user_name] = session_thread
            # 启动会话线程
            session_thread.start()

        # 当self.isOn的值为False时,关闭socket对象
        self.server_socket.close()


# 服务器端会话线程的类
class SessionThread(threading.Thread):
    def __init__(self, client_socket, user_name, server):
        # 调用父类的初始化方法
        threading.Thread.__init__(self)
        self.client_socket = client_socket
        self.user_name = user_name
        self.server = server
        self.isOn = True  # 会话线程是否启动,当创建SessionThread对象时会话线程启动

    def run(self):
        print(f'客户端:{self.user_name}已经和服务器连接成功,服务器启动一个会话线程')
        while self.isOn:
            # 从客户端接收数据存储到data中
            data = self.client_socket.recv(1024).decode('utf-8')
            # 如果客户端点击断开按钮,先给服务器发送一句话。消息自定义:假定 bye 为自定义的结束词
            if data == 'bye':
                self.isOn = False
            else:
                # 其他聊天信息显示给所有的客户端,包含服务器端也显示
                pass
        # 关闭客户端socket
        self.client_socket.close()


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

客户端连接服务器功能实现

client.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)

        '''--------------------------以上代码是客户端界面的绘制--------------------------'''

        '''--------------------------当鼠标点击“连接”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.connect_to_server, conn_btn)
        # 实例属性的设置
        self.client_name = client_name
        self.isConnected = False  # 存储客户端连接服务器的状态,默认为False-->没连
        self.client_socket = None  # 设置客户端的socket对象为空

    def connect_to_server(self, event):
        print(f'客户端{self.client_name}连接服务器成功')
        # 如果客户端没有连接服务器,则开始连接
        if not self.isConnected:  # 等价于self.isConnected==False
            # TCP编程的步骤
            server_host_port = ('127.0.0.1', 8888)
            # 创建socket对象
            self.client_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
            # 发送连接请求
            self.client_socket.connect(server_host_port)
            # 只要连接成功,发送一条数据--->因为我们服务器端编写的逻辑是一旦连接成功先接收客户端发来的客户端名称(客户端名称作为字典的key进程存储)
            self.client_socket.send(self.client_name.encode('utf-8'))
            # 启动一个线程(函数式创建线程),客户端的线程与服务器的会话线程进行会话
            client_thread = threading.Thread(target=self.recv_data)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            client_thread.daemon = True
            # 修改连接状态
            self.isConnected = True
            # 启动线程
            client_thread.start()

    def recv_data(self):
        pass


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    client = LxlClient('lxl')
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

先点击服务器“启动服务”,再点击客户端“连接”:

在这里插入图片描述

服务器端显示聊天信息的功能实现

server.py:

# 注意:编码格式要加
# coding:utf-8
import threading
import time

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

    def start_server(self, event):  # 绑定事件--->event
        # 判断服务器是否已经启动,只有服务器没有启动时才启动
        if not self.isOn:  # 等价于 self.isOn==False
            # 启动服务器
            self.isOn = True
            # 创建主线程对象,函数式创建主线程
            main_thread = threading.Thread(target=self.do_work)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            main_thread.daemon = True
            # 启动主线程
            main_thread.start()

    def do_work(self):
        # 判断isOn的值
        while self.isOn:
            # 接收客户端的连接请求
            session_socket, client_addr = self.server_socket.accept()
            # 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
            user_name = session_socket.recv(1024).decode('utf-8')

            # 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
            # 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
            session_thread = SessionThread(session_socket, user_name, self)
            # 将会话线程存储到字典中
            self.session_thread_dict[user_name] = session_thread
            # 启动会话线程
            session_thread.start()

            # 输出服务器的提示信息到只读文本框
            self.show_info_and_send_client('服务器通知', f'欢迎{user_name}进入聊天室!',
                                           time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

        # 当self.isOn的值为False时,关闭socket对象
        self.server_socket.close()

    # 显示聊天信息并发送给每个客户端进行显示
    def show_info_and_send_client(self, data_source, data, date_time):  # 信息源,信息,发送时间
        # 字符串拼接操作
        send_data = f'{data_source}:{data}\n时间:{date_time}'
        # 将信息显示到只读文本框中
        self.show_text.AppendText('-' * 40 + '\n' + send_data + '\n')
        # 给每个客户端都发送一次。字典session_thread_dict中{客户端的名称key:会话线程value}。会话线程对象有实例属性client_socket、isOn
        for client in self.session_thread_dict.values():
            # 如果当前的会话是开启状态
            if client.isOn:
                client.client_socket.send(send_data.encode('utf-8'))


# 服务器端会话线程的类
class SessionThread(threading.Thread):
    def __init__(self, client_socket, user_name, server):
        # 调用父类的初始化方法
        threading.Thread.__init__(self)
        self.client_socket = client_socket
        self.user_name = user_name
        self.server = server
        self.isOn = True  # 会话线程是否启动,当创建SessionThread对象时会话线程启动

    def run(self):
        print(f'客户端:{self.user_name}已经和服务器连接成功,服务器启动一个会话线程')
        while self.isOn:
            # 从客户端接收数据存储到data中
            data = self.client_socket.recv(1024).decode('utf-8')
            # 如果客户端点击断开按钮,先给服务器发送一句话。消息自定义:假定 bye 为自定义的结束词
            if data == 'bye':
                self.isOn = False
            else:
                # 其他聊天信息显示给所有的客户端,包含服务器端也显示
                self.server.show_info_and_send_client(self.user_name, data,
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        # 关闭客户端socket
        self.client_socket.close()


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

客户端显示服务器通知的功能实现

client.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)

        '''--------------------------以上代码是客户端界面的绘制--------------------------'''

        '''--------------------------当鼠标点击“连接”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.connect_to_server, conn_btn)
        # 实例属性的设置
        self.client_name = client_name
        self.isConnected = False  # 存储客户端连接服务器的状态,默认为False-->没连
        self.client_socket = None  # 设置客户端的socket对象为空

    def connect_to_server(self, event):
        print(f'客户端{self.client_name}连接服务器成功')
        # 如果客户端没有连接服务器,则开始连接
        if not self.isConnected:  # 等价于self.isConnected==False
            # TCP编程的步骤
            server_host_port = ('127.0.0.1', 8888)
            # 创建socket对象
            self.client_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
            # 发送连接请求
            self.client_socket.connect(server_host_port)
            # 只要连接成功,发送一条数据--->因为我们服务器端编写的逻辑是一旦连接成功先接收客户端发来的客户端名称(客户端名称作为字典的key进程存储)
            self.client_socket.send(self.client_name.encode('utf-8'))
            # 启动一个线程(函数式创建线程),客户端的线程与服务器的会话线程进行会话
            client_thread = threading.Thread(target=self.recv_data)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            client_thread.daemon = True
            # 修改连接状态
            self.isConnected = True
            # 启动线程
            client_thread.start()

    def recv_data(self):
        # 如果是连接状态
        while self.isConnected:
            # 接收来自服务器的数据
            data = self.client_socket.recv(1024).decode('utf-8')
            # 显示到只读文本框中
            self.show_text.AppendText('-' * 40 + '\n' + data + '\n')


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    name = input('请输入客户端名称:')
    client = LxlClient(name)
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述

可以将client.py复制一份命名为client2.py,以此来模拟多个客户端。

但是当运行client2.py时会出现 Process finished with exit code -1073740771 (0xC000041D) 的错误,目前还未找到原因。

客户端发送信息到聊天室

client.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)

        '''--------------------------以上代码是客户端界面的绘制--------------------------'''

        '''--------------------------当鼠标点击“连接”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.connect_to_server, conn_btn)
        # 实例属性的设置
        self.client_name = client_name
        self.isConnected = False  # 存储客户端连接服务器的状态,默认为False-->没连
        self.client_socket = None  # 设置客户端的socket对象为空

        '''--------------------------当鼠标点击“发送”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.send_to_server, send_btn)

    # 向服务器端发送数据
    def send_to_server(self, event):
        # 判断连接状态
        if self.isConnected:
            # 从可写文本框中获取内容
            input_data = self.chat_text.GetValue()
            if input_data != '':
                # 向服务器端发送数据
                self.client_socket.send(input_data.encode('utf-8'))
                # 发完数据之后,清空文本框
                self.chat_text.SetValue('')  # 设置为空字符串就相当于清空了

    def connect_to_server(self, event):
        print(f'客户端{self.client_name}连接服务器成功')
        # 如果客户端没有连接服务器,则开始连接
        if not self.isConnected:  # 等价于self.isConnected==False
            # TCP编程的步骤
            server_host_port = ('127.0.0.1', 8888)
            # 创建socket对象
            self.client_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
            # 发送连接请求
            self.client_socket.connect(server_host_port)
            # 只要连接成功,发送一条数据--->因为我们服务器端编写的逻辑是一旦连接成功先接收客户端发来的客户端名称(客户端名称作为字典的key进程存储)
            self.client_socket.send(self.client_name.encode('utf-8'))
            # 启动一个线程(函数式创建线程),客户端的线程与服务器的会话线程进行会话
            client_thread = threading.Thread(target=self.recv_data)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            client_thread.daemon = True
            # 修改连接状态
            self.isConnected = True
            # 启动线程
            client_thread.start()

    def recv_data(self):
        # 如果是连接状态
        while self.isConnected:
            # 接收来自服务器的数据
            data = self.client_socket.recv(1024).decode('utf-8')
            # 显示到只读文本框中
            self.show_text.AppendText('-' * 40 + '\n' + data + '\n')


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    name = input('请输入客户端名称:')
    client = LxlClient(name)
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述

客户端断开连接

client.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)

        '''--------------------------以上代码是客户端界面的绘制--------------------------'''

        '''--------------------------当鼠标点击“连接”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.connect_to_server, conn_btn)
        # 实例属性的设置
        self.client_name = client_name
        self.isConnected = False  # 存储客户端连接服务器的状态,默认为False-->没连
        self.client_socket = None  # 设置客户端的socket对象为空

        '''--------------------------当鼠标点击“发送”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.send_to_server, send_btn)

        '''--------------------------当鼠标点击“断开”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.dis_conn_server, dis_conn_btn)

    # 与服务器断开连接
    def dis_conn_server(self, event):
        # 发送断开信息--->这里自定义的是“bye”
        self.client_socket.send('bye'.encode('utf-8'))
        # 改变连接状态
        self.isConnected = False

    # 向服务器端发送数据
    def send_to_server(self, event):
        # 判断连接状态
        if self.isConnected:
            # 从可写文本框中获取内容
            input_data = self.chat_text.GetValue()
            if input_data != '':
                # 向服务器端发送数据
                self.client_socket.send(input_data.encode('utf-8'))
                # 发完数据之后,清空文本框
                self.chat_text.SetValue('')  # 设置为空字符串就相当于清空了

    def connect_to_server(self, event):
        print(f'客户端{self.client_name}连接服务器成功')
        # 如果客户端没有连接服务器,则开始连接
        if not self.isConnected:  # 等价于self.isConnected==False
            # TCP编程的步骤
            server_host_port = ('127.0.0.1', 8888)
            # 创建socket对象
            self.client_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
            # 发送连接请求
            self.client_socket.connect(server_host_port)
            # 只要连接成功,发送一条数据--->因为我们服务器端编写的逻辑是一旦连接成功先接收客户端发来的客户端名称(客户端名称作为字典的key进程存储)
            self.client_socket.send(self.client_name.encode('utf-8'))
            # 启动一个线程(函数式创建线程),客户端的线程与服务器的会话线程进行会话
            client_thread = threading.Thread(target=self.recv_data)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            client_thread.daemon = True
            # 修改连接状态
            self.isConnected = True
            # 启动线程
            client_thread.start()

    def recv_data(self):
        # 如果是连接状态
        while self.isConnected:
            # 接收来自服务器的数据
            data = self.client_socket.recv(1024).decode('utf-8')
            # 显示到只读文本框中
            self.show_text.AppendText('-' * 40 + '\n' + data + '\n')


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    name = input('请输入客户端名称:')
    client = LxlClient(name)
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

 
server.py:

# 注意:编码格式要加
# coding:utf-8
import threading
import time

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

    def start_server(self, event):  # 绑定事件--->event
        # 判断服务器是否已经启动,只有服务器没有启动时才启动
        if not self.isOn:  # 等价于 self.isOn==False
            # 启动服务器
            self.isOn = True
            # 创建主线程对象,函数式创建主线程
            main_thread = threading.Thread(target=self.do_work)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            main_thread.daemon = True
            # 启动主线程
            main_thread.start()

    def do_work(self):
        # 判断isOn的值
        while self.isOn:
            # 接收客户端的连接请求
            session_socket, client_addr = self.server_socket.accept()
            # 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
            user_name = session_socket.recv(1024).decode('utf-8')

            # 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
            # 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
            session_thread = SessionThread(session_socket, user_name, self)
            # 将会话线程存储到字典中
            self.session_thread_dict[user_name] = session_thread
            # 启动会话线程
            session_thread.start()

            # 输出服务器的提示信息到只读文本框
            self.show_info_and_send_client('服务器通知', f'欢迎{user_name}进入聊天室!',
                                           time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

        # 当self.isOn的值为False时,关闭socket对象
        self.server_socket.close()

    # 显示聊天信息并发送给每个客户端进行显示
    def show_info_and_send_client(self, data_source, data, date_time):  # 信息源,信息,发送时间
        # 字符串拼接操作
        send_data = f'{data_source}:{data}\n时间:{date_time}'
        # 将信息显示到只读文本框中
        self.show_text.AppendText('-' * 40 + '\n' + send_data + '\n')
        # 给每个客户端都发送一次。字典session_thread_dict中{客户端的名称key:会话线程value}。会话线程对象有实例属性client_socket、isOn
        for client in self.session_thread_dict.values():
            # 如果当前的会话是开启状态
            if client.isOn:
                client.client_socket.send(send_data.encode('utf-8'))


# 服务器端会话线程的类
class SessionThread(threading.Thread):
    def __init__(self, client_socket, user_name, server):
        # 调用父类的初始化方法
        threading.Thread.__init__(self)
        self.client_socket = client_socket
        self.user_name = user_name
        self.server = server
        self.isOn = True  # 会话线程是否启动,当创建SessionThread对象时会话线程启动

    def run(self):
        print(f'客户端:{self.user_name}已经和服务器连接成功,服务器启动一个会话线程')
        while self.isOn:
            # 从客户端接收数据存储到data中
            data = self.client_socket.recv(1024).decode('utf-8')
            # 如果客户端点击断开按钮,先给服务器发送一句话。消息自定义:假定 bye 为自定义的结束词
            if data == 'bye':
                self.isOn = False
                # 发送一条服务器通知
                self.server.show_info_and_send_client('服务器通知', f'{self.user_name}离开聊天室',
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
            else:
                # 其他聊天信息显示给所有的客户端,包含服务器端也显示
                self.server.show_info_and_send_client(self.user_name, data,
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        # 关闭客户端socket
        self.client_socket.close()


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述

客户端重置_服务器端保存聊天记录_断开连接功能实现

点击“重置”按钮,会将可写多行文本框(用于输入聊天内容)的内容清空。
client.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)

        '''--------------------------以上代码是客户端界面的绘制--------------------------'''

        '''--------------------------当鼠标点击“连接”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.connect_to_server, conn_btn)
        # 实例属性的设置
        self.client_name = client_name
        self.isConnected = False  # 存储客户端连接服务器的状态,默认为False-->没连
        self.client_socket = None  # 设置客户端的socket对象为空

        '''--------------------------当鼠标点击“发送”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.send_to_server, send_btn)

        '''--------------------------当鼠标点击“断开”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.dis_conn_server, dis_conn_btn)

        '''--------------------------当鼠标点击“重置”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.reset, reset_btn)

    # 重置--->点击“重置”按钮,会将可写多行文本框(用于输入聊天内容)的内容清空。
    def reset(self, event):
        self.chat_text.Clear()

    # 与服务器断开连接
    def dis_conn_server(self, event):
        # 发送断开信息--->这里自定义的是“bye”
        self.client_socket.send('bye'.encode('utf-8'))
        # 改变连接状态
        self.isConnected = False

    # 向服务器端发送数据
    def send_to_server(self, event):
        # 判断连接状态
        if self.isConnected:
            # 从可写文本框中获取内容
            input_data = self.chat_text.GetValue()
            if input_data != '':
                # 向服务器端发送数据
                self.client_socket.send(input_data.encode('utf-8'))
                # 发完数据之后,清空文本框
                self.chat_text.SetValue('')  # 设置为空字符串就相当于清空了

    def connect_to_server(self, event):
        print(f'客户端{self.client_name}连接服务器成功')
        # 如果客户端没有连接服务器,则开始连接
        if not self.isConnected:  # 等价于self.isConnected==False
            # TCP编程的步骤
            server_host_port = ('127.0.0.1', 8888)
            # 创建socket对象
            self.client_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
            # 发送连接请求
            self.client_socket.connect(server_host_port)
            # 只要连接成功,发送一条数据--->因为我们服务器端编写的逻辑是一旦连接成功先接收客户端发来的客户端名称(客户端名称作为字典的key进程存储)
            self.client_socket.send(self.client_name.encode('utf-8'))
            # 启动一个线程(函数式创建线程),客户端的线程与服务器的会话线程进行会话
            client_thread = threading.Thread(target=self.recv_data)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            client_thread.daemon = True
            # 修改连接状态
            self.isConnected = True
            # 启动线程
            client_thread.start()

    def recv_data(self):
        # 如果是连接状态
        while self.isConnected:
            # 接收来自服务器的数据
            data = self.client_socket.recv(1024).decode('utf-8')
            # 显示到只读文本框中
            self.show_text.AppendText('-' * 40 + '\n' + data + '\n')


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    name = input('请输入客户端名称:')
    client = LxlClient(name)
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

 
server.py:

# 注意:编码格式要加
# coding:utf-8
import threading
import time

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

        '''--------------------------当鼠标点击“保存聊天记录”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.save_record, record_btn)

        '''--------------------------当鼠标点击“停止服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.stop_server, stop_server_btn)

    # 停止服务
    def stop_server(self, event):
        print('服务器已停止服务')
        self.isOn = False

    # 保存聊天记录--->将只读文本框的内容写入文件中
    def save_record(self, event):
        # 获取只读文本框的内容
        record_data = self.show_text.GetValue()
        with open('record.log', 'w', encoding='utf-8') as file:
            file.write(record_data)

    def start_server(self, event):  # 绑定事件--->event
        # 判断服务器是否已经启动,只有服务器没有启动时才启动
        if not self.isOn:  # 等价于 self.isOn==False
            # 启动服务器
            self.isOn = True
            # 创建主线程对象,函数式创建主线程
            main_thread = threading.Thread(target=self.do_work)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            main_thread.daemon = True
            # 启动主线程
            main_thread.start()

    def do_work(self):
        # 判断isOn的值
        while self.isOn:
            # 接收客户端的连接请求
            session_socket, client_addr = self.server_socket.accept()
            # 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
            user_name = session_socket.recv(1024).decode('utf-8')

            # 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
            # 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
            session_thread = SessionThread(session_socket, user_name, self)
            # 将会话线程存储到字典中
            self.session_thread_dict[user_name] = session_thread
            # 启动会话线程
            session_thread.start()

            # 输出服务器的提示信息到只读文本框
            self.show_info_and_send_client('服务器通知', f'欢迎{user_name}进入聊天室!',
                                           time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

        # 当self.isOn的值为False时,关闭socket对象
        self.server_socket.close()

    # 显示聊天信息并发送给每个客户端进行显示
    def show_info_and_send_client(self, data_source, data, date_time):  # 信息源,信息,发送时间
        # 字符串拼接操作
        send_data = f'{data_source}:{data}\n时间:{date_time}'
        # 将信息显示到只读文本框中
        self.show_text.AppendText('-' * 40 + '\n' + send_data + '\n')
        # 给每个客户端都发送一次。字典session_thread_dict中{客户端的名称key:会话线程value}。会话线程对象有实例属性client_socket、isOn
        for client in self.session_thread_dict.values():
            # 如果当前的会话是开启状态
            if client.isOn:
                client.client_socket.send(send_data.encode('utf-8'))


# 服务器端会话线程的类
class SessionThread(threading.Thread):
    def __init__(self, client_socket, user_name, server):
        # 调用父类的初始化方法
        threading.Thread.__init__(self)
        self.client_socket = client_socket
        self.user_name = user_name
        self.server = server
        self.isOn = True  # 会话线程是否启动,当创建SessionThread对象时会话线程启动

    def run(self):
        print(f'客户端:{self.user_name}已经和服务器连接成功,服务器启动一个会话线程')
        while self.isOn:
            # 从客户端接收数据存储到data中
            data = self.client_socket.recv(1024).decode('utf-8')
            # 如果客户端点击断开按钮,先给服务器发送一句话。消息自定义:假定 bye 为自定义的结束词
            if data == 'bye':
                self.isOn = False
                # 发送一条服务器通知
                self.server.show_info_and_send_client('服务器通知', f'{self.user_name}离开聊天室',
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
            else:
                # 其他聊天信息显示给所有的客户端,包含服务器端也显示
                self.server.show_info_and_send_client(self.user_name, data,
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        # 关闭客户端socket
        self.client_socket.close()


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()

在这里插入图片描述
在这里插入图片描述

项目完整代码

client.py:

# 注意:编码格式要加
# coding:utf-8
import threading

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlClient(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self, client_name):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 创建面板对象
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        conn_btn = wx.Button(pl, size=(200, 40), label='连接')
        dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
        fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
        self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
        # (聊天内容文本框)添加到box中
        box.Add(self.chat_text, 1, wx.ALIGN_CENTER)  # 聊天内容文本框在盒子中居中--->ALIGN_CENTER

        # 可伸缩的网格布局
        fgz2 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建两个按钮--->按钮放在面板上,参数填面板pl
        reset_btn = wx.Button(pl, size=(200, 40), label='重置')
        send_btn = wx.Button(pl, size=(200, 40), label='发送')

        # 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
        fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz2, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)

        '''--------------------------以上代码是客户端界面的绘制--------------------------'''

        '''--------------------------当鼠标点击“连接”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.connect_to_server, conn_btn)
        # 实例属性的设置
        self.client_name = client_name
        self.isConnected = False  # 存储客户端连接服务器的状态,默认为False-->没连
        self.client_socket = None  # 设置客户端的socket对象为空

        '''--------------------------当鼠标点击“发送”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.send_to_server, send_btn)

        '''--------------------------当鼠标点击“断开”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.dis_conn_server, dis_conn_btn)

        '''--------------------------当鼠标点击“重置”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.reset, reset_btn)

    # 重置--->点击“重置”按钮,会将可写多行文本框(用于输入聊天内容)的内容清空。
    def reset(self, event):
        self.chat_text.Clear()

    # 与服务器断开连接
    def dis_conn_server(self, event):
        # 发送断开信息--->这里自定义的是“bye”
        self.client_socket.send('bye'.encode('utf-8'))
        # 改变连接状态
        self.isConnected = False

    # 向服务器端发送数据
    def send_to_server(self, event):
        # 判断连接状态
        if self.isConnected:
            # 从可写文本框中获取内容
            input_data = self.chat_text.GetValue()
            if input_data != '':
                # 向服务器端发送数据
                self.client_socket.send(input_data.encode('utf-8'))
                # 发完数据之后,清空文本框
                self.chat_text.SetValue('')  # 设置为空字符串就相当于清空了

    def connect_to_server(self, event):
        print(f'客户端{self.client_name}连接服务器成功')
        # 如果客户端没有连接服务器,则开始连接
        if not self.isConnected:  # 等价于self.isConnected==False
            # TCP编程的步骤
            server_host_port = ('127.0.0.1', 8888)
            # 创建socket对象
            self.client_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
            # 发送连接请求
            self.client_socket.connect(server_host_port)
            # 只要连接成功,发送一条数据--->因为我们服务器端编写的逻辑是一旦连接成功先接收客户端发来的客户端名称(客户端名称作为字典的key进程存储)
            self.client_socket.send(self.client_name.encode('utf-8'))
            # 启动一个线程(函数式创建线程),客户端的线程与服务器的会话线程进行会话
            client_thread = threading.Thread(target=self.recv_data)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            client_thread.daemon = True
            # 修改连接状态
            self.isConnected = True
            # 启动线程
            client_thread.start()

    def recv_data(self):
        # 如果是连接状态
        while self.isConnected:
            # 接收来自服务器的数据
            data = self.client_socket.recv(1024).decode('utf-8')
            # 显示到只读文本框中
            self.show_text.AppendText('-' * 40 + '\n' + data + '\n')


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的客户端界面对象
    name = input('请输入客户端名称:')
    client = LxlClient(name)
    client.Show()  # 可以改成LxlClient('lxl').Show()

    # 循环刷新显示
    app.MainLoop()

 
server.py:

# 注意:编码格式要加
# coding:utf-8
import threading
import time

import wx  # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议


class LxlServer(wx.Frame):  # 绘制窗体界面,因此要继承父类Frame
    def __init__(self):
        # 调用父类的初始化方法绘制窗体
        '''
        None表示没有父级窗口
        id表示当前窗口的编号
        pos:窗体的打开位置,DefaultPosition:默认位置
        size:窗体的大小,单位是像素,400宽,450高
        '''
        wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
                          pos=wx.DefaultPosition, size=(400, 450))

        # 窗口上放一个面板
        pl = wx.Panel(self)

        # 在面板上放置盒子
        box = wx.BoxSizer(wx.VERTICAL)  # 盒子里的内容垂直布局--->VERTICAL

        # 可伸缩的网格布局
        fgz1 = wx.FlexGridSizer(wx.HSCROLL)  # 内部水平布局--->HSCROLL

        # 创建三个按钮--->按钮放在面板上,参数填面板pl
        start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
        record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
        stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')

        # 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
        fgz1.Add(start_server_btn, 1, wx.TOP)
        fgz1.Add(record_btn, 1, wx.TOP)
        fgz1.Add(stop_server_btn, 1, wx.TOP)

        # (可伸缩的网格布局)添加到box中
        box.Add(fgz1, 1, wx.ALIGN_CENTER)  # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER

        # 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
        self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
        # (只读文本框)添加到box中
        box.Add(self.show_text, 1, wx.ALIGN_CENTER)  # 只读文本框在盒子中居中--->ALIGN_CENTER

        # 到这里所有内容都在盒子中了,盒子又在面板中
        # 将盒子放到面板中
        pl.SetSizer(box)
        '''--------------------------以上代码都是界面的绘制代码--------------------------'''

        '''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
        self.isOn = False  # 存储服务器的启动状态,默认值False,默认没有启动
        # 服务器端绑定的IP地址和端口
        self.host_port = ('', 8888)  # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
        # 创建socket对象。(这里采用TCP编程)
        self.server_socket = socket(AF_INET, SOCK_STREAM)  # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
        # 绑定IP地址和端口
        self.server_socket.bind(self.host_port)
        # 监听
        self.server_socket.listen(5)
        # 创建一个字典,存储与客户端对话的会话线程
        self.session_thread_dict = {}  # key-value {客户端的名称key:会话线程value}

        '''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)

        '''--------------------------当鼠标点击“保存聊天记录”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.save_record, record_btn)

        '''--------------------------当鼠标点击“停止服务”按钮时,要执行的操作--------------------------'''
        # Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
        self.Bind(wx.EVT_BUTTON, self.stop_server, stop_server_btn)

    # 停止服务
    def stop_server(self, event):
        print('服务器已停止服务')
        self.isOn = False

    # 保存聊天记录--->将只读文本框的内容写入文件中
    def save_record(self, event):
        # 获取只读文本框的内容
        record_data = self.show_text.GetValue()
        with open('record.log', 'w', encoding='utf-8') as file:
            file.write(record_data)

    def start_server(self, event):  # 绑定事件--->event
        # 判断服务器是否已经启动,只有服务器没有启动时才启动
        if not self.isOn:  # 等价于 self.isOn==False
            # 启动服务器
            self.isOn = True
            # 创建主线程对象,函数式创建主线程
            main_thread = threading.Thread(target=self.do_work)
            # 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
            main_thread.daemon = True
            # 启动主线程
            main_thread.start()

    def do_work(self):
        # 判断isOn的值
        while self.isOn:
            # 接收客户端的连接请求
            session_socket, client_addr = self.server_socket.accept()
            # 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
            user_name = session_socket.recv(1024).decode('utf-8')

            # 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
            # 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
            session_thread = SessionThread(session_socket, user_name, self)
            # 将会话线程存储到字典中
            self.session_thread_dict[user_name] = session_thread
            # 启动会话线程
            session_thread.start()

            # 输出服务器的提示信息到只读文本框
            self.show_info_and_send_client('服务器通知', f'欢迎{user_name}进入聊天室!',
                                           time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

        # 当self.isOn的值为False时,关闭socket对象
        self.server_socket.close()

    # 显示聊天信息并发送给每个客户端进行显示
    def show_info_and_send_client(self, data_source, data, date_time):  # 信息源,信息,发送时间
        # 字符串拼接操作
        send_data = f'{data_source}:{data}\n时间:{date_time}'
        # 将信息显示到只读文本框中
        self.show_text.AppendText('-' * 40 + '\n' + send_data + '\n')
        # 给每个客户端都发送一次。字典session_thread_dict中{客户端的名称key:会话线程value}。会话线程对象有实例属性client_socket、isOn
        for client in self.session_thread_dict.values():
            # 如果当前的会话是开启状态
            if client.isOn:
                client.client_socket.send(send_data.encode('utf-8'))


# 服务器端会话线程的类
class SessionThread(threading.Thread):
    def __init__(self, client_socket, user_name, server):
        # 调用父类的初始化方法
        threading.Thread.__init__(self)
        self.client_socket = client_socket
        self.user_name = user_name
        self.server = server
        self.isOn = True  # 会话线程是否启动,当创建SessionThread对象时会话线程启动

    def run(self):
        print(f'客户端:{self.user_name}已经和服务器连接成功,服务器启动一个会话线程')
        while self.isOn:
            # 从客户端接收数据存储到data中
            data = self.client_socket.recv(1024).decode('utf-8')
            # 如果客户端点击断开按钮,先给服务器发送一句话。消息自定义:假定 bye 为自定义的结束词
            if data == 'bye':
                self.isOn = False
                # 发送一条服务器通知
                self.server.show_info_and_send_client('服务器通知', f'{self.user_name}离开聊天室',
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
            else:
                # 其他聊天信息显示给所有的客户端,包含服务器端也显示
                self.server.show_info_and_send_client(self.user_name, data,
                                                      time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        # 关闭客户端socket
        self.client_socket.close()


if __name__ == '__main__':
    # 初始化App()
    app = wx.App()
    # 创建自己的服务器界面对象
    server = LxlServer()
    server.Show()

    # 循环刷新显示
    app.MainLoop()
  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值