使用可以实现程序之间的通信,但是server在同一时刻只能和一个客户端进行通信,如果要实现一个server端可以和多个客户端进行通信可以使用
1.多线程
2.多进程
3.select I/O多路复用
来实现服务器端和多个客户端进行通信,本文将会介绍使用select实现伪并发。
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。I/O多路复用的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select /epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
小试牛刀:利用select监听终端输入
#!/usr/bin/env python#-*- coding:utf-8 -*-import sysimport selectimport timewhile True: readable,writeable,errable = select.select([sys.stdin,],[],[],5) print readable print type(readable) if sys.stdin in readable: print "you input: ",sys.stdin.readline()
执行结果:
select.select()方法的参数解释:
句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间) 参数: 可接受四个参数(前三个必须)返回值:三个列表 select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化5、当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。
由此解释以上代码的执行结果:
1.程序将sys.stdin句柄写入到了select.select()方法的第一个句柄序列中,所以sys.stdin句柄将会被select监听着
2.一旦sys.stdin这个句柄发生了变化,select.select()方法将会返回一个列表,这个列表中包含了变化的句柄,这个列表就是readable
3.判断sys.stdin是否在这个列表中,在的话就执行下边的语句
4.select.select的第四个参数是超时时间,如果3秒内没有文件句柄发送变化,就返回空的列表
5.超时时间select.select方法的第四个参数为3
利用selct监听socket
#!/usr/bin/env python#-*- coding:utf-8 -*-import selectimport socketsk = socket.socket()ip_port = ("127.0.0.1",7777)sk.bind(ip_port)sk.listen(5)sk.setblocking(False)while True: readable,writeable,errable = select.select([sk,],[],[],1) for s in readable: cnn,add = s.accept() cnn.sendall("欢迎") # data = cnn.recv(1024) print add
利用select监听多个端口
#!/usr/bin/env python#-*- coding:utf-8 -*-import selectimport socketsk1 = socket.socket()ip_port = ("127.0.0.1",7777)sk1.bind(ip_port)sk1.listen(5)sk1.setblocking(False)sk2 = socket.socket()ip_port = ("127.0.0.1",7778)sk2.bind(ip_port)sk2.listen(5)sk2.setblocking(False)while True: readable,writeable,errable = select.select([sk1,sk2,],[],[],1) for s in readable: cnn,add = s.accept() cnn.sendall(repr(add)) print add
----------------------------------------------------------我是分割线---------------------------------------------------------------------
以上的内容其实只是为了演示select能够监听变化文件描述符的功能,下面的才是使用select的妙处所在
利用select同时和多个客户端进行交互
#/usr/bin/env python#-*- coding:utf-8 -*-import timeimport socketimport select#创建socket对象sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)sk.setsockopt#设置监听的IP与端口sk.bind(('127.0.0.1',6666))#设置client最大等待连接数sk.listen(5)sk.setblocking(False) #这里设置setblocking为Falseaccept将不在阻塞,但是如果没有收到请求就会报错inputs = [sk,] #将sk这个对象加入到列表中,并且赋值给inputs#原因:看上例conn是客户端对象,客户是一直连接着呢,连接的时候状态变了,连接上之后,连接上之后,还是服务端的socket 有关吗?#是不是的把他改为动态的?while True: readable_list, writeable_list, error_list = select.select(inputs,[],[],1) #把第一个参数设为列表动态的添加 time.sleep(2) #测试使用 print "inputs list :",inputs #打印inputs列表,查看执行变化 print "file descriptor :",readable_list #打印readable_list ,查看执行变化 for r in readable_list: if r == sk: #这里判断,如果是客户端连接过来的话他不是sk,如果是服务端的socket连接过来的话是sk conn,address = r.accept() inputs.append(conn) print address else: #如果是客户端,接受和返回数据 client_data = r.recv(1024) r.sendall(client_data)select socket server - server
#!/usr/bin/env python#-*- coding:utf-8 -*-import socketclient = socket.socket()client.connect(('127.0.0.1',6666))client.settimeout(5)while True: client_input = raw_input('please input message:').strip() client.sendall(client_input) server_data = client.recv(1024) print server_dataselect socket server - client
交互过程详解:
#1 默认,sk这个对象文件句柄就在inputs列表中select监听客户端的请求,当有客户端请求过来 client1 ---> server#用户捕获了变化readable_list = [sk,] 那么循环是有值得,判断r = sk 说明是一个新的请求链接,然后把client链接加入到inputs里 inputs = [sk,conn1,]#如果现在什么都不做,那么select无法捕获到变化:readable_list = []#执行看下:inputs list : [] #默认inputs list 就有一个server socket sk 对象file descriptor : [ ] #当有客户端请求过来时候,sk发生了变化,select捕获到了('127.0.0.1', 62495)inputs list : [ , ] #第二次循环的时候,inputs = [sk,conn1,]file descriptor : [] #第二次循环的时候readable_list = [] 因为客户端没有做任何操作,没有捕获到变化所以为空#2 又有一个新的链接过来了,谁变化了? sk 他变化了,有人向他发起了一个请求链接,那么现在inputs = [sk,conn1,conn2] readable_list = [sk]#本次循环完成之后再循环的时候 inputs = [sk,conn1,conn2,] readable_list = [] 因为我们没有继续做操作#第一个链接inputs list : [ ] #默认只有一个对象file descriptor : []inputs list : [ ] file descriptor : [ ] #当捕获到,判断是否是新链接,如果是加入到inputs列表中监控('127.0.0.1', 62539)inputs list : [ , ] #inputs列表变更为了[sk,conn1]file descriptor : [] #因为没有后续的操作,这里没有捕获到异常所以列表为空#第二个链接inputs list : [ , ] #第一个链接没有做任何操作file descriptor : [ ] #第二个链接过来了被捕获到,判断是否为新链接('127.0.0.1', 62548)inputs list : [ , , ] #加入到inputs列表中file descriptor : []inputs list : [ , , ]file descriptor : []inputs list : [ , , ]file descriptor : []
优化点一:当某一个客户端断开连接之后,该客户端的socket描述符还是在服务端的监听列表中的,能不能将已经断开连接的客户端的socket描述符删除掉?
#/usr/bin/env python#-*- coding:utf-8 -*-import timeimport socketimport select#创建socket对象sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)sk.setsockopt#设置监听的IP与端口sk.bind(('127.0.0.1',6666))#设置client最大等待连接数sk.listen(5)sk.setblocking(False) #这里设置setblocking为Falseaccept将不在阻塞,但是如果没有收到请求就会报错inputs = [sk,] #将sk这个对象加入到列表中,并且赋值给inputs#原因:看上例conn是客户端对象,客户是一直连接着呢,连接的时候状态变了,连接上之后,连接上之后,还是服务端的socket 有关吗?#是不是的把他改为动态的?while True: readable_list, writeable_list, error_list = select.select(inputs,[],[],1) #把第一个参数设为列表动态的添加 time.sleep(2) #测试使用 print "inputs list :",inputs #打印inputs列表,查看执行变化 print "file descriptor :",readable_list #打印readable_list ,查看执行变化 for r in readable_list: if r == sk: #这里判断,如果是客户端连接过来的话他不是sk,如果是服务端的socket连接过来的话是sk conn,address = r.accept() inputs.append(conn) print address else: #如果是客户端,接受和返回数据 client_data = r.recv(1024) if client_data: r.sendall(client_data) else: inputs.remove(r)#如果没有收到客户端端数据,则移除客户端句柄 因为,不管是正常关闭还是异常关闭,client端的系统底层都会发送一个消息select socket server - server release client-connect
优化点二:假如说我们需要将客户端发来的数据写入到数据库中,不同客户端发来的消息要存放在不同的表中,那怎么办?
难点:
1.我们并不知道消息和客户端的对应关系
2.假如说在同一时间发送了大量的消息,来不及写怎么办?
解决这一问题我们需要引入一个新的数据结构------->
#!/usr/bin/env python#-*- coding:utf-8 -*-__author__ = 'luo_t'import selectimport socketimport Queueimport timesk = socket.socket()sk.bind(('127.0.0.1',6666))sk.listen(5)sk.setblocking(False) #定义非阻塞inputs = [sk,] #定义一个列表,select第一个参数监听句柄序列,当有变动是,捕获并把socket server加入到句柄序列中outputs = [] #定义一个列表,select第二个参数监听句柄序列,当有值时就捕获,并加入到句柄序列message = {}#message的样板信息#message = { # 'c1':队列,[这里存放着用户C1发过来的消息]例如:[message1,message2]# 'c2':队列,[这里存放着用户C2发过来的消息]例如:[message1,message2]#}while True: readable_list, writeable_list, error_list = select.select(inputs,outputs,[],1) #文件描述符可读 readable_list 只有第一个参数变化时候才捕获,并赋值给readable_list #文件描述符可写 writeable_list 只要有值,第二个参数就捕获并赋值给writeable_list #time.sleep(2) print 'inputs:',inputs print 'output:' print 'readable_list:',readable_list print 'writeable_list:',writeable_list print 'message',message for r in readable_list: #当readable_list有值得时候循环 if r == sk: #判断是否为链接请求变化的是否是socket server conn,addr = r.accept() #获取请求 inputs.append(conn) #把客户端对象(句柄)加入到inputs里 message[conn] = Queue.Queue() #并在字典里为这个客户端连接建立一个消息队列 else: client_data = r.recv(1024) #如果请求的不是sk是客户端接收消息 if client_data:#如果有数据 outputs.append(r)#把用户加入到outpus里触发select第二个参数 message[r].put(client_data)#在指定队列中插入数据 else: inputs.remove(r)#没有数据,删除监听链接 del message[r] #当数据为空的时候删除队列~~ for w in writeable_list:#如果第二个参数有数据 try: data = message[w].get_nowait()#去指定队列取数据 并且不阻塞 w.sendall(data) #返回请求输入给client端 except Queue.Empty:#反之触发异常 pass outputs.remove(w) #因为第二个参数有值得时候就触发捕获值,所以使用完之后需要移除它 #del message[r] print '%s' %('-' * 40)select socket server - server read | write separation
总结:
使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
但这个模型依旧有着很多问题。首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很 多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了queue,Solaris提供了/dev/poll,…。如果需要实现 更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具 有较好跨平台能力的服务器会比较困难。 其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。如下例,庞大的执行体1的将直接导致响应事件2的执行体迟迟得不到执行,并在很大程度上降低了事件探测的及时性。I/O多路复用的使用场景
#(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。#(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。#(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。#(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。#(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。'''与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。'''
参考资料:
http://www.cnblogs.com/luotianshuai/p/5098408.html