一、事件驱动模型简介
通俗描述:一种编程的范式,编程的风格,编程的网格1、编程模型传统的编程模式 例如:线性模式大致流程开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束(每步发生什么事情都是控制好的)每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,唯一能够改变这个流程的是数据。输入不同的数据,根据条件语句判断,流程或许就改为A--->C--->E...--->结束。每一次程序运行顺序或许都不同,但它的控制流程是由输入数据和你编写的程序决定的。如果你知道这个程序当前的运行状态(包括输入数据和程序本身),那你就知道接下来甚至一直到结束它的运行流程。例如:事件驱动型程序模型大致流程(根据事件触发)
描述:如在京东购买商品,打开网页后,浏览某个功能,实际对于服务端而言都是一个未知的状态,对于这种未知状态而触发的事件驱动开始--->初始化--->等待 与上面传统编程模式不同,事件驱动程序在启动之后,就在那等待,等待什么呢?等待被事件触发。传统编程下也有“等待”的时候,比如在代码块D中,你定义了一个input(),需要用户输入数据。但这与下面的等待不同,传统编程的“等待”,比如input(),你作为程序编写者是知道或者强制用户输入某个东西的,或许是数字,或许是文件名称,如果用户输入错误,你还需要提醒他,并请他重新输入。事件驱动程序的等待则是完全不知道,也不强制用户输入或者干什么。只要某一事件发生,那程序就会做出相应的“反应”。这些事件包括:输入信息、鼠标、敲击键盘上某个键还有系统内部定时器触发。实例:html通过一个点击可以触发一个响应,实际是通过调用某个函数来完成某个动作
分析:只写一个事件,实际本身是一个阻塞状态,不触发这个事件时,这个函数永远不会被调用,此时CPU可以被其他的任务所调用,无论有多少个事件,它会做一个整体的监听,相互不影响[root@node2 event]# cat test.htmlTitle click me
#JS的事件驱动方式
需求:点击鼠标就触发一个操作
分析:要用一个监测鼠标的程序,时刻监测鼠标是否有点击某个内容,再触发某个操作,要一直监听鼠标传统:CPU一直占用def f(): passwhile 1: mouse listen:f()
2.事件驱动模型
目前大部分UI编程的事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件代表鼠标按下事件.事件驱动模型大体思路如下:
分析:onclick是鼠标的单击,首先要有一个事件,是它内部有的,当鼠标按下时,就会向队列里放一个事件,点击完可能还会做其他的动作,总之有一个动作就插入到队列中,一直在那Put数据,还有一个处理线程去get事件,队列有三种的工作方式,分另是先进先出,先进后出,和优先集,默认是按先进先出。 实际这个过程是一方循环的去触发事件,内部有一个线程的去提取事件,提取的事件是按先进先出的原则,取出一个把指定的函数执行完毕。a.有一个事件(消息)队列b.鼠标按下时,往这个队列中增加一个点击事件(消息)c.有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick(),onKeyDown()等d.事件(消息)一般都自保存各自处理函数指针,这样,每个消息都有独立的处理函数事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它特点是包含事件循环。当外部事件发生使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程对比单线程、多线程以及事件驱动编程的模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成。
每个任务都 在等待I/O操作时阻塞时自身。阻塞在I/O操作上所花费的时间已经用浅蓝色框标示出来了。分析:单线程时,执行三个任务时,要停就停,要走就走,效率比较低多线程时,三个线程一起进行,要运行就运行,要停就要停异步进行:没有浅蓝色的,就是没有I/O阻塞
二、IO模型要理解的概念
1.用户空间和内核空间现在操作系统都是采用虚拟存储器,那么32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限.为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间分来两部分,一部分为内核空间,另一部分为用户空间.针对linux操作系统而言,将最高的6字节(从虚拟地址0x000000到0xFFFFFFFF),供内核使用,称为内核空间,而将内核较低的36字节(从虚拟地址0x000000到0xFFFFFFFF),供各个进程使用,称为用户空间
2.进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行.这种行为称为进程切换,这种切换是由操作系统来完成的.因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的.从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:a. 保存处理上下文件,包括程序计算器和其他寄存器b. 更新PCB信息c. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列 d. 选择另一个进程执行,并更新其PCBe. 更新内存管理的数据结构f. 恢复处理上下文注:消耗CPU资源
3.进程的阻塞(进程本身所发生的阻塞)
描述:如socket从上往下走,遇到accept()之前还有listen,bind之类的动作,一直向下走,进程都是拿着CPU的权限在运行,代码执行到accpet时,是进程自身决定不在向下走的,在等待具体的信息,然后把CPU的权限交给其他的进程,阻塞住,阻塞的过程,这个进程是没有CPU的操作权限的,直到资源和数据过来后,如accpet后,重新建立连接,才会重新获得CPU的权限向下继续执行代码.正在执行进程,由于期待的某些事伯未发生,如果请求系统失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(block),使用自己运行状态变为阻塞状态。可见,进程阻塞是进程自身一种主动行为,也因此处于运行的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
4.文件描述符fd
文件描述符(file description)是计算机科学中的一个术语,是一个用于表术指向文件的引用抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护进程打开文件的记录表。当程序打开一个现有文件或者创建一个新的文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及涉及底层的程序编写往往会围绕着文件描述符进行展开。但是文件描述这一概念只适用于UNIX、linux这样的操作系统。分析:文件描述符是一个索引值,实际指向系统文件的一张表,这张表与socket的对象有关系,所有的内容都在系统文件表中存储着,所以做 socket进行数据传输时,都是围绕文件描述符在工作的.
In [1]: import socketIn [2]: print(socket.socket()) #fd=9是文件描述符
5.缓存I/O(**)
缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统的缓冲区中,然后才会从操作系统内核缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝分析:在socket的使用过程中,一收一发,接收方收到数据后先到这个操作系统的内核区,再从内核区拷贝到用户区(用户程序没有操作系统访问权限),缓存I/O的缺点:数据在传输过程中需要应用程序地址空间和内核进行多次数据拷贝操作,这些数据操作所带来的CPU以及内存开销是非常大的.同步(synchronous)IO和异步(asynchronous)IO,阻塞(blocking)IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?
分析:有不同的看法,如wiki,认为asynchronousIO和non-blockingIO是一个东西。这其实因为不同人的知识背景不同,产且在讨论这个问题的时候上下文(context)也不相同。所以,为了更好的回答这个问题,先限定下文的上下文。在linux环境下的network IO stevens在文章中一共比较了五种IO MODEL- blocking IO- nonblocking IO- IO multiplexing- singal driven IO(不常用)- aynchronous IO
三、IO发生时涉及的对象和步骤
对于一个network IO(以read为例),它会涉及到两个系统对象,一个是调用这个IO的process(or thread),另一个就是系统内核(kernel).当一个read操作系统发生时,它会经历两个阶段:a. 等待数据准备(waiting for the data to be ready)描述:如accept函数,当在server端写到accept函数时,它的上面无论是做了bind,listen等都没有任何的阻塞,会一直执行到accept,阻塞的原因是等待连接,涉及到等待数据准备这个过程,也就是等待用户端来连接,因为并不知道时间有多长。accept接收的是一个元组,conn和address(clientip ,clientport),还有就是client的socket对象,实际就是fd,这个过程中等待它来连接,没人来连接时,数据不在server端b. 将数据从内核拷贝到进程中(copying the data from the kernel to the process)描述:数据到了操作系统,把数据从内核态拷贝到用户态
1.blocking IO(阻塞IO)
在linux中,默认情况下所有socket都是blocking描述:process blocks in call in recvfrom系统发起调用,kernel做了两件,等待数据和复制数据从内核到用户态,在等待过程中,从没有数据到数据准备好,第二个过程从内核到用户态,kernel返回结果,用户进程才解除block状态,重新运行起来.recvfrom是accept()执行就会发起系统调用,然后操作系统就会执行以上所说的步骤.整个过程中,是进程被block住了!
阻塞
[root@node2 io]# cat test.py#!/usr/local/python3/bin/python3import socketsk=socket.socket()sk.bind()sk.listen(3)conn.addr=sk.accpet() #操作系统把数据拷贝到用户态时,才能真正接收数据,整个过程处理阻塞状态,第一个过程准备数据需要阻塞,数据来后,#第二个过程对于进程来说,也是阻塞的conn.send('hello') #这个conn不能使用,因为会一直卡在上一个conn,数据没有到用户程序(数据要拷贝到用户态)sk.accpet()
2.Non-blocking IO(非阻塞IO)
场景:产生非阻塞I/O的原因主要是阻塞I/O的操作过程中,效率比较低描述:非阻塞I/O是可以在程序中设定这种模式,不管有无数据,程序都会继续向下运行,如果有数据就直接取回来,没有数据就返回一个错,循环的去发recvfrom,如果有数据就取走,没有就返回无数据,没数据可以继续向下走,再发一个系统调用,还没有过一段时间再去询问,反复的轮询的查看,不询问时可以做其他的事情,把整个过程划分成多段,中间节省的时间(进程可以使用CPU)去完成其他的事情. 每个recvfrom之间不是阻塞的,有空余的时间来操作CPU. copy datagram的过程是阻塞的,这个过程与阻塞的IO一样. 缺点:a. 等待准备数据的过程中不断的轮询,会消耗资源 b. 在轮询的过程中不能及时得到数据是否准备好(有延迟),如去饭店吃饭等做饭时,你去问,服务员没有及时回来你,直到饭上
3.IO multiplexing(IO多路复用)
如select,epoll,还有些地方称这种方式为event driven IO.select/epoll的好处就在于单个process可以同时处理多个网络连接的IO.它的基本原理就是select/epoll这个function会不断轮询负责所有sokcet,当某个socket有数据到达了,就通知用户进程。 描述:a.当使用select函数时,会发一个system call系统调用,这时kernel也是在等待数据,在这个过程也是要阻塞,但是但数据准备好时,反馈给进程这数据可读(相当于数据准备好) b.进程再发一个系统调用,进行拷贝数据,与之前的阻塞和非阻塞是连续操作的一个过程 c.select,epoll都是使用以上的机制来实现分析:a.select等待过程是监听着,处于阻塞状态的,使用recvfrom发起一个系统调用进行数据拷贝也是阻塞的 b.***select可以侦听多个描述符,无论那个过来都可以进行连接,以实现这种并发(select过程是通过调用底层机制来实现,并不是经过进程来阻塞) c.select是跨平台4.Asynchronous I/O(异步IO)描述:当程序执行时,应用的aio_read发起一个系统调用,立刻返回一个return值(是否有数据,有立刻返回,没有直接返回数据完成复制的通知),但是在wait for data和copy datagram过程是由操作系统来完成的,最后通知程序已经完成数据的copy,在中间过程中,是非阻塞的,可以使用CPU去操作别的.
5. Summary
a.blocking vs non-blocking: 调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还在准备数据的情况下会立刻返回
b.synchronous vs asynchronous两者的区别在于synchronous IO做IO operation时会将process阻塞.按照这个定义,之前所述的blocking IO,non-blocking IO, IO multiplexing都属于synchronous IO.有人可能会说,non-blocking IO会说并没有被Block,这里有个非常"狡猾"的地方.但是例子中的recvfrom这个system call.non-blocking IO执行recvfrom这个system call时。non-blocking IO在执行recvfrom这个system call时,如果kernel的数据没有准备好,这时候不会block进程.但是当kernel中数据准备好时,recvfrom会将数据从kernel拷贝到用户内存中,这时进程被block了,在这段时间内,进程没有被block的。而asynchronous IO则不一样,当进程发起IO操作之后,就直接返回再也不理会了,直到kernel发送一个信号,通知进程IO完成.这个过程中,进程没有被Block。注:select, poll, epoll都属于IO多路复用,而IO多路复用又属于同步,所以epoll只是一个伪异步通俗:同步是一直阻塞,异步IO是不会被阻塞c.生活买票来描述四个io modeblocking IO: 要排队等票,不能离开,票不到不能离开non-blocking IO: 买票等待时,每隔一段时间再回来看票好了没,时间是自定义,这段时间可以去吃饭或做其他事情io multiplexing: 调用select时,可以不在队伍上排队,在大厅等待,通知数据到了,再去取票,不同的窗口可以通知,整个过程不可以做其他事情asynchronous io: 如VIP服务,买时直接准备好票,上车时直接到刷身份证,但是异步实现的机制比较复杂
6.select poll epoll IO多路复用
- select 分析:轮询监听,如事件驱动中的队列,有个专门的线程去取数据,只要有数据在队列就证明事件被触发了,就有个线程执行对应的函数,select也相似,它会监听文件描述符有无数据更新,如果有是活动的,有新的内容的,这时就把内容返回,再拷贝数据,如果没有就一直监听,睡眠。它是监听所有的文件描述符,如有10个一个个一遍遍的查,如果有40个也就要一直去查40后,才能确定那个有数据,是一个低效的表现,如果监听10000个对象,只有两个对象有数据更新,就很低效 ,而且它监听的限制是1024个文件描述符在最早于1983年出现在4.2BSD中,它通过select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这此文件描述符从而进行后续的读写操作select目前几乎所有的平台上支持select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在linux的一般为1024,不过可以通过修改宏观定义甚至重新编译内核的方式提升这一限制另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符的数量的增大,其复制的开销也线性增长,同时,由于网络响应时的延迟使得大量TCP连接处于非活动状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销- poll
它和select在本质上没有大的区别,但是poll没有最大文件描述符数量的限制,一般也不使用它,但是过渡阶段- epoll
描述:如考试交试卷,有人考完要交试卷,桌子上有灯可以通知老师有人完成,但是老师并不知道那个人完成,要一个个人的去问,这是select的做法,epoll却可按下灯的同时知道那个同学要交试卷,不用再轮询,节省了大量的CPU的时间. 因为是IO多路复用的一种,它在拷贝数据时是会阻塞的,epoll是同步的直到linux2.6才出现了内核直接支持的实现方法,那就是epoll,被公认为linux2.6下性能最多路IO就绪通知方法,在windows下不支持没有最大文件描述符的限制比如100个连接,有两个小活跃了,epoll会告诉用户两个活跃了,直接取就OK了,而select是循环一遍(了解)epoll可以同时支持水平触发和边缘触发(edge triggered),只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果没法采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能更高一些,但是代码实现相当复杂。另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述就绪进,内核会采用类似callback回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时使用得到通知.所以市面上见到的所谓异步IO,如nginx,Tornado等,叫它是异步IO,实际是IO多路复用阻塞IO
[root@node2 io]# cat server.py #!/usr/local/python3/bin/python3import socketsk=socket.socket()sk.bind(('127.0.0.1',8080))sk.listen(3)while 1: conn,addr=sk.accept() while 1: data=conn.recv(1024) print(data.decode('utf8')) conn.sendall(data) [root@node2 io]# cat client.py #!/usr/local/python3/bin/python3import socketsk=socket.socket()sk.connect(('127.0.0.1',8080))while 1: inp=input('>>>') sk.sendall(inp.encode('utf8')) data=sk.recv(1024) print(data.decode('utf8'))[root@node2 io]# python3 server.pyreidhello[root@node2 io]# python3 client.py >>>reidreid>>>hellohello
非阻塞IO
[root@node2 io]# cat server.py #!/usr/local/python3/bin/python3import socketimport timesk=socket.socket()sk.bind(('127.0.0.1',8080))sk.listen(3)sk.setblocking(False) ######设置非阻塞while 1: try: #设置捕获,否则一启动,没有数据就会报错 conn,addr=sk.accept() #accept发出的系统调用询问 print(addr) data=conn.recv(1024) print(data.decode('utf8')) conn.close() except Exception as e: print('error: ',e) time.sleep(3) [root@node2 io]# cat client.py #!/usr/local/python3/bin/python3import socketsk=socket.socket()while 1: #inp=input('>>>') sk.connect(('127.0.0.1',8080)) sk.sendall('hello'.encode('utf8')) data=sk.recv(1024) print(data.decode('utf8'))[root@node2 io]# python3 server.pyerror: [Errno 11] Resource temporarily unavailable #没收到数据前一直在询问error: [Errno 11] Resource temporarily unavailableerror: [Errno 11] Resource temporarily unavailableerror: [Errno 11] Resource temporarily unavailable('127.0.0.1', 34066)helloerror: [Errno 11] Resource temporarily unavailableerror: [Errno 11] Resource temporarily unavailable
IO多路复用
描述:是两个阻塞,但是通过select可以监听多个文件描述符[root@node2 io]# cat select_server.py#!/usr/local/python3/bin/python3import socketimport selectsk1=socket.socket()sk1.bind(('127.0.0.1',8080))sk1.listen(3)sk2=socket.socket()sk2.bind(('127.0.0.1',8081))sk2.listen(3)while 1: r,w,e=select.select([sk1,sk2],[],[]) #select 监听三个参数读、写、错误,里面写监听的对象,r有更新信息时的socket的对象,如一旦有client来连接sk1,这时r就是sk1,实际r就是一个list print('rrr') #卡在这,等socket的连接,使用select函数调用系统,卡在数据准备阶段 for obj in r: #如果连接是sk1,就是列表[sk1,] conn,addr=obj.accept() print(addr) conn.send('hello,i am server'.encode('utf8'))[root@node2 io]# cat client1.py #!/usr/local/python3/bin/python3import socketsk=socket.socket()sk.connect(('127.0.0.1',8080))while 1: data=sk.recv(1024) print(data.decode('utf8')) inp=input('>>>') sk.sendall(inp.encode('utf8'))[root@node2 io]# cat client2.py #!/usr/local/python3/bin/python3import socketsk=socket.socket()sk.connect(('127.0.0.1',8081))while 1: data=sk.recv(1024) print(data.decode('utf8')) inp=input('>>>') sk.sendall(inp.encode('utf8'))[root@node2 io]# python3 select_server.py rrr('127.0.0.1', 34070)rrr('127.0.0.1', 42502)[root@node2 io]# python3 client1.pyhello,i am server>>>i amd reid[root@node2 io]# python3 client2.pyhello,i am server>>>i amd reid
另一个例子
[root@node2 io]# cat sel_server.py #!/usr/local/python3/bin/python3import socketimport select#创建两个socket对象sk=socket.socket()sk.bind(("127.0.0.1",8800))sk.listen(5)sk1=socket.socket()sk1.bind(("127.0.0.1",6667))sk1.listen(5)while True: r,w,e=select.select([sk,sk1],[],[],5) #(这时是阻塞的)监听到socket有发生变化就会取,也就是当有连接过来时,5是阻塞时间 for i in r: #[sk1,]接收的是sk1,是server端的一个socket对象,r是一个列表(存的是有数据更新的socket对象) conn,add=i.accept() #i是上一行所监听到的sk1,sk对角可以调用accept方法,接收到就是client的信息 print(conn) #conn是客户端连接server的socket的对象 print('hello') print('>>',r) #r存的是一个列表[sk1,],实际r就是i,r.accept()得到的结果就是conn,conn是客户端的对象 [root@node2 io]# cat sel_client.py#!/usr/local/python3/bin/python3import timeimport socketsk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)while True: sk.connect(('127.0.0.1',6667)) print("hello") sk.sendall(bytes("hello","utf8")) time.sleep(2) break[root@node2 io]# python3 sel_server.py #两个不一样的文件描述符hello>> [ ]
问题1:
sel_server.py执行完,sel_client.py执行两次,server的while循环一直在监听,sel_client.py再执行一次时,conn会被操作系统重新分配,重新绑定某个端口时,是固定的,也就是r还是不变的,但是conn打印的是不同的对象,文件描述不一样问题2:
r,w,e=select.select([sk,sk1],conn,[],[]) 这时可以绑定sk1,sk2,也可以绑定conn,conn本质上是一样的,都是一个文件描述符,addr不能放,它是一个元组,不是socket对象,没有文件描述符问题3:
把conn,add=i.accpet() 和print('conn',conn)注释,server启动,client一旦连接上,server会产生不断的打印最后一行的r,select会把里面产生的数据都监测到,只有数据不可读时就停下来,当加上conn,add=i.accpet() 和print('conn',conn)时,i.accept已经把数据取走,这时select不会监听到原因分析:IO多路复用中有两种触发方式在linux的IO多路复用中有水来触发,边缘触发两种模式水平触发:如果文件描述已经就绪可以非阻塞的执行IO操作了,此时会触发通知,允许在任意时刻重复检测的IO状态,没有必要每次描述符就绪后尽可能多的执行IP.select,poll就属于水平触发边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知,在这个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知执行完IO那么需要等到下一次新的IO活动到来才获取就绪的描述符,信号驱动式IO就以属于边缘触发
epoll既可以采用水平触发,也可以采用边缘触发
举例说明:一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll,这时如果水平触发的,epoll会立即返回,因为数据准备好了,如果边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在没有新的数据到来,直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读到(当然需要这次,要尽可能多读取)
从电子角度理解:
水平触发:也就是只有高电平(1)或低电平(0)时才能触发通知,只要在这两种状态就能得到通知.上面提到的只要有数据可读(描述就绪)那么水平触发的epoll就立即返回边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知,上面提到的即使有数据可读,但是没有新的IO活动到来epoll也不会立即返回
四、模型代码设计
描述:一个server,多个client,select体现事件驱动,监听多少下文件描述符,其中的发生任何变化都不执行不会等待其他的,由外在来触发[root@node2 io]# cat chat_server.py#!/usr/local/python3/bin/python3import socketimport selectsk=socket.socket()#先连接到sksk.bind(('127.0.0.1',8800))sk.listen(3)inp=[sk,]while 1: inputs,outputs,errors=select.select(inp,[],[],5) #for第一次完成后,再次循环检测时,这里有两个内容sk,conn for obj in inputs: #第二次天循环时,这个inputs不是sk,变成conn,然后就走else #第一个执行是if,因为obj是sk if obj==sk: conn,addr=sk.accept() #通过sk取得conn,addr print(conn) inp.append(conn) #把conn追加到列表 else: #else当obj是conn时 data=obj.recv(1024) #obj就是里面的conn print(data.decode('utf8')) Inputs=input('response %s>>> '%inp.index(obj)) #inp是一个列表,不断加数据时,相当于不断加client,每个conn都有自身的顺序,就可以取出obj的位置,第0个是sk,所以刚好从1开始 obj.sendall(Inputs.encode('utf8'))[root@node2 io]# cat chat_client.py#!/usr/local/python3/bin/python3import timeimport socketsk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)sk.connect(('127.0.0.1',8800))while True: inp=input('>>> ') sk.sendall(bytes(inp,"utf8")) data=sk.recv(1024) print(data.decode('utf8'))
结果分析:当两个基本同时发时,先到先接收,直接第一个处理完,再到第二个
client1response 1>>> server1client2response 2>>> server2 #response取决于那个先连接[root@node2 io]# python3 chat_client.py >>> client1server1[root@node2 io]# python3 chat_client.py >>> client2server2