现在的软件开发基本都需要 网络通讯 ,程序如何把消息发出,接收方有如何收到消息? 这就涉及到操作系统 提供的 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 ()
更新于 阅读次数