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服务实现

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服务实现

# 全双工
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
    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进程守护
	# 窗口初始化
    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('连接已关闭')

完整代码
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的显示。

完整代码

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


我的一键三连去哪儿了

---------------------------END---------------------------

题外话

在这里插入图片描述

感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。

👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

img

二、Python兼职渠道推荐*

学的同时助你创收,每天花1-2小时兼职,轻松稿定生活费.
在这里插入图片描述

三、最新Python学习笔记

当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

img

四、实战案例

纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

img

👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)

若有侵权,请联系删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值