现在的软件开发基本都需要
网络通讯
,程序如何把消息发出,接收方有如何收到消息? 这就涉及到操作系统 提供的socket
编程接口。
# Socket 简介
从一个程序发送消息到另一个程序接收大致需要经过以下过程:
发送信息的应用程序,通过
socket 编程接口
把信息给操作系统的 TCP/IP 协议栈通讯模块;通讯模块一层层传递给 其他通讯模块(网卡驱动等),最后再通过网卡等硬件设备发送到网络上去;
经过 网络上路由器的一次次转发,最终到了 目的程序 所在的 终端设备, 再通过 其操作系统的
TCP/IP 协议栈通讯模块
一层层上传。最后接收信息的程序,通过 socket 编程接口 接收到了 传输的信息。
这个过程可以用下图来表示
Python 中可以使用 requests 库 发送 HTTP 请求消息,requests 库底层也是使用的 socket 编程接口发送 HTTP 请求消息。HTTP 传输的消息 底层也是通过 TCP/IP 协议 传输的, HTTP 加上了一些额外的规定, 比如传输消息的格式。
# TCP Socket
要进行 socket 编程,发送网络消息,我们可以使用 Python 内置的 socket 库 。
目前的 socket 编程,使用的最多的就是通过 TCP 协议进行网络通讯的。
TCP 进行通讯的程序双方,分为服务端和客户端。
TCP 协议进行通讯的双方,是需要先建立一个虚拟连接的。然后双方程序才能发送业务数据信息。
我们现在来看一个 TCP 协议进行通讯的 socket 服务端程序和客户端程序。
# Server
TCP Socket
在服务端先实例化一个 socket 对象,绑定地址和端口后设置监听状态,此时,程序会在 listenSocket.accept ()
阻塞,等待客户端连接。
先启动服务端,再运行客户端。
当客户端调用connect ()
方法时,会发送 TCP 第一次握手(SYN)
服务端listenSocket.accept ()
接收后会进行第二次握手(SYN, ACK)
客户端接收后会进行第三次握手(ACK),此时 TCP 连接建立完成
TCP 连接建立后,服务端
listenSocket.accept ()
会返回元组accept () -> (socket object, address info)
,Socket 是一个新的 socket,用于传输数据,address 是客户端 IP 地址和端口
TCP 连接建立后, 调用新 Socket 的 recv 方法收发数据,此时数据的类型是
字节串(byte)
类型,在解码和解码时时,需要双方明确格式,示例代码中encode ()
,decode ()
不带参数默认utf-8
编码。
服务端的代码示例如下
# 导入 socket 库 | |
from socket import * | |
# 主机地址为空字符串或 0.0.0.0,表示绑定本机所有网络接口 ip 地址 | |
# 等待客户端来连接 | |
IP = '' | |
# 端口号 | |
PORT = 50000 | |
# 定义一次从 socket 缓冲区最多读入 1024 个字节数据 | |
BUFLEN = 1024 | |
# 实例化一个 socket 对象 | |
# 参数 AF_INET 表示该 socket 网络层使用 IP 协议 | |
# 参数 SOCK_STREAM 表示该 socket 传输层使用 TCP 协议 | |
listenSocket = socket (AF_INET, SOCK_STREAM) | |
# socket 绑定地址和端口 | |
listenSocket.bind ((IP, PORT)) | |
# 使 socket 处于监听状态,等待客户端的连接请求 | |
# 参数 8 表示 最多接受 8 个等待连接的客户端 | |
listenSocket.listen (8) | |
print (f' 服务端启动成功,在 {PORT} 端口等待客户端连接...') | |
dataSocket, addr = listenSocket.accept () | |
print (' 接受一个客户端连接:', addr) | |
while True: | |
# 尝试读取对方发送的消息 | |
# BUFLEN 指定从接收缓冲里最多读取多少字节 | |
recved = dataSocket.recv (BUFLEN) | |
# 如果返回空 bytes,表示对方关闭了连接 | |
# 退出循环,结束消息收发 | |
if not recved: | |
break | |
# 读取的字节数据是 bytes 类型,需要解码为字符串 | |
info = recved.decode () | |
print (f' 收到对方信息: {info}') | |
# 发送的数据类型必须是 bytes,所以要编码 | |
dataSocket.send (f' 服务端接收到了信息 {info}'.encode ()) | |
# 服务端也调用 close () 关闭 socket | |
dataSocket.close () | |
listenSocket.close () |
# Client
TCP Socket
在客户端先实例化一个 socket 对象,直接调用 connect 方法连接指定的服务端 IP 和端口。
客户端的代码示例如下
from socket import * | |
IP = '127.0.0.1' | |
SERVER_PORT = 50000 | |
BUFLEN = 1024 | |
# 实例化一个 socket 对象,指明协议 | |
dataSocket = socket (AF_INET, SOCK_STREAM) | |
# 连接服务端 socket | |
dataSocket.connect ((IP, SERVER_PORT)) | |
while True: | |
# 从终端读入用户输入的字符串 | |
toSend = input ('>>> ') | |
if toSend =='exit': | |
break | |
# 发送消息,也要编码为 bytes | |
dataSocket.send (toSend.encode ()) | |
# 等待接收服务端的消息 | |
recved = dataSocket.recv (BUFLEN) | |
# 如果返回空 bytes,表示对方关闭了连接 | |
if not recved: | |
break | |
# 打印读取的信息 | |
print (recved.decode ()) | |
dataSocket.close () |
运行代码后你会发现,如果将服务端的固定回复改为 input,此时服务端只能在客户端回应后发一条消息,要解决这个问题,就需要使用多线程。
# 案例
# TCP 聊天器
# UI 部分
在实现 TCP Socket
发送消息的逻辑之前,我们先用 PySide 写一个简单的消息发送界面。
下面这段代码实现了一个简易的发送 UI,检测到用户发送信息后,将信息内容显示在 富文本框中,现在,我们只需要完善函数 sendMessage
中的消息发送操作和 receiveMessage
中的消息接收操作即可。
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, | |
QHBoxLayout, QLineEdit, QTextEdit, | |
QPushButton) | |
from datetime import datetime | |
class MainWindow (QWidget): | |
def __init__(self): | |
super ().__init__() | |
self.resize (500, 400) | |
self.setup_ui () | |
self.Initialization () | |
self.bind () | |
def Initialization (self): | |
self.uname = ' 用户 1' | |
def bind (self): | |
self.sendButton.clicked.connect (self.sendMessage) | |
self.lineEdit.returnPressed.connect (self.sendMessage) | |
def setup_ui (self): | |
self.mainLayout = QVBoxLayout () | |
self.textEdit = QTextEdit () | |
self.textEdit.setReadOnly (True) | |
self.mainLayout.addWidget (self.textEdit) | |
self.sendLayout = QHBoxLayout () | |
self.lineEdit = QLineEdit () | |
self.lineEdit.setPlaceholderText ("请输入内容...") | |
self.sendButton = QPushButton ("发送") | |
self.sendLayout.addWidget (self.lineEdit) | |
self.sendLayout.addWidget (self.sendButton) | |
self.mainLayout.addLayout (self.sendLayout) | |
self.setLayout (self.mainLayout) | |
# 发送消息 | |
def sendMessage (self): | |
Message = self.lineEdit.text () | |
if not Message: | |
return | |
self.lineEdit.clear () | |
self.RefreshTextEdit (uname=self.uname, Message=Message) | |
# socket 发送 | |
# 接收消息 | |
def receiveMessage (self): | |
# socker 接收 | |
pass | |
# 刷新消息框 | |
def RefreshTextEdit (self, uname, Message): | |
current_time = datetime.now ().time () | |
formatted_time = current_time.strftime ('% H:% M:% S') | |
newMessage = f"[{formatted_time}]{uname}: {Message}" | |
self.textEdit.append (newMessage) | |
if __name__ == '__main__': | |
app = QApplication () | |
Window = MainWindow () | |
Window.show () | |
app.exec () |
# UDP 聊天器
使用 UDP 实现一个简易的聊天器,可在本地运行两个代码或者在局域网的两台终端运行,本地运行需要修改 port 确保两端代码使用端口不同
import socket | |
import threading | |
def recv_msg (udp_socket): | |
"""接收数据并显示""" | |
while True: | |
recv_data = udp_socket.recvfrom (1024) | |
print (recv_data) | |
def send_msg (udp_socket, dest_ip, dest_port): | |
"""发送数据""" | |
while True: | |
send_data = input (">>>") | |
udp_socket.sendto (send_data.encode ("utf-8"), (dest_ip, dest_port)) | |
def main (): | |
"""""UDP 聊天器""" | |
# 创建套接字 | |
udp_socket = socket.socket (socket.AF_INET, socket.SOCK_DGRAM) | |
# 绑定本地信息 | |
udp_socket.bind (("0.0.0.0",7890)) | |
# 获取对方 ip | |
dest_ip = input ("请输入对方 ip:") | |
dest_port = int (input ("请输入对方的 port:")) | |
# 创建两个线程 | |
t_recv = threading.Thread (target=recv_msg, args=(udp_socket,)) | |
t_send = threading.Thread (target=send_msg, args=(udp_socket, dest_ip, dest_port)) | |
t_recv.start () | |
t_send.start () | |
if __name__ == "__main__": | |
main () |