1.利用异常处理和非阻塞

  1. # ################### socket服务端(接收者)###################
  2. import socket
  3. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  4. sock.setblocking(False) # 加上就变为了非阻塞
  5. sock.bind(('127.0.0.1', 8001))
  6. sock.listen(5)
  7. # 非阻塞
  8. conn, addr = sock.accept()
  9. # 非阻塞
  10. client_data = conn.recv(1024)
  11. print(client_data.decode('utf-8'))
  12. conn.close()
  13. sock.close()
  14. # ################### socket客户端(发送者) ###################
  15. import socket
  16. client = socket.socket()
  17. client.setblocking(False) # 加上就变为了非阻塞
  18. # 非阻塞
  19. client.connect(('127.0.0.1', 8001))
  20. client.sendall('alex正在吃翔'.encode('utf-8'))
  21. client.close()

image.png
如果代码变成了非阻塞,程序运行时一旦遇到 accept、recv、connect 就会抛出 BlockingIOError 的异常。
这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。
非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。

2.IO多路复用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

2.1服务端IO多路复用

IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求,例如:

  1. # ################### socket服务端 ###################
  2. import select
  3. import socket
  4. server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  5. server.setblocking(False) # 加上就变为了非阻塞
  6. server.bind(('127.0.0.1', 8001))
  7. server.listen(5)
  8. inputs = [server, ] # socket对象列表 -> [server, 第一个客户端连接conn ]
  9. while True:
  10. # 当 参数1 序列中的socket对象发生可读时(accetp和read),则获取发生变化的对象并添加到 r列表中。
  11. # r = []
  12. # r = [server,]
  13. # r = [第一个客户端连接conn,]
  14. # r = [server,]
  15. # r = [第一个客户端连接conn,第二个客户端连接conn]
  16. # r = [第二个客户端连接conn,]
  17. r, w, e = select.select(inputs, [], [], 0.05)
  18. for sock in r:
  19. # server
  20. if sock == server:
  21. conn, addr = sock.accept() # 接收新连接。
  22. print("有新连接")
  23. # conn.sendall()
  24. # conn.recv("xx")
  25. inputs.append(conn)
  26. else:
  27. data = sock.recv(1024)
  28. if data:
  29. print("收到消息:", data)
  30. else:
  31. print("关闭连接")
  32. inputs.remove(sock)
  33. # 干点其他事 20s
  34. """
  35. 优点:
  36. 1. 干点那其他的事。
  37. 2. 让服务端支持多个客户端同时来连接。
  38. """

可以处理多个客户端

  1. # ################### socket客户端 ###################
  2. import socket
  3. client = socket.socket()
  4. # 阻塞
  5. client.connect(('127.0.0.1', 8001))
  6. while True:
  7. content = input(">>>")
  8. if content.upper() == 'Q':
  9. break
  10. client.sendall(content.encode('utf-8'))
  11. client.close()
  1. # ################### socket客户端 ###################
  2. import socket
  3. client = socket.socket()
  4. # 阻塞
  5. client.connect(('127.0.0.1', 8001))
  6. while True:
  7. content = input(">>>")
  8. if content.upper() == 'Q':
  9. break
  10. client.sendall(content.encode('utf-8'))
  11. client.close() # 与服务端断开连接(四次挥手),默认会想服务端发送空数据。

2.2客户端IO多路复用

IO多路复用 + 非阻塞,可以实现让TCP的客户端同时发送多个请求,例如:去某个网站发送下载图片的请求。

  1. import socket
  2. import select
  3. import uuid
  4. import os
  5. client_list = [] # socket对象列表
  6. for i in range(5):
  7. client = socket.socket()
  8. client.setblocking(False)
  9. try:
  10. # 连接百度,虽然有异常BlockingIOError,但向还是正常发送连接的请求
  11. client.connect(('47.98.134.86', 80))
  12. except BlockingIOError as e:
  13. pass
  14. client_list.append(client)
  15. recv_list = [] # 放已连接成功,且已经把下载图片的请求发过去的socket
  16. while True:
  17. # w = [第一个socket对象,]
  18. # r = [socket对象,]
  19. r, w, e = select.select(recv_list, client_list, [], 0.1)
  20. for sock in w:
  21. # 连接成功,发送数据
  22. # 下载图片的请求
  23. sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
  24. recv_list.append(sock)
  25. client_list.remove(sock)
  26. for sock in r:
  27. # 数据发送成功后,接收的返回值(图片)并写入到本地文件中
  28. data = sock.recv(8196)
  29. content = data.split(b'\r\n\r\n')[-1]
  30. random_file_name = "{}.png".format(str(uuid.uuid4()))
  31. with open(os.path.join("images", random_file_name), mode='wb') as f:
  32. f.write(content)
  33. recv_list.remove(sock)
  34. if not recv_list and not client_list:
  35. break
  36. """
  37. 优点:
  38. 1. 可以伪造除并发的现象。
  39. """

3.IO多路复用其他用途

基于 IO多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中

  • IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。
  • 非阻塞,socket的connect、recv过程不再等待。

注意:IO多路复用只能用来监听 IO对象 是否发生变化,常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。

socket + 非阻塞+ IO多路复用(IO操作对象都可以监测 + 文件)。

4.IO多路复用其他模式

在Linux操作系统化中 IO多路复用 有三种模式,分别是:select,poll,epoll。(windows 只支持select模式)

  1. select (有上限,1024,线性机制)
  2. select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
  3. select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
  4. select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
  5. 另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
  6. poll (无上限,线性机制)
  7. poll1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
  8. pollselect同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
  9. 另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
  10. epoll (无上限,回调机制——只有Linux能用)
  11. 直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
  12. epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
  13. epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
  14. 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。