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()