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()方法清理现场

我的一键三连去哪儿了

相关推荐
假装我不帅23 分钟前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹26 分钟前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田1 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术1 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱2 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19802 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi2 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇3 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生4 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料4 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理