文章目录
- Python 中的Socket编程
- 一、数据传输方式
- 1.1 同步与异步
- 1.2 阻塞非阻塞
- 1.3 IO模型
- 1.3.1 同步阻塞
- 1.3.2 同步非阻塞
- 1.3.3 IO多路复用
- 1.3.4 异步
- 1.3.5 对比
- 二、Socket API
- 三、实验代码
- 3.1 同步阻塞
- 3.1.1 服务端
- 3.1.2 客户端
- 3.1.3 测试
- 3.2 非阻塞
- 3.2.1 服务端
- 3.2.2 用户端
- 3.2.3 测试
- 3.3 多路复用IO
- 3.3.1 服务端
- 3.3.2 客户端
- 3.3.3 测试
- 3.4 异步
- 3.4.1 服务端与客户端
- 3.4.2 测试
- 3.5 异步非阻塞
- 3.5.1 服务端
- 3.5.2 测试
Python 中的Socket编程
一、数据传输方式
考虑同步异步,阻塞非阻塞的时候,要从进程线程,用户态内核态的角度考虑
1.1 同步与异步
同步与异步是对于线程之间,区别在于当前调用无返回时,操作系统是否会去运行其他线程
同步调用:调用者需要等待被调用者返回结果,才会进行下一步操作
异步调用:调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果
1.2 阻塞非阻塞
阻塞和非阻塞对于同一个线程来说,区别在于线程等待消息的时候 , 当前进/线程是否挂起
阻塞调用:调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
非阻塞调用:在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
对比
1.3 IO模型
1.3.1 同步阻塞
图1.1 同步阻塞IO模型
当在用户态调用read操作的时候,如果这时候kernel还没有准备好数据,那么用户态会一直阻塞等待,直到有数据返回。当kernel准备好数据之后,用户态继续等待kernel把数据从内核态拷贝到用户态之后才可以使用。这里会发生两种等待:一个是用户态等待kernel有数据可以读,另外一个是当有数据可读时用户态等待数据从内核态拷贝到用户态。
1.3.2 同步非阻塞
无数据时返回异常
图1.2 同步非阻塞IO模型
对比第一张同步阻塞IO的图就会发现,在同步非阻塞模型下第一个阶段是不等待的,无论有没有数据准备好,都是立即返回。第二个阶段仍然是需要等待的,用户态需要等待内核态把数据拷贝过来才能使用。对于同步非阻塞模式的处理,需要每隔一段时间就去询问一下内核数据是不是可以读了,如果内核说可以,那么就开始第二阶段等待。
1.3.3 IO多路复用
图1.3 多路复用IO模型
select
,poll
,epoll
都是IO多路复用
的机制。I/O多路复用
就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select
,poll
,epoll
本质上都是同步I/O
,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O
则无需自己负责进行读写,异步I/O
的实现会负责把数据从内核
拷贝到用户空间
。
1.3.4 异步
图1.4 异步IO模型
异步模式下,前面提到的两个阶段都不会等待。使用异步模式,用户态调用read方法的时候,相当于告诉内核数据发送给我之后告诉我一声我先去干别的事情了。在这两个阶段都不会等待,只需要在内核态通知数据准备好之后使用即可。通常情况下使用异步模式都会使用callback,当数据可用之后执行callback函数。
异步阻塞
内核态阻塞,但在用户进程里,是多线程异步的。发送方向接收方请求后,不等待响应,可以继续其他工作。 接收方处理请求时进行IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作。
异步非阻塞
发送方向接收方请求后,不等待响应,可以继续其他工作。 接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回去做其他事情。 当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方。
1.3.5 对比
图1.5 IO模型对比
二、Socket API
图2 Socket API 的调用顺序
socket()
,的 socket 地址族参数socket.AF_INET
表示因特网 IPv4 地址族,SOCK_STREAM
表示使用 TCP 的 socket 类型,协议将被用来在网络中传输消息。也可以使用socket.SOCK_DGRAM
创建 UDP Socketbind()
用来关联 socket 到指定的网络接口(IP 地址)和端口号,bind()
方法的入参取决于 socket 的地址族listen()
方法有一个backlog
参数。它指定在拒绝新的连接之前系统将允许使用的 未接受的连接 数量。从 Python 3.5 开始,这是可选参数。如果不指定,Python 将取一个默认值
如果你的服务器需要同时接收很多连接请求,增加 backlog 参数的值可以加大等待链接请求队列的长度,最大长度取决于操作系统。accept()
方法阻塞并等待传入连接。当一个客户端连接时,它将返回一个新的 socket 对象,对象中有表示当前连接的 conn 和一个由主机、端口号组成的 IPv4/v6 连接的元组。必须记住调用accept()
方法拥有了一个新的 socket 对象,因为将用这个 socket 对象和客户端进行通信。和监听socket 不同的是后者只用来授受新的连接请求。connect()
方法,客户端调用它来建立与服务器的链接,并开始三次握手。
实例:(使用 with语句,这样就不用再手动调用s.close()
来关闭 socket 了)
三、实验代码
3.1 同步阻塞
3.1.1 服务端
import socket
HOST = '127.0.0.1'
PORT = 65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.bind((HOST,PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by',addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
3.1.2 客户端
import socket
HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 65432 # 监听的端口 (非系统级的端口: 大于 1023)
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((HOST,PORT))
s.sendall(b'Hello, world')
data = s.recv(1024)
print('Received', repr(data))
3.1.3 测试
图3.1 服务端输出
图3.2 客户端输出
3.2 非阻塞
图4.1 accept和recv存在阻塞现象
图4.2 阻塞过程
accept阻塞
在没有新的套接字来之前,不能处理已经建立连接的套接字的请求
recv 阻塞
在没有接受到客户端请求数据之前,不能与其他客户端建立连接
图4.3 非阻塞过程
非阻塞忙询:非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
3.2.1 服务端
import socket
HOST = '127.0.0.1'
PORT = 65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.setblocking(False) #设置为非阻塞
s.bind((HOST,PORT))
s.listen()
conns = []
while True:
try:
conn, addr = s.accept()
print('Connected by', addr)
conns.append(conn)
conn.setblocking(False)
except BlockingIOError as e:
pass
tmp_list = [conn for conn in conns]
for conn in tmp_list:
try:
data = conn.recv(1024) # 接收数据1024字节
if data:
print('收到的数据是{}'.format(data.decode()))
conn.send(data)
else:
print('close conn', conn)
conn.close()
conns.remove(conn)
print('还有客户端=>', len(conns))
except IOError:
pass
3.2.2 用户端
import socket
HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 65432 # 监听的端口 (非系统级的端口: 大于 1023)
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((HOST,PORT))
while True:
msg = input(">>>")
if msg != 'q':
s.send(msg.encode())
data = s.recv(1024)
print('收到的数据{}'.format(data.decode()))
else:
s.close()
print('close client socket')
break
3.2.3 测试
图5.1 服务端输出
图5.2 客户端1
图5.3 客户端2
可以看到在等待client的数据的时候依然可以接收tmp进程的输入(字符串"bb")
非阻塞IO模型缺点:不停地轮询recv,占用较多的CPU资源。 对应BlockingIOError的异常处理也是无效的CPU花费 (非阻塞中无数据时返回异常)
如何解决:多路复用IO
3.3 多路复用IO
python中有封装好的类socketServer
内部使用IO多路复用,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个线程或者“进程专门负责处理当前客户端的所有请求。
基于select
的多路复用:(epoll
效率更高,但Windows下不支持)
3.3.1 服务端
import select,socket,queue
HOST = '127.0.0.1'
PORT = 65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.bind((HOST,PORT))
s.listen()
s.setblocking(False)
msg_dic = dict() #定义一个队列字典
inputs = [s, ] # 由于设置成非阻塞模式,accept和recive都不阻塞了,没有值就会报错,因此最开始需要最开始需要监控服务端本身,等待客户端连接
outputs = []
while True:
# exceptional表示如果inputs列表中出现异常,会输出到这个exceptional中
readable, writeable, exceptional = select.select(inputs, outputs, inputs) # 如果没有任何客户端连接,就会阻塞在这里
for r in readable: # 没有个r代表一个socket链接
if r is s: # 如果这个socket是server的话,就说明是是新客户端连接了
conn, addr = r.accept() # 新连接进来了,接受这个连接,生成这个客户端实例
print("Connected by", addr)
inputs.append(conn) # 为了不阻塞整个程序,我们不会立刻在这里开始接收客户端发来的数据, 把它放到inputs里, 下一次loop时,这个新连接
# 就会被交给select去监听
msg_dic[conn] = queue.Queue() # 初始化一个队列,后面存要返回给这个客户端的数据
else: # 如果不是server,就说明是之前建立的客户端来数据了
data = r.recv(1024)
print('收到的数据是{}'.format(data.decode()))
msg_dic[r].put(data) # 收到的数据先放到queue里,一会返回给客户端
outputs.append(r) # 为了不影响处理与其它客户端的连接 , 这里不立刻返回数据给客户端
for w in writeable: # 要返回给客户端的链接列表
data_to_client = msg_dic[w].get()
w.send(data_to_client) # 返回给客户端的源数据
outputs.remove(w) # 确保下次循环的时候writeable,不返回这个已经处理完的这个连接了
for e in exceptional: # 处理异常的连接
if e in outputs: # 因为e不一定在outputs,所以先要判断
outputs.remove(e)
inputs.remove(e) # 删除inputs中异常连接
del msg_dic[e] # 删除此连接对应的队列
3.3.2 客户端
和上一个一样
3.3.3 测试
和上一次测试一样开了两个客户端,在关闭其中一个客户端之后服务端输出如下
图7 客户端输出
可以看到select一直在主动查询数据
3.4 异步
asyncore
,socketserver
都是异步socket的包装,下面基于socketserver
实现一个异步服务器。
(貌似最好用Twisted
暂时没看)
3.4.1 服务端与客户端
import socket
import threading
import socketserver
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
"""
处理request的类
self.request代表当前的request对象,self.server代表服务器对象。
对于TCP链接,self.request是当前request的socket。
此处实现的是回显,对于客户端发送信息返回线程名和信息
"""
def handle(self):
data = self.request.recv(1024)
cur_thread = threading.current_thread()
response = f"{cur_thread.name}: {data}"
self.request.sendall(bytes(response, encoding = "utf8"))
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""
混合类ThreadingMixin,产生一个新的线程,提供异步处理的能力
"""
pass
def client(ip, port, message):
"""
客户端
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))
try:
sock.sendall(message)
response = sock.recv(1024)
print (f'收到的数据是: {response}')
finally:
sock.close()
HOST, PORT = '127.0.0.1', 0 #0意味着选择任意未使用的端口
server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
ip, port = server.server_address
# 服务器会启动一个线程
# 以后的每次request会开启新线程
server_thread = threading.Thread(target=server.serve_forever)
# 主程序终止时退出服务器线程
server_thread.daemon = True
server_thread.start()
print ("服务器线程:", server_thread.name)
client(ip, port, b"1")
client(ip, port, b"2")
client(ip, port, b"3")
server.shutdown()
server.server_close()
3.4.2 测试
图8 程序输出
可以看到每个客户端开启一个新线程
3.5 异步非阻塞
3.5.1 服务端
import socket
import threading
import socketserver
HOST = '127.0.0.1'
PORT = 65432
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
print(f'与{ self.client_address}连接')
while True:
data = self.request.recv(1024)
if not data:
break
cur_thread = threading.current_thread()
response = f"{cur_thread.name}: {data}"
self.request.sendall(bytes(response, encoding = "utf8"))
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
server.serve_forever()
3.5.2 测试
图9 服务器输出
图10 客户端1输出
图11 客户端2输出