Python网络编程—socket(二)

本文深入探讨了I/O多路复用技术,包括select、poll和epoll的工作原理及优劣对比,同时介绍了多线程的基本概念和Python中线程的使用方法。

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  socket
import  select                #select的监听是有个数限制1024
 
sk  =  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  in  rlist:
         if  = =  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  socket
 
sk  =  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  socket
import  select
 
sk  =  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  in  rlist:
         if  = =  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  socket
import  select
 
sk  =  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  in  rlist:
         if  = =  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  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  socket
import  select
import  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_address
message_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:
         =  fd_to_socket[fd]
         #可读事件
         if  flag & (select.POLLIN | select.POLLPRI) :
             if  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, select
import 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" , 9999 )
serversocket.bind(server_address)
serversocket.listen( 1 )
print "服务器启动成功,监听IP:" , server_address
serversocket.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 threading
 
t = 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 threading
import time
 
class 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: 1
running 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 f1
ret = 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 x
 
ret = test()
print ret

     因为python的作用域为函数,在函数中为局部作用域,定义在函数外的为全局作用域,因为python作用域在代码执行之前已经确定,原定义的那个作用域,就去那个作用域里找,这里的结果为9。

 

    今天就介绍到这里,我们今天主要介绍了I/O多路复用的知识和多线程的定义,虽然I/O多路复用在我们平常写代码的时候用的比较少,但我们理解了后,方便我们以后去读懂源码。​





转载于:https://www.cnblogs.com/phennry/p/5673635.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值