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

PythonSocket编程实战

2023-09-304.4k 阅读

Python Socket 编程基础

什么是 Socket

Socket(套接字)是计算机网络中进程间通信的一种机制,它为应用程序提供了一种通用的方式来与网络上的其他程序进行通信。Socket 可以看作是两个网络应用程序之间的端点,通过这个端点,应用程序可以发送和接收数据。在 Python 中,Socket 模块提供了创建和操作套接字的功能,使得开发者能够轻松地实现网络编程。

Socket 地址族

在 Python 的 Socket 编程中,地址族(Address Family)用于指定套接字使用的地址类型。常见的地址族有以下两种:

  1. AF_INET:用于 IPv4 地址。这是最常用的地址族,适用于大多数基于 IPv4 的网络应用。
  2. AF_INET6:用于 IPv6 地址。随着 IPv6 的逐渐普及,越来越多的应用开始支持 IPv6 地址族。

Socket 类型

除了地址族,Socket 还具有不同的类型,常见的类型有:

  1. SOCK_STREAM:基于 TCP(传输控制协议)的套接字类型。TCP 是一种可靠的、面向连接的协议,适用于对数据准确性和顺序要求较高的应用,如文件传输、HTTP 等。
  2. SOCK_DGRAM:基于 UDP(用户数据报协议)的套接字类型。UDP 是一种不可靠的、无连接的协议,适用于对实时性要求较高但对数据准确性要求相对较低的应用,如视频流、音频流等。

基于 TCP 的 Socket 编程

TCP 服务器端编程

  1. 创建套接字:使用 socket.socket() 函数创建一个 TCP 套接字。例如:
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1. 绑定地址和端口:将套接字绑定到指定的地址和端口。地址可以是 IP 地址或主机名,端口号是一个 16 位的整数。例如:
server_address = ('127.0.0.1', 8888)
server_socket.bind(server_address)
  1. 监听连接:使用 listen() 方法开始监听客户端的连接请求。参数指定了等待连接的最大队列长度。例如:
server_socket.listen(5)
print('Server is listening on {}:{}'.format(*server_address))
  1. 接受连接:使用 accept() 方法接受客户端的连接。该方法会阻塞,直到有客户端连接进来。它返回一个新的套接字对象和客户端的地址。例如:
while True:
    client_socket, client_address = server_socket.accept()
    print('Accepted connection from {}:{}'.format(*client_address))
  1. 数据传输:通过新的套接字对象 client_socket 与客户端进行数据传输。可以使用 recv() 方法接收数据,sendall() 方法发送数据。例如:
    try:
        data = client_socket.recv(1024)
        print('Received data: {}'.format(data.decode('utf - 8')))
        response = 'Message received successfully!'
        client_socket.sendall(response.encode('utf - 8'))
    except socket.error as e:
        print('Socket error: {}'.format(str(e)))
    finally:
        client_socket.close()
  1. 关闭套接字:当服务器完成任务后,需要关闭套接字以释放资源。例如:
server_socket.close()

TCP 客户端编程

  1. 创建套接字:与服务器端类似,使用 socket.socket() 函数创建一个 TCP 套接字。
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1. 连接服务器:使用 connect() 方法连接到服务器的地址和端口。例如:
server_address = ('127.0.0.1', 8888)
client_socket.connect(server_address)
  1. 数据传输:通过套接字 client_socket 与服务器进行数据传输。例如:
message = 'Hello, server!'
try:
    client_socket.sendall(message.encode('utf - 8'))
    data = client_socket.recv(1024)
    print('Received response: {}'.format(data.decode('utf - 8')))
except socket.error as e:
    print('Socket error: {}'.format(str(e)))
finally:
    client_socket.close()

基于 UDP 的 Socket 编程

UDP 服务器端编程

  1. 创建套接字:使用 socket.socket() 函数创建一个 UDP 套接字。
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  1. 绑定地址和端口:与 TCP 服务器类似,将 UDP 套接字绑定到指定的地址和端口。
server_address = ('127.0.0.1', 9999)
server_socket.bind(server_address)
  1. 接收和发送数据:使用 recvfrom() 方法接收数据,该方法会返回接收到的数据和发送方的地址。使用 sendto() 方法发送数据,需要指定目标地址。例如:
while True:
    data, client_address = server_socket.recvfrom(1024)
    print('Received data from {}:{}: {}'.format(*client_address, data.decode('utf - 8')))
    response = 'Message received successfully!'
    server_socket.sendto(response.encode('utf - 8'), client_address)
  1. 关闭套接字:当服务器完成任务后,关闭套接字。
server_socket.close()

UDP 客户端编程

  1. 创建套接字:创建 UDP 套接字。
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DUDP)
  1. 发送和接收数据:使用 sendto() 方法向服务器发送数据,使用 recvfrom() 方法接收服务器的响应。例如:
server_address = ('127.0.0.1', 9999)
message = 'Hello, UDP server!'
try:
    client_socket.sendto(message.encode('utf - 8'), server_address)
    data, server_address = client_socket.recvfrom(1024)
    print('Received response from server: {}'.format(data.decode('utf - 8')))
except socket.error as e:
    print('Socket error: {}'.format(str(e)))
finally:
    client_socket.close()

Socket 编程中的异常处理

在 Socket 编程中,可能会遇到各种异常情况,如连接超时、网络中断、端口被占用等。因此,正确的异常处理是非常重要的。以下是一些常见的异常及其处理方法:

  1. socket.error:这是所有 Socket 相关异常的基类。当发生 Socket 操作错误时,会抛出此异常。可以通过捕获该异常并获取其错误信息来进行相应的处理。例如:
try:
    client_socket.connect(server_address)
except socket.error as e:
    print('Socket connection error: {}'.format(str(e)))
  1. ConnectionRefusedError:当客户端尝试连接到一个拒绝连接的服务器时,会抛出此异常。这通常表示服务器未运行或防火墙阻止了连接。可以提示用户检查服务器状态或网络配置。
try:
    client_socket.connect(server_address)
except ConnectionRefusedError as e:
    print('Connection refused. Please check if the server is running and the port is open.')
  1. TimeoutError:当 Socket 操作(如连接、接收数据)超时时,会抛出此异常。可以在创建套接字时设置超时时间,以避免长时间等待。例如:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.settimeout(5)  # 设置超时时间为 5 秒
try:
    client_socket.connect(server_address)
except TimeoutError as e:
    print('Connection timed out.')

高级 Socket 编程技巧

多线程 Socket 编程

在实际应用中,服务器可能需要同时处理多个客户端的连接。使用多线程可以有效地实现这一点。每个客户端连接可以在一个单独的线程中处理,这样服务器就可以并发地处理多个请求。以下是一个简单的多线程 TCP 服务器示例:

import socket
import threading


def handle_client(client_socket, client_address):
    try:
        data = client_socket.recv(1024)
        print('Received data from {}:{}: {}'.format(*client_address, data.decode('utf - 8')))
        response = 'Message received successfully!'
        client_socket.sendall(response.encode('utf - 8'))
    except socket.error as e:
        print('Socket error: {}'.format(str(e)))
    finally:
        client_socket.close()


server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8888)
server_socket.bind(server_address)
server_socket.listen(5)
print('Server is listening on {}:{}'.format(*server_address))

while True:
    client_socket, client_address = server_socket.accept()
    client_thread = threading.Thread(target = handle_client, args = (client_socket, client_address))
    client_thread.start()


server_socket.close()

非阻塞 Socket 编程

默认情况下,Socket 操作是阻塞的,即当执行 recv()connect() 等方法时,程序会暂停并等待操作完成。在非阻塞模式下,这些方法会立即返回,无论操作是否完成。如果操作未完成,会抛出 BlockingIOError 异常。可以通过设置套接字的 setblocking(0) 方法来将其设置为非阻塞模式。以下是一个简单的非阻塞 TCP 客户端示例:

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.setblocking(0)
server_address = ('127.0.0.1', 8888)

try:
    client_socket.connect(server_address)
except BlockingIOError:
    pass

message = 'Hello, server!'
try:
    client_socket.sendall(message.encode('utf - 8'))
    while True:
        try:
            data = client_socket.recv(1024)
            if data:
                print('Received response: {}'.format(data.decode('utf - 8')))
                break
        except BlockingIOError:
            pass
except socket.error as e:
    print('Socket error: {}'.format(str(e)))
finally:
    client_socket.close()

使用 Select 模块进行多路复用

Select 模块是 Python 提供的一个用于实现多路复用的模块。它允许程序同时监控多个套接字的状态,从而提高 I/O 效率。通过 select.select() 函数,可以检查多个套接字是否可读、可写或有错误发生。以下是一个使用 select 实现的简单 TCP 服务器示例:

import socket
import select


server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8888)
server_socket.bind(server_address)
server_socket.listen(5)
print('Server is listening on {}:{}'.format(*server_address))

inputs = [server_socket]
outputs = []

while inputs:
    readable, writable, exceptional = select.select(inputs, outputs, inputs)

    for s in readable:
        if s is server_socket:
            client_socket, client_address = server_socket.accept()
            print('Accepted connection from {}:{}'.format(*client_address))
            inputs.append(client_socket)
        else:
            try:
                data = s.recv(1024)
                if data:
                    print('Received data from client: {}'.format(data.decode('utf - 8')))
                    outputs.append(s)
                else:
                    print('Client disconnected')
                    inputs.remove(s)
                    if s in outputs:
                        outputs.remove(s)
                    s.close()
            except socket.error as e:
                print('Socket error: {}'.format(str(e)))
                inputs.remove(s)
                if s in outputs:
                    outputs.remove(s)
                s.close()

    for s in writable:
        response = 'Message received successfully!'
        try:
            s.sendall(response.encode('utf - 8'))
        except socket.error as e:
            print('Socket error: {}'.format(str(e)))
        finally:
            outputs.remove(s)

    for s in exceptional:
        print('Exception on socket {}'.format(s.getpeername()))
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()


server_socket.close()

应用案例:简单的文件传输

使用 TCP 进行文件传输

  1. 服务器端
import socket


server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8888)
server_socket.bind(server_address)
server_socket.listen(5)
print('Server is listening on {}:{}'.format(*server_address))

while True:
    client_socket, client_address = server_socket.accept()
    print('Accepted connection from {}:{}'.format(*client_address))

    try:
        file_name = client_socket.recv(1024).decode('utf - 8')
        with open(file_name, 'wb') as file:
            while True:
                data = client_socket.recv(1024)
                if not data:
                    break
                file.write(data)
        print('File {} received successfully'.format(file_name))
        response = 'File received successfully'
        client_socket.sendall(response.encode('utf - 8'))
    except socket.error as e:
        print('Socket error: {}'.format(str(e)))
    finally:
        client_socket.close()


server_socket.close()
  1. 客户端
import socket


client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8888)
client_socket.connect(server_address)

file_name = 'example.txt'
client_socket.sendall(file_name.encode('utf - 8'))

with open(file_name, 'rb') as file:
    while True:
        data = file.read(1024)
        if not data:
            break
        client_socket.sendall(data)

data = client_socket.recv(1024)
print('Received response: {}'.format(data.decode('utf - 8')))

client_socket.close()

使用 UDP 进行文件传输

UDP 由于其无连接的特性,在文件传输中需要更多的机制来确保数据的完整性。以下是一个简单的基于 UDP 的文件传输示例,它使用了序列号和校验和来保证数据的正确性。

  1. 服务器端
import socket
import hashlib


server_socket = socket.socket(socket.AF_INET, socket.SOCK_DUDP)
server_address = ('127.0.0.1', 9999)
server_socket.bind(server_address)

while True:
    data, client_address = server_socket.recvfrom(1024)
    seq_num = int.from_bytes(data[:4], byteorder = 'big')
    checksum = data[4:36]
    file_data = data[36:]
    received_checksum = hashlib.sha256(file_data).digest()
    if received_checksum == checksum:
        with open('received_file.txt', 'ab') as file:
            file.write(file_data)
        response = 'Data received successfully'
        server_socket.sendto(response.encode('utf - 8'), client_address)
    else:
        response = 'Checksum error'
        server_socket.sendto(response.encode('utf - 8'), client_address)


server_socket.close()
  1. 客户端
import socket
import hashlib


client_socket = socket.socket(socket.AF_INET, socket.SOCK_DUDP)
server_address = ('127.0.0.1', 9999)

file_name = 'example.txt'
seq_num = 0
with open(file_name, 'rb') as file:
    while True:
        data = file.read(1000)
        if not data:
            break
        checksum = hashlib.sha256(data).digest()
        packet = seq_num.to_bytes(4, byteorder = 'big') + checksum + data
        client_socket.sendto(packet, server_address)
        response, server_address = client_socket.recvfrom(1024)
        print('Received response: {}'.format(response.decode('utf - 8')))
        seq_num += 1


client_socket.close()

安全 Socket 编程

使用 SSL/TLS 加密

在网络通信中,数据的安全性至关重要。Python 的 ssl 模块提供了对 SSL/TLS 协议的支持,可以对 Socket 通信进行加密。以下是一个使用 SSL/TLS 加密的 TCP 服务器和客户端示例:

  1. 服务器端
import socket
import ssl


context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(certfile = 'cert.pem', keyfile = 'key.pem')

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8888)
server_socket.bind(server_address)
server_socket.listen(5)
print('Server is listening on {}:{}'.format(*server_address))

while True:
    client_socket, client_address = server_socket.accept()
    ssl_socket = context.wrap_socket(client_socket, server_side = True)
    try:
        data = ssl_socket.recv(1024)
        print('Received data: {}'.format(data.decode('utf - 8')))
        response = 'Message received successfully!'
        ssl_socket.sendall(response.encode('utf - 8'))
    except ssl.SSLError as e:
        print('SSL error: {}'.format(str(e)))
    finally:
        ssl_socket.close()


server_socket.close()
  1. 客户端
import socket
import ssl


context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations(cafile = 'cert.pem')

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8888)
ssl_socket = context.wrap_socket(client_socket, server_hostname = '127.0.0.1')
ssl_socket.connect(server_address)

message = 'Hello, server!'
try:
    ssl_socket.sendall(message.encode('utf - 8'))
    data = ssl_socket.recv(1024)
    print('Received response: {}'.format(data.decode('utf - 8')))
except ssl.SSLError as e:
    print('SSL error: {}'.format(str(e)))
finally:
    ssl_socket.close()

认证和授权

除了加密,认证和授权也是安全 Socket 编程的重要方面。认证用于验证通信双方的身份,授权用于确定通信双方是否有权限进行特定的操作。可以使用用户名密码、证书等方式进行认证,通过权限表等方式进行授权。在实际应用中,这些机制通常与服务器的用户管理系统集成在一起。例如,在服务器端可以在接受连接后,要求客户端发送用户名和密码进行认证,认证通过后再进行后续的通信。

总结 Socket 编程在不同场景的应用

网络爬虫

在网络爬虫中,Socket 编程可以用于直接与网站服务器进行通信,获取网页内容。相比于使用 HTTP 库,直接使用 Socket 可以更灵活地控制请求和响应,例如设置自定义的请求头、处理重定向等。通过建立 TCP 连接,向服务器发送 HTTP 请求,然后接收服务器返回的 HTML 内容,从而实现网页的抓取。

实时通信应用

如在线聊天、实时游戏等应用,需要实时地传输数据。基于 UDP 的 Socket 编程由于其低延迟的特点,非常适合这类应用。通过在客户端和服务器之间建立 UDP 连接,实时地发送和接收消息、游戏状态等数据。同时,为了保证数据的可靠性,可以在应用层添加一些机制,如序列号、重传等。

物联网设备通信

在物联网场景中,各种设备需要与服务器进行通信,上传传感器数据或接收控制指令。Socket 编程为设备与服务器之间提供了一种可靠的通信方式。可以根据设备的网络环境和通信需求,选择 TCP 或 UDP 协议。例如,对于一些对数据准确性要求较高的传感器数据传输,可以使用 TCP 协议;而对于一些实时性要求较高的控制指令传输,可以使用 UDP 协议。通过合理地运用 Socket 编程技术,可以实现物联网设备之间高效、稳定的通信。

在 Python 的 Socket 编程中,深入理解其原理和掌握各种技巧,对于开发高性能、可靠的网络应用至关重要。无论是基础的 TCP 和 UDP 编程,还是高级的多线程、非阻塞以及安全 Socket 编程,都为开发者提供了丰富的工具和方法,以满足不同场景下的网络通信需求。