MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python使用Socket进行数据传输

2023-09-094.7k 阅读

一、Socket 基础概念

(一)什么是 Socket

Socket 中文常称为“套接字”,它是计算机网络中进程间通信(Inter - Process Communication,IPC)的一种机制,主要用于不同主机或同一主机上不同进程之间的数据传输。从本质上来说,Socket 是应用层与传输层协议之间的一个抽象层,它为应用程序提供了一种通用的接口,使得应用程序可以方便地使用网络协议进行通信,而不必关心底层网络协议的具体细节。

Socket 起源于 Unix 操作系统,在 Unix 一切皆文件的理念下,Socket 也被视为一种特殊的文件描述符。通过对这个文件描述符的读写操作,就可以实现网络数据的收发。在网络通信中,Socket 提供了一种端点的概念,通信的两端各自都有一个 Socket,数据通过网络在这两个 Socket 之间进行传输。

(二)Socket 通信原理

Socket 通信基于客户 - 服务器(Client - Server)模型。在这个模型中,服务器端程序首先创建一个 Socket,并将其绑定到一个特定的地址(通常是 IP 地址和端口号)上,然后开始监听该地址,等待客户端的连接请求。客户端程序同样创建一个 Socket,然后使用该 Socket 向服务器端的地址发起连接请求。当服务器端接收到客户端的连接请求后,会接受这个连接,从而在客户端和服务器端之间建立起一条通信链路。

一旦连接建立,双方就可以通过各自的 Socket 进行数据的发送和接收。在数据传输过程中,Socket 会根据所使用的传输层协议(如 TCP 或 UDP)来处理数据的打包、传输、校验以及重传等操作。例如,TCP 协议提供可靠的面向连接的字节流服务,会保证数据按顺序、无差错地到达对方;而 UDP 协议则提供不可靠的无连接的数据报服务,数据传输速度快但不保证数据的准确性和顺序性。

(三)Socket 类型

  1. TCP Socket(SOCK_STREAM):基于传输控制协议(TCP),提供可靠的、面向连接的字节流传输服务。在数据传输前,需要先在客户端和服务器端之间建立连接,就像打电话一样,只有双方接通后才能进行通话。连接建立后,数据会按照顺序、无差错地传输。如果数据在传输过程中出现丢失或错误,TCP 协议会自动重传,确保数据的完整性。由于其可靠性,TCP Socket 常用于对数据准确性和顺序要求较高的应用场景,如文件传输、远程登录、网页浏览等。
  2. UDP Socket(SOCK_DGRAM):基于用户数据报协议(UDP),提供不可靠的、无连接的数据报传输服务。与 TCP 不同,UDP 在发送数据前不需要建立连接,就像寄信一样,直接把信件投进邮箱,不关心对方是否收到。UDP 数据报的大小是有限制的,且不保证数据按顺序到达,也不提供数据重传机制。虽然 UDP 不可靠,但它的优点是传输速度快、开销小,适用于对实时性要求较高而对数据准确性要求相对较低的应用场景,如视频流传输、音频流传输、实时游戏等。

二、Python 中的 Socket 模块

(一)模块介绍

在 Python 中,通过内置的 socket 模块来实现 Socket 编程。socket 模块提供了一套丰富的函数和类,用于创建、管理和使用 Socket。使用 socket 模块,开发者可以轻松地编写基于 TCP 或 UDP 协议的网络应用程序。

(二)常用函数与类

  1. socket.socket():用于创建一个新的 Socket 对象。其语法为:socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)。其中,family 参数指定地址族,常见的有 AF_INET(用于 IPv4 地址)和 AF_INET6(用于 IPv6 地址);type 参数指定 Socket 类型,如 SOCK_STREAM(TCP 类型)或 SOCK_DUDP(UDP 类型);proto 参数通常设置为 0,表示使用默认协议;fileno 参数如果指定,则使用给定的文件描述符创建 Socket,否则创建一个新的 Socket。
  2. Socket 对象的方法
    • bind(address):将 Socket 绑定到指定的地址。对于 IPv4 地址,address 是一个包含 IP 地址和端口号的元组,例如 ('127.0.0.1', 8888)
    • listen(backlog):开始监听传入的连接请求。backlog 参数指定在拒绝连接之前,操作系统可以挂起的最大连接数量。
    • accept():接受一个连接。该方法会阻塞,直到有客户端连接到来,返回一个新的 Socket 对象(用于与客户端通信)和客户端的地址。
    • connect(address):客户端使用此方法连接到服务器指定的地址。
    • send(data):发送数据到连接的 Socket。对于 TCP Socket,data 必须是字节类型;对于 UDP Socket,此方法用于向指定地址发送数据报。
    • recv(bufsize):从 Socket 接收数据。bufsize 参数指定接收缓冲区的大小,返回接收到的数据(字节类型)。
    • sendto(data, address):UDP Socket 专用方法,用于向指定的 address 发送数据报。
    • recvfrom(bufsize):UDP Socket 专用方法,接收数据报并返回接收到的数据和发送方的地址。

三、基于 TCP 的 Socket 数据传输

(一)服务器端代码实现

下面是一个简单的基于 TCP 的服务器端 Python 代码示例:

import socket


def tcp_server():
    # 创建一个 TCP Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置地址重用,避免端口被占用
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定到本地地址和端口 8888
    server_socket.bind(('127.0.0.1', 8888))
    # 开始监听,最大挂起连接数为 5
    server_socket.listen(5)
    print('Server is listening on port 8888...')

    while True:
        # 接受客户端连接
        client_socket, client_address = server_socket.accept()
        print(f'Connected by {client_address}')
        try:
            # 接收客户端发送的数据,最多接收 1024 字节
            data = client_socket.recv(1024)
            print(f'Received data: {data.decode("utf - 8")}')
            # 向客户端发送响应数据
            response = 'Message received successfully!'
            client_socket.send(response.encode('utf - 8'))
        except Exception as e:
            print(f'Error occurred: {e}')
        finally:
            # 关闭客户端连接
            client_socket.close()


if __name__ == '__main__':
    tcp_server()

在上述代码中,首先创建了一个 TCP Socket,并设置了地址重用选项,然后将其绑定到本地的 127.0.0.1:8888 地址。接着,服务器开始监听客户端连接,当有客户端连接时,接受连接并接收客户端发送的数据,打印接收到的数据后,向客户端发送响应数据。最后,关闭与客户端的连接。

(二)客户端代码实现

以下是与上述服务器端对应的客户端代码:

import socket


def tcp_client():
    # 创建一个 TCP Socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 连接到服务器
    client_socket.connect(('127.0.0.1', 8888))
    # 要发送的数据
    message = 'Hello, Server!'
    client_socket.send(message.encode('utf - 8'))
    try:
        # 接收服务器的响应数据,最多接收 1024 字节
        data = client_socket.recv(1024)
        print(f'Received response: {data.decode("utf - 8")}')
    except Exception as e:
        print(f'Error occurred: {e}')
    finally:
        # 关闭客户端 Socket
        client_socket.close()


if __name__ == '__main__':
    tcp_client()

客户端代码创建了一个 TCP Socket 并连接到服务器的 127.0.0.1:8888 地址,然后向服务器发送一条消息,并接收服务器的响应消息,最后关闭客户端 Socket。

(三)代码解析与注意事项

  1. 地址重用:在服务器端代码中,通过 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 设置了地址重用选项。这是因为当服务器程序结束后,其绑定的端口可能会处于 TIME_WAIT 状态,短时间内无法再次被绑定。通过设置地址重用,可以避免这种情况,使得服务器程序能够快速重启并重新绑定到相同的端口。
  2. 阻塞与非阻塞模式:默认情况下,socket 模块的方法是阻塞的。例如,在服务器端的 accept() 方法和 recv() 方法,以及客户端的 connect() 方法和 recv() 方法。阻塞意味着在操作完成之前,程序会暂停执行,等待操作完成。在一些情况下,可能需要将 Socket 设置为非阻塞模式,以便在等待操作完成时可以执行其他任务。可以通过 socket.setblocking(0) 方法将 Socket 设置为非阻塞模式,但在非阻塞模式下,需要更加小心地处理错误和重试逻辑。
  3. 数据编码与解码:在 Python 3 中,socket 模块发送和接收的数据都是字节类型。因此,在发送数据前,需要将字符串等其他类型的数据编码为字节类型(如使用 str.encode() 方法),在接收数据后,需要将字节类型的数据解码为合适的类型(如使用 bytes.decode() 方法)。

四、基于 UDP 的 Socket 数据传输

(一)服务器端代码实现

下面是一个基于 UDP 的服务器端 Python 代码示例:

import socket


def udp_server():
    # 创建一个 UDP Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 绑定到本地地址和端口 9999
    server_socket.bind(('127.0.0.1', 9999))
    print('UDP Server is listening on port 9999...')

    while True:
        # 接收数据报,最多接收 1024 字节
        data, client_address = server_socket.recvfrom(1024)
        print(f'Received data from {client_address}: {data.decode("utf - 8")}')
        # 向客户端发送响应数据报
        response = 'Message received successfully!'
        server_socket.sendto(response.encode('utf - 8'), client_address)


if __name__ == '__main__':
    udp_server()

在上述代码中,创建了一个 UDP Socket 并将其绑定到本地的 127.0.0.1:9999 地址。服务器进入一个循环,不断接收客户端发送的数据报,并打印接收到的数据和客户端地址,然后向客户端发送响应数据报。

(二)客户端代码实现

以下是与上述服务器端对应的客户端代码:

import socket


def udp_client():
    # 创建一个 UDP Socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DUDP)
    # 要发送的数据
    message = 'Hello, UDP Server!'
    server_address = ('127.0.0.1', 9999)
    client_socket.sendto(message.encode('utf - 8'), server_address)
    try:
        # 接收服务器的响应数据报,最多接收 1024 字节
        data, server_address = client_socket.recvfrom(1024)
        print(f'Received response from {server_address}: {data.decode("utf - 8")}')
    except Exception as e:
        print(f'Error occurred: {e}')
    finally:
        # 关闭客户端 Socket
        client_socket.close()


if __name__ == '__main__':
    udp_client()

客户端代码创建了一个 UDP Socket,向服务器 127.0.0.1:9999 发送一条消息,并接收服务器的响应消息,最后关闭客户端 Socket。

(三)代码解析与注意事项

  1. 无连接特性:与 TCP 不同,UDP 是无连接的。在 UDP 服务器端,不需要像 TCP 服务器那样先监听连接,而是直接接收数据报。在 UDP 客户端,也不需要先连接到服务器,直接使用 sendto() 方法向服务器地址发送数据报即可。
  2. 数据报大小限制:UDP 数据报有大小限制,不同的操作系统和网络环境可能有所不同。在实际应用中,如果需要发送的数据量较大,可能需要将数据分割成多个数据报进行发送,并在接收端进行重组。
  3. 可靠性问题:由于 UDP 不保证数据的可靠性和顺序性,在一些对数据准确性要求较高的应用场景中,需要在应用层实现一些机制来保证数据的完整性和顺序性,例如添加序列号、校验和等。

五、Socket 数据传输中的常见问题与解决方案

(一)网络延迟与超时

  1. 问题表现:在网络通信中,由于网络拥塞、路由问题等原因,数据传输可能会出现延迟,甚至长时间没有响应。如果程序一直等待数据的接收或发送完成,可能会导致程序无响应。
  2. 解决方案
    • 设置超时时间:在 socket 模块中,可以通过 socket.settimeout(timeout) 方法设置 Socket 的超时时间。timeout 参数为超时的秒数。例如,server_socket.settimeout(5) 表示设置服务器端 Socket 在 5 秒内如果没有数据接收或发送完成,就会抛出 socket.timeout 异常。在客户端和服务器端都可以设置超时时间,这样可以避免程序长时间阻塞在 recv()send() 等方法上。
    • 心跳机制:对于长时间保持连接的应用场景,可以使用心跳机制。客户端和服务器端定期向对方发送一个简单的消息(如“心跳包”),以确认对方是否仍然在线。如果在一定时间内没有收到心跳包,则认为连接已断开,采取相应的处理措施,如重新连接。

(二)数据粘包与拆包

  1. 问题表现:在 TCP 数据传输中,由于 TCP 是基于字节流的协议,当连续发送多个小数据时,这些数据可能会被合并成一个大的数据包发送,或者接收端一次性接收到多个数据包,导致数据的边界不清晰,这就是数据粘包问题。而拆包问题则是指当发送的数据量较大时,可能会被拆分成多个较小的数据包进行传输,接收端需要正确地将这些数据包组装成完整的数据。
  2. 解决方案
    • 固定长度包头:在发送数据前,先定义一个固定长度的包头,包头中包含数据的长度等信息。接收端首先接收包头,解析出数据长度,然后根据数据长度接收完整的数据。例如,定义一个 4 字节的包头表示数据长度,发送数据时先将数据长度打包成 4 字节(如使用 struct.pack('!I', data_length)),然后发送包头和数据。接收端先接收 4 字节的包头,解析出数据长度(如使用 struct.unpack('!I', header)[0]),再接收相应长度的数据。
    • 自定义分隔符:在数据中添加自定义的分隔符来标识数据的边界。例如,在字符串数据后添加 \r\n 作为分隔符。发送端发送数据时,在每个数据后添加分隔符;接收端接收数据时,根据分隔符来分割数据。但这种方法在处理二进制数据时可能不太适用,因为二进制数据中可能包含与分隔符相同的字节序列。

(三)多线程与多进程处理

  1. 问题背景:在服务器端,如果需要同时处理多个客户端的连接请求,单线程或单进程的方式会导致服务器在处理一个客户端连接时,无法及时响应其他客户端的请求。为了提高服务器的并发处理能力,需要使用多线程或多进程技术。
  2. 解决方案
    • 多线程:在 Python 中,可以使用 threading 模块来实现多线程。在服务器端,每当接受一个客户端连接时,创建一个新的线程来处理该客户端的通信。例如:
import socket
import threading


def handle_client(client_socket, client_address):
    try:
        data = client_socket.recv(1024)
        print(f'Received data from {client_address}: {data.decode("utf - 8")}')
        response = 'Message received successfully!'
        client_socket.send(response.encode('utf - 8'))
    except Exception as e:
        print(f'Error occurred: {e}')
    finally:
        client_socket.close()


def multi_threaded_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('127.0.0.1', 8888))
    server_socket.listen(5)
    print('Multi - threaded Server is listening on port 8888...')

    while True:
        client_socket, client_address = server_socket.accept()
        # 创建新线程处理客户端连接
        client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
        client_thread.start()


if __name__ == '__main__':
    multi_threaded_server()
- **多进程**:使用 `multiprocessing` 模块来实现多进程。与多线程类似,每当接受一个客户端连接时,创建一个新的进程来处理该客户端的通信。多进程可以充分利用多核 CPU 的优势,但由于进程间通信开销较大,在一些情况下可能不如多线程灵活。例如:
import socket
import multiprocessing


def handle_client(client_socket, client_address):
    try:
        data = client_socket.recv(1024)
        print(f'Received data from {client_address}: {data.decode("utf - 8")}')
        response = 'Message received successfully!'
        client_socket.send(response.encode('utf - 8'))
    except Exception as e:
        print(f'Error occurred: {e}')
    finally:
        client_socket.close()


def multi_processed_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('127.0.0.1', 8888))
    server_socket.listen(5)
    print('Multi - processed Server is listening on port 8888...')

    while True:
        client_socket, client_address = server_socket.accept()
        # 创建新进程处理客户端连接
        client_process = multiprocessing.Process(target=handle_client, args=(client_socket, client_address))
        client_process.start()


if __name__ == '__main__':
    multi_processed_server()

六、Socket 数据传输的应用场景

(一)网络爬虫

在网络爬虫中,Socket 可以用于直接与目标服务器进行通信,获取网页内容。与使用 requests 等高级库相比,使用 Socket 可以更细粒度地控制网络请求,例如设置自定义的 HTTP 头、处理复杂的网络认证等。通过创建 TCP Socket 并按照 HTTP 协议的规范发送请求,接收服务器返回的响应数据,从而实现网页数据的抓取。

(二)实时通信应用

如即时通讯软件、在线游戏等实时通信应用,通常需要使用 Socket 进行数据传输。TCP Socket 可以保证消息的可靠传输,适用于对消息准确性要求较高的即时通讯场景;而 UDP Socket 由于其低延迟的特点,常用于在线游戏中实时位置信息、状态信息等数据的传输,虽然可能会丢失一些数据,但不影响游戏的整体流畅性。

(三)分布式系统

在分布式系统中,不同节点之间需要进行数据交互和协调。Socket 提供了一种基本的通信机制,节点之间可以通过 Socket 进行数据传输,实现任务分配、数据同步等功能。例如,在分布式文件系统中,客户端与各个存储节点之间通过 Socket 进行文件的上传和下载操作。

(四)物联网(IoT)

物联网设备通常需要与服务器进行数据交互,将传感器数据上传到服务器,并接收服务器的控制指令。由于物联网设备的资源有限,对网络通信的效率和稳定性有较高要求。Socket 可以根据不同的需求选择 TCP 或 UDP 协议,实现设备与服务器之间的高效通信。例如,智能家居设备通过 Socket 与家庭网关或云服务器进行通信,实现远程控制和数据监测。