Python搓一个单用户的C/S全双工聊天室

Python搓一个单用户的C/S全双工聊天室

利用socket库,搓一个单用户全双工的聊天室,GUI使用PyQt6,Python版本3.10,面相对象编程方式

socket服务端编写

单工,半双工和全双工是通讯技术中的术语,指的是在通讯过程中,两端设备的信息传输方式不同。

  • 单工(单向通信):只有一端设备可以发送信息,另一端只能接收信息,两端不能同时发送和接收信息。 如:BB机、收音机
  • 半双工(半双向通信):两端设备同时可以发送和接收信息,但不能同时发送。两端各自占有通讯频道,在不同的时间段内交替发送和接收信息。如:对讲机、发报机
  • 全双工(全双向通信):两端设备同时可以发送和接收信息,并且可以同时发送。两端设备可以同时占有通讯频道,并且同时进行信息的传输
    • QQ、微信不同的通讯方式适用于不同的场景,例如电话通话属于半双工通信,而网络数据传输通常属于全双工通信。在选择通讯技术时,需要根据具体应用场景,考虑通讯的实际需求,以选择最合适的通讯方式

实现思想也比较简单:

  1. 服务端
    1. 创建socket对象
    2. 服务端创建连接监听
    3. 服务端收发数据
    4. 关闭连接
  2. 客户端
    1. 创建socket对象
    2. 客户端连接
    3. 客户端收发数据
    4. 关闭连接

注意:服务端连接监听时会阻断进程/线程,在具体实现时使用线程隔离监听程序并设置线程守护

server服务实现

js 复制代码
import socket


class server_chat():
    # 全局变量
    connection = socket.socket
    address = ()

    def init(self):
        # 实例socket对象
        # socket.AF_INET ==》网络间通信
        # socket.SOCK_STREAM ==》tcp连接/流式传输
        # socket.SOCK_DGRAM ==》udp连接/报式传输
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # server服务创建
        self.server_socket.bind(('192.168.112.19', 7777))
        # 客户端连接监听
        self.server_socket.listen()
        print('等待连接')
        # 等待连接会阻塞进程/线程
        # 接收连接并传递返回值
        # connection ==》网络连接
        # address ==》客户端ip
        self.connection, self.address = self.server_socket.accept()
        print('连接成功')

    # server接收数据
    def receive_data(self):
        # recv() 接收client数据
        # 1024 ==》接收数据最大长度
        # decode("utf-8") ==》按utf-8解码数据
        data = self.connection.recv(1024).decode("utf-8")
        if len(data) != 0:
            return data

    # server发送数据
    def send_data(self, data=''):
        # 过滤无效字符
        if data == '' or data == None or len(data) == 0:
            pass
        else:
            # sendall() 发送数据
            # data.encode("utf-8") ==》按utf-8编码转换数据
            self.connection.sendall(data.encode("utf-8"))

    # 关闭连接方法
    def close_socket(self):
        self.connection.close()

client服务实现

js 复制代码
# 全双工
import socket


class client_chat():
    # 全局变量
    client_socket = socket.socket

    def init(self):
        # 实例socket对象
        self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 发起客户端连接
        self.client_socket.connect(('192.168.112.19', 7777))

    # client接收数据
    def receive_data(self):
        data = self.client_socket.recv(1024).decode("utf-8")
        if len(data) != 0:
            return data

    # client发送数据
    def send_data(self, s=''):
        if s == '' or s == None or len(s) == 0:
            pass
        else:
            data = s
            self.client_socket.sendall(data.encode("utf-8"))

    # 关闭连接
    def close_socket(self):
        self.client_socket.close()

GUI实现

使用PyQt6作为GUI,server和client界面分离。

serverUI

  • 由于server服务监听时,会阻断进程/线程,在socket连接后,数据接收需要使用循环进行监听,也会阻断阻断进程/线程。循环的目的是为了能够随时接收client的消息,达到双全工的效果。

在程序编写的过程中,消息显示使用QTextedit,在利用类函数调用QTextedit的setPlainText方法时会杀死进程,我的解决办法是使用insertPlainText方法在QTextedit内进行插入

初始化GUI

js 复制代码
    def initUi(self):
        # 组件相关================================================================
        # 大标题
        lab_title = QLabel('Socket Chat聊天室', self)  # 实例化标签
        lab_title.resize(320, 160)  # 设置标签大小
        lab_title.setFont(QFont('Arial', 32, QFont.Weight.Bold))  # 设置字体字号
        lab_title.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 文本在标签内居中
        # 输入框
        lab_text = QLabel('请输入server发送的内容:')  # 实例化标签
        self.text = QLineEdit()  # 单行文本编辑框
        self.text.setAlignment(Qt.AlignmentFlag.AlignLeft)  # 文本在标签内靠左
        # 显示文本框
        self.showtext = QTextEdit()
        self.showtext.setFixedHeight(420)
        # 设置初始内容为空
        self.showtext.setPlainText(self.show_content)
        # 编辑框只读
        self.showtext.setReadOnly(True)
        # 按钮
        btn_server_send = QPushButton('发送')  # 按钮
        self.btn_close = QPushButton('断开连接')

        # 布局相关================================================================
        # 使用水平布局管理器布局lab_acc控件和account控件,左右留白10像素
        hbox_server = QHBoxLayout()  # 水平布局管理器
        hbox_server.addSpacing(10)  # 水平布局间隔
        hbox_server.addWidget(lab_text)  # 将实例化标签放入
        hbox_server.addWidget(self.text)
        hbox_server.addWidget(btn_server_send)  # 将实例化标签放入
        hbox_server.addWidget(self.btn_close)
        hbox_server.addSpacing(10)
        # 使用垂直布局管理器布局上面3个水平布局管理器
        vbox = QVBoxLayout()
        vbox.addSpacing(10)
        vbox.addWidget(lab_title)
        vbox.addSpacing(5)
        vbox.addLayout(hbox_server)
        vbox.addSpacing(5)
        vbox.addWidget(self.showtext)
        vbox.addStretch(1)
        vbox.addSpacing(10)
        # 将垂直布局管理器应用到窗口
        self.setLayout(vbox)
        # 按钮事件,关联方法不加()不然会直接执行
        btn_server_send.clicked.connect(self.server_send)
        self.btn_close.clicked.connect(self.close_socket)

socket相关函数

  • 初始化socket需要调用上述编写的server服务,在调用server服务类时,由于连接等待会阻断主进程而妨碍UI的显示,所以借助线程分支进行隔离,并设置daemon进程守护
js 复制代码
	# 窗口初始化
    def init(self):
        self.connection = socket.socket
        self.address = ()
        self.setWindowTitle('Server Chat聊天室')  # 设置窗口标题
        self.setGeometry(50, 50, 600, 600)  # 设置窗位置和大小 wx, wy, width, height
        self.initUi()  # 初始化界面
        self.show()  # 显示窗口
        # 一层隔离连接等待
        threading.Thread(target=self.initSocket, daemon=True).start()

	# 初始化socket服务
    def initSocket(self):
        self.serverChat = server_chat()
        self.serverChat.init()
        # 二层隔离消息接收
        threading.Thread(target=self.recv, daemon=True).start()

	# 消息发送
    def server_send(self):
        """点击按钮触发Send和显示事件"""
        try:
            # QTextEdit文本需要转存,QLineEdit文本直接使用方法
            # 拼接新内容
            self.show_content = ''
            self.show_content = 'server发送的消息: ' + self.text.text() + '\n'
            # 重新设置文本内容
            self.showtext.insertPlainText(self.show_content)
            # 发送数据
            self.serverChat.send_data(str(self.text.text()))
            # 置空发送编辑框
            self.text.setText('')
        except Exception as e:
            # e.errno ==》异常编号 int类型
            if e.errno == 10038 or e.errno == 10054 or e.errno == 10053:
                self.showtext.insertPlainText('连接已关闭')

    # 接收数据
    def recv(self):
        """接收Socket消息"""
        while True:
            # 循环调用server中的方法接收client数据
            data = self.serverChat.receive_data()
            if len(data) != 0:
                # 接收client数据,判断连接状态
                if data == '连接已关闭':
                    self.showtext.insertPlainText('连接已关闭')
                else:
                    # 拼接新内容
                    self.show_content = ''
                    self.show_content = 'client发送的消息: ' + data + '\n'
                    # 重新设置文本内容
                    self.showtext.insertPlainText(self.show_content)

    def close_socket(self):
        try:
            # 关闭连接前发送数据告知client
            self.serverChat.send_data('连接已关闭')
            # 关闭连接
            self.serverChat.close_socket()
            self.showtext.insertPlainText('连接已关闭')
        except Exception as e:
            # e.errno ==》异常编号 int类型
            if e.errno == 10038 or e.errno == 10054 or e.errno == 10053:
                self.showtext.insertPlainText('连接已关闭')

完整代码

js 复制代码
import socket
import sys
import threading

from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QTextEdit
from PyQt6.QtGui import QFont
from PyQt6.QtCore import Qt

from Socket.SocketServer import server_chat


class SocketServerChat(QWidget):
    show_content = ''

    # 窗口初始化
    def init(self):
        self.connection = socket.socket
        self.address = ()
        self.setWindowTitle('Server Chat聊天室')  # 设置窗口标题
        self.setGeometry(50, 50, 600, 600)  # 设置窗位置和大小 wx, wy, width, height
        self.initUi()  # 初始化界面
        self.show()  # 显示窗口
        # 一层隔离连接等待
        threading.Thread(target=self.initSocket, daemon=True).start()

    # 初始化socket服务
    def initSocket(self):
        self.serverChat = server_chat()
        self.serverChat.init()
        # 二层隔离消息接收
        threading.Thread(target=self.recv, daemon=True).start()

    # 初始化UI
    def initUi(self):
        # 组件相关================================================================
        # 大标题
        lab_title = QLabel('Socket Chat聊天室', self)  # 实例化标签
        lab_title.resize(320, 160)  # 设置标签大小
        lab_title.setFont(QFont('Arial', 32, QFont.Weight.Bold))  # 设置字体字号
        lab_title.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 文本在标签内居中
        # 输入框
        lab_text = QLabel('请输入server发送的内容:')  # 实例化标签
        self.text = QLineEdit()  # 单行文本编辑框
        self.text.setAlignment(Qt.AlignmentFlag.AlignLeft)  # 文本在标签内靠左
        # 显示文本框
        self.showtext = QTextEdit()
        self.showtext.setFixedHeight(420)
        # 设置初始内容为空
        self.showtext.setPlainText(self.show_content)
        # 编辑框只读
        self.showtext.setReadOnly(True)
        # 按钮
        btn_server_send = QPushButton('发送')  # 按钮
        self.btn_close = QPushButton('断开连接')

        # 布局相关================================================================
        # 使用水平布局管理器布局,左右留白10像素
        hbox_server = QHBoxLayout()  # 水平布局管理器
        hbox_server.addSpacing(10)  # 水平布局间隔
        hbox_server.addWidget(lab_text)  # 将实例化标签放入
        hbox_server.addWidget(self.text)
        hbox_server.addWidget(btn_server_send)  # 将实例化标签放入
        hbox_server.addWidget(self.btn_close)
        hbox_server.addSpacing(10)
        # 使用垂直布局管理器布局水平布局管理器
        vbox = QVBoxLayout()
        vbox.addSpacing(10)
        vbox.addWidget(lab_title)
        vbox.addSpacing(5)
        vbox.addLayout(hbox_server)
        vbox.addSpacing(5)
        vbox.addWidget(self.showtext)
        vbox.addStretch(1)
        vbox.addSpacing(10)
        # 将垂直布局管理器应用到窗口
        self.setLayout(vbox)
        # 按钮事件,关联方法不加()不然会直接执行
        btn_server_send.clicked.connect(self.server_send)
        self.btn_close.clicked.connect(self.close_socket)

    # Socket相关

    # 消息发送
    def server_send(self):
        """点击按钮触发Send和显示事件"""
        try:
            # QTextEdit文本需要转存,QLineEdit文本直接使用方法
            # 拼接新内容
            self.show_content = ''
            self.show_content = 'server发送的消息: ' + self.text.text() + '\n'
            # 重新设置文本内容
            self.showtext.insertPlainText(self.show_content)
            # 发送数据
            self.serverChat.send_data(str(self.text.text()))
            # 置空发送编辑框
            self.text.setText('')
        except Exception as e:
            # e.errno ==》异常编号 int类型
            if e.errno == 10038 or e.errno == 10054 or e.errno == 10053:
                self.showtext.insertPlainText('连接已关闭')

    # 接收数据
    def recv(self):
        """接收Socket消息"""
        while True:
            # 循环调用server中的方法接收client数据
            data = self.serverChat.receive_data()
            if len(data) != 0:
                # 接收client数据,判断连接状态
                if data == '连接已关闭':
                    self.showtext.insertPlainText('连接已关闭')
                else:
                    # 拼接新内容
                    self.show_content = ''
                    self.show_content = 'client发送的消息: ' + data + '\n'
                    # 重新设置文本内容
                    self.showtext.insertPlainText(self.show_content)

    def close_socket(self):
        try:
            # 关闭连接前发送数据告知client
            self.serverChat.send_data('连接已关闭')
            # 关闭连接
            self.serverChat.close_socket()
            self.showtext.insertPlainText('连接已关闭')
        except Exception as e:
            # e.errno ==》异常编号 int类型
            if e.errno == 10038 or e.errno == 10054 or e.errno == 10053:
                self.showtext.insertPlainText('连接已关闭')


if __name__ == '__main__':
    app = QApplication(sys.argv)  # 创建应用程序,接收来自命令行的参数列表,此处并无命令行输入
    win = SocketServerChat()  # 创建窗口,这里初始化的时候不需要QWidget入参
    win.init()  # 初始化窗口
    sys.exit(app.exec())  # 应用程序主循环结束后,调用sys.exit()方法清理现场

clientUI

client的UI实现与server相似,调用socket的client服务,同样是使用线程进行等待隔离和接收隔离,以保证UI的显示。

完整代码

js 复制代码
import socket
import sys
import threading

from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QTextEdit
from PyQt6.QtGui import QFont
from PyQt6.QtCore import Qt

from Socket.SocketClient import client_chat


class SocketClientChat(QWidget):
    show_content = ''
    _recv_data = ''
    old = 'old'

    # 窗口初始化
    def init(self):
        self.connection = socket.socket
        self.address = ()
        self.setWindowTitle('Client Chat聊天室')  # 设置窗口标题
        self.setGeometry(650, 50, 600, 600)  # 设置窗位置和大小 wx, wy, width, height
        self.initUi()  # 初始化界面
        self.show()  # 显示窗口
        # 一层隔离连接
        threading.Thread(target=self.initSocket, daemon=True).start()

    # 初始化client连接
    def initSocket(self):
        self.clientChat = client_chat()
        self.clientChat.init()
        # 二层隔离消息接收
        threading.Thread(target=self.recv, daemon=True).start()

    def initUi(self):
        # 组件相关================================================================
        # 大标题
        lab_title = QLabel('Socket Chat聊天室', self)  # 实例化标签
        lab_title.resize(320, 160)  # 设置标签大小
        lab_title.setFont(QFont('Arial', 32, QFont.Weight.Bold))  # 设置字体字号
        lab_title.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 文本在标签内居中
        # 输入框
        lab_text = QLabel('请输入client发送的内容:')  # 实例化标签
        self.text = QLineEdit()  # 单行文本编辑框
        self.text.setAlignment(Qt.AlignmentFlag.AlignLeft)  # 文本在标签内靠左
        # 显示文本框
        self.showtext = QTextEdit()
        self.showtext.setFixedHeight(420)
        # 设置初始内容为空
        self.showtext.setPlainText(self.show_content)
        # 编辑框只读
        self.showtext.setReadOnly(True)
        # 按钮
        btn_client_send = QPushButton('发送')  # 按钮
        self.btn_close = QPushButton('断开连接')

        # 布局相关================================================================
        # 使用水平布局管理器布局,左右留白10像素
        hbox_server = QHBoxLayout()  # 水平布局管理器
        hbox_server.addSpacing(10)  # 水平布局间隔
        hbox_server.addWidget(lab_text)  # 将实例化标签放入
        hbox_server.addWidget(self.text)
        hbox_server.addWidget(btn_client_send)  # 将实例化标签放入
        hbox_server.addWidget(self.btn_close)  # 将实例化标签放入
        hbox_server.addSpacing(10)
        # 使用垂直布局管理器布局水平布局管理器
        vbox = QVBoxLayout()
        vbox.addSpacing(10)
        vbox.addWidget(lab_title)
        vbox.addSpacing(5)
        vbox.addLayout(hbox_server)
        vbox.addSpacing(5)
        vbox.addWidget(self.showtext)
        vbox.addStretch(1)
        vbox.addSpacing(10)
        # 将垂直布局管理器应用到窗口
        self.setLayout(vbox)
        # 按钮事件,关联方法不加()不然会直接执行
        btn_client_send.clicked.connect(self.client_send)
        self.btn_close.clicked.connect(self.close_socket)

    # Socket相关

    # 消息发送
    def client_send(self):
        """点击按钮触发Send和显示事件"""
        try:
            # QTextEdit文本需要转存,QLineEdit文本直接使用方法
            # 拼接新内容
            self.show_content = ''
            self.show_content = 'client发送的消息: ' + self.text.text() + '\n'
            # 重新设置文本内容
            self.showtext.insertPlainText(self.show_content)
            # 发送数据
            self.clientChat.send_data(str(self.text.text()))
            # 置空发送编辑框
            self.text.setText('')
        except Exception as e:
            # e.errno ==》异常编号 int类型
            if e.errno == 10038 or e.errno == 10054 or e.errno == 10053:
                self.showtext.insertPlainText('连接已关闭')

    # 接收数据
    def recv(self):
        """接收Socket消息"""
        while True:
            res = self.clientChat.receive_data()
            if len(res) != 0:
                if res == '连接已关闭':
                    self.showtext.insertPlainText('连接已关闭')
                else:
                    self.show_content = ''
                    self.show_content = 'server发送的消息: ' + res + '\n'
                    self.showtext.insertPlainText(self.show_content)

    def close_socket(self):
        try:
            self.clientChat.send_data('连接已关闭')
            self.clientChat.close_socket()
            self.showtext.setPlainText('连接已关闭')
        except Exception as e:
            # e.errno ==》异常编号 int类型
            if e.errno == 10038 or e.errno == 10054 or e.errno == 10053:
                self.showtext.insertPlainText('连接已关闭')


if __name__ == '__main__':
    app = QApplication(sys.argv)  # 创建应用程序,接收来自命令行的参数列表,此处并无命令行输入
    win = SocketClientChat()  # 创建窗口,这里初始化的时候不需要QWidget入参
    win.init()  # 初始化窗口
    sys.exit(app.exec())  # 应用程序主循环结束后,调用sys.exit()方法清理现场

我的一键三连去哪儿了

相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境2 小时前
提升 Python 代码健壮性的方法大盘点
后端·python