Python搓一个单用户的C/S全双工聊天室
利用socket库,搓一个单用户全双工的聊天室,GUI使用PyQt6,Python版本3.10,面相对象编程方式
socket服务端编写
单工,半双工和全双工是通讯技术中的术语,指的是在通讯过程中,两端设备的信息传输方式不同。
- 单工(单向通信):只有一端设备可以发送信息,另一端只能接收信息,两端不能同时发送和接收信息。 如:BB机、收音机
- 半双工(半双向通信):两端设备同时可以发送和接收信息,但不能同时发送。两端各自占有通讯频道,在不同的时间段内交替发送和接收信息。如:对讲机、发报机
- 全双工(全双向通信):两端设备同时可以发送和接收信息,并且可以同时发送。两端设备可以同时占有通讯频道,并且同时进行信息的传输
- QQ、微信不同的通讯方式适用于不同的场景,例如电话通话属于半双工通信,而网络数据传输通常属于全双工通信。在选择通讯技术时,需要根据具体应用场景,考虑通讯的实际需求,以选择最合适的通讯方式
实现思想也比较简单:
- 服务端
- 创建socket对象
- 服务端创建连接监听
- 服务端收发数据
- 关闭连接
- 客户端
- 创建socket对象
- 客户端连接
- 客户端收发数据
- 关闭连接
注意:服务端连接监听时会阻断进程/线程,在具体实现时使用线程隔离监听程序并设置线程守护
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()方法清理现场
我的一键三连去哪儿了