Python中的socket
模块为网络通信提供了基础API,使我们能够在应用程序中实现低级的网络交互。使用socket编程,可以创建TCP、UDP和RAW sockets来进行数据通信。
以下是Python socket
编程的简要概述:
1. 核心概念
- Socket: 通信的端点,通常用于建立多个系统之间的连接。
- Bind: 将套接字与特定的IP地址和端口号关联。
- Listen: 在套接字上监听传入的连接。
- Accept: 接受传入的连接请求。
- Connect: 初始化与服务器的连接。
2. 基本流程
服务器端:
- 创建套接字:
socket.socket()
- 绑定套接字到地址:
bind((host, port))
- 监听连接:
listen()
- 接受连接:
accept()
- 读取/发送数据:
recv()
/send()
- 关闭套接字:
close()
客户端:
- 创建套接字:
socket.socket()
- 连接到服务器:
connect((host, port))
- 读取/发送数据:
recv()
/send()
- 关闭套接字:
close()
3. 主要函数/方法
socket()
: 创建新的socket对象。bind()
: 绑定地址到套接字。listen()
: 开始监听传入的连接。accept()
: 接受客户端连接,并返回(connection, address)。connect()
: 连接到远程地址。recv()
: 从套接字接收数据。send()
: 将数据发送到套接字。close()
: 关闭套接字。
4. TCP
一个简单的TCP服务器和客户端的示例。
TCP服务器
python
import socket
import json
def start_server():
# 创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取主机名称
# host = socket.gethostname()
host = "127.0.0.1"
port = 12345
# 绑定到端口
server_socket.bind((host, port))
# 设置最大连接数,超过后排队
server_socket.listen(5)
print("Server is listening...")
while True:
# 建立客户端连接
client_socket, addr = server_socket.accept()
print(f"Connection from {addr}")
data = client_socket.recv(1024).decode('utf-8')
json_data = json.loads(data)
print(f"Received {json_data} from {addr}")
client_socket.send(data.encode('utf-8'))
client_socket.close()
if __name__ == '__main__':
start_server()
TCP客户端
python
import socket
import json
def start_client():
# 创建socket对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取本地主机名
# host = socket.gethostname()
host = "127.0.0.1"
port = 12345
print(host)
# 连接到服务器
client_socket.connect((host, port))
message = {
"name": "Alice",
"age": 30
}
json_message = json.dumps(message)
# 发送数据
client_socket.send(json_message.encode('utf-8'))
# 接收数据,最多接收1024字节
data = client_socket.recv(1024).decode('utf-8')
json_data = json.loads(data)
print(f"Received from server: {json_data}")
client_socket.close()
if __name__ == '__main__':
start_client()
运行结果如下:
5. UDP
与TCP不同,UDP是一个无连接的协议。使用UDP,不需要建立和断开连接。每个数据报都是一个独立的消息。
UDP 服务器
python
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 12345))
while True:
data, addr = s.recvfrom(1024)
print(f"Received {data.decode('utf-8')} from {addr}")
s.sendto(data, addr)
UDP 客户端
python
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
message = input("Enter message: ").encode('utf-8')
s.sendto(message, ('127.0.0.1', 12345))
data, addr = s.recvfrom(1024)
print(f"Received {data.decode('utf-8')} from {addr}")
运行结果如下:
6. ICMP
RAW sockets是一种低级的通信机制,允许我们直接发送和接收底层网络协议的数据包,如ICMP、IP等。在Python中使用RAW sockets通常需要root权限或适当的权限,因为它涉及到操作系统级的网络操作。
以下是使用RAW socket在Python中发送一个ICMP Echo Request(通常称为ping请求)的示例。注意,此代码是在Linux上工作的,因为Windows上的raw socket行为与Linux不同。
python
import socket
import struct
import time
def checksum(data):
s = 0
n = len(data) % 2
for i in range(0, len(data)-n, 2):
s += (data[i] + (data[i+1] << 8))
if n:
s += data[i+1]
while (s >> 16):
s = (s & 0xFFFF) + (s >> 16)
s = ~s & 0xFFFF
return s
def create_icmp_echo_request():
icmp_type = 8
icmp_code = 0
icmp_checksum = 0
icmp_identifier = 1
icmp_sequence_number = 1
# ICMP header
header = struct.pack("!BBHHH", icmp_type, icmp_code, icmp_checksum, icmp_identifier, icmp_sequence_number)
data = struct.pack("!d", time.time())
icmp_checksum = checksum(header + data)
header = struct.pack("!BBHHH", icmp_type, icmp_code, icmp_checksum, icmp_identifier, icmp_sequence_number)
return header + data
def main():
target_host = "172.20.7.84"
icmp_proto = socket.getprotobyname('icmp')
# Create a raw socket
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp_proto)
s.sendto(create_icmp_echo_request(), (target_host, 0))
# Wait for a reply
while True:
data, addr = s.recvfrom(1024)
if addr[0] == target_host:
print(data)
print(f"Received reply from {addr[0]}")
break
if __name__ == "__main__":
main()
这只是一个简单的示例,它发送一个ICMP请求并等待一个响应。在生产环境中使用RAW sockets时,需要处理更多的边缘情况和错误,以及考虑多种协议和包格式。
7. 错误处理
在socket编程中,特别是在网络中,总是可能发生各种错误。为了编写健壮的应用程序,应该捕获socket.error
异常并据此采取适当的行动。
8. 高级
除了基础的socket编程,Python还提供了更高级的工具和模块,例如selectors
或asyncio
,用于处理多并发连接或异步IO。
总之,socket编程是计算机网络和分布式系统中的一个基本概念。Python提供了一个强大而简单的API来处理套接字,使得网络编程变得相对容易。
以下是关于ICMP示例程序的几点说明:
(1)当在Python中使用struct.pack
方法,表示正在执行结构化的打包操作,将多个数据项打包成一个字节串。它通常用于处理二进制数据和底层的数据结构,例如网络协议。
在ICMP示例代码段中:
python
header = struct.pack("!BBHHH", icmp_type, icmp_code, icmp_checksum, icmp_identifier, icmp_sequence_number)
我们逐一解析这行代码:
-
!
: 这是字节顺序标记。感叹号!
表示网络字节顺序,也就是大端字节序。在网络通讯中,大端字节序是常用的标准。 -
BBHHH
: 这是格式字符串,它告诉struct.pack
如何打包接下来的数据。B
: 无符号字符(1个字节)H
: 无符号短整数(2个字节)
因此,
BBHHH
表示打包了2个1字节的无符号字符和3个2字节的无符号短整数,总共8个字节。 -
后面的参数列表(
icmp_type, icmp_code, icmp_checksum, icmp_identifier, icmp_sequence_number
)是要打包的实际数据。这些数据的顺序和大小应该与格式字符串BBHHH
匹配。
具体到ICMP头部的内容:
icmp_type
: ICMP消息的类型(1字节)icmp_code
: 与ICMP类型相关的特定代码(1字节)icmp_checksum
: 对整个ICMP数据包计算得到的校验和(2字节)icmp_identifier
: 用于唯一标识此请求的标识符,通常是发送进程的PID(2字节)icmp_sequence_number
: 该请求的序列号,通常是从0开始递增的(2字节)
通过struct.pack
,这些数据被格式化和打包成一个连续的8字节的字节串,然后可以直接发送到网络上。
(2)在下面的代码片段中,使用struct.pack
来将一个双精度浮点数(即Python中的float
)打包为一个二进制格式的字符串。这样的操作常用于将高级的数据类型转换为可以在网络上发送或在二进制文件中存储的格式。
python
data = struct.pack("!d", time.time())
让我们逐步解析这段代码:
-
struct.pack
: 这是Python中的struct
模块提供的函数,用于将给定的数据格式化(或打包)为一个二进制字符串。 -
"!d"
: 这是一个格式字符串,它告诉struct.pack
函数如何格式化后续的数据。-
!
: 指定字节顺序为网络字节顺序(也就是大端字节序)。 -
d
: 表示一个双精度浮点数。这通常占用8个字节。
-
-
time.time()
: 这是Python中time
模块的一个函数,返回当前时间的时间戳,类型为float
。这表示从某个固定的起点(通常是1970年1月1日0点,称为Unix纪元)到现在的秒数。
这段代码的目的是将当前的时间戳转换为一个8字节的二进制字符串。这在网络编程中很有用,尤其是当我们想在数据包中包含一个时间戳,以便在接收端可以解码并使用它。例如,在ICMP的ping工具中,发送时间戳可以帮助计算往返时间(RTT)。
(3)下面这段代码使用socket
模块的getprotobyname
方法来查询指定协议名的协议号。具体来说,它查询"icmp"协议的协议号。
python
icmp_proto = socket.getprotobyname('icmp')
让我们详细了解这段代码:
-
socket
: 这是Python的标准库之一,它提供了套接字编程的工具和函数。 -
getprotobyname
: 这个函数的作用是返回与给定的协议名称关联的协议号。 -
'icmp'
: 这是传递给getprotobyname
函数的参数,代表互联网控制消息协议(ICMP)。
当这个函数被调用时,它会查找系统的协议配置文件(通常是/etc/protocols
)来找到与"icmp"匹配的协议号。通常,ICMP的协议号为1
,所以函数通常会返回1
。
为什么这个是重要的?当创建原始套接字(raw socket)用于发送或接收ICMP消息时,我们需要告诉系统正在使用哪种协议。icmp_proto
这个变量保存的值(即ICMP的协议号)就是这个目的。当创建一个原始套接字并指定ICMP协议时,系统就知道我们要处理ICMP数据包。