Python网络编程之socket模块
socket模块
【一】概要
-
在Python中,Socket模块是内置的标准库之一,它允许开发者使用Socket API进行网络通信。
-
Socket API(Application Programming Interface)是一组用于进行网络编程的接口函数,它定义了应用程序和操作系统或网络协议栈之间的通信规则。Socket API允许开发者通过调用这些接口函数来实现网络通信,包括建立连接、发送和接收数据等操作。
【二】常用方法
【1】TCP套接字编程
-
服务端(server)
'''服务端'''
# 导入模块
import socket
# 定义端口和IP # 一般设为常量
IP = '127.0.0.1' # 回环地址 # 代表本地
PORT = 9999 # 端口 # 端口号最好超过8001 # 必须是整数类型
# 创建套接字对象
'''family默认参数【-1】为AF_INET网络协议,可以通过查看源码查看'''
'''type参数默认【-1】为流式协议,也就是TCP协议'''
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 上述等价于 socket.socket() 啥也不填,因为默认参数就是上面的,如有需要再修改参数即可
# 监听
server.bind((IP, PORT))
# 设置半连接池
server.listen() # 默认为5
# 链接客户端
'''返回链接对象和链接对象的地址'''
conn, addr = server.accept()
# 接收信息
'''recv()默认为1024,含义是这一次接收1024个字节大小的'''
'''如果数据超过1024,将导致粘包问题,具体看详解'''
msg = conn.recv(1024)
# 发送数据
'''发送给客户端,必须是二进制数据'''
server.send(b'code')
# 关闭链接
conn.close()
# 关闭服务端
server.close()
- 客户端(client)
'''客户端'''
import socket
IP = '127.0.0.1' # 如果服务端IP改变,此处也要改变
PORT = 9999 # 与服务端保持一致
client = socket.socket()
# 链接服务端
client.connect((IP, PORT))
# 向服务端发送消息
client.send(b'') # 必须是二进制数据
# 接收服务端消息
msg = client.recv(1024) # 接收服务端的1024个字节的数据
# 断开链接
client.close()
【2】UDP套接字编程
- 服务端
import socket
# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 报式协议 # 也就是UDP协议
# 监听
server.bind((IP, PORT))
# 接收信息
data, addr = server.recvfrom(1024)
print(data)
# 回复信息
server.sendto(b'server', addr)
# 关闭连接
server.close()
- 客户端
import socket
# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送消息
client.sendto(b'client', (IP, PORT))
# 接收消息
data, addr = client.recvfrom(1024)
print(data)
# 关闭连接
client.close()
【三】详解
【1】socket抽象层
- Socket(套接字)是一种提供网络通信的抽象层,它允许不同计算机上的应用程序通过网络进行通信。Socket为应用程序提供了一种通用的编程接口,使得程序员可以使用常见的套接字API进行网络通信,而不必关心底层网络细节。
- socket抽象层位于传输层和应用层之间
【1.1】Socket抽象层的一些关键特点和概念:
- 通信端点:
- Socket定义了通信的两个端点,一个是发送数据的端点,另一个是接收数据的端点。
- 通常,一个Socket用于表示一个端点,而一对Socket则形成了一个完整的通信连接。
- 协议族和协议类型:
- Socket通过协议族(Protocol Family)和协议类型(Socket Type)来确定通信的方式和特性。
- 常见的协议族包括IPv4、IPv6,常见的协议类型包括TCP、UDP。
- 套接字地址:
- 每个Socket都有一个关联的套接字地址,用于标识其在网络中的位置。
- 对于IPv4,套接字地址通常包含IP地址和端口号;对于IPv6,还包含了流标签等信息。
- Socket API:
- Socket提供了一组通用的API,使得程序员能够方便地进行网络通信。这些API包括socket()、bind()、connect()、listen()、accept()等。
- 面向连接和无连接:
- Socket可以基于连接(面向连接)或者不基于连接(无连接)进行通信。
- 面向连接的Socket通常使用TCP协议,而无连接的Socket通常使用UDP协议。
- 服务器端和客户端:
- 在Socket通信中,通信的一方通常是服务器端(提供服务),另一方是客户端(请求服务)。
- 服务器端通常通过调用listen()等待连接请求,而客户端通常通过调用connect()请求连接。
- 阻塞和非阻塞:
- Socket可以设置为阻塞模式或非阻塞模式,决定在进行某些操作时是否会阻塞程序执行。
- 多路复用:
- Socket提供了多路复用的机制,允许一个进程同时处理多个Socket连接,提高程序的性能和效率。
【1.2】Socket API
- Socket API(Application Programming Interface)是一组用于进行网络编程的接口函数,它定义了应用程序和操作系统或网络协议栈之间的通信规则。Socket API允许开发者通过调用这些接口函数来实现网络通信,包括建立连接、发送和接收数据等操作。
在常见的操作系统中,Socket API通常包括以下基本函数:
- socket():
- 创建一个新的Socket对象,并返回其文件描述符或句柄。该函数指定了协议族(IPv4、IPv6)、协议类型(TCP、UDP)等参数。
- bind():
- 将Socket与特定的地址和端口绑定,用于服务器端。
- listen():
- 用于TCP服务器端,开始监听连接请求。
- accept():
- 用于TCP服务器端,接受客户端的连接请求,返回新的Socket对象用于与客户端通信。
- connect():
- 用于TCP客户端,与服务器建立连接。
- send() / recv():
- 用于发送和接收数据,适用于面向连接的Socket。
- sendto() / recvfrom():
- 用于发送和接收数据,适用于无连接的Socket(如UDP)。
- close():
- 关闭Socket连接。
【2】套接字家族(Socket Family)
- AF_INET / AF_INET6 : 网络编程中常用
- AF_UNIX : 用于本地进程间通信
套接字家族(Socket Family)是一组相关的协议和参数,用于指定套接字的地址格式和通信方式。套接字家族定义了套接字的地址结构以及与之相关的协议类型。在Socket编程中,常见的套接字家族包括:
- AF_INET(IPv4):
- AF_INET表示IPv4套接字家族,用于指定IPv4地址格式。在这个家族中,套接字地址通常由一个IPv4地址和一个端口号组成。
- AF_INET6(IPv6):
- AF_INET6表示IPv6套接字家族,用于指定IPv6地址格式。IPv6的套接字地址包括一个IPv6地址和一个端口号。
- AF_UNIX / AF_LOCAL(Unix域套接字):
- AF_UNIX或AF_LOCAL表示Unix域套接字家族,用于本地进程间通信。在这个家族中,套接字地址通常是一个文件路径。
- AF_ISO(ISO协议):
- AF_ISO表示ISO协议套接字家族,用于支持ISO协议。
- AF_NS(Xerox Network Systems协议):
- AF_NS表示Xerox Network Systems协议套接字家族,用于支持Xerox Network Systems协议。
这些套接字家族对应不同的协议和地址结构,开发者在创建套接字时需要选择合适的套接字家族。在Socket API中,通常使用socket.AF_INET
表示IPv4套接字家族,使用socket.AF_INET6
表示IPv6套接字家族,使用socket.AF_UNIX
或socket.AF_LOCAL
表示Unix域套接字家族。
【3】TCP套接字编程
【3.1】TCP套接字工作流程
- 服务端server
- 创建套接字对象
socket.socket()
- 绑定地址
server.bind()
- 监听链接
server.listen()
- 接受链接
server.accept()
,返回客户端的套接字对象和客户端地址,一般命名为conn,addr = server.accept()
- 接收数据
conn.recv()
【上图中的read()
】 - 发送数据
conn.send()
【上图中的write()
】 - 关闭链接
conn.close()
,服务器可以只关闭与客户端的连接,因为客户端可能不止一个
- 创建套接字对象
- 客户端client
- 创建套接字对象
socket.socket()
- 连接到服务端
client.connect()
- 发送数据
client.send()
【上图中的write()
】 - 接收数据
client.recv()
【上图中的read()
】 - 关闭于服务端的连接
client.close()
- 创建套接字对象
【e.g.】打电话
- 你(服务端)
- 【1】你有手机
socket()
- 【2】你插上卡
bind()
- 【3】你等待接听
listen()
- 【7】接听对象打给你的电话
recv()
- 【8】回复对象
send()
- 【11】收到对象挂断电话,你也挂断电话
close()
- 【12】关闭手机
- 【1】你有手机
- 对象(客户端)
- 【4】对象有手机
socket()
- 【5】对象知道你的手机号
connect()
- 【6】对象给你打电话
send()
- 【9】收到回复
recv()
- 【10】对象挂断电话
close()
,关闭手机
- 【4】对象有手机
- 需要注意,打电话时,可以一方说很多话,另一方等待,但是在socket中,必须一条
send
对应一条recv
,如果没有send
直接recv
,那么将一直处于等待状态 - 并且挂断电话是双向的,可以客户端提出挂断,也可以服务端提出挂断
- 但是,如果服务端挂断的电话,而客户端并没有挂断电话,将会报错
【3.2】基于TCP协议的套接字编程
【3.2.1】基本模板
-
我懒狗,用的
b"english"
来发送的数据,如果是中文或者其他类型,需要进行编码 -
服务端
'''服务端'''
# 导入模块
import socket
# 定义端口和IP # 一般设为常量
IP = '127.0.0.1' # 回环地址 # 代表本地
PORT = 9999 # 端口
# 创建套接字对象
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 监听
server.bind((IP, PORT))
# 设置半连接池
server.listen() # 默认为5
# 链接客户端
'''返回链接对象和链接对象的地址'''
conn, addr = server.accept()
# 接收信息
msg = conn.recv(1024)
print(f"【{addr}】{msg.decode('ascii')}") # 【('127.0.0.1', 9565)】I'm client!
# 发送数据
conn.send(b"Receive the message from client!I'm server!")
# 关闭链接
conn.close()
# 关闭服务端
server.close()
- 客户端
'''客户端'''
import socket
IP = '127.0.0.1' # 如果服务端IP改变,此处也要改变
PORT = 9999 # 与服务端保持一致
client = socket.socket()
# 链接服务端
client.connect((IP, PORT))
# 向服务端发送消息
client.send(b"I'm client!") # 必须是二进制数据
# 接收服务端消息
msg = client.recv(1024) # 接收服务端的1024个字节的数据
print(msg.decode('ascii')) # Receive the message from client!I'm server!
# 断开链接
client.close()
【3.2.2】通讯循环
- 为了多次交流,使用循环进行持续交互!
# 服务端
import socket
IP = '127.0.0.1'
PORT = 8090
# 创建socket对象
server = socket.socket()
# 监听
server.bind((IP, PORT))
# 半连接池
server.listen()
conn, addr = server.accept()
# 连接
while True:
msg = conn.recv(1024)
msg = msg.decode('utf8')
print(f"来自{addr}的消息:{msg}")
if msg == 'q':
conn.close()
# 关闭此次连接后,为下一个客户端创建连接
conn, addr = server.accept()
else:
send_msg = input("(server)请输入消息:")
if len(send_msg) == 0:
print("消息不能为空!")
continue
elif send_msg == 'q':
conn.send(b'break connect')
conn.close()
break
conn.send(send_msg.encode('utf8'))
server.close()
# 客户端
import socket
IP = '127.0.0.1'
PORT = 8090
client = socket.socket()
client.connect((IP, PORT))
while True:
send_msg = input("(client)请输入消息:")
if len(send_msg) == 0:
print("消息不能为空!")
continue
client.send(send_msg.encode('utf8'))
accept_msg = client.recv(1024)
print(accept_msg.decode('utf8'))
if accept_msg == b'break connect':
# 服务端关闭连接
break
if send_msg == 'q':
break
client.close()
【3.2.3】提高代码健壮性以及优化链接循环
-
当客户端意外退出,如果不做异常捕获,将会导致服务端崩溃
-
服务端
import socket
server = socket.socket()
IP = '127.0.0.1'
PORT = 8080
server.bind((IP, PORT))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try: # 当客户端发生意外断开,为了保证服务器运行,进行异常捕获并关闭客户端的链接
# 接收信息
msg = conn.recv(1024)
msg = msg.decode('utf8')
print(f"【{addr}】的消息:{msg}")
# 回复信息
send_msg = input("请输入回复消息:")
send_msg = send_msg.encode('utf8')
conn.send(send_msg)
if msg == 'q':
# 关闭客户端链接
break
except Exception as e:
print(e)
break
conn.close()
server.close()
- 客户端
# 客户端
import socket
IP = '127.0.0.1'
PORT = 8080
client = socket.socket()
client.connect((IP, PORT))
while True:
send_msg = input("(client)请输入消息:")
if len(send_msg) == 0:
print("消息不能为空!")
continue
client.send(send_msg.encode('utf8'))
accept_msg = client.recv(1024)
print(f"【server】的消息:{accept_msg.decode('utf8')}")
if send_msg == 'q':
break
client.close()
【补】半连接池
在网络编程中,"半连接池"通常指的是处理TCP半连接(SYN_RCVD状态)的一种机制。在TCP三次握手中,服务器在接收到客户端的SYN报文时,会创建一个半连接(半开放连接)状态,此时服务器会分配资源来处理该连接,但还未进行最终的ACK确认。
半连接池的目的是管理这些半连接状态,防止资源泄漏和恶意连接。以下是半连接池的一般工作流程:
- 接收SYN报文:
- 服务器接收到客户端发送的SYN报文,创建一个半连接,将连接放入半连接池。
- 资源分配:
- 为半连接分配资源,可能包括分配内存、创建数据结构等。
- 处理连接:
- 进行一些处理,可能涉及业务逻辑的初步检查。
- 发送SYN+ACK报文:
- 如果一切正常,服务器发送SYN+ACK报文,将连接状态从半连接改为已连接。
- 接收ACK报文:
- 等待客户端发送ACK报文,完成TCP三次握手。
- 连接完全建立:
- 一旦收到客户端的ACK报文,连接完全建立,服务器可以开始正常地进行数据传输。
- 超时处理和连接释放:
- 如果在一定时间内没有收到客户端的ACK报文,可以考虑进行超时处理,释放半连接资源。
- 简而言之,TCP协议通讯时,在同一时间只能于一位客户端通讯,其他客户端处于半连接状态。当客户端的半连接数量超过半连接池的最大容量时,将会拒绝客户端的连接请求。
- 半连接池的数量在
server.listen(int)
阶段声明
【4】UDP套接字编程
【4.1】UDP套接字工作流程
- 服务端server
- 创建套接字对象
socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
- 绑定地址
server.bind()
- 接收数据
data,addr = server.recfromv(1024)
- 【可选】发送数据
server.sendto(b'',addr)
- 关闭链接
server.close()
- 创建套接字对象
- 客户端client
- 创建套接字对象
socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- 发送数据
client.sendto(b'',(IP,PORT))
- 接收数据
data, addr = client.recvfrom(1024)
- 关闭连接
client.close()
- 创建套接字对象
【4.2】基于UDP协议的套接字编程
【4.2.1】通讯循环
-
基本模板就是开头常用方法中的,不再做赘述
-
服务端
import socket
# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 报式协议 # 也就是UDP协议
# 监听
server.bind((IP, PORT))
while True:
# 接收信息
data, addr = server.recvfrom(1024)
print(data)
if data == b'break':
server.sendto(data, addr)
break
# 回复信息
server.sendto(data.upper(), addr)
# 关闭连接
server.close()
- 服务端
import socket
# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
send_msg = input("【server will return x.upper()】input:").strip()
# 发送消息
client.sendto(send_msg.encode('utf8'), (IP, PORT))
# 接收消息
data, addr = client.recvfrom(1024)
print(data.decode('utf8'))
if data == b'break':
break
# 关闭连接
client.close()