同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
有三种io方式
blocking IO: 发起IO操作后阻塞当前线程直到IO结束,标准的同步IO,如默认行为的posix read和write。
non-blocking IO: 发起IO操作后不阻塞,用户可阻塞等待多个IO操作同时结束。non-blocking也是一种同步IO:“批量的同步”。如linux下的poll,select, epoll,BSD下的kqueue。
asynchronous IO: 发起IO操作后不阻塞,用户得递一个回调待IO结束后被调用。如windows下的OVERLAPPED + IOCP。linux的native AIO只对文件有效。
from socket import *
import time
#用来存储所有的新链接socket
g_socket_list = list()
def mian():
server_socket = socket(AF_INET,SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,True)
server_socket.bind(('',7890))
server_socket.listen(128)
server_socket.setblocking(False)
while True:
# time.sleep(0.1)
try:
newClientInfo = server_socket.accept()
except Exception as result:
print(result)
else:
print('一个新的客户端到来',str(newClientInfo))
newClientInfo[0].setblocking(False)#切换为非阻塞
g_socket_list.append(newClientInfo)
for clien_socket,client_addr in g_socket_list:
try:
recvData = clien_socket.recv(1024)
if recvData:
print('客户端已关闭',client_addr)
clien_socket.close()
g_socket_list.remove((clien_socket,client_addr))
except Exception as result:
print(result)
pass
print(g_socket_list)
if __name__ == '__main__':
mian()
简单来说,程序一直轮询io端口,如果没有响应不会发生阻塞等待有数据来领,而是抛出异常,跳过,继续轮询。
设置非阻塞scoket在server_socket.setblocking(False)
非阻塞acept,非阻塞connect,非阻塞write,非阻塞read等也是一样
import time
from socket import *
import sys
import re
class WSGIServer(object):
def __init__(self,port):
self.server_socket = socket(AF_INET,SOCK_STREAM)
self.server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,True)
self.server_socket.setblocking(False)
self.server_socket.bind(('',port))
self.server_socket.listen(128)
self.socket_list = []
self.root = '/Users/keith/Downloads/wurenjizhineng/www.aspku.com'
def run(self):
while True:
try:
new_socket,new_addr = self.server_socket.accept()
except Exception as e:
print('-----1------',e)
else:
new_socket.setblocking(False)
self.socket_list.append(new_socket)
#socket轮询了~
for client_socket in self.socket_list:
try:
recv_data = client_socket.recv(1024).decode('utf-8')
except Exception as e:
print('------2------',e)
else:
if recv_data:
self.deal_with_request(recv_data,client_socket)
else:
client_socket.close()
self.socket_list.remove(client_socket)
print(self.socket_list)
#处理请求
def deal_with_request(self,recv_data,client_socket):
if not recv_data:
return
request_lines =recv_data.splitlines()
for i ,line in enumerate(request_lines):
print(i,line)
#提取请求的文件(index.html)
#GET /a/b/c/d/e/index.html HTTP/1.1
ret =re.match(r'[^/]+([^ ])+',request_lines[0])
if ret:
print('正则提取数据:',ret.group(1))
file_name = ret.group(1)
if file_name == '/':
file_name = '/index.html'
try:
f = open(self.root+file_name,'rb')
except:
response_body = 'file not found,请输入正确的url'
response_line = 'HTTP/1.1 404\r\n'
response_header = 'Content-Type: text/html;charset=utf-8\r\n'
response_data = (response_line+response_line+'\r\n'+response_body).encode('utf-8')
#返回404
client_socket.send(response_data)
else:
content = f.read()
f.close()
response_body = content
response_line = 'HTTP/1.1 200 OK\r\n'
response_header = 'Content-Type: text/html;charset=utf-8\r\n\r\n'
response_data = (response_line+response_header).encode('utf-8')+response_body
#发送数据
client_socket.send(response_data)
def main():
if len(sys.argv) == 2:
port = sys.argv[1]
if port.isdigit():
port = int(port)
else:
print('按照格式输入')
return
print('服务器用的端口是',port)
http_server = WSGIServer(port)
http_server.run()
if __name__ == '__main__':
main()
就是我们常说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,epoll()函数就可以返回。 所以, IO多路复用,本质上不会有并发的功能,因为任何时候还是只有一个进程或线程进行工作,它之所以能提高效率是因为select\epoll 把进来的socket放到他们的 ‘监视’ 列表里面,当任何socket有可读可写数据立马处理,那如果select\epoll 手里同时检测着很多socket, 一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理高效率。
当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换页会耗掉更多的系统资源。 所以我们可以结合IO多路复用和多进程/多线程 来高性能并发,IO复用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑。
import socket
import select
# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
# 绑定本机信息
s.bind(("",7788))
# 变为被动
s.listen(10)
# 创建一个epoll对象
epoll = select.epoll()
# 测试,用来打印套接字对应的文件描述符
# print(s.fileno())
# print(select.EPOLLIN|select.EPOLLET)
# 注册事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已经注册过,则会发生异常
# 将创建的套接字添加到epoll的事件监听中
epoll.register(s.fileno(), select.EPOLLIN|select.EPOLLET)
connections = {}
addresses = {}
# 循环等待客户端的到来或者对方发送数据
while True:
# epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
epoll_list = epoll.poll()
# 对事件进行判断
for fd, events in epoll_list:
# print fd
# print events
# 如果是socket创建的套接字被激活
if fd == s.fileno():
new_socket, new_addr = s.accept()
print('有新的客户端到来%s' % str(new_addr))
# 将 conn 和 addr 信息分别保存起来
connections[new_socket.fileno()] = new_socket
addresses[new_socket.fileno()] = new_addr
# 向 epoll 中注册 新socket 的 可读 事件
epoll.register(new_socket.fileno(), select.EPOLLIN|select.EPOLLET)
# 如果是客户端发送数据
elif events == select.EPOLLIN:
# 从激活 fd 上接收
recvData = connections[fd].recv(1024).decode("utf-8")
if recvData:
print('recv:%s' % recvData)
else:
# 从 epoll 中移除该 连接 fd
epoll.unregister(fd)
# server 侧主动关闭该 连接 fd
connections[fd].close()
print("%s---offline---" % str(addresses[fd]))
del connections[fd]
del addresses[fd]