Python学习笔记整理总结【网络编程】【线程/进程/协程/IO多路模型/select/poll/epoll/selector】

一、socket(单链接)

1、socket:应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面;也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序;而程序的pid是同一台机器上不同进程或者线程的标识。

2、套接字:用于在同一台主机上多个应用程序之间的通讯。套接字有两种(或者称为有两个种族),分别是基于文件型(AF_UNIX)和基于网络型(AF_INET)。

3、基于TCP的套接字(类型一)

工作原理:先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket                         #导入socket模块
 5 ip_port = ("127.0.0.1",9999)          #设置服务器ip和端口
 6 server = socket.socket()              #创建server实例   //声明socket类型同时生成socket对象
 7 server.bind(ip_port)                  #套接字绑定ip与端口
 8 server.listen(5)                      #监听连接//允许5个客户端排队
 9 conn,addr = server.accept()           #等待客户端连接   // 客户端连接后,返回新的套接字与IP地址
10 client_data = conn.recv(1024)         #接收数据//把接收的数据实例化
11 #client_data = b'hello'
12 conn.sendall(client_data.upper())     #把数据发送到客户端  //upper() 字母变成大写
13 conn.close()                          #关闭连接
#TCP_server
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket                         #导入socket模块
 5 ip_port = ("127.0.0.1",9999)          #设置服务器ip和端口
 6 client = socket.socket()              #创建client实例
 7 client.connect(ip_port)               #设置要连接的ip和端口
 8 info = "hello world"                  #要发送的数据
 9 client.sendall(info.encode("utf-8"))  #发送数据// 把str转换为bytes类型
10 server_data = client.recv(1024)       #接收数据
11 client.close()                        #关闭连接
#TCP_client
 1 ① server = socket.socket()
 2 套接字格式:socket(family,type[,protocal]) 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。
 3 参数一:地址簇
 4   socket.AF_INET IPv4(默认)
 5   socket.AF_INET6 IPv6
 6   socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
 7 参数二:类型
 8   socket.SOCK_STREAM  流式socket  ,    for TCP (默认)
 9   socket.SOCK_DGRAM   数据报式socket , for UDP
10   socket.SOCK_RAW        原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
11   socket.SOCK_RDM       是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
12   socket.SOCK_SEQPACKET 可靠的连续数据包服务
13 参数三:协议
14   0  (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
15 #创建TCP Socket:server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
16 #创建UDP Socket:server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
17 注意点:
18 1)TCP发送数据时,已建立好TCP连接,所以不需要指定地址。UDP是面向无连接的,每次发送要指定是发给谁。
19 2)服务端与客户端不能直接发送列表,元组,字典。需要字符串化repr(data)
20 ② server.bind(address)
21   将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址
22 ③ server.listen(backlog)
23   开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5,这个值不能无限大,因为要在内核中维护连接队列
24 ④ server.setblocking(bool)
25   是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错
26 ⑤ conn,addr = server.accept() 
27   接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。接收TCP 客户的连接(阻塞式)等待连接的到来
28 ⑥ client.connect(address)
29   连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
30 ⑦ client.connect_ex(address)
31   同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
32 ⑧ client.close()
33   关闭套接字
34 ⑨ client.recv(bufsize[,flag])
35   接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略
36 ⑩ client.recvfrom(bufsize[.flag])
37   与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址
38 ⑪ server.send(string[,flag])
39   发送TCP数据;将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送
40 ⑫ server.sendall(string[,flag])  
41   完整发送TCP数据;将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常;内部通过递归调用send,将所有内容发送出去
42 ⑬ server.sendto(string[,flag],address)
43   将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议
44 ⑭ sk.settimeout(timeout)
45   设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
46 ⑮ sk.getpeername()
47   返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)
48 ⑯ sk.getsockname()
49   返回套接字自己的地址。通常是一个元组(ipaddr,port)
50 ⑰ sk.fileno()
51 套接字的文件描述符
52 
53 
54 ###服务端套接字函数###
55 s.bind()    #绑定(主机,端口号)到套接字
56 s.listen()  #开始TCP监听
57 s.accept()  #被动接受TCP客户的连接,(阻塞式)等待连接的到来
58 
59 ###客户端套接字函数###
60 s.connect()     #主动初始化TCP服务器连接
61 s.connect_ex()  #connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
62 
63 ###公共用途的套接字函数###
64 s.recv()            #接收TCP数据
65 s.send()            #发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
66 s.sendall()      #发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
67 s.recvfrom()        #接收UDP数据
68 s.sendto()          #发送UDP数据
69 s.getpeername()     #连接到当前套接字的远端的地址
70 s.getsockname()     #当前套接字的地址
71 s.getsockopt()      #返回指定套接字的参数
72 s.setsockopt()      #设置指定套接字的参数
73 s.close()           #关闭套接字
74 
75 ###面向锁的套接字方法###
76 s.setblocking()     #设置套接字的阻塞与非阻塞模式
77 s.settimeout()      #设置阻塞套接字操作的超时时间
78 s.gettimeout()      #得到阻塞套接字操作的超时时间
79 
80 ###面向文件的套接字的函数###
81 s.fileno()          #套接字的文件描述符
82 s.makefile()        #创建一个与该套接字相关的文件
#参数功能解释

4、基于UDP的套接字(类型二)
udp是无链接的,先启动哪一端都不会报错且可以同时多个客户端去跟服务端通信

 1 #UDP server
 2 ss = socket()                      #创建一个服务器的套接字
 3 ss.bind()                          #绑定服务器套接字
 4 inf_loop:                          #服务器无限循环
 5     cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
 6 ss.close()                         # 关闭服务器套接字
 7 
 8 
 9 #UDP client
10 cs = socket()                   # 创建客户套接字
11 comm_loop:                      # 通讯循环
12     cs.sendto()/cs.recvfrom()   # 对话(发送/接收)
13 cs.close()                      # 关闭客户套接字
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket
 5 ip_port=('127.0.0.1',9000)
 6 BUFSIZE=1024
 7 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
 8 udp_server_client.bind(ip_port)
 9 
10 while True:
11     msg,addr=udp_server_client.recvfrom(BUFSIZE)
12     print(msg,addr)
13     udp_server_client.sendto(msg.upper(),addr)
#UDP_server
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket
 5 ip_port=('127.0.0.1',9000)
 6 BUFSIZE=1024
 7 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
 8 
 9 while True:
10     msg=input('>>: ').strip()
11     if not msg:continue
12     udp_server_client.sendto(msg.encode('utf-8'),ip_port)
13     back_msg,addr=udp_server_client.recvfrom(BUFSIZE)
14     print(back_msg.decode('utf-8'),addr)
15 udp_client_socket.close()
#UDP_client

##qq聊天(由于udp无连接,所以可以同时多个客户端去跟服务端通信)##

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket
 5 ip_port=('127.0.0.1',8081)
 6 udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #买手机
 7 udp_server_sock.bind(ip_port)
 8 
 9 while True:
10     qq_msg,addr=udp_server_sock.recvfrom(1024)
11     print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
12     back_msg=input('回复消息: ').strip()
13 
14     udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
#UDP_server
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket
 5 BUFSIZE=1024
 6 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
 7 
 8 qq_name_dic={
 9     '狗哥alex':('127.0.0.1',8081),
10     '瞎驴':('127.0.0.1',8081),
11     '一棵树':('127.0.0.1',8081),
12     '武大郎':('127.0.0.1',8081),
13 }
14 
15 
16 while True:
17     qq_name=input('请选择聊天对象: ').strip()
18     while True:
19         msg=input('请输入消息,回车发送: ').strip()
20         if msg == 'quit':break
21         if not msg or not qq_name or qq_name not in qq_name_dic:continue
22         udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
23 
24         back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
25         print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
26 
27 udp_client_socket.close()
#UDP_client1
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import socket
 5 BUFSIZE=1024
 6 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
 7 
 8 qq_name_dic={
 9     '狗哥alex':('127.0.0.1',8081),
10     '瞎驴':('127.0.0.1',8081),
11     '一棵树':('127.0.0.1',8081),
12     '武大郎':('127.0.0.1',8081),
13 }
14 
15 
16 while True:
17     qq_name=input('请选择聊天对象: ').strip()
18     while True:
19         msg=input('请输入消息,回车发送: ').strip()
20         if msg == 'quit':break
21         if not msg or not qq_name or qq_name not in qq_name_dic:continue
22         udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
23 
24         back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
25         print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
26 
27 udp_client_socket.close()
#UDP_client2

5、粘包现象:①发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,TCP有一个Nagle算法会把数据合到一起,产生粘包)
                       ②接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

原因:接收方不知道消息之间的界限,不知道一次性提取多少字节的数据。

TCP有粘包现象,UDP永远不会粘包:tcp是基于数据流的,收发两端都要有一一成对的socket,TCP采用Nagle优化算法消息进行消息处理机制,面向流的通信是无消息保护边界的。而udp是基于数据报的,支持的是一对多的模式,套接字缓冲区采用了链式结构来记录每一个到达的UDP包,在每个UDP包中添加了消息头(消息来源地址,端口等信息),面向消息的通信是有消息保护边界的。

tcp是可靠传输,udp是不可靠传输:tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对应端,对应端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的;而udp发送数据,对端是不会返回确认信息的,因此不可靠。

解决粘包:问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

第一种解决方案:(low)

 1 #server端
 2 
 3 #!/usr/bin/env python
 4 # -*- coding:utf-8 -*-
 5 # _author_soloLi
 6 from socket import *    #由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
 7 import subprocess
 8 ip_port=('127.0.0.1',8080)
 9 back_log=5
10 buffer_size=1024
11 
12 server=socket(AF_INET,SOCK_STREAM)
13 server.bind(ip_port)
14 server.listen(back_log)
15 
16 while True:       #链接循环
17     conn,addr=server.accept()
18     print('新的客户端链接',addr)
19     while True:   #通信循环
20         
21         ##//收数据//##
22         try:
23             cmd=conn.recv(buffer_size)
24             if not cmd:break
25             print('收到客户端的命令',cmd)
26 
27             #执行命令,得到命令的运行结果cmd_res
28             
29             #subprocess模块提供了一种一致的方法来创建和处理附加进程,与标准库中的其它模块相比,提供了一个更高级的接口。用于替换如下模块:os.system() , os.spawnv() , os和popen2模块中的popen()函数,以及 commands(). 
30             res=subprocess.Popen(cmd.decode('utf-8'),shell=True, #解码(bytes->str)
31                                  stderr=subprocess.PIPE,
32                                  stdout=subprocess.PIPE,
33                                  stdin=subprocess.PIPE)
34             
35             err=res.stderr.read()
36             if err:
37                 cmd_res=err
38             else:
39                 cmd_res=res.stdout.read()                ##编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码且只能从管道里读一次结果
40 
41          ##//发数据//##
42             if not cmd_res:
43                 cmd_res='执行成功'.encode('gbk')    #转换编码(gbk->unicode)(转换显示中文的字符串)
44 
45             length=len(cmd_res)
46             conn.send(str(length).encode('utf-8'))  #编码(str->bytes)
47             client_ready=conn.recv(buffer_size)
48             if client_ready == b'ready':
49                 conn.send(cmd_res)
50         except Exception as e:
51             print(e)
52             break
TCP_serve
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 from socket import *
 5 ip_port=('127.0.0.1',8080)
 6 back_log=5
 7 buffer_size=1024
 8 
 9 client=socket(AF_INET,SOCK_STREAM)
10 client.connect(ip_port)
11 
12 while True:
13     cmd=input('>>: ').strip()
14     if not cmd:continue
15     if cmd == 'quit':break
16 
17     client.send(cmd.encode('utf-8'))  #编码(str->bytes)
18 
19     #解决粘包
20     length=client.recv(buffer_size) 
21     client.send(b'ready')
22 
23     length=int(length.decode('utf-8')) #解码(bytes->str)
24 
25     recv_size=0
26     recv_msg=b''
27     while recv_size < length:
28         recv_msg += tcp_client.recv(buffer_size)
29         recv_size=len(recv_msg) #1024
30 
31     print('命令的执行结果是 ',recv_msg.decode('gbk')) #转换编码(gbk->unicode)(转换显示中文的字符串)
32 client.close()
TCP_client

low的原因:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

第二种解决方案:(NB)

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 from socket import *    #由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
 5 import subprocess
 6 ip_port=('127.0.0.1',8080)
 7 back_log=5
 8 buffer_size=1024
 9 
10 server=socket(AF_INET,SOCK_STREAM)
11 server.bind(ip_port)
12 server.listen(back_log)
13 
14 while True:       #链接循环
15     conn,addr=server.accept()
16     print('新的客户端链接',addr)
17     while True:   #通信循环
18         
19         ##//收数据//##
20         try:
21             cmd=conn.recv(buffer_size)
22             if not cmd:break
23             print('收到客户端的命令',cmd)
24 
25             #执行命令,得到命令的运行结果cmd_res
26             
27             #subprocess模块提供了一种一致的方法来创建和处理附加进程,与标准库中的其它模块相比,提供了一个更高级的接口。用于替换如下模块:os.system() , os.spawnv() , os和popen2模块中的popen()函数,以及 commands(). 
28             res=subprocess.Popen(cmd.decode('utf-8'),shell=True, #解码(bytes->str)
29                                  stderr=subprocess.PIPE,
30                                  stdout=subprocess.PIPE,
31                                  stdin=subprocess.PIPE)
32             
33             err=res.stderr.read()
34             if err:
35                 cmd_res=err
36             else:
37                 cmd_res=res.stdout.read()                ##编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码且只能从管道里读一次结果
38 
39          ##//发数据//##
40             if not cmd_res:
41                 cmd_res='执行成功'.encode('gbk')    #转换编码(gbk->unicode)(转换显示中文的字符串)
42 
43             length=len(cmd_res)
44             conn.send(str(length).encode('utf-8'))  #编码(str->bytes)
45             client_ready=conn.recv(buffer_size)
46             if client_ready == b'ready':
47                 conn.send(cmd_res)
48         except Exception as e:
49             print(e)
50             break
TCP_server
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 from socket import *
 5 import struct                  ##NB##//struct模块可以把一个类型,如数字,转成固定长度的bytes 
 6 from functools import partial  ##NB##
 7 ip_port=('127.0.0.1',8080)
 8 back_log=5
 9 buffer_size=1024
10 
11 client=socket(AF_INET,SOCK_STREAM)
12 client.connect(ip_port)
13 
14 while True:
15     cmd=input('>>: ').strip()
16     if not cmd:continue
17     if cmd == 'quit':break
18 
19     client.send(cmd.encode('utf-8'))  #编码(str->bytes)
20 
21     #解决粘包
22     length_data=tcp_client.recv(4)##NB##为字节流加上自定义固定长度报头,报头中包含字节流长度,
23                                        #然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据     
24     length=struct.unpack('i',length_data)[0] ##NB##
25     length=int(length.decode('utf-8')) #解码(bytes->str)
26 
27     recv_size=0
28     recv_msg=b''
29     while recv_size < length:
30         recv_msg += tcp_client.recv(buffer_size)
31         recv_size=len(recv_msg) #1024
32 
33     print('命令的执行结果是 ',recv_msg.decode('gbk')) #转换编码(gbk->unicode)(转换显示中文的字符串)
34 client.close()
TCP_client

第二种方案的引申:(NBST)

 1 import json,struct
 2 #假设通过客户端上传1T:1073741824000的文件a.txt
 3 
 4 #为避免粘包,必须自定制报头
 5 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值
 6 
 7 #为了该报头能传送,需要序列化并且转为bytes
 8 head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输
 9 
10 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
11 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
12 
13 #客户端开始发送
14 conn.send(head_len_bytes) #先发报头的长度,4个bytes
15 conn.send(head_bytes) #再发报头的字节格式
16 conn.sendall(文件内容) #然后发真实内容的字节格式
17 
18 #服务端开始接收
19 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
20 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
21 
22 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
23 header=json.loads(json.dumps(header)) #提取报头
24 
25 #最后根据报头的内容提取真实的数据,比如
26 real_data_len=s.recv(header['file_size'])
27 s.recv(real_data_len)
 1 #我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
 2 #发送时:
 3 1.先发报头长度
 4 2.再编码报头内容然后发送
 5 3.最后发真实内容
 6 
 7 #接收时:
 8 1.先手报头长度,用struct取出来
 9 2.根据取出的长度收取报头内容,然后解码,反序列化
10 3.从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
 1 import socket,struct,json
 2 import subprocess
 3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
 5 
 6 phone.bind(('127.0.0.1',8080))
 7 
 8 phone.listen(5)
 9 
10 while True:
11     conn,addr=phone.accept()
12     while True:
13         cmd=conn.recv(1024)
14         if not cmd:break
15         print('cmd: %s' %cmd)
16 
17         res=subprocess.Popen(cmd.decode('utf-8'),
18                              shell=True,
19                              stdout=subprocess.PIPE,
20                              stderr=subprocess.PIPE)
21         err=res.stderr.read()
22         print(err)
23         if err:
24             back_msg=err
25         else:
26             back_msg=res.stdout.read()
27 
28         headers={'data_size':len(back_msg)}
29         head_json=json.dumps(headers)
30         head_json_bytes=bytes(head_json,encoding='utf-8')
31 
32         conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
33         conn.send(head_json_bytes) #再发报头
34         conn.sendall(back_msg) #在发真实的内容
35 
36     conn.close()
37 
38  服务端:定制稍微复杂一点的报头
TCP_server
 1 from socket import *
 2 import struct,json
 3 
 4 ip_port=('127.0.0.1',8080)
 5 client=socket(AF_INET,SOCK_STREAM)
 6 client.connect(ip_port)
 7 
 8 while True:
 9     cmd=input('>>: ')
10     if not cmd:continue
11     client.send(bytes(cmd,encoding='utf-8'))
12 
13     head=client.recv(4)
14     head_json_len=struct.unpack('i',head)[0]
15     head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
16     data_len=head_json['data_size']
17 
18     recv_size=0
19     recv_data=b''
20     while recv_size < data_len:
21         recv_data+=client.recv(1024)
22         recv_size+=len(recv_data)
23 
24     print(recv_data.decode('utf-8'))
25     #print(recv_data.decode('gbk')) #windows默认gbk编码
26 
27 客户端
TCP_client

 

二、socketserver(多链接)

基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环
socket:收到一个链接后直接进入通信循环,其他链接要等之前的那个链接通信循环结束后才能进入通信循环
socketserver:模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
socketserver模块是标准库中很多服务器框架的基础。内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。
ThreadingTCPServer(多线程,真并发)实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。
使用ThreadingTCPServer:
1、创建一个继承自 SocketServer.BaseRequestHandler 的类
2、类中必须定义一个名称为 handle 的方法
3、启动ThreadingTCPServer

 1 from socket import *
 2 import subprocess
 3 import struct
 4 ip_port=('127.0.0.1',8080)
 5 back_log=5
 6 buffer_size=1024
 7 
 8 tcp_server=socket(AF_INET,SOCK_STREAM)
 9 tcp_server.bind(ip_port)
10 tcp_server.listen(back_log)
11 
12 while True:
13     conn,addr=tcp_server.accept()
14     print('新的客户端链接',addr)
15     while True:
16         #
17         try:
18             cmd=conn.recv(buffer_size)
19             if not cmd:break
20             print('收到客户端的命令',cmd)
21 
22             #执行命令,得到命令的运行结果cmd_res
23             res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
24                                  stderr=subprocess.PIPE,
25                                  stdout=subprocess.PIPE,
26                                  stdin=subprocess.PIPE)
27             err=res.stderr.read()
28             if err:
29                 cmd_res=err
30             else:
31                 cmd_res=res.stdout.read()
32 
33             #
34             if not cmd_res:
35                 cmd_res='执行成功'.encode('gbk')
36 
37             length=len(cmd_res)
38 
39             data_length=struct.pack('i',length)
40             conn.send(data_length)
41             conn.send(cmd_res)
42         except Exception as e:
43             print(e)
44             break
#socket_TCP_server
 1 from socket import *
 2 import struct
 3 from functools import partial
 4 ip_port=('127.0.0.1',8080)
 5 back_log=5
 6 buffer_size=1024
 7 
 8 tcp_client=socket(AF_INET,SOCK_STREAM)
 9 tcp_client.connect(ip_port)
10 
11 while True:
12     cmd=input('>>: ').strip()
13     if not cmd:continue
14     if cmd == 'quit':break
15 
16     tcp_client.send(cmd.encode('utf-8'))
17 
18 
19     #解决粘包
20     length_data=tcp_client.recv(4)
21     length=struct.unpack('i',length_data)[0]
22 
23     recv_size=0
24     recv_data=b''
25     while recv_size < length:
26         recv_data+=tcp_client.recv(buffer_size)
27         recv_size=len(recv_data)
28     print('命令的执行结果是 ',recv_data.decode('gbk'))
29 tcp_client.close()
#socket_TCP_client1
 1 from socket import *
 2 import struct
 3 from functools import partial
 4 ip_port=('127.0.0.1',8080)
 5 back_log=5
 6 buffer_size=1024
 7 
 8 tcp_client=socket(AF_INET,SOCK_STREAM)
 9 tcp_client.connect(ip_port)
10 
11 while True:
12     cmd=input('>>: ').strip()
13     if not cmd:continue
14     if cmd == 'quit':break
15 
16     tcp_client.send(cmd.encode('utf-8'))
17 
18 
19     #解决粘包
20     length_data=tcp_client.recv(4)
21     length=struct.unpack('i',length_data)[0]
22 
23     recv_size=0
24     recv_data=b''
25     while recv_size < length:
26         recv_data+=tcp_client.recv(buffer_size)
27         recv_size=len(recv_data)
28     print('命令的执行结果是 ',recv_data.decode('gbk'))
29 tcp_client.close()
#socket_TCP_client2

 

 1 import socketserver
 2 
 3 
 4 '''
 5     def __init__(self, request, client_address, server):
 6         self.request = request
 7         self.client_address = client_address
 8         self.server = server
 9         self.setup()
10         try:
11             self.handle()
12         finally:
13             self.finish()
14 
15 '''
16 
17 class MyServer(socketserver.BaseRequestHandler):
18 
19     def handle(self):
20         print('conn is: ',self.request)   #conn
21         print('addr is: ',self.client_address) #addr
22 
23         while True:
24             try:
25             #收消息
26                 data=self.request.recv(1024)
27                 if not data:break
28                 print('收到客户端的消息是',data,self.client_address)
29 
30                 #发消息
31                 self.request.sendall(data.upper())
32 
33             except Exception as e:
34                 print(e)
35                 break
36 
37 if __name__ == '__main__':
38     s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) #多线程
39     # s=socketserver.ForkingTCPServer(('127.0.0.1',8080),MyServer) #多进程
40 
41     # self.server_address = server_address
42     # self.RequestHandlerClass = RequestHandlerClass
43     print(s.server_address)
44     print(s.RequestHandlerClass)
45     print(MyServer)
46     print(s.socket)
47     print(s.server_address)
48     s.serve_forever()
#socketserver_TCP_server
 1 from socket import *
 2 ip_port=('192.168.12.63',8080)
 3 back_log=5
 4 buffer_size=1024
 5 
 6 tcp_client=socket(AF_INET,SOCK_STREAM)
 7 tcp_client.connect(ip_port)
 8 
 9 while True:
10     msg=input('>>: ').strip()
11     if not msg:continue
12     if msg == 'quit':break
13 
14     tcp_client.send(msg.encode('utf-8'))
15 
16     data=tcp_client.recv(buffer_size)
17     print('收到服务端发来的消息:',data.decode('utf-8'))
18 
19 tcp_client.close()
#socketserver_TCP_client1
 1 from socket import *
 2 import struct
 3 from functools import partial
 4 ip_port=('192.168.12.63',8080)
 5 back_log=5
 6 buffer_size=1024
 7 
 8 tcp_client=socket(AF_INET,SOCK_STREAM)
 9 tcp_client.connect(ip_port)
10 
11 while True:
12     msg=input('>>: ').strip()
13     if not msg:continue
14     if msg == 'quit':break
15 
16     tcp_client.send(msg.encode('utf-8'))
17 
18     data=tcp_client.recv(buffer_size)
19     print('收到服务端发来的消息:',data.decode('utf-8'))
20 
21 
22 
23 tcp_client.close()
#socketserver_TCP_client2

源码剖析:

     server类:

 

        request类:

 

     继承关系:

 基于TCP的socketserver源码

ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()

查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer
  1、实例化得到ftpserver,先找类ThreadingTCPServer的__init__,在TCPServer中找到,进而执行server_bind,server_active
  2、找ftpserver下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
  3、执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)

  4、在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)

  5、上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....

源码分析总结:

基于tcp的socketserver我们自己定义的类中的
  1、self.server即套接字对象
    2、self.request即一个链接
    3、self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的
  self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  self.client_address即客户端地址

SocketServerThreadingTCPServer之所以可以同时处理请求得益于 select 和 Threading 两个东西,其实本质上就是在服务器端为每一个客户端创建一个线程,当前线程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。

 

三、线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

1、threading模块建立在thread 模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading 模块通过对thread进行二次封装,提供了更方便的api来处理线程。
线程创建有2种方式:
①直接调用(主流写法)

 1 import  threading,time
 2 def run(n):       #定义每个线程要运行的函数
 3     print("test...",n)
 4     time.sleep(2)
 5 if __name__ == '__main__':
 6     t1 = threading.Thread(target=run,args=("t1",))  #生成一个线程实例   //要加“,”号
 7     t2 = threading.Thread(target=run,args=("t2",))  #生成另一个线程实例
 8     # 两个同时执行,然后等待两秒程序结束
 9     t1.start()#启动线程
10     t2.start()#启动另一个线程
11     print(t1.getName()) #获取线程名
12     print(t2.getName())
13 # 程序输出
14 # test... t1
15 # test... t2
View Code

②继承式调用(非主流写法)(了解)

 1 import threading,time
 2 class MyThread(threading.Thread):
 3     def __init__(self,num):
 4        # threading.Thread.__init__(self)
 5         super(MyThread,self).__init__()
 6         self.num =num
 7     def run(self):#定义每个线程要运行的函数
 8         print("running on number:%s" %self.num)
 9         time.sleep(2)
10 if __name__ == '__main__':
11     # 两个同时执行,然后等待两秒程序结束
12     t1 = MyThread(1)
13     t2 = MyThread(2)
14     t1.start()
15     t2.start()
16 # 程序输出
17 # running on number:1
18 # running on number:2
View Code

2、join(等待线程):等待线程执行完后,其他线程再继续执行(串行)

 1 import  threading,time
 2 def run(n,sleep_time):
 3     print("test...",n)
 4     time.sleep(sleep_time)
 5     print("test...done", n)
 6 if __name__ == '__main__':
 7     t1 = threading.Thread(target=run,args=("t1",2))
 8     t2 = threading.Thread(target=run,args=("t2",3)
 9     # 两个同时执行,然后等待t1执行完成后,主线程和子线程再开始执行
10     t1.start()
11     t2.start()
12     t1.join()   # 等待t1
13     print("main thread")
14 # 程序输出
15 # test... t1
16 # test... t2
17 # test...done t1
18 # main thread
19 # test...done t2
View Code

3、Daemon(守护线程):守护进程,主程序执行完毕时,守护线程会同时退出,不管是否执行完任务

 1 import threading,time
 2 def run(n):
 3     print('[%s]------running----\n' % n)
 4     time.sleep(2)
 5     print('--done--')
 6 def main():
 7     for i in range(5):
 8         t = threading.Thread(target=run, args=[i, ])
 9         t.start()
10         t.join(1)
11         print('starting thread', t.getName())
12 m = threading.Thread(target=main, args=[])
13 m.setDaemon(True)  # 将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,
14                     # m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务
15 m.start()          #注意:setDaemon一定在start之前设置
16 m.join(timeout=2)
17 print("---main thread done----")
18 # 程序输出
19 # [0]------running----
20 # starting thread Thread-2
21 # [1]------running----
22 # --done--
23 # ---main thread done----
View Code
 1 其它方法
 2 # run():  线程被cpu调度后自动执行线程对象的run方法
 3 # start():启动线程活动。
 4 # isAlive(): 返回线程是否活动的。
 5 # getName(): 返回线程名。
 6 # setName(): 设置线程名。
 7 
 8 threading模块提供的一些方法:
 9 # threading.currentThread(): 返回当前的线程变量。
10 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
11 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
#一些方法

4、Mutex(同步锁)(互斥锁):由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁 - 同一时刻允许一个线程执行操作。(*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁)

 1 import time
 2 import threading
 3 def addNum():
 4     global num      # 在每个线程中都获取这个全局变量
 5     print('--get num:', num)
 6     time.sleep(1)
 7     lock.acquire()  # //修改数据前加锁//
 8     num -= 1        # 对此公共变量进行-1操作
 9     lock.release()  # //修改后释放//
10 num = 100           # 设定一个共享变量
11 thread_list = []
12 lock = threading.Lock()  # //生成全局锁//
13 for i in range(100):
14     t = threading.Thread(target=addNum)
15     t.start()
16     thread_list.append(t)
17 for t in thread_list:    # 等待所有线程执行完毕
18     t.join()
19 print('final num:', num)
View Code

注:GIL,无论启多少个线程,你有多少个cpu, Python在执行的时候在同一时刻只允许一个线程运行

5、死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁
RLock(递归锁:解决死锁;为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

 1 import threading, time
 2 
 3 def run1():
 4     print("grab the first part data")
 5     lock.acquire()
 6     global num
 7     num += 1
 8     lock.release()
 9     return num
10 
11 def run2():
12     print("grab the second part data")
13     lock.acquire()
14     global num2
15     num2 += 1
16     lock.release()
17     return num2
18 
19 def run3():
20     lock.acquire()
21     res = run1()
22     print('--------between run1 and run2-----')
23     res2 = run2()
24     lock.release()
25     print(res, res2)
26 
27 if __name__ == '__main__':
28     num, num2 = 0, 0
29     lock = threading.RLock()
30     for i in range(10):
31         t = threading.Thread(target=run3)
32         t.start()
33 
34 while threading.active_count() != 1:
35     print(threading.active_count())
36 else:
37     print('----all threads done---')
38     print(num, num2)
View Code

6、Semaphore (信号量:Mutex 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1;计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release(); BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。

 1 import threading, time
 2 def run(n):
 3     semaphore.acquire() ###
 4     time.sleep(1)
 5     print("run the thread: %s\n" % n)
 6     semaphore.release() ###
 7 if __name__ == '__main__':
 8     num = 0
 9     semaphore = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
10     for i in range(20):
11         t = threading.Thread(target=run, args=(i,))
12         t.start()
13 while threading.active_count() != 1:
14     pass  # print threading.active_count()
15 else:
16     print('----all threads done---')
17     print(num)
View Code

7、Event(事件):实现两个或多个线程间的交互
下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。

 1 import threading,time
 2  
 3 def light():
 4     count = 0
 5     while True:
 6         if count < 10:      #红灯
 7             print("\033[41;1m红灯\033[0m",10-count)
 8         elif count >= 10 and count < 30:    #绿灯
 9             event.set()  # 设置标志位
10             print("\033[42;1m绿灯\033[0m",30-count)
11         else:
12             event.clear() #把标志位清空
13             count = 0
14         time.sleep(1)
15         count +=1
16  
17 def car(n):
18     while True:
19         if event.is_set(): #检测是否有标志位
20             print("\033[32;0m[%s]在路上飞奔.....\033[0m"%n)
21         else:
22             print("\033[31;0m[%s]等红灯等的花都谢了.....\033[0m" % n)
23         time.sleep(1)
24  
25 if __name__ == "__main__":
26     event = threading.Event()
27     
28     light = threading.Thread(target=light)
29     light.start()
30     
31     car = threading.Thread(target=car,args=("tesla",))
32     car.start()
View Code

8、queue(队列)-- 多线程利器:实现解耦、队列;先进先出,后进后出(当get不到数据时,会一直卡着等待数据)

列表是不安全的数据结构

 1 import threading,time
 2 
 3 li=[1,2,3,4,5]
 4 
 5 def pri():
 6     while li:
 7         a=li[-1]
 8         print(a)
 9         time.sleep(1)
10         try:
11             li.remove(a)
12         except Exception as e:
13             print('----',a,e)
14 
15 t1=threading.Thread(target=pri,args=())
16 t1.start()
17 t2=threading.Thread(target=pri,args=())
18 t2.start()
View Code
######queue列队类的方法#######

#创建一个“队列”对象import Queue
②q = Queue.Queue(maxsize = 10)
③Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

#将一个值放入队列中
①q.put(10)
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为
1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。

#将一个值从队列中取出
①q.get()调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,
②get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

#Python Queue模块有三种队列及构造函数:
①Python Queue模块的FIFO队列先进先出。     class queue.Queue(maxsize)
②LIFO类似于堆,即先进后出。               class queue.LifoQueue(maxsize)
③还有一种是优先级队列级别越低越先出来。   class queue.PriorityQueue(maxsize)

#此包中的常用方法(q = Queue.Queue()):
①q.qsize() 返回队列的大小
②q.empty() 如果队列为空,返回True,反之False
③q.full()  如果队列满了,返回True,反之False
④q.full    与 maxsize 大小对应
⑤q.get([block[, timeout]]) 获取队列,timeout等待时间
⑥q.get_nowait() 相当q.get(False) 非阻塞 
⑦q.put(item) 写入队列,timeout等待时间
⑧q.put_nowait(item) 相当q.put(item, False)
⑨q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
⑩q.join() 实际上意味着等到队列为空,再执行别的操作
 1 import queue
 2  
 3  
 4 q = queue.Queue()
 5 for i in range(10):
 6     q.put(i)
 7  
 8 for t in range(10):
 9     print(q.get())
10      
11 # 0
12 # 1
13 # 2
14 # 3
15 # 4
16 # 5
17 # 6
18 # 7
19 # 8
20 # 9
#一个简单的例子
1 ##为什么要使用生产者和消费者模式?
2 在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。
3 
4 ##什么是生产者消费者模式?
5 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。例如:在餐厅,厨师做好菜,不需要直接和客户交流,而是交给前台,而客户去饭菜也不需要不找厨师,直接去前台领取即可,这也是一个结耦的过程。
#生产者消费者模型
 1 import time,random
 2 import queue,threading
 3 
 4 q = queue.Queue()
 5 
 6 def Producer(name):
 7   count = 0
 8   while count <10:
 9     print("making........")
10     time.sleep(random.randrange(3))
11     q.put(count)
12     print('Producer %s has produced %s baozi..' %(name, count))
13     count +=1
14     #q.task_done()
15     #q.join()
16     print("ok......")
17 def Consumer(name):
18   count = 0
19   while count <10:
20     time.sleep(random.randrange(4))
21     if not q.empty():
22         data = q.get()
23         #q.task_done()
24         #q.join()
25         print(data)
26         print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data))
27     else:
28         print("-----no baozi anymore----")
29     count +=1
30 
31 p1 = threading.Thread(target=Producer, args=('A',))
32 c1 = threading.Thread(target=Consumer, args=('B',))
33 # c2 = threading.Thread(target=Consumer, args=('C',))
34 # c3 = threading.Thread(target=Consumer, args=('D',))
35 p1.start()
36 c1.start()
37 # c2.start()
38 # c3.start()
View Code

python多线程,不适合cpu密集操作型的任务,适合io操作密集型的任务。

 由于GIL的存在,python中的多线程其实并不是真正的多线程(上下文的切换),如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。

 

四、进程

进程:一个整体的形式暴露给操作系统管理,里面包含了对各种资源的调用,内存的管理,网络接口的调用等;对各种资源的管理集合

1、multiprocessing(多进程模块)
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。

Process类:

#构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
  group: 线程组,目前还没有实现,库引用中提示必须是None; 
  target: 要执行的方法; 
  name: 进程名; 
  args/kwargs: 要传入方法的参数。
#实例方法:
  is_alive():返回进程是否在运行。
  join([timeout]):阻塞当前上下文环境的进程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
  start():进程准备就绪,等待CPU调度
  run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
  terminate():不管任务是否完成,立即停止工作进程
#属性:
  daemon:和线程的setDeamon功能一样
  name:进程名字。
  pid:进程号。
 1 import multiprocessing,time
 2 import threading
 3  
 4 def thread_run():
 5     print("thread id ",threading.get_ident()) #获取当前线程号
 6  
 7 def run(name):
 8     time.sleep(1)
 9     print("process----",name)
10     t = threading.Thread(target=thread_run,) #进程里面可以创建线程
11     t.start()
12  
13 if __name__ == "__main__":
14  
15     for i in range(10):
16         p = multiprocessing.Process(target=run,args=("solo",))
17         p.start()
#创建多进程
 1 #多进程id
 2  
 3 from multiprocessing import Process
 4 import os
 5  
 6 def info(title):
 7     print(title)
 8     print('module name:', __name__)
 9     print('parent process:', os.getppid())  # 父进程id
10     print('process id:', os.getpid())       # 子进程id
11  
12 def f(name):
13     info('\033[31;1mfunction f\033[0m')
14     print('hello', name)
15  
16 if __name__ == '__main__':
17     info('\033[32;1mmain process line\033[0m')
18     p = Process(target=f, args=('bob',))
19     p.start()
20     p.join()
21  
22 # 输出
23 # main process line
24 # module name: __main__
25 # parent process: 7668
26 # process id: 7496
27 # function f
28 # module name: __mp_main__
29 # parent process: 7496
30 # process id: 7188
31 # hello bob
#每个子进程都有父进程启动

2、进程间通信
不同进程间内存是不共享的,要想实现两个进程间的数据交换要用一些中间键。
① Queue
Queue使用方法跟threading里的queue差不多。

注:不是修改同一份数据,而是修改传递的数据。

 1 from multiprocessing import Process, Queue
 2 
 3 def f(q):
 4     q.put([42, None, 'hello'])
 5 
 6 if __name__ == '__main__':
 7     q = Queue()
 8     p = Process(target=f, args=(q,))
 9     p.start()
10     print(q.get())    # 进程p从进程q中拿数据
11     p.join()
12 
13 #输出 [42, None, 'hello']
#Queue 进程间通信

② Pipe

 1 import multiprocessing
 2 
 3 def f(conn):
 4     conn.send("hello from child")
 5     conn.close()
 6 
 7     pass
 8 
 9 if __name__ == "__main__":
10     parent_conn,child_conn = multiprocessing.Pipe()
11     p = multiprocessing.Process(target=f,args=(child_conn,))
12     p.start()
13     print(parent_conn.recv())
14     p.join()
15 ##不是单向,是双向的
16 
17 #输出 hello from child
#Pipe 进程间通信

③ Manager
Queue和pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。注:manager本身已经有锁了,不需要再加锁。

 1 from multiprocessing import Process, Manager
 2 import os
 3 def f(d, l):
 4     d[os.getpid()] =os.getpid()
 5     l.append(os.getpid())     #给列表添加进程号
 6     print(l)
 7 
 8 if __name__ == '__main__':
 9     with Manager() as manager:
10         d = manager.dict()    #{} #生成一个字典,可在多个进程间共享和传递
11 
12         l = manager.list(range(5))#生成一个列表,可在多个进程间共享和传递
13         p_list = []               #进程p的空列表
14         for i in range(10):
15             p = Process(target=f, args=(d, l))
16             p.start()
17             p_list.append(p)
18         for res in p_list: #等待结果
19             res.join()
20 
21         print(d)
22         print(l)
23  
24 #输出
25 # [0, 1, 2, 3, 4, 7420]
26 # [0, 1, 2, 3, 4, 7420, 8756]
27 # [0, 1, 2, 3, 4, 7420, 8756, 9404]
28 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844]
29 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852]
30 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852, 9288]
31 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852, 9288, 6360]
32 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852, 9288, 6360, 10076]
33 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852, 9288, 6360, 10076, 1728]
34 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852, 9288, 6360, 10076, 1728, 4536]
35 # {1728: 1728, 9852: 9852, 10076: 10076, 6360: 6360, 8756: 8756, 7420: 7420, 9288: 9288, 9404: 9404, 4536: 4536, 8844: 8844}
36 # [0, 1, 2, 3, 4, 7420, 8756, 9404, 8844, 9852, 9288, 6360, 10076, 1728, 4536]
#manager共享

④ 进程同步
进程独立运行,不涉及多个进程同事修改同一份数据。加锁是为了防止屏幕共享打印的时候出现混乱现象。

 1 from multiprocessing import Process, Lock
 2  
 3 def f(l, i):
 4     l.acquire()
 5     print('hello world', i)
 6     l.release()
 7  
 8 if __name__ == '__main__':
 9     lock = Lock()
10     for num in range(10):
11         Process(target=f, args=(lock, num)).start()
View Code

3、进程池
进程创建子进程的过程,子进程克隆了一遍父进程里的数据,如果父进程占用空间特别大,子进程启动过多就会导致系统空间不够用,所以引出了进程池的概念;进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池中有两个方法:
             ①apply   同步执行(串行)
             ②apply_async  异步执行(并行)

 1 from  multiprocessing import Process, Pool
 2 import time,os
 3   
 4 def Foo(i):
 5     time.sleep(2)
 6     print("in process",os.getpid())
 7     return i + 100
 8  
 9 def Bar(arg):
10     print('-->exec done:',arg,os.getpid())
11  
12 if __name__ == "__main__":  #执行此本程序(脚本)以下代码被自动执行本程序(脚本)被当做模块被导入,则以下代码不执行
13     #freeze_support()
14     pool = Pool(5)      #允许进程池同时放入5个进程
15     print("主进程:",os.getpid())
16     for i in range(10):
17         #pool.apply_async(func=Foo, args=(i,), callback=Bar) #callback回调 执行完func后再执行callback,由程序调用行
18         pool.apply(func=Foo, args=(i,))        #串行
19         #pool.apply_async(func=Foo, args=(i,)) #并行
20     pool.close()
21     pool.join()  # 进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
22 
23 #输出
24 #主进程: 733
25 # in process 736
26 # in process 735
27 # in process 737
28 # in process 738
29 # in process 734
30 # -->exec done: 102 733
31 # -->exec done: 101 733
32 # -->exec done: 103 733
33 # -->exec done: 104 733
34 # -->exec done: 100 733
35 # in process 734
36 # in process 737
37 # in process 735
38 # in process 736
39 # in process 738
40 # -->exec done: 109 733
41 # -->exec done: 107 733
42 # -->exec done: 106 733
43 # -->exec done: 108 733
44 # -->exec done: 105 733
View Code

 

三、协程

协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
好处:
  ①无需线程上下文切换的开销
  ②无需原子操作锁定及同步的开销
  ③方便切换控制流,简化编程模型
  ④高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
  ①无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.②当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  ③进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

1、生成器(函数+yield)实现协程操作 ,单线程下实现多并发的效果

 1 def consumer(name):
 2     print("------>starting eating baozi..")
 3     while True:
 4         new_baozi = yield   #等待下一次唤醒赋值
 5         print("[%s] is eating baozi %s"%(name,new_baozi))
 6  
 7 def producer():
 8     n = 0
 9     while n < 5 :
10         n +=1
11         con.send(n)          #唤醒yield并且传值
12         con2.send(n)
13         print("\033[32;1m[producer]\033[0m is making baozi %s" % n)
14  
15 if __name__ == "__main__":
16     con = consumer("c1")     #生成生成器
17     con2 = consumer("c2")
18     con.__next__()           #唤醒yield
19     con2.__next__()
20     producer()
21  
22 # 输出
23 # ------>starting eating baozi..
24 # ------>starting eating baozi..
25 # [c1] is eating baozi 1
26 # [c2] is eating baozi 1
27 # [producer] is making baozi 1
28 # [c1] is eating baozi 2
29 # [c2] is eating baozi 2
30 # [producer] is making baozi 2
31 # [c1] is eating baozi 3
32 # [c2] is eating baozi 3
33 # [producer] is making baozi 3
34 # [c1] is eating baozi 4
35 # [c2] is eating baozi 4
36 # [producer] is making baozi 4
37 # [c1] is eating baozi 5
38 # [c2] is eating baozi 5
39 # [producer] is making baozi 5
#yield实现并发

注:协程之所以可以出来高并发,原理遇到I/O操作就切换,只剩下CPU操作(CPU操作非常快)。用yield显然没有实现此效果。

2、greenlet :封装好的协程,利用.swith对协程操作进行手动切换。(居然是手动。。。) 

 1 from greenlet import greenlet
 2  
 3 def test1():
 4     print("in test1 12")
 5     gr2.switch()
 6     print("in test1 34")
 7     gr2.switch()
 8  
 9 def test2():
10     print("in test2 56")
11     gr1.switch()
12     print("in test2 78")
13 gr1 = greenlet(test1)       #启动一个协程
14 gr2 = greenlet(test2)
15 gr1.switch()               #切换操作 类似于yeild里的next()
16  
17 # 输出
18 # in test1 12
19 # in test2 56
20 # in test1 34
21 # in test2 78
#greenlet

3、gevent:是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。(自动的)

 1 import gevent
 2  
 3 def foo():
 4     print("runing in foo")
 5     gevent.sleep(2)
 6     print("context swith to foo again")
 7  
 8 def bar():
 9     print("context to bar")
10     gevent.sleep(1)
11     print("context to swith bar to bar")
12  
13 gevent.joinall([        #启动协程
14     gevent.spawn(foo),
15     gevent.spawn(bar),
16 ])
17  
18 #输出
19 # runing in foo
20 # context to bar
21 # context to swith bar to bar
22 # context swith to foo again
#gevent

①生产环境下,利用gevent做同步与异步的性能对比

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import urllib.request
 5 import gevent,time
 6 from gevent import monkey
 7 monkey.patch_all()  #monkey.patch_all()执行后可以识别urllib里面的I/0操作
 8 
 9 def f(url):
10     print("GET: %s"%url)
11     resp = urllib.request.urlopen(url)
12     data = resp.read()
13     print("%d bytes received from %s"%(len(data),url))
14 
15 # 同步开销
16 urls = [
17      'https://www.python.org/',
18     'https://www.yahoo.com/',
19     'https://github.com/',
20 ]
21 time_start = time.time()
22 for url in urls:
23     f(url)
24 print("同步cost time",time.time()-time_start)
25 
26 # 异步开销
27 async_time_start = time.time()
28 gevent.joinall([
29     gevent.spawn(f,'https://www.python.org/'),
30     gevent.spawn(f,'https://www.yahoo.com/'),
31     gevent.spawn(f,'https://github.com/')
32 ])
33 print("异步cost time",time.time()-async_time_start)
34 
35 # 输出
36 # GET: https://www.python.org/
37 # 48853 bytes received from https://www.python.org/
38 # GET: https://www.yahoo.com/
39 # 515012 bytes received from https://www.yahoo.com/
40 # GET: https://github.com/
41 # 51379 bytes received from https://github.com/
42 # 同步cost time 4.631264925003052
43 # GET: https://www.python.org/
44 # GET: https://www.yahoo.com/
45 # GET: https://github.com/
46 # 515013 bytes received from https://www.yahoo.com/
47 # 51381 bytes received from https://github.com/
48 # 48853 bytes received from https://www.python.org/
49 # 异步cost time 1.8811075687408447
# 同步、异步性能对比

由上面程序可知,同步开销时间为4秒,异步开销为2.5秒,大大节省了开销,这就是协程的魅力;monkey.patch_all()使gevent能识别到urllib中的I/O操作(原始的gevent不能识别socket、urllib的I/O操作)

②通过gevent实现单线程下的多socket并发

 1 import sys
 2 import socket
 3 import time
 4 import gevent
 5  
 6 from gevent import socket,monkey
 7 monkey.patch_all()
 8  
 9  
10 def server(port):
11     s = socket.socket()
12     s.bind(('0.0.0.0', port))
13     s.listen(5)
14     while True:
15         conn, addr = s.accept()
16         gevent.spawn(handle_request, conn)
17  
18  
19  
20 def handle_request(conn):
21     try:
22         while True:
23             data = conn.recv(1024)
24             print("recv:", data)
25             conn.send(data)
26             if not data:
27                 conn.shutdown(socket.SHUT_WR)
28  
29     except Exception as  ex:
30         print(ex)
31     finally:
32         conn.close()
33 if __name__ == '__main__':
34     server(8001)
#socket_server
 1 import socket
 2  
 3 HOST = 'localhost'    # The remote host
 4 PORT = 8001           # The same port as used by the server
 5 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 6 s.connect((HOST, PORT))
 7 while True:
 8     msg = bytes(input(">>:"),encoding="utf8")
 9     s.sendall(msg)
10     data = s.recv(1024)
11     #print(data)
12  
13     print('Received', repr(data))
14 s.close()
#socket_client

知道了异步的优点,当遇到I/0操作时会进行切换操作,那么程序是如何知道之前的I/O执行完毕再切换回来的呢?。。。follow me

 

四、事件驱动与异步IO

通常,我们写服务器处理模型的程序时,有以下几种模型:
  (1)每收到一个请求,创建一个新的进程,来处理该请求;
  (2)每收到一个请求,创建一个新的线程,来处理该请求;
  (3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
分析:
  第(1)种模型,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
  第(2)种模型,由于要涉及到线程的同步,有可能会面临死锁等问题。
  第(3)种模型,在写应用程序代码时,逻辑比前面两种都复杂。
综述:一般普遍认为第(3)种方式是大多数网络服务器采用的方式

看图说话讲事件驱动模型
在UI编程中,常常要对鼠标点击进行响应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
   ①. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
   ②. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
     ③. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的

方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
   ①. 有一个事件(消息)队列;
     ②. 鼠标按下时,往这个队列中增加一个点击事件(消息);
     ③. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
     ④. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了

(1)单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

(2)多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

(3)事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。
当我们面对如下的环境时,事件驱动模型通常是一个好的选择:
        ①程序中有许多任务,而且…
        ②任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
        ③在等待事件到来时,某些任务会阻塞。
        ④当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。
网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。

总结:异步IO涉及到了事件驱动模型,进程中维护一个消息队列,当客户端又请求时,就会把请求添加到消息队列中,线程从消息队列中轮询取要处理的请求,遇到I/O阻塞时(操作系统处理调用I/O接口处理,与程序无关),则进行上下文切换,处理其他请求,当I/O操作完成时,调用回调函数,告诉线程处理完成,然后再切换回来,处理完成后返回给客户端 Nginx能处理高并发就是用的这个原理

 

五、I/O五种网络模式

一、概念说明

同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的环境给出的答案是不同的。所以先限定一下本文的环境。本文讨论的背景是Linux环境下的network IO

在进行解释之前,首先要说明几个概念:
  - 用户空间和内核空间
  - 进程切换
  - 进程的阻塞
  - 文件描述符
  - 缓存 I/O

①用户空间与内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

②进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  2. 更新PCB信息。
  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  4. 选择另一个进程执行,并更新其PCB。
  5. 更新内存管理的数据结构。
  6. 恢复处理机上下文。
注:总而言之就是很耗资源,具体的可以参考这篇文章:进程切换

③进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

④文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

⑤缓存 I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

二、I/O模式
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。
  - ①阻塞 I/O(blocking IO)
  - ②非阻塞 I/O(nonblocking IO)
  - ③I/O 多路复用( IO multiplexing)
  - ④信号驱动 I/O( signal driven IO)
  - ⑤异步 I/O(asynchronous IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model

①阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
所以,blocking IO的特点就是在IO执行的两个阶段都被block了,大大消耗了程序执行的时间

②非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有,wait for data阶段进程没有等待,但是copydata从内核拷贝到用户进程时,程序是阻塞状态

③I/O 多路复用( IO multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block

④异步 I/O(asynchronous IO)
linux下的asynchronous IO其实用得很少。先看一下它的流程
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了

三、总结

①blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

②synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:

通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

 

六、剖析IO多路复用(多连接):协程和IO多路复用都是单线程
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
  -当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  -当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  -如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  -如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  -如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

select模块(实现伪并发)
Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用

1 Windows Python:
2     提供: select
3 Mac Python:
4     提供: select
5 Linux Python:
6     提供: select、poll、epoll

select
  select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

总结:select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。开销随着文件描述符数量的增加而线性增大

select直接通过操作系统提供的C的网络接口进行操作,而不是通过Python的解释器。

poll
  poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

总结:poll没有最大文件描述符数量的限制;但开销随着文件描述符数量的增加而线性增大;采用水平触发(告诉进程哪些文件描述符刚刚变为就绪状态,如果我们没有采取行动,下次调用select()和poll()的时候将再次报告这些文件描述符,一般不会丢失就绪的消息)

epoll
  直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()

总结:epoll没有最大文件描述符数量的限制;可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知);使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。采用基于事件的就绪通知方式;事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

select.select方法:

  select函数需要3个序列作为它的必选参数,此外还有一个可选的以秒单位的超时时间作为第4个参数。3个序列用于输入、输出以及异常情况(错误);如果没有给定超时时间,select会阻塞(也就是处于等待状态),知道其中的一个文件描述符以及为行动做好了准备,如果给定了超时时间,select最多阻塞给定的超时时间,如果超时时间为0,那么就给出一个连续的poll(即不阻塞);select的返回值是3个序列,每个代表相应参数的一个活动子集。第一个序列用于监听socket对象内部是否发生变化,如果有变化表示有新的连接

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 # _author_soloLi
 4 import select
 5 import socket
 6 import sys
 7 import queue
 8 
 9 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   
10 server.setblocking(0)        
11 server_address = ('localhost', 8002)
12 print(sys.stderr, 'starting up on %s port %s' % server_address)
13 server.bind(server_address)  
14 server.listen(5)             
15 
16 inputs = [ server ]            
17 outputs = [ ]               
18 message_queues = {}          
19 
20 while inputs:   
21     print( '\nwaiting for the next event')
22     readable, writable, exceptional = select.select(inputs, outputs, inputs)    
23 
24     for s in readable:
25         if s is server:
26             connection, client_address = s.accept()
27             print('new connection from', client_address)
28             connection.setblocking(0)
29             inputs.append(connection)  
30             message_queues[connection] = queue.Queue()  
31         else:   
32             data = s.recv(1024)            
33             if data:
34                 print(sys.stderr, 'received "%s" from %s' % (data, s.getpeername()) )
35                 message_queues[s].put(data)   
36                 if s not  in outputs:
37                     outputs.append(s)         
38             else:
39                 print('closing', client_address, 'after reading no data')
40                 if s in outputs:
41                     outputs.remove(s)   
42                 inputs.remove(s)        
43                 s.close()               
44                 del message_queues[s]   
45 
46     for s in writable:
47         try:
48             next_msg = message_queues[s].get_nowait()
49         except queue.Empty:
50             print('output queue for', s.getpeername(), 'is empty')
51             outputs.remove(s) 
52         else:
53             print( 'sending "%s" to %s' % (next_msg, s.getpeername()))
54             s.send(next_msg)    
55 
56     for s in exceptional:
57         print('handling exceptional condition for', s.getpeername() )
58         inputs.remove(s)
59         if s in outputs:
60             outputs.remove(s)
61         s.close()                    
62         del message_queues[s]  
63 
64 server.close()
#server(纯代码)
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# _author_soloLi
import select
import socket
import sys
import queue

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # 创建一个TCP socket
server.setblocking(0)        # 设置为不阻塞
server_address = ('localhost', 8002)
print(sys.stderr, 'starting up on %s port %s' % server_address)
server.bind(server_address)  # 绑定IP地址和端口
server.listen(5)             # 监听链接

inputs = [ server ]          # 创建inputs[]通信列表,被select()方法监控和接收所有外部发过来的data(outgoing data)//#自己也要监测呀,因为server本身也是个fd
#inputs = [server,conn]//[conn,]
#inputs = [server,conn,conn2]//[conn2,]          
outputs = [ ]                # 创建outputs[]通信列表,被select()方法监控和接收所有要发出去的data(outgoing data)
#outputs = [r1,] #
message_queues = {}          # 创建message_queues = {}字典,每个连接要把输入或输出的数据先缓存到queue里


#所有客户端的进来的连接和数据将会被server的主循环程序放在上面的list中处理,我们现在的server端需要等待连接可写(writable)之后才能过来,然后接收数据并返回(因此不是在接收到数据之后就立刻返回),因为每个连接要把输入或输出的数据先缓存到queue里,然后再由select取出来再发出去。

while inputs:   #此程序的主循环,调用select()时会阻塞和等待直到新的连接和数据进来//#如果没有任何fd就绪,那程序就会一直阻塞在这里


    print( '\nwaiting for the next event')
    readable, writable, exceptional = select.select(inputs, outputs, inputs)    
    
    #把inputs,outputs,exceptional(这里跟inputs共用)传给select()后,它返回3个新的列表,readable,writable,exceptional
    #(1)readable列表:监听服务端对象(内部是否发生变化,如果有变化表示有新的连接),当inputs列表有变化时,变化的值会赋值给readable_list中,
            # ①如果有新的连接进来,sk会发生变化,此时readable_list—的值为sk
            # ②如果conn对象发生变化,表示客户端发送了新的消息过来,此时readable_list的值为客户端连接
    #(2)writeable列表:实现读写分离,存放可以进行发送信息的conn对象
    #(3)exceptional列表:存放连接通信出现的error

    
    ##(一)操作inputs列表:客户端发进来的数据列表
    for s in readable:
                
        # 1、当客户端第一次连接服务端时,未在inputs里(新连接进来了,接受这个连接)
        if s is server:
            connection, client_address = s.accept()
            print('new connection from', client_address)
            connection.setblocking(0)
            inputs.append(connection)  #添加到inputs
                                       #因为这个新建立的连接还没发数据过来,现在就接收的话程序就报错了,
                                       #所以要想实现这个客户端发数据来时server端能知道,就需要让select再监测这个conn
            message_queues[connection] = queue.Queue()  #初始化一个队列,接收到客户端的数据后,不立刻返回,暂存在队列里,以后发送
                
        # 2、当客户端连接上服务端之后,再次发送数据时,已经存在inputs(s不是server的话,那就只能是一个与客户端建立的连接的fd了)
        else:   
            data = s.recv(1024)            
            # ①当客户端正常打开程序时(正常接收客户端发送的数据)
            if data:
                print(sys.stderr, 'received "%s" from %s' % (data, s.getpeername()) )
                message_queues[s].put(data)   #收到的数据先放到queue里,一会返回给客户端
                if s not  in outputs:
                    outputs.append(s)         #放入返回的连接队列里
                                
            # ②当客户端关闭程序时
            else:
                # Interpret empty result as closed connection
                print('closing', client_address, 'after reading no data')
                # Stop listening for input on the connection
                if s in outputs:
                    outputs.remove(s)   #既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉
                inputs.remove(s)        #inputs中也删除掉
                s.close()               #把这个连接关闭掉
                del message_queues[s]   #队列中也要删掉
    
        
    ##(二)操作outputs列表:要返回给客户端的连接列表
    for s in writable:
        try:
            next_msg = message_queues[s].get_nowait()
        except queue.Empty:
        
        #①客户端连接在跟它对应的queue里没有数据
            print('output queue for', s.getpeername(), 'is empty')
            outputs.remove(s)    #把这个连接从outputs列表中移除(确保下次循环的时候writeable,不返回这个已经处理完的连接了)
        
        #②客户端连接在跟它对应的queue里有数据
        else:
            print( 'sending "%s" to %s' % (next_msg, s.getpeername()))
            s.send(next_msg)     #把这个数据发给客户端


    ##(三)操作exceptional列表:连接通信过程中出现的错误
    for s in exceptional:
        print('handling exceptional condition for', s.getpeername() )
        # 把这个连接对象在inputs\outputs\message_queue中都删除
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()              # 连接关闭掉       
        del message_queues[s]  # 从队列中移除此链接信息

server.close()
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# _author_soloLi
import socket
import sys
 
messages = [ 'This is the message. ',
             'It will be sent ',
             'in parts.',
             ]
server_address = ('localhost', 10000)
 
# Create a TCP/IP socket
socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
          socket.socket(socket.AF_INET, socket.SOCK_STREAM),
          ]
 
# Connect the socket to the port where the server is listening
print >>sys.stderr, 'connecting to %s port %s' % server_address
for s in socks:
    s.connect(server_address)
 
for message in messages:
 
    # Send messages on both sockets
    for s in socks:
        print >>sys.stderr, '%s: sending "%s"' % (s.getsockname(), message)
        s.send(message)
 
    # Read responses on both sockets
    for s in socks:
        data = s.recv(1024)
        print >>sys.stderr, '%s: received "%s"' % (s.getsockname(), data)
        if not data:
            print >>sys.stderr, 'closing socket', s.getsockname()
            s.close()

②select.poll方法:

  poll方法使用起来比select简单。在调用poll时,会得到一个poll对象。然后就可以使用poll的对象的register方法注册一个文件描述符(或者是带有fileno方法的对象)。注册后可以使用unregister方法移出注册的对象。注册了一些对象(比如套接字)以后,就可以调用poll方法(带有一个可选的超时时间参数)并得到一个(fd,event)格式列表(可能为空),其中fd是文件描述符,event则告诉你发生了什么。这是一个位掩码(bitmask),意思是它是一个整数,这个整数的每个位对应不同的事件。那些不同的事件是select模块的常量,为了验证是否设置了一个定位(也就是说,一个给定的事件是否发生了),可以使用按位与操作符(&):if event & select.POLLIN

select模块中的polling事件常量:

事件名                                描述
 
POLLIN                              #读取来自文件描述符的数据
POLLPRT                             #读取来自文件描述符的紧急数据
POLLOUT                             #文件描述符已经准备好数据,写入时不会发生阻塞
POLLERR                             #与文件描述符有关的错误情况
POLLHUP                             #挂起,连接丢失
POLLNVAL                            #无效请求,连接没有打开
#poll 异步I/O
 
import socket,select
 
s = socket.socket()
host = "127.0.0.1"
port = 8002
s.bind((host,port))
 
fdmap = {s.fileno():s}      #文件描述符到套接字对象的映射
 
s.listen(5)
p = select.poll()           #poll对象
p.register(s)               #注册一个文件描述符(带有fileno方法的对象)
while True:
    events = p.poll()
    for fd,event in events:
        if fd == s.fileno():        #新的连接进来
            c,addr = s.accept()
            print("Got connectins from",addr)
            p.register(c)           #注册一个文件描述符(带有fileno方法的对象)
            fdmap[c.fileno()] = c   #添加到fdmap
        elif event & select.POLLIN:     #读取来自文件描述符的数据
            data = fdmap[fd].recv(1024)
            if not data:                #表示客户端断开
                print(fdmap[fd].getpeername(),"disconnected")
                p.unregister(fd)        #清除文件描述符
                del fdmap[fd]           #删除fdmap对应的key值
            else:
                print(data.decode())
#poll 异步I/O
 
import socket
 
sk = socket.socket()
sk.connect(("127.0.0.1",8002))
 
while True:
    command = input("--->>>")
    sk.sendall(command.encode())
sk.close()

③select.epoll方法:

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll操作过程:
epoll操作过程需要三个接口,分别如下:

int epoll_create(int size);#创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# _author_soloLi
import socket, logging
import select, errno

logger = logging.getLogger("network-server")

def InitLog():
    logger.setLevel(logging.DEBUG)

    fh = logging.FileHandler("network-server.log")
    fh.setLevel(logging.DEBUG)
    ch = logging.StreamHandler()
    ch.setLevel(logging.ERROR)

    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)
    fh.setFormatter(formatter)

    logger.addHandler(fh)
    logger.addHandler(ch)


if __name__ == "__main__":
    InitLog()

    try:
        # 创建 TCP socket 作为监听 socket
        listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    except socket.error as  msg:
        logger.error("create socket failed")

    try:
        # 设置 SO_REUSEADDR 选项
        listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except socket.error as  msg:
        logger.error("setsocketopt SO_REUSEADDR failed")

    try:
        # 进行 bind -- 此处未指定 ip 地址,即 bind 了全部网卡 ip 上
        listen_fd.bind(('', 2003))
    except socket.error as  msg:
        logger.error("bind failed")

    try:
        # 设置 listen 的 backlog 数
        listen_fd.listen(10)
    except socket.error as  msg:
        logger.error(msg)

    try:
        # 创建 epoll 句柄
        epoll_fd = select.epoll()
        # 向 epoll 句柄中注册 监听 socket 的 可读 事件
        epoll_fd.register(listen_fd.fileno(), select.EPOLLIN)
    except select.error as  msg:
        logger.error(msg)

    connections = {}
    addresses = {}
    datalist = {}
    while True:
        # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
        epoll_list = epoll_fd.poll()

        for fd, events in epoll_list:
            # 若为监听 fd 被激活
            if fd == listen_fd.fileno():
                # 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄
                conn, addr = listen_fd.accept()
                logger.debug("accept connection from %s, %d, fd = %d" % (addr[0], addr[1], conn.fileno()))
                # 将连接 socket 设置为 非阻塞
                conn.setblocking(0)
                # 向 epoll 句柄中注册 连接 socket 的 可读 事件
                epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
                # 将 conn 和 addr 信息分别保存起来
                connections[conn.fileno()] = conn
                addresses[conn.fileno()] = addr
            elif select.EPOLLIN & events:
                # 有 可读 事件激活
                datas = ''
                while True:
                    try:
                        # 从激活 fd 上 recv 10 字节数据
                        data = connections[fd].recv(10)
                        # 若当前没有接收到数据,并且之前的累计数据也没有
                        if not data and not datas:
                            # 从 epoll 句柄中移除该 连接 fd
                            epoll_fd.unregister(fd)
                            # server 侧主动关闭该 连接 fd
                            connections[fd].close()
                            logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
                            break
                        else:
                            # 将接收到的数据拼接保存在 datas 中
                            datas += data
                    except socket.error as  msg:
                        # 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况
                        # 这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理
                        if msg.errno == errno.EAGAIN:
                            logger.debug("%s receive %s" % (fd, datas))
                            # 将已接收数据保存起来
                            datalist[fd] = datas
                            # 更新 epoll 句柄中连接d 注册事件为 可写
                            epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT)
                            break
                        else:
                            # 出错处理
                            epoll_fd.unregister(fd)
                            connections[fd].close()
                            logger.error(msg)
                            break
            elif select.EPOLLHUP & events:
                # 有 HUP 事件激活
                epoll_fd.unregister(fd)
                connections[fd].close()
                logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
            elif select.EPOLLOUT & events:
                # 有 可写 事件激活
                sendLen = 0
                # 通过 while 循环确保将 buf 中的数据全部发送出去
                while True:
                    # 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置
                    sendLen += connections[fd].send(datalist[fd][sendLen:])
                    # 在全部发送完毕后退出 while 循环
                    if sendLen == len(datalist[fd]):
                        break
                # 更新 epoll 句柄中连接 fd 注册事件为 可读
                epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET)
            else:
                # 其他 epoll 事件不进行处理
                continue

epoll socket echo server

selectors模块

selectors模块已经封装了epoll,select方法;epoll优先级大于select

import selectors
import socket
  
sel = selectors.DefaultSelector()
  
def accept(sock, mask):
    conn, addr = sock.accept()  # Should be ready
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)
  
def read(conn, mask):
    data = conn.recv(1000)  # Should be ready
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    else:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()
  
sock = socket.socket()
sock.bind(('localhost', 10000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)

 

posted @ 2017-11-26 21:59  李小小小伟  阅读(977)  评论(1编辑  收藏  举报