Python网络编程—socket(二)
http://www.cnblogs.com/phennry/p/5645369.html
接着上篇博客我们继续介绍socket网络编程,今天主要介绍的内容:IO多路复用、多线程、补充知识点。
一、IO多路复用
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用于以下场合:
当客户端处理多个描述符时(一般是交互式输入和网络套接字),必须使用IO复用;
当一个客户通过处理过个套接字时,而这种情况是可能的,但很少出现;
如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到IO复用;
如果服务器纪要处理TCP,又要处理UDP时;
如果一个服务器要处理多个服务或多个协议时,使用IO复用。
IO多路复用的事件方式有三种,分别是:select、poll、epoll。
下面我们就介绍下这三种事件方式:
1、select
首先select是可以跨平台的,select函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,知道有描述符就绪(有数据可读、可写或者有except异常),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。具体用法,请看下面代码:
服务器端:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import socketimport select #select的监听是有个数限制1024sk = socket.socket()sk.bind(('127.0.0.1', 9999,))sk.listen(5)inputs = [sk,]while True: rlist,w,x, = select.select(inputs,[],[],1) print(len(inputs),len(rlist)) #监听sk(服务端)对象如果sk对象发生变化,表示有客户端来连接了,此时rlist值为[sk,] #监听conn对象,如果conn发生变化时,表示客户端有新消息发送过来,此时rlist的值为[客户端] #当s1向服务端发送消息时,rlist =[s1] for r in rlist: if r == sk: #判断新客户来连接 conn,address = r.accept() #conn也是socket的对象 inputs.append(conn) conn.sendall(bytes('hello',encoding='utf-8')) else: r.recv(1024) #等待接收客户端发来消息 |
客户端:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#!/usr/bin/env python# -*- coding: utf-8 -*-import socketsk = socket.socket()sk.connect(("127.0.0.1",9999,))data = sk.recv(1024)print(data)while True: inp = input('>>>:') sk.sendall(bytes(inp,encoding='utf-8')) print(sk.recv(1024))sk.close() |
select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。上面的例子只用来监视sk对象和conn对象。
从上面的例子我们可以判断出如果同时多个客户端连接过来,某一个断开的话,服务器端会报错,为了解决这个问题我们将代码修改如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import socketimport selectsk = socket.socket()sk.bind(('127.0.0.1', 9999,))sk.listen(5)inputs=[sk,]while True: rlist,w,x = select.select(inputs,[],[],1) print(len(inputs),len(rlist)) for r in rlist: if r == sk: conn,address = r.accept() inputs.append(conn) conn.sendall(bytes('hello',encoding='utf-8')) else: print('=====================') try: ret = r.recv(1024) r.sendall(ret) if not ret: #如果接收的数据Wie空的话,主动触发下面的raise错误 raise Exception('断开连接!!!') except Exception as e: inputs.remove(r) #如果客户端断开的话,移除监听的连接 |
下面我们就使用select来实现一下socketserver服务端的功能,具体代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import socketimport selectsk = socket.socket() #创建套接字sk.bind(('127.0.0.1', 9999,)) #绑定套接字sk.listen(5) #等待连接队列长度inputs=[sk,] #初始化读取数据的监听列表,最开始时希望从sk这个套接字上读取数据outputs=[] #初始化写入数据的监听列表,最开始时并没有客户端连接进来,所以列表为空messages = {} #创建字典,用来记录发往客户端的数据while True: rlist,wlist,elist = select.select(inputs,outputs,[],1) #调用select监听所有列表中的套接字,并将准备好的套接字加入到对应的列表中 print(len(inputs),len(rlist),len(wlist),len(outputs)) for r in rlist: if r == sk: conn,address = r.accept() inputs.append(conn) messages[conn] = [] conn.sendall(bytes('hello',encoding='utf-8')) else: print('=====================') try: ret = r.recv(1024) if not ret: #如果接收的数据为空的话,主动触发下面的raise错误 raise Exception('断开连接!!!') else: outputs.append(r) messages[r].append(ret) except Exception as e: inputs.remove(r) #如果客户端断开的话,移除监听的连接 del messages[r]#所有给我发过消息的人 for w in wlist: msg = messages[w].pop() resp = msg + bytes('response',encoding='utf-8') w.sendall(resp) outputs.remove(w) |
在上面的例子中监控文件句柄有某一处发生了变化,可写、可读、异常属于Linux中的网络编程,属于同步I/O操作,属于I/O复用模型的一种:
rlist-->等待到准备好读;
wlist-->等待到准备好写;
xlist-->等待到一种异常。
如果sk这个套接字可读,则说明有新链接到来,此时在sk套接字上调用accept,生成一个与客户端通讯的套接字,并将与客户端通讯的套接字加入到inputs列表,下一次可以通过select检查链接是否可读,然后在发往客户端的缓冲加入一项,键名为:与客户端通讯的套接字,键值为空队列,select系统调用是用来让我们的程序监视多个文件句柄(file descriptor)的状态变化的。程序会停在select这里等待,知道被监视的文件句柄有某一个会多个发生了状态改变。
若可读的套接字不是sk套接字,有两种情况:一种是有数据到来,另一种是链接断开。
如果有数据到来,先接收数据,然后将收到的数据填入往客户端的缓存区中的对应位置,最后将于客户端通讯的套接字加入到写数据的监听列表;
如果套接字可读,但没有接收到数据,则说明客户端已经断开,这时需要关闭与客户端链接的套接字,进行资源清理。
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理的,这样所带来的缺点是:
select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SIZE设置,默认值是1024。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看。32位的系统默认为1024,64位的系统默认为2048。
对socket进行扫描是采用的轮询的方法,效率较低当套接字比较多的时候,不管哪个socket是活跃的,都要遍历一遍,这样会浪费CPU时间。
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
2、poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd,这个过程经历了多次无谓的遍历。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
import socketimport selectimport Queue server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.setblocking(False) #设置成非阻塞server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server_address = ("127.0.0.1", 9999)server.bind(server_address)server.listen(5)print "服务器启动成功,监听IP:" , server_addressmessage_queues = {} #超时,毫秒timeout = 5000 #监听哪些事件READ_ONLY = ( select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR)READ_WRITE = (READ_ONLY|select.POLLOUT)#新建轮询事件对象poller = select.poll()#注册本机监听socket到等待可读事件事件集合poller.register(server,READ_ONLY)#文件描述符到socket映射fd_to_socket = {server.fileno():server,}while True: print "等待活动连接......" #轮询注册的事件集合 events = poller.poll(timeout) if not events: print "poll超时,无活动连接,重新poll......" continue print "有" , len(events), "个新事件,开始处理......" for fd ,flag in events: s = fd_to_socket[fd] #可读事件 if flag & (select.POLLIN | select.POLLPRI) : if s is server : #如果socket是监听的server代表有新连接 connection , client_address = s.accept() print "新连接:" , client_address connection.setblocking(False) fd_to_socket[connection.fileno()] = connection #加入到等待读事件集合 poller.register(connection,READ_ONLY) message_queues[connection] = Queue.Queue() else : #接收客户端发送的数据 data = s.recv(1024) if data: print "收到数据:" , data , "客户端:" , s.getpeername() message_queues[s].put(data) #修改读取到消息的连接到等待写事件集合 poller.modify(s,READ_WRITE) else : # Close the connection print " closing" , s.getpeername() # Stop listening for input on the connection poller.unregister(s) s.close() del message_queues[s] #连接关闭事件 elif flag & select.POLLHUP : print " Closing ", s.getpeername() ,"(HUP)" poller.unregister(s) s.close() #可写事件 elif flag & select.POLLOUT : try: msg = message_queues[s].get_nowait() except Queue.Empty: print s.getpeername() , " queue empty" poller.modify(s,READ_ONLY) else : print "发送数据:" , data , "客户端:" , s.getpeername() s.send(msg) #异常事件 elif flag & select.POLLERR: print " exception on" , s.getpeername() poller.unregister(s) s.close() del message_queues[s] |
3、epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。先对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理过个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪状态,并且只会通知一次。还有一个特点是,epoll使用"事件"的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
使用epoll的优点:
没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你"活跃"的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll;
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap减少
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认的模式,LT模式与ET模式的区别如下:
LT模式(缺省工作模式):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件;
ET模式(高速工作模式):当epoll_wait检测到描述符事件发生将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
下面举一个epoll事件处理的方式,来监听socket套接字的变化,请看下面代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
#!/usr/bin/python# -*- coding: utf-8 -*-import socket, selectimport Queue serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server_address = ("127.0.0.1", )serversocket.bind(server_address)serversocket.listen(1)print "服务器启动成功,监听IP:" , server_addressserversocket.setblocking(0)timeout = 10#新建epoll事件对象,后续要监控的事件添加到其中epoll = select.epoll()#添加服务器监听fd到等待读事件集合epoll.register(serversocket.fileno(), select.EPOLLIN)message_queues = {} fd_to_socket = {serversocket.fileno():serversocket,}while True: print "等待活动连接......" #轮询注册的事件集合 events = epoll.poll(timeout) if not events: print "epoll超时无活动连接,重新轮询......" continue print "有" , len(events), "个新事件,开始处理......" for fd, event in events: socket = fd_to_socket[fd] #可读事件 if event & select.EPOLLIN: #如果活动socket为服务器所监听,有新连接 if socket == serversocket: connection, address = serversocket.accept() print "新连接:" , address connection.setblocking(0) #注册新连接fd到待读事件集合 epoll.register(connection.fileno(), select.EPOLLIN) fd_to_socket[connection.fileno()] = connection message_queues[connection] = Queue.Queue() #否则为客户端发送的数据 else: data = socket.recv(1024) if data: print "收到数据:" , data , "客户端:" , socket.getpeername() message_queues[socket].put(data) #修改读取到消息的连接到等待写事件集合 epoll.modify(fd, select.EPOLLOUT) #可写事件 elif event & select.EPOLLOUT: try: msg = message_queues[socket].get_nowait() except Queue.Empty: print socket.getpeername() , " queue empty" epoll.modify(fd, select.EPOLLIN) else : print "发送数据:" , data , "客户端:" , socket.getpeername() socket.send(msg) #关闭事件 elif event & select.EPOLLHUP: epoll.unregister(fd) fd_to_socket[fd].close() del fd_to_socket[fd]epoll.unregister(serversocket.fileno())epoll.close()serversocket.close() |
二、多线程
多线程,多进程:
1,一个应用程序,可以有多进程和多线程
2,默认:单进程,单线程
3,单进程,多线程下:
python多线程:IO操作是不会占用CPU,多线程会提高并发
计算性操作,占用CPU,多进程提高并发
4,GIL,全局解释器锁
首先我们先看一个多线程的例子,然后在详细介绍,请看代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#!/usr/bin/env python#-*- coding:utf-8 -*-import time #多线程的程序def f1(args): time.sleep(5) print(args)import threadingt= threading.Thread(target=f1,args=(123,)) #创建子线程t.setDaemon(True) #True表示主线程不等子线程t.start() #不代表当前线程会被立即执行t.join(2) #表示主线程到此,等待...直到子线程执行完毕, #参数,表示主线程在此最多等N秒print('end') |
上面这个例子是开启一个线程,主线程最多等子线程两分钟的时间然后执行。下面我们一起看下threading的更多方法:
start 线程准备就绪,等待CPU调度;
setName 为线程设置名称;
getName 获取线程名称;
setDaemon 设置为后台线程或前台线程(默认),是否等待子线程,值为True或False;
join 逐个执行每个线程,执行完毕后据需往下执行,该方法使得多线程变得无意义;
run 线程被CPU调度后自动执行线程对象的run方法。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#!/usr/bin/env python#-*- coding:utf-8 -*-import threadingimport timeclass MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self): #定义每个线程要运行的函数 print('running on number:%s'%self.num) time.sleep(2)if __name__=='__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()#结果:running on number:1running on number:2 |
多线程先介绍到这里,下篇博客在详细介绍,多线程,多进程和协程的用法。
三、知识点补充
1,python作用域
通过两个简单的代码我们在来补充一下python作用域的问题,请看代码:
|
1
2
3
4
5
6
7
|
if 1==1: name = 'jack1' #一个代码块print(name)def func(): name = 'eric'print(name) |
|
1
2
3
4
5
6
7
8
|
name= 'jack'def f1(): print(name)def f2(): name = 'eric' return f1ret = f2()ret() |
分析一下上面代码的执行结果,在第一个例子中函数中的eric是无法输出的,因为python中函数为作用域的,在Python中无块级作用域,python中以函数为作用域,而在Java或C#中存在块级作用域。
python作用域链,由内向外找,直到找不到报错,并且python作用域在代码执行之前已经确定,原定义的那个作用域,就去那个作用域里找。
python和JavaScript的作用域是类似的,武sir大神给我们总结了五句话,方便理解作用域,详细介绍请参考链接:
http://www.cnblogs.com/wupeiqi/p/5649402.html
2,XX公司面试题
|
1
2
3
4
5
6
7
8
9
|
li = [lambda :x for x in range(10)]#li列表#li列表中的元素:[函数、函数、函数.....]#函数在没有执行前,内部代码不执行#li[0]是个函数#执行第一个函数()#返回值是???r = li[0]()print(r) |
我们一起分析一下这个程序的结果是什么,这用到了我们上面补充的作用域知识,我们可以先将lambda函数修改成正常函数的方式,在一步一步分析:
|
1
2
3
4
5
6
7
8
9
|
#!/usr/bin/env python#-*- coding:utf-8 -*-for x in range(10): def test(): return xret=test()print ret |
因为python的作用域为函数,在函数中为局部作用域,定义在函数外的为全局作用域,因为python作用域在代码执行之前已经确定,原定义的那个作用域,就去那个作用域里找,这里的结果为9。
今天就介绍到这里,我们今天主要介绍了I/O多路复用的知识和多线程的定义,虽然I/O多路复用在我们平常写代码的时候用的比较少,但我们理解了后,方便我们以后去读懂源码。
Python网络编程—socket(二)的更多相关文章
- Python 网络编程(二)
Python 网络编程 上一篇博客介绍了socket的基本概念以及实现了简单的TCP和UDP的客户端.服务器程序,本篇博客主要对socket编程进行更深入的讲解 一.简化版ssh实现 这是一个极其简单 ...
- Python网络编程socket
网络编程之socket 看到本篇文章的题目是不是很疑惑,what is this?,不要着急,但是记住一说网络编程,你就想socket,socket是实现网络编程的工具,那么什么是socket,什么是 ...
- Day07 - Python 网络编程 Socket
1. Python 网络编程 Python 提供了两个级别访问网络服务: 低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口 ...
- python网络编程-socket编程
一.服务端和客户端 BS架构 (腾讯通软件:server+client) CS架构 (web网站) C/S架构与socket的关系: 我们学习socket就是为了完成C/S架构的开发 二.OSI七层 ...
- python网络编程socket /socketserver
提起网络编程,不同于web编程,它主要是C/S架构,也就是服务器.客户端结构的.对于初学者而言,最需要理解的不是网络的概念,而是python对于网络编程都提供了些什么模块和功能.不同于计算机发展的初级 ...
- python --- 网络编程Socket
网络编程 定义:所为网络编程即是对信息的发送和接收. 主要工作: (1)发送端:将信息以规定的协议组装成数据包. (2)接收端:对收到的数据包解析,以提取所需要的信息. Socket:两个在网络上的程 ...
- 第十三章:Python の 网络编程进阶(二)
本課主題 SQLAlchemy - Core SQLAlchemy - ORM Paramiko 介紹和操作 上下文操作应用 初探堡垒机 SQLAlchemy - Core 连接 URL 通过 cre ...
- Day10 Python网络编程 Socket编程
一.客户端/服务器架构 1.C/S架构,包括: 1.硬件C/S架构(打印机) 2.软件C/S架构(web服务)[QQ,SSH,MySQL,FTP] 2.C/S架构与socket的关系: 我们学习soc ...
- python网络编程——socket基础篇
python的网络编程比c语言简单许多, 封装许多底层的实现细节, 方便程序员使用的同时, 也使程序员比较难了解一些底层的东西. 1 TCP/IP 要想理解socket,首先得熟悉一下TCP/IP协议 ...
随机推荐
- NOIP 2013 花匠 神仙操作
题目:https://www.luogu.org/problemnew/show/P1970 今天又学习了一个新的神仙操作: 标签是DP,想了一下,没什么心情写,默默打开题解——(狂喜!) 一位大佬( ...
- 【152】C# 操作 Excel 杂记
前面写了一篇博文是关于 C# 操作 Excel 的文章,但是里面有些中规中矩,搞的我不知道怎么写了,所以另开一帖.. 注意:基本应用如下所示! //首先是引用 using Excel = Micros ...
- bzoj 1633: [Usaco2007 Feb]The Cow Lexicon 牛的词典【dp】
预处理出g[i][j]表示原串第i个匹配第j个单词需要去掉几个字母(匹配不上为-1) 设f[i]为i及之后满足条件要去掉的最少字母 倒着dp! f[i]初始为f[i+1]+1,转移方程为f[i]=mi ...
- 【js】callback时代的变更
最近团队开始越来越多的使用es7标准的async/await,从最开始的promise到后面的generator,再到现在async,对于异步,每个时期都有着其特有的解决方案,今天笔者就以自己的接触为 ...
- [C++ STL] set使用详解
一.set介绍: set容器内的元素会被自动排序,set与map不同,set中的元素即是键值又是实值,set不允许两个元素有相同的键值.不能通过set的迭代器去修改set元素,原因是修改元素会破坏se ...
- 【BZOJ4059】Non-boring sequences(分析时间复杂度)
题目: BZOJ4059 分析: 想了半天没什么想法,百度到一个神仙做法-- 设原数列为 \(a\),对于每一个 \(i\) 求出前一个和后一个和 \(a_i\) 相等的位置 \(pre[i]\) 和 ...
- 无法收集统计信息,怎样优化SQL。
特殊情况如下 客户的统计信息是固定的,没办法收集统计信息 . SQL profile 是最后考虑方案,因为同样写法sql 比较多,几十条. Parallle 并行客户一般不考虑接受,OLTP 系统. ...
- STL内存分配方式
关于STL用的很多比如map, vector, set, queue, stack等等.很少关注它的内存分配情况,但是经常遇到比如使用map,不停的在map中插入了一些很小的对象,然后释放了一些,然后 ...
- Objective-C设计模式——外观Faced(接口适配)
外观模式 外观设计模式和适配器差不多,不过它门对对象控制的粒度不同,适配器一般只是控制一个系统和客户端的对接.外观则是用来抽象多个系统一起工作. 外观一般具有多个子系统,所以外观应持有多个子系统的引用 ...
- SQL Server的安装笔记
SQL安装笔记 安装SQL Server 2008 打开SQL Server 2008中的setup.exe,显示SQL安装程序的对话框. 提示必须安装相关组件Microsoft.NET Framew ...