python - socket模块2
本篇博文参考:
http://www.cnblogs.com/jishuweiwang/p/5660933.html
http://www.cnblogs.com/wupeiqi/articles/5040823.html
概述
默认应用程序:是单进程、单线程的。
进程是资源分配的最小单位。与程序相比,程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体。进程是程序在某个数据集上 的执行,是一个动态实体。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消,反映了一个程序在一定的数据集上运行的 全部动态过程。每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。
线程是轻量级的进程或子进程,是CPU调度的最小单位,所有的线程都存在于相同的进程。所以线程基本上是轻量级的进程,它负责在单个程序里执行 多任务。通常由操作系统负责多个线程的调度和执行。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。
由于Python中GIL的存在,GIL 会在进程级别加的一个逻辑锁,这个锁粒度很大,把整个系统资源看做一个整体,所以GIL 不管你有多少CPU核心,都看做一个CPU核心来用,虽然单进程多线程的程序拥有多个线程,但是同一时间之会有一个线程利用到CPU资源。因此为了提高 CPU利用率,通常会启用多进程,即启动多个Python进程来提高CPU的利率用,从而提高工作效率。
对比
对比维度 |
多进程 |
多线程 |
总结 |
数据共享、同步 |
数据共享复杂,需要用IPC;数据是分开的,同步简单 |
因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 |
各有优势 |
内存、CPU |
占用内存多,切换复杂,CPU利用率低 |
占用内存少,切换简单,CPU利用率高 |
线程占优 |
创建销毁、切换 |
创建销毁、切换复杂,速度慢 |
创建销毁、切换简单,速度很快 |
线程占优 |
编程、调试 |
编程简单,调试简单 |
编程复杂,调试复杂 |
进程占优 |
可靠性 |
进程间不会互相影响 |
一个线程挂掉将导致整个进程挂掉 |
进程占优 |
分布式 |
适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 |
适应于多核分布式 |
进程占优 |
选用
单进程,多线程的程序(io操作不占用CPU):如果是CPU密集型,那么则不能提高效率。如果是IO密集型,那么则能提高效率。
多进程,单线程的程序:CPU密集型的,一般用多进程提高并发效率。
小结:
CPU密集型:多进程
IO密集型:多线程
ThreadingTCPServer
socketserver内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。
ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。
ThreadingTCPServer基础
使用ThreadingTCPServer:
- 创建一个继承自 SocketServer.BaseRequestHandler 的类
- 类中必须定义一个名称为 handle 的方法
- 启动ThreadingTCPServer
#!/usr/bin/env python # -*- coding:utf-8 -*- # Auther: pangguoping import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): #print self.request , self.client_address,self.server conn = self.request conn.sendall(bytes('欢迎致电10086,请输入1xxx,0转人工服务',encoding='utf-8')) Flag = True while Flag: data = conn.recv(1024) print(str(data.decode())) if str(data.decode()) == 'exit': Flag = False elif str(data.decode()) == '0': conn.sendall(bytes('通过可能会被录音',encoding='utf-8')) else: conn.sendall(bytes('请重新输入',encoding='utf-8')) if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer) server.serve_forever()
#!/usr/bin/env python # -*- coding:utf-8 -*- # Auther: pangguoping import socket ip_port = ('127.0.0.1',8090) sk = socket.socket() sk.connect(ip_port) sk.settimeout(5) while True: data = sk.recv(1024) print('recive:',data.decode()) #相当于print('recive:',data,encoding='utf-8') inp = input('please input:') sk.sendall(bytes(inp,encoding='utf-8')) if inp == 'exit': break sk.close()
例子:
一个简单实现多并发案例:
#!/usr/bin/env python # -*- coding: utf-8 -*- # auth : pangguoping import socketserver class MyClass(socketserver.BaseRequestHandler): def handle(self): pass #创建socket对象 #accept #server_address = ('127.0.0.1',9999) #RequestHandlerClass = MyClass == () #self.RequestHandlerClass() = MyClass() == () #1、obj封装了self.RequestHandlerClass = MyClass #2、创建了socket,bind,listen obj = socketserver.ThreadingTCPServer(('127.0.0.1',9999),MyClass) obj.serve_forever()
例1:使用socketserver 实现多并发执行命令
server端:
#!/usr/bin/env python # -*- coding: utf-8 -*- # auth : pangguoping #socketserver 实现多并发命令执行 import socketserver import subprocess class MyServer(socketserver.BaseRequestHandler): def handle(self): #print self.request,self.client_address,self.server conn = self.request conn.sendall(bytes('欢迎您的到来!',encoding='utf-8')) while True: data = conn.recv(1024) print("--->",len(data)) if len(data) == 0:break print("[%s] says:%s" % (self.client_address,data.decode())) cmd = subprocess.Popen(data.decode(),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) cmd_res = cmd.stdout.read() if not cmd_res: cmd_res = cmd.stderr.read() if len(cmd_res) == 0: # cmd has not output cmd_res = bytes('cmd has output',encoding='utf-8') #解决粘包问题 ready_tag = 'Ready|%s' %len(cmd_res) conn.send(bytes(ready_tag,encoding='utf-8')) feedback = conn.recv(1024) #收到客户端发送过来的Start feedback = str(feedback,encoding='utf-8') #把收到的feedback转换为str if feedback.startswith('Start'): conn.send(cmd_res) if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',8070),MyServer) server.serve_forever()
client端:
#!/usr/bin/env python # -*- coding: utf-8 -*- # auth : pangguoping import socket ip_port = ('127.0.0.1',8070) sk = socket.socket() sk.connect(ip_port) welcome_msg = sk.recv(1024) print("from server:",welcome_msg.decode()) while True: send_data = input(">>>:").strip() if len(send_data) == 0:continue sk.send(bytes(send_data,encoding='utf-8')) #解决粘包的问题 ready_tag = sk.recv(1024) #收到的格式为 Ready|9999 ready_tag = str(ready_tag,encoding='utf-8') if ready_tag.startswith('Ready'): msg_size = int(ready_tag.split('|')[-1]) start_tag = 'Start' sk.send(bytes(start_tag,encoding='utf-8')) #给server发送Start,告诉server可以准备发送数据了 recv_sise = 0 #初始化数据大小 recv_msg = b'' while recv_sise < msg_size: #解决粘包问题 recv_data = sk.recv(1024) recv_msg += recv_data recv_sise += len(recv_data) print('MSG SIZE %s RECV SIZE %s' %(msg_size,recv_sise)) print(str(recv_msg,encoding='utf-8')) sk.close()
例2:使用socketserver 实现ftp上传文件功能
ftp_server端:
#!/usr/bin/env python # -*- coding: utf-8 -*- # auth : pangguoping #ftp server 上传文件 import socketserver import json class MyServer(socketserver.BaseRequestHandler): def handle(self): #print self.request,self.client_address,self.server conn = self.request conn.sendall(bytes('欢迎您的到来!',encoding='utf-8')) while True: data = conn.recv(1024) if len(data) == 0:break print("[%s] says:%s" % (self.client_address,data.decode())) #[('127.0.0.1', 51852)] says:{"file_size": 1670262, "filename": "IMG_20160714_172632.jpg", "action": "put"} task_data = json.loads(str(data,encoding='utf-8')) task_action = task_data.get("action") if hasattr(self,"task_%s" %task_action): func = getattr(self,"task_%s" %task_action) func(task_data) else: print("task action is not supported",task_action) def task_put(self,*args,**kwargs): print("put",args,kwargs) #put ({'file_size': 1670262, 'action': 'put', 'filename': 'IMG_20160714_172632.jpg'},) {} file_size = args[0].get('file_size') file_name = args[0].get('filename') server_response = {"status":200} #解决粘包问题 self.request.send(bytes(json.dumps(server_response),encoding='utf-8')) f = open(file_name,'wb') recv_size = 0 while recv_size < file_size: data = self.request.recv(4096) f.write(data) recv_size += len(data) print('filesize: %s recvsize:%s' %(file_size,recv_size)) print('file reve success') f.close() if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',8010),MyServer) server.serve_forever()
ftp_client端:
#!/usr/bin/env python # -*- coding: utf-8 -*- # auth : pangguoping #ftp 上传 import socket import os import json ip_port = ('127.0.0.1',8010) sk = socket.socket() sk.connect(ip_port) welcome_msg = sk.recv(1024) print("from server:",welcome_msg.decode()) while True: send_data = input(">>>:").strip() if len(send_data) == 0:continue cmd_list = send_data.split() if len(cmd_list) < 2:continue task_type = cmd_list[0] if task_type == 'put': abs_filepath = cmd_list[1] if os.path.isfile(abs_filepath): file_size = os.stat(abs_filepath).st_size filename = abs_filepath.split('/')[-1] print('file:%s size:%s' %(abs_filepath,file_size)) msg_data = {"action":"put", "filename":filename, "file_size":file_size} sk.send(bytes(json.dumps(msg_data),encoding='utf-8')) server_confirmation_msg = sk.recv(1024) #解决粘包问题 confirm_data = json.loads(server_confirmation_msg.decode()) if confirm_data['status'] == 200: print('start sending file',filename) f = open(abs_filepath,'rb') for line in f: sk.send(line) print('send file done') else: print("\033[31;1mfile [%s] is not exist\033[0m" %abs_filepath) continue else: print('do not support task type',task_type) continue #sk.send(bytes(send_data,encoding='utf-8')) #收消息 recv_data = sk.recv(1024) print(str(recv_data,encoding='utf-8')) sk.close()
ThreadingTCPServer源码剖析
ThreadingTCPServer的类图关系如下:
1. 在实例化的时候,会调用socketserver.ThreadingTCPServer(...)的__init__方法,加载顺序如下
2. 调用对象的serve_forever方法
3. 最终进入自定义的类MyServer处理环节:从第二部最后一步中,执行了 self.self.RequestHandlerClass(request, client_address, self),即创建了一个MyServer的实例,那么在实例化的时候,就会执行MyServer类的__init__方法。明显地,MyServer中 没有定义__init__方法,那么从父类的__init__方法中继承。
发现在socketserver.BaseRequestHandler类中,__init__包含如下代码
def __init__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() finally: self.finish()
发现会调用self.handler()方法,最终又回到了MyServer定义的handler方法中,来处理客户端请求。
至此,socketserver源码简单剖析完毕。
IO多路复用
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
IO多路复用适用如下场合:
(1)当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
注:IO多路复用不支持文件操作,支持其他IO操作,监控内部是否发生变化
监听socket对象内部是否变化了?sk = socket.socket() ,这个sk对象变化,表示有新连接来了。
什么时候变化? 连接或收发消息。 conn,address = sk.accept() , 这个conn变化表示要收发消息了
服务器端有新连接来了,服务器端的socket对象就会发生变化。
IO多路复用 --- 监听socket对象内部是否变化了?
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
http://www.zhihu.com/question/32163005 这个链接里深入的介绍了IO多路复用的起源以及核心原理,这里不再赘述。
其中重要的三项复用技术就是:select,poll,epoll
select,poll,epoll简介
select |
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是: 1 单个进程可监视的fd数量被限制 2 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 3 对socket进行扫描时是线性扫描 |
Poll |
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入 一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无 谓的遍历。 它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 |
Epoll |
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。 在前面说到的复制问题上,epoll使用mmap减少复制开销。 还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知 |
注:水平触发(level-triggered)——只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你)
边缘触发(edge-triggered)——每当状态变化时,触发一个事件。
Select |
Poll |
Epoll |
|
支持最大连接数 |
1024 |
无上限 |
无上限 |
IO效率 |
每次调用进行线性遍历,时间复杂度为O(N) |
每次调用进行线性遍历,时间复杂度为O(N) |
使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面,这样epoll_wait返回的时候我们就拿到了就绪的fd。时间发复杂度O(1) |
fd拷贝 |
每次select都拷贝 |
每次poll都拷贝 |
调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝 |
python中使用select实现为并发:
简单的为并发
import socket import select sk = socket.socket() sk.bind(('127.0.0.01', 9999)) sk.listen(5) inputs = [sk, ] while True: rlist, wlist, e, = select.select(inputs, [], [], 1) # 监听sk对象,如果sk对象发生变化,表示有客户端连接来了,此时rlist为[sk,] # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist为【客户端】 # 1 表示检测间隔 print(len(inputs), len(rlist)) # rlist中是socket对象列表 for r in rlist: if r == sk: # 新客户端来连接 conn, address = r.accept() # conn是什么? 其实socket对象 inputs.append(conn) conn.sendall(bytes('hello',encoding='utf8')) else: # 有人给我发消息了 try: # 捕捉客户端断开连接异常 ret = r.recv(1024) r.sendall(ret) # 这里也可以给客户端发送消息,但是不推荐,这样读写混淆到一起了。推荐使用读写分离的写法 if not ret: # 捕捉不同系统的客户端断开连接 raise Exception except Exception as e: inputs.remove(r) select伪并发简单案例
import socket import select sk = socket.socket() sk.bind(('127.0.0.01', 9999)) sk.listen(5) inputs = [sk, ] outputs = [] # 包含了所有给我发消息的人 while True: # rlist, w, e, = select.select([sk,], [], [],) rlist, wlist, e, = select.select(inputs, outputs, [], 1) # 监听sk对象,如果sk对象发生变化,表示有客户端连接来了,此时rlist为[sk,] # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist为【客户端】 # 1 表示检测间隔 print(len(inputs), len(rlist), len(wlist), len(outputs)) # rlist中是socket对象列表 for r in rlist: if r == sk: # 新客户端来连接 conn, address = r.accept() # conn是什么? 其实socket对象 inputs.append(conn) conn.sendall(bytes('hello',encoding='utf8')) else: # 有人给我发消息了 print("=========") try: # 捕捉客户端断开连接异常 ret = r.recv(1024) if not ret: # 捕捉不同系统的客户端断开连接 raise Exception("断开连接") else: outputs.append(r) except Exception as e: inputs.remove(r) # 循环所有给我发消息的人 for w in wlist: w.sendall(bytes('response', encoding='utf8')) outputs.remove(w) select读写分离式伪并发
import socket import select sk = socket.socket() sk.bind(('127.0.0.01', 9999)) sk.listen(5) inputs = [sk, ] outputs = [] # 包含了所有给我发消息的人 messages = {} # 消息队列 # messages = { sk1:[], sk2: [], sk3: [].....} while True: # rlist, w, e, = select.select([sk,], [], [],) rlist, wlist, e, = select.select(inputs, outputs, [], 1) # 监听sk对象,如果sk对象发生变化,表示有客户端连接来了,此时rlist为[sk,] # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist为【客户端】 # 1 表示检测间隔 print(len(inputs), len(rlist), len(wlist), len(outputs)) # rlist中是socket对象列表 for r in rlist: if r == sk: # 新客户端来连接 conn, address = r.accept() # conn是什么? 其实socket对象 inputs.append(conn) messages[conn] = [] conn.sendall(bytes('hello',encoding='utf8')) else: # 有人给我发消息了 print("=========") try: # 捕捉客户端断开连接异常 ret = r.recv(1024) if not ret: # 捕捉不同系统的客户端断开连接 raise Exception("断开连接") else: outputs.append(r) messages[r].append(ret) except Exception as e: inputs.remove(r) # 记住: 要如果断开,要清空message对应对象的key del messages[r] # 循环所有给我发消息的人 for w in wlist: msg = messages[w].pop() msg += bytes('response', encoding='utf8') w.sendall(msg) outputs.remove(w) select读写分离+消息队列伪并发