Python搓一个单用户的C/S全双工聊天室
利用socket库,搓一个单用户全双工的聊天室,GUI使用PyQt6,Python版本3.10,面相对象编程方式
socket服务端编写
单工,半双工和全双工是通讯技术中的术语,指的是在通讯过程中,两端设备的信息传输方式不同。
- 单工(单向通信):只有一端设备可以发送信息,另一端只能接收信息,两端不能同时发送和接收信息。 如:BB机、收音机
- 半双工(半双向通信):两端设备同时可以发送和接收信息,但不能同时发送。两端各自占有通讯频道,在不同的时间段内交替发送和接收信息。如:对讲机、发报机
- 全双工(全双向通信):两端设备同时可以发送和接收信息,并且可以同时发送。两端设备可以同时占有通讯频道,并且同时进行信息的传输
- QQ、微信不同的通讯方式适用于不同的场景,例如电话通话属于半双工通信,而网络数据传输通常属于全双工通信。在选择通讯技术时,需要根据具体应用场景,考虑通讯的实际需求,以选择最合适的通讯方式
实现思想也比较简单:
- 服务端
- 创建socket对象
- 服务端创建连接监听
- 服务端收发数据
- 关闭连接
- 客户端
- 创建socket对象
- 客户端连接
- 客户端收发数据
- 关闭连接
注意:服务端连接监听时会阻断进程/线程,在具体实现时使用线程隔离监听程序并设置线程守护
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()方法清理现场
我的一键三连去哪儿了
题外话
感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。
👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python兼职渠道推荐*
学的同时助你创收,每天花1-2小时兼职,轻松稿定生活费.
三、最新Python学习笔记
当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。
四、实战案例
纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)
若有侵权,请联系删除