线程、进程、队列、IO多路模型
操作系统工作原理介绍、线程、进程演化史、特点、区别、互斥锁、信号、事件、join、GIL、进程间通信、管道、队列。
生产者消息者模型、异步模型、IO多路复用模型、select\poll\epoll 高性能IO模型源码实例解析、高并发FTP server开发
```
一、问答题
1、简述计算机操作系统中的“中断”的作用?
cpu会切:io阻塞、程序运行时间过长
使计算机可以更好更快利用有限的系统资源解决系统响应速度和运行效率的一种控制技术。
实时响应 + 系统调用
2、简述计算机内存中的“内核态”和“用户态”;
操作系统由操作系统的内核(运行于内核态,管理硬件资源)以及
系统调用(运行于用户态,为应用程序员写的应用程序提供系统调用接口)两部分组成;
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
3、什么是进程?
进程:正在进行的一个过程或者说一个任务。而负责执行任务则是cpu。
4、什么是线程?
线程顾名思义,就是一条流水线工作的过程(流水线的工作需要电源,电源就相当于cpu),而一条流水线必须属于一个车间,
一个车间的工作过程是一个进程,车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一条流水线。
所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
5、简述程序的执行过程;
1.激活了python的解释器,有一个解释器级别的垃圾回收线程(GIL锁)。 2.一个进程下的多个线程去访问解释器的代码,拿到执行权限,将程序当作参数传递给解释器的代码去执行。 3.保护不同的数据应该用不同的锁。 4.python程序是顺序执行的! 5.一段python程序以.py文件运行时,文件属性__name__==__main__;作为模块导入时,文件属性__name__为文件名。
6、什么是“系统调用”?
所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情,例如从硬盘读取数据,或者从键盘获取输入等,
而唯一能做这些事情的就是操作系统,所以此时程序就需要向操作系统请求以程序的名义来执行这些操作。
这时,就需要一个机制:用户态程序切换到内核态,但是不能控制在内核态中执行的指令。这种机制就叫系统调用。
7、threading模块event和condition的区别;
condition参考:https://blog.csdn.net/a349458532/article/details/51590040 https://blog.csdn.net/u013346751/article/details/78500412 condition: 某些事件触发或达到特定的条件后才处理数据,默认创建了一个lock对象。 con = threading.Condition() con.acquire() con.notify() con.wait() con.release()
event:其他线程需要通过判断某个线程的状态来确定自己的下一步操作,就可以用event。 from threading import Event event = Event() event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.is_set():返回event的状态值; event.wait():如果 event.is_set()==False将阻塞线程; event.clear():恢复event的状态值为False。
8、进程间通信方式有哪些?
管道、信号量、信号、消息队列、共享内存、套接字
9、简述你对管道、队列的理解;
管道通常指无名管道 1、它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端 2、它只能用于具有亲缘关系的进程中通信(也就是父与子进程或者兄弟进程之间) 3、数据不可反复读取了,即读了之后欢喜红区中就没有了 消息队列 1、消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级 2、消息队列独立于发送与接收进程。进程终止时,消息队列及其内容不会被删除。 3、消息队列可以实现消息随机查询。 mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。 队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的, 可以让我们从复杂的锁问题中解脱出来,因而队列才是进程间通信的最佳选择。 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题, 而且在进程数目增多时,往往可以获得更好的可展性。 队列 = 管道 + 锁 from multiprocessing import Queue,Process queue = Queue() queue.put(url) url = queue.get() from multiprocessing import Pipe,Process pipe = Pipe() pipe.send(url) pipe.recv()
10、请简述你对join、daemon方法的理解,举出它们在生产环境中的使用场景;
join: 等待一个任务执行完毕;可以将并发变成串行。 daemon: 守护进程(守护线程)会等待主进程(主线程)运行完毕后被销毁。 运行完毕: 1.对主进程来说,运行完毕指的是主进程代码运行完毕。 2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
11、请简述IO多路复用模型的工作原理;
IO多路复用实际上就是用select,poll,epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。好处就是单个进程可以处理多个socket。 1.当用户进程调用了select,那么整个进程会被block; 2.而同时,kernel会“监视”所有select负责的socket; 3.当任何一个socket中的数据准备好了,select就会返回; 4.这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。 总结: 1.I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。 2.IO多路复用:需要两个系统调用,system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,
用select的优势在于它可以同时处理多个connection。 3.如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。 4.select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
12、threading中Lock和RLock的相同点和不同点;
Lock():互斥锁,只能被acquire一次,可能会发生死锁情况。 RLock():递归锁,可以连续acquire多次。 RLock = Lock + counter counter:记录了acquire的次数,直到一个线程所有的acquire都被release,其他线程才能获得资源。
13、什么是select,请简述它的工作原理,简述它的优缺点;
python中的select模块专注于I/O多路复用,提供了select poll epoll三个方法;后两个在linux中可用,windows仅支持select。 fd:文件描述符 fd_r_list,fd_w_list,fd_e_list = select.select(rlist,wlist,xlist,[timeout]) 参数:可接受四个参数(前三个必须) rlist:等到准备好阅读 wlist:等到准备写作 xlist:等待“异常情况” 超时:超时时间 返回值:三个列表 select监听fd变化的过程分析: 用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后, 就会发送信号给用户进程数据已到; 用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除, 这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。 该模型的优点: 相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。 如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。 该模型的缺点: 首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。 很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。 如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异, 所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
14、什么是epoll,请简述它的工作原理,简述它的优缺点;
epoll: 性能最好的多路复用I/O就绪通知方法。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。 因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。 epoll:同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值, 你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描, 而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符, 当进程调用epoll_wait()时便得到通知。从以上可知,epoll是对select、poll模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的C/S架构中。 python中的epoll: 只适用于unix/linux操作系统
15、简述select和epoll的区别;
select: 调用select()时 1、上下文切换转换为内核态 2、将fd从用户空间复制到内核空间 3、内核遍历所有fd,查看其对应事件是否发生 4、如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历 5、返回遍历后的fd 6、将fd从内核空间复制到用户空间 select: 缺点 1、当文件描述符过多时,文件描述符在用户空间与内核空间进行copy会很费时 2、当文件描述符过多时,内核对文件描述符的遍历也很浪费时间 3、select最大仅仅支持1024个文件描述符 epoll很好的改进了select: 1、epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。 2、epoll会在epoll_ctl时把指定的fd遍历一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。 epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。 3、epoll对文件描述符没有额外限制。
16、简述多线程和多进程的使用场景;
多进程用于计算密集型,如金融分析;利用多核实现并发。
多线程用于IO密集型,如socket,爬虫,web。
17、请分别简述threading.Condition、threading.event、threading.semaphore、的使用场景;
condition: 某些事件触发或达到特定的条件后才处理数据。 event: 用来通知线程有一些事情已发生,从而启动后继任务的开始。 semaphore: 为控制一个具有有限数量用户资源而设计。
18、假设有一个名为threading_test.py的程序里有一个li = [1, 2, 3, 4]的列表,另有a,b两个函数分别往该列表中增加元素,
a函数需要修改li之前需要获得threading.Lock对象,b函数不需要,请问当线程t1执行a函数获取到Lock对象之后并没有release该对象的情况下,
线程t2执行b函是否可以修改li,为什么?
可以,线程的数据是共享的,a 函数虽然上了锁,没有释放。由于b 函数不需要上锁,就可以访问资源。
19、简述你对Python GIL的理解;
GIL(global interpreter lock)全局解释器锁 GIL是CPython的一个概念,本质是一把互斥锁,将并发运行变成串行。 解释器的代码是所有线程共享的,所以垃圾回收线程也有可能访问到解释器的代码去执行。 因此需要有GIL锁,保证python解释器同一时间只能执行一个任务的代码。 GIL:解释器级别的锁(保护的是解释器级别的数据,比如垃圾回收的数据) Lock:应用程序的锁(保护用户自己开发的应用程序的数据)
20、请列举你知道的进程间通信方式;
消息队列 管道 信号量 信号 共享内存 套接字
21、什么是同步I/O,什么是异步I/O?
同步I/O,用户进程需要主动读写数据。
异步I/O,不需要主动读写数据,只需要读写数据完成的通知。
22、什么是管道,如果两个进程尝试从管道的同一端读写数据,会出现什么情况?
管道:是两个进程间进行单向通信的机制。由于管道传递数据的单向性。管道又称为半双工管道。
管道传递数据是单向性的,读数据时,写入管道应关闭。写数据时,读取管道应关闭。
23、为什么要使用线程池/进程池?
对服务端开启的进程数或线程数加以控制,让机器在一个自己可以承受的范围内运行,这就是进程池或线程池的用途.
24、如果多个线程都在等待同一个锁被释放,请问当该锁对象被释放的时候,哪一个线程将会获得该锁对象?
这个由操作系统的调度决定。
25、import threading;s = threading.Semaphore(value=-1)会出现什么情况?
当threading.Semaphore(1) 为1时,表示只有一个线程能够拿到许可,其他线程都处于阻塞状态,直到该线程释放为止。 当然信号量不可能永久的阻塞在那里。信号量也提供了超时处理机制。如果传入了 -1,则表示无限期的等待。
26、请将二进制数10001001转化为十进制;
10001001 = 1*10^7 + 1*10^3 + 1* 10^0 = 10001001
27、某进程在运行过程中需要等待从磁盘上读入数据,此时该进程的状态将发生什么变化?
一个程序有三种状态:运行态,阻塞态,就绪态;
遇到IO阻塞,进程从运行态转到阻塞态,cpu切走,保存当前状态;
28、请问selectors模块中DefaultSelector类的作用是什么;
IO多路复用:select poll epoll select: 列表循环,效率低。windows 支持。 poll: 可接收的列表数据多,效率也不高。linux 支持。 epoll: 效率最高 异步操作 + 回调函数。linux 支持。 selectors 模块: sel=selectors.DefaultSelector() 自动根据操作系统选择select/poll/epoll
29、简述异步I/O的原理;
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,
首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,
当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
30、请问multiprocessing模块中的Value、Array类的作用是什么?举例说明它们的使用场景
通常,进程之间彼此是完全孤立的,唯一的通信方式是队列或管道。但可以使用两个对象来表示共享数据。
其实,这些对象使用了共享内存(通过mmap模块)使访问多个进程成为可能。
python 多进程通信Queue Pipe Value Array
queue和pipe用来在进程间传递消息;
Value + Array 是python*享内存映射文件的方法;速度比较快.
31、请问multiprocessing模块中的Manager类的作用是什么?与Value和Array类相比,Manager的优缺点是什么?
Python multiprocessing.Manager(进程间共享数据):Python中进程间共享数据,除了基本的queue,pipe和value+array外,
还提供了更高层次的封装,使用multiprocessing.Manager可以简单地使用这些高级接口。
Manager支持的类型有list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Queue,Value和Array。
32、请说说你对multiprocessing模块中的Queue().put(), Queue.put_nowait(), Queue.get(), Queue.get_nowait()的理解;
q = Queue(3) 队列 先进先出 进程间通信; 队列 = 管道 + 锁 q.put() q.put_nowait() # 无阻塞,当队列满时,直接抛出异常queue.Full q.get() q.get_nowait() # 无阻塞,当队列为空时,直接抛出异常queue.Empty
33、什么是协程?使用协程与使用线程的区别是什么?
协程:单线程下的并发。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。 1.python的线程是属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会*交出cpu执行权限, 切换其他的线程运行) 2.单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率 (!!非io操作的切换与效率无关)
34、asyncio的实现原理是什么?
https://www.cnblogs.com/earendil/p/7411115.html Python异步编程:asyncio库和async/await语法 asyncio是Python 3.4 试验性引入的异步I/O框架,提供了基于协程做异步I/O编写单线程并发代码的基础设施。 其核心组件有事件循环(Event Loop)、协程(Coroutine)、任务(Task)、未来对象(Future)以及其他一些扩充和辅助性质的模块。 synchronous io: 做”IO operation”的时候会将process阻塞;”IO operation”是指真实的IO操作 blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类. asynchronous io: 当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号, 告诉进程说IO完成。在这整个过程中,进程完全没有被block。异步io的实现会负责把数据从内核拷贝到用户空间。
二、编程题
1、请写一个包含10个线程的程序,主线程必须等待每一个子线程执行完成之后才结束执行,每一个子线程执行的时候都需要打印当前线程名、当前活跃线程数量以及当前线程名称;
from threading import Thread, currentThread, activeCount import time def task(n): print('线程名:%s %s'%(currentThread(), n )) time.sleep(2) print('当前活跃线程数量:%s'%activeCount()) if __name__ == '__main__': t_li = [] for i in range(10): t = Thread(target=task, args=(i, )) t.start() t_li.append(t) for t in t_li: t.join() print('主,---end---') 打印: 线程名:<Thread(Thread-1, started 6212)> 0 线程名:<Thread(Thread-2, started 780)> 1 线程名:<Thread(Thread-3, started 6616)> 2 线程名:<Thread(Thread-4, started 5344)> 3 线程名:<Thread(Thread-5, started 6352)> 4 线程名:<Thread(Thread-6, started 6724)> 5 线程名:<Thread(Thread-7, started 1168)> 6 线程名:<Thread(Thread-8, started 1540)> 7 线程名:<Thread(Thread-9, started 6812)> 8 线程名:<Thread(Thread-10, started 6824)> 9 当前活跃线程数量:11 当前活跃线程数量:10 当前活跃线程数量:9 当前活跃线程数量:8 当前活跃线程数量:7 当前活跃线程数量:6 当前活跃线程数量:5 当前活跃线程数量:4 当前活跃线程数量:4 当前活跃线程数量:4 主,---end---
2、请写一个包含10个线程的程序,并给每一个子线程都创建名为"name"的线程私有变量,变量值为“Alex”;
from threading import Thread def task(name): print('%s is running'%name) #print('end---') if __name__ == '__main__': for i in range(10): t = Thread(target=task, args=('alex_%s'%i,) ) t.start() print('----主-----') 打印: alex_0 is running alex_1 is running alex_2 is running alex_3 is running alex_4 is running alex_5 is running alex_6 is running alex_7 is running alex_8 is running alex_9 is running ----主-----
3、请使用协程写一个消费者生产者模型;
def consumer(): while True: x = yield print('消费:', x) def producter(): c = consumer() next(c) for i in range(10): print('生产:', i) c.send(i) producter() 打印: 生产: 0 消费: 0 生产: 1 消费: 1 生产: 2 消费: 2 生产: 3 消费: 3 生产: 4 消费: 4 生产: 5 消费: 5 生产: 6 消费: 6 生产: 7 消费: 7 生产: 8 消费: 8 生产: 9 消费: 9
4、写一个程序,包含十个线程,子线程必须等待主线程sleep 10秒钟之后才执行,并打印当前时间;
from threading import Thread,Event import time import datetime def task(): event.wait(10)i print('time:', datetime.datetime.now()) if __name__ == '__main__': event = Event() for i in range(10): t = Thread(target=task ) t.start() time.sleep(10) event.set() 打印: time: 2018-05-01 17:31:15.896462 time: 2018-05-01 17:31:15.897462 time: 2018-05-01 17:31:15.897462 time: 2018-05-01 17:31:15.897462 time: 2018-05-01 17:31:15.897462 time: 2018-05-01 17:31:15.897462 time: 2018-05-01 17:31:15.900462 time: 2018-05-01 17:31:15.900462 time: 2018-05-01 17:31:15.901462 time: 2018-05-01 17:31:15.901462
5、写一个程序,包含十个线程,同时只能有五个子线程并行执行;
from threading import Thread, Semaphore, currentThread import time def task(n): sm.acquire() print('%s--'%n,currentThread().name ) time.sleep(1) print('end') sm.release() if __name__ == '__main__': sm = Semaphore(5) for i in range(10): t = Thread(target=task, args=(i, )) t.start() 打印: 0-- Thread-1 1-- Thread-2 2-- Thread-3 3-- Thread-4 4-- Thread-5 end 5-- Thread-6 end end end 6-- Thread-7 end 7-- Thread-8 8-- Thread-9 9-- Thread-10 end end end end end
6、写一个程序 ,包含一个名为hello的函数,函数的功能是打印字符串“Hello, World!”,该函数必须在程序执行30秒之后才开始执行(不能使用time.sleep());
from threading import Timer def hello(name): print('%s say '%name, 'Hello World!') if __name__ == '__main__': t = Timer(5, hello, args=('alex', )) t.start() 打印: alex say Hello World!
7、写一个程序,利用queue实现进程间通信;
from multiprocessing import Process, current_process, Queue import time def consumer(q): while True: res = q.get() #接结果 if not res:break print('消费了:', res, '---', current_process().name) def producter(q): for i in range(5): print('生产:', i) time.sleep(1) q.put(i) if __name__ == '__main__': q = Queue() p1 = Process(target=producter, args=(q, )) p2 = Process(target=producter, args=(q, )) c1 = Process(target=consumer, args=(q, )) c2 = Process(target=consumer, args=(q, )) c3= Process(target=consumer, args=(q, )) p1.start() p2.start() c1.start() c2.start() c3.start() p1.join() p2.join() q.put(None) #None代表结束信号,有几个消费者来几个信号 q.put(None) #在主进程里边确保所有的生产者都生产结束之后才发结束信号 q.put(None) print('主') 打印: 生产: 0 生产: 0 生产: 1 生产: 1 生产: 2 消费了: 1 --- Process-4 生产: 2 消费了: 1 --- Process-4 生产: 3 消费了: 2 --- Process-4 消费了: 2 --- Process-4 生产: 3 生产: 4 消费了: 3 --- Process-4 生产: 4 消费了: 3 --- Process-4 消费了: 4 --- Process-4 消费了: 4 --- Process-4 主
8、写一个程序,利用pipe实现进程间通信;
from multiprocessing import Process, Pipe def task(conn): conn.send('hello world') conn.close() if __name__ == '__main__': parent_conn, child_conn = Pipe() p = Process(target=task, args=(child_conn, )) p.start() p.join() print(parent_conn.recv()) 打印: hello world
9、使用selectors模块创建一个处理客户端消息的服务器程序;
#server import socket import selectors sel = selectors.DefaultSelector() def accept(server_fileobj, mask): conn, addr = server_fileobj.accept() print(addr) sel.register(conn,selectors.EVENT_READ,read) def read(conn,mask): try: data = conn.recv(1024) if not data: print('closing..',conn) sel.unregister(conn) conn.close() return conn.send(data.upper()) except Exception: print('closeing...',conn) sel.unregister(conn) conn.close() server_fileobj = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server_fileobj.bind(('127.0.0.1',8080)) server_fileobj.listen(5) server_fileobj.setblocking(False) sel.register(server_fileobj,selectors.EVENT_READ,accept) while True: events = sel.select() for sel_obj,mask in events: callback = sel_obj.data callback(sel_obj.fileobj,mask) ##client import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg = input('>>>:').strip() if not msg:continue client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8'))
10、使用socketserver创建服务器程序时,如果使用fork或者线程服务器,一个潜在的问题是,恶意的程序可能会发送大量的请求导致服务器崩溃,请写一个程序,避免此类问题;
# server socketserver 模块内部使用IO多路复用 和多进程/多线程 import socketserver class Handler(socketserver.BaseRequestHandler): def handle(self): print('new connection:',self.client_address) while True: try: data = self.request.recv(1024) if not data:break print('client data:',data.decode()) self.request.send(data.upper()) except Exception as e: print(e) break
11、请使用asyncio实现一个socket服务器端程序;