Python Socket 多任务(多进程/线程、I/O 多路复用、事件驱动开发框架)

0. 概述

1. 循环版实现多连接

2. threading.Thread 多线程

3. SockerServer 实现多任务

3.1 ForkingMixIn - 多进程(限 linux)

3.2 ThreadingMixIn - 多线程

3.3 ThreadingTCPServer - 线程池

4. I/O 多路复用

4.1 select

4.2 epoll

5. Twisted(基于事件驱动的网络开发框架)

5.1 Twisted 简介

5.2 Twisted 官网示例

5.3 Twisted 应用示例:聊天室点对点私聊

5.4 Twisted 应用示例:日志服务器

5.5 Twisted+Select 应用示例:广播聊天室(限 Linux)

 

 

0. 概述

最基础的 TCP 的 Socket 编程,它是阻塞 I/O 模型,基本上只能一对一通信,那为了服务更多的客户端,我们需要改进网络 I/O 模型。

比较传统的方式是使用多进程/线程模型,每来一个客户端连接,就分配一个进程/线程,然后后续的读写都在对应的进程/线程,这种方式处理 100 个客户端没问题,但是当客户端增大到 10000 个时(即C10K,是用来命名并发处理 10k 个连接的 socket 优化问题),10000 个进程/线程的调度、上下文切换以及它们占用的内存,都会成为瓶颈。

为了解决上面这个问题,就出现了 I/O 的多路复用可以只在一个进程里处理多个文件的 I/O,Linux 下有三种提供 I/O 多路复用的 API,分别是:select、poll、epoll。

select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。

在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。

很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。

epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。

  • epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。

  • epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。

而且,epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高

 

1. 循环版实现多连接

以下例子算狭义上实现多用户访问服务,但都是同步执行,也就是一个用户连接关闭,下个用户才可以开始执行向服务发送请求数据。

其实现的核心是服务器接收连接部分写在死循环内,可以一直保持接收新客户端发起的连接请求。

服务器端

 1 import socket
 2 
 3 HOST = '127.0.0.1'
 4 PORT = 50008
 5 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 6 s.bind((HOST, PORT))
 7 s.listen(5)
 8 
 9 while True:
10        print ("开始进入监听状态...")
11        conn, addr = s.accept()
12        print ("接收到连接:", addr)
13        while True:
14            try:
15                data = conn.recv(1024)
16                if not data:
17                    print("断开客户端连接")
18                    break
19                print ("收到客户端数据:", data.decode("utf-8"))
20                msg = "这是一个循环版多连接服务测试"
21                conn.sendall(msg.encode("utf-8"))
22            except socket.error:
23                break
24        conn.close()

客户端

 1 import socket
 2 
 3 HOST = '127.0.0.1'
 4 PORT = 50008
 5 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 6 
 7 s.connect((HOST, PORT))  
 8 times = 3
 9 while times>0:
10     cmd = input("向服务器发送数据:")   
11     s.sendall(cmd.encode("utf-8")) 
12     data = s.recv(1024)
13     print ("接收到服务器端的数据:", data.decode("utf-8"))
14     times -= 1
15 s.close()  # 关闭连接

 

2. threading.Thread 多线程

代码实现:传输文件

  • 服务端定义一个传输内容的规则,客户端按照此内容进行传输;
  • 服务端按照此内容进行解析。

服务器端

 1 import socket, time, socketserver, struct, os, threading
 2 
 3 # 固定的server启动流程
 4 host = '127.0.0.1'
 5 port = 12307
 6 # 定义socket类型
 7 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
 8 # 绑定需要监听的ip和端口号
 9 s.bind((host, port))  
10 s.listen(1)
11 
12 
13 # 定义一个线程的任务函数
14 def conn_thread(connection, address):  
15     while True:
16         try:
17             connection.settimeout(600)
18             # struct.calcsize--->计算12个字符和1个长整型的数据有多长
19             #12个字符对应的是客户端发来的文件名,1个长整型对应的是文件内容的长度大小
20             fileinfo_size = struct.calcsize('36sl')  # 12s表示12个字符,l表示一个长整型数
21             buf = connection.recv(fileinfo_size)  # 正好接收12个字符长度和一个整数的数据
22             # 如果不加这个if,第一个文件传输完成后会自动走到下一句并阻塞,需要拿到文件大小信息才可以继续执行
23             if buf:  
24                 filename, filesize = struct.unpack('36sl', buf)
25                 filename_f = filename.decode("utf-8").strip('\00')  # C语言中'\0'是一个ASCII码为0的字符,在python中表示占一个位置的空字符
26                 print("****filename:",filename_f)
27                 # 拿到文件名之后,我们要拼接一个新的文件绝对路径,在服务器上保存下来这个问题件
28                 filenewname = os.path.join('e:\\', os.path.basename(filename_f))
29                 print(u'文件名称: %s , 文件大小: %s' % (filenewname, filesize))
30                 recvd_size = 0  # 收的文件内容有多大了
31                 file = open(filenewname,'wb')
32                 print(u"开始传输文件内容")
33                 while not recvd_size == filesize:  # 如果收到的文件长度不等于文件真实长度,就一直循环收
34                     if filesize - recvd_size > 1024:  # 文件大小和已经收的大小相差大于1024字节
35                         rdata = connection.recv(1024)  # 每次收1024个字节的内容
36                         recvd_size += len(rdata)  # 收到的长度在recvd_size累加
37                     else:
38                         # 用实际的文件大小减去已经收的差值去收(收最后剩余的大小)
39                         rdata = connection.recv(filesize-recvd_size)
40                         recvd_size = filesize  # 把实际收到的长度相加,recvd_size == filesize
41                     file.write(rdata)  # 把文件的内容写进去
42                 file.close()
43                 print('receive done')
44                 # connection.close()
45         except socket.timeout:
46             connection.close()
47 
48 while True:
49     print(u"开始进入监听状态")
50     connection, address = s.accept()
51     print('Connected by ', address)
52     # 起一个子线程去收文件
53     thread = threading.Thread(target=conn_thread, args=(connection, address))
54     thread.start()
55     thread.join()  # 阻塞,等都收完了才会关掉连接
56     # s.close()

客户端

 1 import socket, os, struct
 2 
 3 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 4 s.connect(('127.0.0.1', 12307))
 5 while True:
 6     filepath = input('请输入要传输的文件绝对路径:')
 7     print(type(filepath))
 8     print(len(filepath.encode("utf-8")))
 9     if os.path.isfile(filepath):  # 判断文件在不在,在就传输。
10         #fileinfo_size = struct.calcsize('36sl')  # 定义打包规则(需与服务器端规则一致)
11         # 定义文件头信息,包含文件名和文件大小
12         fhead = struct.pack('36sl', filepath.encode("utf-8"), os.stat(filepath).st_size)
13         print(os.stat(filepath).st_size)
14         s.send(fhead)
15         print (u'文件路径:', filepath)
16         # with open(filepath,'rb') as fo: 这样发送文件有问题,发送完成后还会发一些东西过去
17         fo = open(filepath, 'rb')
18         while True:
19             filedata = fo.read(1024)
20             if not filedata:
21                 break
22             s.send(filedata)
23         fo.close()
24         print (u'传输成功')
25         # s.close()

 

3. SockerServer 实现多任务

Socket 编程在模块创建时无法进行多进程的处理,当有大量请求时,请求就会阻塞在队列中,甚至发生请求丢弃,如果需要大量 socket 就需要许多的 socket 绑定端口,写很多重复性得代码。

SocketServer 简化了网络服务器的编写。在进行 socket 创建时,使用 SocketServer 会大大减少创建的步骤,并且 SocketServer 使用了 select,它有4个类:TCPServer、UDPServer、UnixStreamServer 和 UnixDatagramServer,这4个类是同步进行处理的,另外通过 ForkingMixIn 和 ThreadingMixIn 类来支持异步。

ForkingMixIn 和 ThreadingMixIn 两个混合类,它们都提供 Server 类中 process_request 方法的新实现,前者在处理每次用户连接的时候都会开启新的进程,而后者会开启新的线程。想要让 Server 类实现并发处理,只用利用多继承即可,或者直接使用已经混合好的类。

使用步骤:

  1. 创建一个请求处理的类,是 BaseRequestHandler 的子类并重写其 handle 方法;
  2. 实例化一个服务器类,传入服务器的地址和请求处理的程序类;
  3. 调用 handle_request(),一般是调用其他事件循环或者使用 select 或 serve_forever。

集成 ThreadingMixIn 类时需要处理异常关闭。daemon_threads 指示服务器是否要等待线程终止,要是线程互相独立,必须要设置为 True,默认是 False。

 

3.1 ForkingMixIn - 多进程(限 linux)

多个连接同时到达服务器端的时候,主进程都会生成一个子进程专门处理此连接,而主进程则依旧保持监听状态。

因主进程和子进程是同时进行的,所以不会阻塞新的连接。但由于创建进程所消耗的资源比较大,这种处理方式在有大量连接时会带来性能问题。

服务器端

 1 from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler
 2 import time
 3 
 4 
 5 # 自定义服务器类
 6 class Server(ForkingMixIn, TCPServer):  
 7     pass
 8 
 9 # 处理请求的程序类
10 class MyHandler(StreamRequestHandler):
11 
12     # 重写父类的handle函数
13     def handle(self):  
14         addr = self.request.getpeername()  # 获得客户端的地址
15         print('接收到连接:', addr)  # 打印客户端地址
16         data = self.rfile.readline().strip()  # 客户端发送的信息必须带有回车,否则会一直等待客户端继续发送数据
17         print("从客户端接收到的请求:", data.decode("utf-8"))
18         time.sleep(1)
19         if data:
20             self.wfile.write('这是从服务端进程中发出的消息'.encode("utf-8"))  # 给客户端发送信息
21 
22 
23 host = ""
24 port = 18001
25 # 实例化一个服务器类,传入服务器的地址和请求处理的程序类
26 server = Server((host, port), MyHandler)
27 print("开始监听状态...")
28 # 开始侦听并处理连接
29 server.serve_forever()

客户端

1 if __name__ == '__main__':
2     import socket
3     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4     sock.connect(('127.0.0.1', 18001))
5     import time
6     time.sleep(2)
7     sock.send('ls -al /home/wxh'.encode("utf-8")+"\n".encode("utf-8"))
8     print (sock.recv(1024).decode("utf-8"))
9     sock.close()

执行效果

[gr@gloryroad juno]$ python3 server.py
开始监听状态...
接收到连接: ('127.0.0.1', 42444)
从客户端接收到的请求: ls -al /home/wxh
接收到连接: ('127.0.0.1', 42446)
从客户端接收到的请求: ls -al /home/wxh

 

3.2 ThreadingMixIn - 多线程

既然进程间上下文切换的“包袱”很重,那我们就搞个比较轻量级的模型来应对多用户的请求 —— 多线程模型

线程是运行在进程中的一个“逻辑流”,比 Fork 消耗的资源更少,单进程中可以运行多个线程,而且主线程和子线程之间共享相同的内存空间,处理效率高。但大量的使用线程会带来线程之间的数据同步问题,处理不好可能使服务程序失去响应。

以下示例与 Fork 方式中代码基本相同,仅仅是采用的 ThreadingMixIn 类不同。

服务器端

 1 from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler
 2 import time
 3 
 4 
 5 # 自定义服务器类
 6 class Server(ThreadingMixIn, TCPServer):
 7     pass
 8 
 9 # 处理请求的程序类
10 class MyHandler(StreamRequestHandler):
11 
12     # 重写父类的handle函数
13     def handle(self):
14         addr = self.request.getpeername()  # 获得客户端的地址
15         print('接收到连接:', addr)  # 打印客户端地址
16         data = self.rfile.readline().strip()  # 客户端发送的信息必须带有回车,否则会一直等待客户端继续发送数据
17         print("从客户端接收到的请求:", data.decode("utf-8"))
18         time.sleep(1)
19         if data:
20             self.wfile.write('这是从服务端线程中发出的消息'.encode("utf-8"))  # 给客户端发送信息
21 
22 
23 host = ''
24 port = 18001
25 # 实例化一个服务器类,传入服务器的地址和请求处理的程序类
26 server = Server((host, port), MyHandler)
27 print("开始监听状态...")
28 # 开始侦听并处理连接
29 server.serve_forever()

客户端

1 if __name__ == '__main__':
2     import socket
3     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4     sock.connect(('127.0.0.1', 18001))
5     import time
6     time.sleep(2)
7     sock.send('ls -al /home/wxh'.encode("utf-8")+"\n".encode("utf-8"))
8     print (sock.recv(1024).decode("utf-8"))
9     sock.close()

 

3.3 ThreadingTCPServer - 线程池

 服务器端

 1 import socketserver
 2 import threading
 3 
 4 # 自定义任务线程类
 5 class MyTCPHandler(socketserver.BaseRequestHandler):
 6     # 重写 handle 方法
 7     def handle(self):
 8         while True:
 9             print("接收到连接: ", self.client_address)
10             self.data = self.request.recv(1024).strip()
11             cur_thread = threading.current_thread()
12             print("当前线程: ", cur_thread)
13             if not self.data:
14                 print("客户端[%s]退出!" % self.client_address[0])
15                 break
16             print("客户端[%s]请求数据: %s" % (self.client_address[0], self.data.decode("utf-8")))
17             self.request.sendall(self.data.upper())
18 
19 if __name__ == "__main__":
20     HOST, PORT = "", 18001
21     server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
22     print("开始监听状态...")
23     server.serve_forever()

客户端

1 if __name__ == '__main__':
2     import socket
3     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4     sock.connect(('127.0.0.1', 18001))
5     import time
6     time.sleep(2)
7     sock.send('ls -al /home/wxh'.encode("utf-8")+"\n".encode("utf-8"))
8     print (sock.recv(1024).decode("utf-8"))
9     sock.close()

执行效果

开始监听状态...
接收到连接:  ('127.0.0.1', 17215)
当前线程:  <Thread(Thread-1, started 9620)>
客户端[127.0.0.1]请求数据: ls -al /home/wxh
接收到连接:  ('127.0.0.1', 17215)
当前线程:  <Thread(Thread-1, started 9620)>
客户端[127.0.0.1]退出!
接收到连接:  ('127.0.0.1', 17216)
当前线程:  <Thread(Thread-2, started 9496)>
客户端[127.0.0.1]请求数据: ls -al /home/wxh
接收到连接:  ('127.0.0.1', 17216)
当前线程:  <Thread(Thread-2, started 9496)>
客户端[127.0.0.1]退出!

 

4. I/O 多路复用

什么是 I/O 多路复用?

上面基于进程或者线程模型的,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程,如果一台机器维护 1 万个连接,相当于要维护 1 万个进程/线程,操作系统就算死扛也是扛不住的。

既然为每个请求分配一个进程/线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术。

一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。

我们熟悉的 select/epoll 内核都使用了 I/O 多路复用机制,给用户态提供了多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件——可以监视多个描述符的读/写等事件,一旦某个描述符就绪(一般是读或者写事件发生了),就能够将发生的事件通知对应的应用程序去处理该事件。

select/epoll 是如何获取网络事件的呢?

在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。

select 和 epoll 的本质区别在哪里呢?

当套接字比较多的时候,每次 select() 都要通过遍历 FD_SETSIZE 个 Socket 来完成调度,不管哪个 Socket 是活跃的,统统都遍历一遍。

epoll 使用“事件”的就绪通知方式,给套接字注册某个回调函数,只有活跃可用的 FD,自动完成相关操作,避免了轮询,提升了效率。

举个生活中类似的例子:

假如时光倒流,我们回到大学读书。你去女生宿舍,找你女朋友。于是你找到了宿管大妈,宿管大妈就会带着你,挨个房间去找,直到找到你女朋友(这就是 select 版)。

而 epoll 版呢,你来了,把你女朋友的名字和宿舍房号报给舍管大妈,大妈就直接帮你找到你女朋友。

 

4.1 select

在 python 中,select 函数是一个对底层操作系统的直接访问的接口。它用来监控 sockets、files 和 pipes,等待 I/O 完成(Waiting for I/O completion)。当有可读、可写或是异常事件产生时,select 可以很容易的监控到。

Select 模块在 Windows、Unix 和 Linux 下均可使用,但在 Windows 下,select 只能用于处理 socket。

select 实现多路复用的方式

select 实现多路复用的方式是,将已连接的 socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 socket,然后再对其处理。

所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。

select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

select.select(rlist, wlist, xlist[, timeout]) 参数含义

  1. rlist:输入而观察的文件对象列表
  2. wlist:输出而观察的文件对象列表
  3. xlist:观察错误异常的文件列表
  4. timeout:可选参数,表示超时秒数,如果为 None 或者为空则阻塞直到事件发生。

其返回1个 tuple,分别是 3 个准备好的对象列表,它和前边的参数是一样的顺序。

服务器端代码实现

 1 import socket
 2 import select
 3 
 4 s = socket.socket()
 5 s.bind(("127.0.0.1", 8888))
 6 s.listen(5)
 7 
 8 # 要监控的对象列表
 9 r_list = [s, ] 
10 num = 0
11 
12 # server的死循环
13 while True:
14     print("监听中...")
15 
16     # 第1个实参 r_list:可读的对象,监听两种事件 -- 新客户端连接与客户端发送消息
17     # 第2个实参:可写的对象(本例不用)
18     # 第3个实参:出现异常的对象(本例不用)
19     # 这三个参数内容都是被操作系统监控的,即select.select()会执行系统内核代码
20     #   1)当有事件发生时,立马往下执行代码;否则阻塞监控10秒
21     #   2)若监控10秒了仍无事件发生,才往下执行
22     rl, wl, error = select.select(r_list, [], [], 10)
23 
24     # rl:监听某个文件描述符是否发生了读的事件(1. 有client进行连接;2. client给server发了数据)
25     #     rl列表一开始为空,只有当s发生事件了(如首先收到连接请求),才会将s加到rl中
26     # wl:监听某个文件描述符是否发生了写的事件(如server给client发了数据)
27     # error:监听某个文件描述符是否发生了异常事件
28 
29     num += 1
30     print("执行次数:%s" % num)
31     print("rl's length is: %s" % len(rl))
32     print("r_list's length: %s" % len(r_list))
33     print("r1中的对象:", [i for i in rl])
34 
35     # 只有发生两种事件(有新连接或收到数据)时,rl列表中才会有对象元素,for循环才会往下执行
36     for fd in rl:
37         # 如果发生事件的对象是服务器端对象(s),则代表有新客户端连接
38         if fd == s:
39             conn, addr = fd.accept()  # 建立与客户端的连接
40             r_list.append(conn)  # 将连接对象放到监听列表r_list中
41             # 只有当客户端断开连接(close)了,conn才会从r_list中剔除
42             msg = conn.recv(200).decode("utf-8")  # 接收客户端的数据
43             print("%s First request data: %s" % (addr, msg))
44             # 把收到的数据变大写返回给客户端
45             conn.send(msg.upper().encode("utf-8"))
46             # s处理完后,则从rl中剔除了
47         # 如果发生事件的对象是连接对象(conn),则代表收到客户端请求数据
48         else:
49             try:
50                 msg = fd.recv(200).decode("utf-8")
51                 if msg != "":
52                     print("%s Else request data: %s" % (fd.getpeername(), msg))
53                     fd.send(msg.upper().encode("utf-8"))
54                 else:
55                     # 如果拿到b""的内容,则判断客户端断开连接
56                     r_list.remove(fd)  # 将连接对象从监听列表去掉
57                     print("%s connection closed." % str(fd.getpeername()))
58                     fd.close()  # 断开客户端连接
59             except (ConnectionAbortedError, ConnectionResetError):
60                 r_list.remove(fd)
61                 print("%s 发生连接异常,与客户端断开连接" % str(fd.getpeername()))
62                 fd.close()
63             except Exception as e:
64                 print("%s 发生了其它异常: %s" % (fd.getpeername(), e))
65             # conn处理完后,则从rl中剔除了
66 s.close()

 客户端代码实现

 1 import socket
 2 
 3 HOST = '127.0.0.1'
 4 PORT = 8888
 5 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 6 
 7 s.connect((HOST, PORT))  
 8 for i in range(4):
 9     cmd = input("向服务器发送数据:")   
10     s.sendall(cmd.encode("utf-8")) 
11     data = s.recv(1024)
12     print ("接收到服务器端的数据:", data.decode("utf-8"))
13 s.close()  # 关闭连接

执行效果

服务器端:

E:\>python server.txt
监听中...
执行次数:1
rl's length is: 0
r_list's length: 1
r1中的对象: []
监听中...
执行次数:2
rl's length is: 0
r_list's length: 1
r1中的对象: []
监听中...
执行次数:3
rl's length is: 1
r_list's length: 1
r1中的对象: [<socket.socket fd=480, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888)>]
('127.0.0.1', 22613) First request data: hello 1
监听中...
执行次数:4
rl's length is: 1
r_list's length: 2
r1中的对象: [<socket.socket fd=476, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888), raddr=('127.0.0.1', 22613)>]
('127.0.0.1', 22613) Else request data: hello 2
监听中...
执行次数:5
rl's length is: 1
r_list's length: 2
r1中的对象: [<socket.socket fd=476, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888), raddr=('127.0.0.1', 22613)>]
('127.0.0.1', 22613) connection closed.
监听中...
执行次数:6
rl's length is: 0
r_list's length: 1
r1中的对象: []
监听中...
执行次数:7
rl's length is: 1
r_list's length: 1
r1中的对象: [<socket.socket fd=480, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888)>]
('127.0.0.1', 22616) First request data: hello 3
监听中...
执行次数:8
rl's length is: 1
r_list's length: 2
r1中的对象: [<socket.socket fd=476, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888), raddr=('127.0.0.1', 22616)>]
('127.0.0.1', 22616) connection closed.
...
...

客户端:

E:\>python client.txt
向服务器发送数据:hello 1
接收到服务器端的数据: HELLO 1
向服务器发送数据:hello 2
接收到服务器端的数据: HELLO 2
向服务器发送数据:Traceback (most recent call last):
  File "client.txt", line 9, in <module>
    cmd = input("向服务器发送数据:")
KeyboardInterrupt

E:\>python client.txt
向服务器发送数据:hello 3
接收到服务器端的数据: HELLO 3
...
...

 

4.2 epoll

epoll 通过两个方面,很好解决了 select/poll 的问题。

  1. epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
  2. epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

下图可以看到 epoll 相关的接口作用:

 

epoll 的方式即使监听的 socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器

epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET水平触发(level-triggered,LT

这两个术语还挺抽象的,其实它们的区别还是很好理解的。

  • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;

  • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

这就是两者的区别,水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

  • 如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
  • 如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK

一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。

另外,使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用。因为多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。

 

5. Twisted(基于事件驱动的网络开发框架)

5.1 Twisted 简介

Twisted 是用 python 实现的基于事件驱动的网络开发框架。Twisted 自身提供选项来支持 epoll 或 iocp。

  • IOCP(Input/Output Completion Port, 输入输出完成端口)是支持多个同时发生的异步的应用程序编程接口,在 Windows NT 的 3.5 版本以后,或 AIX 5 版以后或 Solaris 第十版以后,开始支持。

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

Twisted 诞生于 2000 年初,在当时的网络游戏开发者看来,无论他们使用哪种语言,手中都鲜有可兼顾扩展性及跨平台的网络库。Twisted 的作者试图在当时现有的环境下开发游戏,这一步走的非常艰难,他们迫切地需要一个可扩展性高、基于事件驱动、跨平台的网络开发框架,为此他们决定自己实现一个,并从那些之前的游戏和网络应用程序的开发者中学习,汲取他们的经验教训。

Twisted 可以做到当时 python 中已有的网络平台所无法做到的事情:

  • 使用基于事件驱动的编程模型,而不是多线程模型。
  • 跨平台:为主流操作系统平台暴露出的事件通知系统提供统一的接口。
  • “内置电池”的能力:提供流行的应用层协议实现,因此 Twisted 马上就可为开发人员所用。
  • 符合 RFC 规范,已经通过健壮的测试套件证明了其一致性。
  • 能很容易的配合多个网络协议一起使用(TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC 以及 FTP 等)。
  • 可扩展。

下图展示随着时间的推移,单线程、多线程以及事件驱动编程模型这三种模式下程序所做的工作。这个程序有 3 个任务需要完成,每个任务都在等待 I/O 操作时阻塞自身。阻塞在 I/O 操作上所花费的时间已经用灰色框标示出来了。

  • 单线程同步模型中,任务按照顺序执行。如果某个任务因为 I/O 而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要地降低了运行速度。
  • 多线程版本中,这 3 个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的 bug。
  • 事件驱动版本的程序中,3 个任务交错执行,但仍然在一个单独的线程控制中。当处理 I/O 或者其他昂贵的操作时,注册一个回调到事件循环中,然后当 I/O 操作完成时继续执行,回调描述了该如何处理某个事件。事件循环轮询所有事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能地得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

安装

  • py -3 -m pip install twisted
  • py -3 -m pip install Pywin32(Pywin32 作用:Python 是没有自带访问 Windows 系统 API 的库的)

Twisted 组成

Reactor

在单线程环境中调度多个事件源产生的事件到它们各自的事件处理例程中去。Twisted 缺省的 reactor 是只能建 512 个连接的,若换成 iocp reactor 或 epoll reactor 能提高连接数和数据处理效率。

from twisted.internet import epollreactor
from twisted.internet import iocpreactor

Transports

用来收发数据,服务器端与客户端的数据收发与处理都是基于这个模块。

Transports 代表网络中两个通信结点之间的连接。Transports 负责描述连接的细节,比如连接是面向流式的还是面向数据报的,流控以及可靠性。TCP、UDP 和 Unix 套接字可作为 transports 的例子。它们被设计为“满足最小功能单元,同时具有最大程度的可复用性”,而且从协议实现中分离出来,这让许多协议可以采用相同类型的传输。Transports 实现了 ITransports 接口,它包含如下的方法:

将 transports 从协议中分离出来也使得对这两个层次的测试变得更加简单。可以通过简单地写入一个字符串来模拟传输,用这种方式来检查。

Protocols

ProtocolFactory:是工厂模式的体现,在这里面生成协议。

Protocols 对象实现协议内容,即通信的内容协议。

Protocols 描述了如何以异步的方式处理网络中的事件。TCP、UDP 应用层例子,Protocols 实现了 IProtocol 接口,它包含如下的方法:

 

5.2 Twisted 官网示例

Twisted 框架(服务器端)代码实现步骤:

  1. 先定义继承自 Protocol 类的子类实例(本例中是 Echo 类)
  2. 实现自 Factory 类的子类(本例中是 EchoFactory),在类中必须实现 buildProtocol 方法,此方法必须返回一个 Protocol 类的子类实例(本例中就是 echo 的实例)
  3. 启动端口监听:reactor.listenTCP(1200, Factory)
    • 使用 tcp 协议做监听
    • 1200:监听的端口
    • Factory:EchoFactory 类的实例
  4. 运行监听:reactor.run()

服务器端

  • 协议工厂继承自 twisted.internet.protocol.Factory,需实现 buildProtocol 方法,协议工厂负责实例化协议类,不应该保存与连接相关的状态信息,因为协议工厂类仅创建一个。
  • 协议类继承自 twisted.internet.protocol.Protocol,需实现 dataReceived 等方法,在协议类中实现应用协议,每一个客户端连接都会创建一个新的协议类对象。
  • transport 就是连接对象,通过它进行网络写数据。
 1 from twisted.internet.protocol import Protocol
 2 from twisted.internet.protocol import Factory
 3 from twisted.internet import reactor
 4 
 5 
 6 # 实现server的业务处理逻辑
 7 class Echo(Protocol):
 8     "协议类实现用户的服务协议,例如 http,ftp,ssh 等"
 9 
10     def __init__(self, factory):
11         self.factory = factory
12 
13     def connectionMade(self):
14         "连接建立时被回调的方法"
15         # 客户端连接数+1
16         self.factory.numProtocols = self.factory.numProtocols + 1
17         # 给客户端返回数据
18         self.transport.write("目前有 %d 个开放式连接".encode("utf-8") % (self.factory.numProtocols,))
19         print("[%s] connected." % self.transport.getPeer())
20         # self.transport.getPeer().host/port/type:分别获得客户端的IP、端口、协议类型
21 
22     def connectionLost(self, reason):
23         "连接关闭时被回调的方法"
24         # 客户端连接数-1
25         self.factory.numProtocols = self.factory.numProtocols - 1
26         print("[%s] closed." % self.transport.getPeer())
27 
28     def dataReceived(self, data):
29         "接收数据的方法,当有数据到达时被回调"
30         # 打印接收的数据
31         print("[%s] request data: %s" % (self.transport.getPeer(), data.decode("utf-8")))
32         # 给客户端返回数据
33         self.transport.write(data)
34 
35 
36 class EchoFactory(Factory):
37     "协议工厂类,当客户端建立连接的时候,创建协议对象,协议对象与客户端连接一一对应"
38     
39     # 记录客户端连接数量
40     numProtocols = 0
41 
42     # protocol = Echo
43     # 每当有一个新连接,就会实例化Echo对象
44     def buildProtocol(self, addr):
45         # 返回一个具有业务逻辑处理能力的实例对象
46         return Echo(self)
47 
48 
49 if __name__ == '__main__':
50 
51     # 工厂类只需实例化一次
52     FACTORY = EchoFactory()
53     
54     # 使用tcp协议做监听,并用1200作为监听端口
55     # FACTORY:需要是一个继承自FACTORY类的子类实例,本例中就是EchoFactory的一个实例
56     # 子类中必须实现buildProtocol方法,且返回一个继承自Protocol的子类实例
57     reactor.listenTCP(1200, FACTORY)
58 
59     # 开始监听事件
60     print("监听中..")
61     reactor.run()

客户端

 1 if __name__ == '__main__':
 2     import socket  
 3     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
 4     sock.connect(('localhost', 1200))  
 5     import time  
 6     time.sleep(2)  
 7     print("收到服务器端数据:",sock.recv(1024).decode("utf-8") )  # 建立连接时,server返回的数据
 8     sock.send('ls -al /home/wxh'.encode("utf-8"))  
 9     print("收到服务器端数据:",sock.recv(1024).decode("utf-8"))  # 首次发送数据后,server返回的数据(ls -al /home/wxh)
10     sock.send('ipconfig'.encode("utf-8"))  
11     print("收到服务器端数据:",sock.recv(1024).decode("utf-8"))  # 第二次发送数据后,server返回的数据(ipconfig)
12     sock.close()

执行效果

E:\>python twisted-server.txt
监听中..
[IPv4Address(type='TCP', host='127.0.0.1', port=23354)] connected.
[IPv4Address(type='TCP', host='127.0.0.1', port=23354)] request data: ls -al /home/wxh
[IPv4Address(type='TCP', host='127.0.0.1', port=23354)] request data: ipconfig
[IPv4Address(type='TCP', host='127.0.0.1', port=23354)] closed.
[IPv4Address(type='TCP', host='127.0.0.1', port=23355)] connected.
[IPv4Address(type='TCP', host='127.0.0.1', port=23355)] request data: ls -al /home/wxh
[IPv4Address(type='TCP', host='127.0.0.1', port=23355)] request data: ipconfig
[IPv4Address(type='TCP', host='127.0.0.1', port=23355)] closed.
...
...

 

5.3 Twisted 应用示例:聊天室点对点私聊

服务器端

  1 from twisted.internet.protocol import Factory
  2 from twisted.protocols.basic import LineReceiver  # 事件处理器
  3 from twisted.internet import reactor
  4 
  5 
  6 # 每一个客户端连接都会对应一个不同的Chat对象
  7 class Chat(LineReceiver):  
  8 
  9     message_dict={}  # 存储每个客户端发送的聊天消息的,放到字典里
 10 
 11     def __init__(self, users):
 12         self.users = users      # 实参为存储了所有连接用户信息的字典
 13         self.name = None        # 当前连接客户端的昵称,先做个初始化
 14         self.state = "GETNAME"  # 当前连接初始化为起名状态
 15         
 16     
 17     # 这个客户端的状态在新建连接成功,让他处于起昵称的状态
 18     # 每个客户端都有一个昵称,存在self.name里面
 19     # 昵称起完后,也会同时放入到self.user里面,self.name是key,value则是客户端的链接对象
 20     def connectionMade(self):
 21         "客户端连接成功后,业务逻辑规定,必须先给自己起个昵称,才可以开始聊天"
 22         self.sendLine("请输入你的昵称".encode("utf-8")) # server端给客户端发了一个消息
 23 
 24     def connectionLost(self, reason):
 25         "断开连接时候自动触发"
 26          # 根据实例中的name,从self.users里面删除掉对应客户端的数据
 27         if self.name in self.users:
 28             del self.users[self.name]
 29         try:
 30             # 删除该用户所有的聊天消息
 31             del Chat.message_dict[self.name]  
 32         except:
 33             print("%s用户退出,用户聊天记录删除失败" % self.name)
 34                 
 35     def lineReceived(self, line):  
 36         "对接收内容开始做处理,只要收到客户端消息,自动触发此方法"
 37         # 根据状态开始选择不同的内容处理
 38         # GETNAME:还没起昵称的阶段,调用handle_GETNAME方法
 39         if self.state == "GETNAME": 
 40             self.handle_GETNAME(line)
 41         # 走聊天的逻辑:通过handle_CHAT
 42         else:
 43             self.handle_CHAT(line)
 44 
 45     def handle_GETNAME(self, name):
 46         "起名阶段的处理方法:判断是否重名、存储昵称"
 47         if name in self.users:
 48             # self.sendLine给客户端发送消息的方法:提示重名了
 49             self.sendLine("很遗憾该昵称已存在,请重新起名.".encode("utf-8"))
 50             return
 51         # 若不重名,则提示欢迎
 52         self.sendLine("欢迎, %s!".encode("utf-8") % (name,))
 53         # 把客户端发来的昵称,存到连接对象的实例变量self.name里面
 54         self.name = name.decode("utf-8")
 55         # 将昵称和连接对象存到全局的self.users字典里面
 56         self.users[name.decode("utf-8")] = self 
 57         # 将连接对象的状态改为聊天状态,后续所有的消息都会走聊天逻辑
 58         self.state = "CHAT"
 59         print("当前用户列表:%s", self.users)
 60 
 61     def handle_CHAT(self, message):
 62         # 客户端给其他用户发送消息的格式 --> 接收者名:聊天的信息
 63         # 例如发给user2的消息 --> user2:do you eat lunch?
 64         
 65         message = message.decode("utf-8")
 66         
 67         # 判断消息中有没有;且没有getmessage关键字,则保存别人发送给某个用户私聊消息
 68         if ":" in message and "getmessage" not in message:
 69             username = message.split(":")[0]  # 用冒号分割获取接收此消息的用户名
 70             if username not in Chat.message_dict:  # 若用户名不在聊天消息字典,说明是个新连接的用户
 71                 Chat.message_dict[username] = []  # 在聊天消息字典中,给该接收者新建一个列表来存储发给他的聊天消息
 72             Chat.message_dict[username].append(self.name+":"+message)  # 别人发送的消息,根据接收者的用户名,存到对应的消息字典中
 73             print("-->", "增加了用户发送的消息:%s" % message)  # 打印消息
 74             print("最新聊天消息记录:%s" % Chat.message_dict)
 75             return
 76 
 77         # 客户端命令"getmessage"用于获取指定用户发送给自己的消息,格式为 --> user1:getmessage
 78         elif "getmessage" in message:
 79             username = message.split(":")[0]
 80             print("当前消息字典:", Chat.message_dict)  # 打印一下消息字典中有什么消息
 81             # 用户名在消息字典不存在,或者消息为空,则发送一个消息提示没有数据
 82             if (username not in Chat.message_dict) or Chat.message_dict[username] == []:
 83                 self.users[username].sendLine("未有新消息".encode("utf-8"))
 84                 print("-->", "未有新消息")
 85                 return
 86             message_indict = Chat.message_dict[username].pop(0)  # 发送存储的第一条消息
 87             print("即将发送的消息:", message_indict)
 88             sender = message_indict.split(":")[0]  # 获得发送用户名
 89             send_message = message_indict.split(":")[2]  # 获取消息体
 90             username = message_indict.split(":")[1]  # 获取用户名
 91             if username in self.users:  # 遍历所有的连接对象,通过用户名找到对应的连接对象
 92                 print(username,self.users[username],self.users[username].name)
 93                 # 把消息体发送给这个用户名对应的连接对象中
 94                 self.users[username].sendLine(("%s:%s" % (sender, send_message)).encode("utf-8"))
 95             else:
 96                 self.sendLine(("指定的用户'%s'不存在" % username).encode("utf-8"))
 97             
 98         # 客户端查看当前在线的用户名
 99         elif message.strip() == "list":
100             print("list response")
101             self.sendLine(str([username for username in self.users]).encode("utf-8"))
102             print("-->", [username for username in self.users])
103             return
104             
105         # 客户端断开连接
106         elif message.strip() == "bye":
107             print("list response")
108             self.sendLine(("Bye from server!").encode("utf-8"))
109             print("-->", str([username for username in self.users]))
110             return
111             
112         # 用户输入命令都不是上面的命令,则执行else逻辑
113         else:
114             send_message= ("请指定用户名,按照格式'用户名:消息'来进行发送。\n或者输入list查看当前登录用户\n输入getmessage\n")
115             # print(type(send_message))
116             self.sendLine(send_message.encode("utf-8"))
117             print("-->",send_message)
118             return                
119                 
120 class ChatFactory(Factory):
121 
122     def __init__(self):
123         # 有多个连接的时候,会有多个chat对象
124         # self.users 在内存地址中,只有一份,所有连接对象都只使用同一个实例变量 self.users(等价于一个全局变量)
125         self.users = {}  
126         # key: 客户端自定义的昵称;value:连接对象本身
127 
128     # 一个客户端连接会实例化一个新的Chat对象
129     def buildProtocol(self, addr):
130         print(type(addr),addr)
131         # 返回一个处理具体业务请求的对象,参数传递了字典(存有所有连接对象)
132         return Chat(self.users)
133 
134 
135 if __name__ == '__main__':
136     # 设定监听端口和对象
137     # 使用Tcp协议,实例化ChatFactory
138     # 处理业务的工厂对象:ChatFactory
139     reactor.listenTCP(1200, ChatFactory())
140                                           
141     print ("开始进入监听状态...")
142     reactor.run()  # 开始监听

客户端

 1 from socket import *
 2 import time
 3 
 4 s = socket(AF_INET, SOCK_STREAM)
 5 remote_host = gethostname()
 6 print ('remote_host:', remote_host)
 7 port = 1200
 8 s.connect((remote_host, port))  # 发起连接
 9 print ("连接从", s.getsockname())  # 返回本地IP和端口
10 print ("连接到", s.getpeername())  # 返回服务端IP和端口
11 
12 print ('从服务器返回消息: ', end="")
13 print (s.recv(1200).decode("utf-8").strip())
14 
15 sername = input("请输入你要使用的英文用户名:")
16 s.send(('%s\r\n' % sername.strip()).encode("utf-8"))  # 发送一行字符串(以\r\n结束)到服务器端
17 print ('从服务器返回消息:')
18 print (s.recv(1200).decode("utf-8").strip())
19 print("*"*50)
20 print("""查看当前登录用户列表的命令:list
21 查看别人给你发送的消息命令要求:getmessage 
22 给别人发送消息的数据格式:username:要发送的消息  
23 """)
24 print("*"*50)
25 
26 
27 while 1:
28     send_message=input("请输入发送的信息:")    
29     if send_message == "getmessage" :
30         s.send(('%s:%s\r\n' % (sername,send_message)).encode("utf-8"))
31         print ('从服务器返回消息: ', end="")
32         s.settimeout(2)
33         try:
34             print (s.recv(1200).decode("utf-8").strip())
35         except:
36             pass
37     elif send_message == "list":
38         s.send(('%s\r\n' %send_message).encode("utf-8"))
39         print ('从服务器返回消息: ', end="")
40         s.settimeout(2)
41         try:
42             print (s.recv(1200).decode("utf-8").strip())
43         except:
44             pass
45     elif send_message == "bye":
46         s.send(('%s\r\n' %send_message).encode("utf-8"))
47         print ('从服务器返回消息: ', end="")
48         s.settimeout(2)
49         try:
50             print (s.recv(1200).decode("utf-8").strip())
51         except:
52             pass
53         s.close()
54         break
55     else:
56         s.send(('%s\r\n' %send_message.strip()).encode("utf-8"))
57         s.settimeout(2)
58         try:
59             print (s.recv(1200).decode("utf-8").strip())
60         except:
61             pass
62         continue

执行效果

 

5.4 Twisted 应用示例:日志服务器

服务器端

 1 from twisted.internet.protocol import Factory
 2 from twisted.protocols.basic import LineReceiver
 3 from twisted.internet import reactor
 4 
 5 
 6 class LoggingProtocol(LineReceiver):
 7 
 8     # 收到客户端消息时,将消息写入文件
 9     def lineReceived(self, line):
10         host_info = str(self.transport.getPeer().host) + ":" + str(self.transport.getPeer().port)
11         content = host_info+" === "+line.decode("utf-8")+"\n"
12         self.factory.fp.write(content)
13         self.factory.fp.flush()  # 实时刷新文件内容
14         self.sendLine("写入成功!".encode("utf-8"))
15         print("接收消息并写入成功:%s" % content)
16 
17 
18 class LogfileFactory(Factory):
19 
20     # 设定了一个类变量,指向的是LoggingProtocol类对象
21     protocol = LoggingProtocol
22 
23     def __init__(self, fileName):
24         # 把文件名存储在实例变量里面
25         self.file = fileName
26 
27     # 当服务器端启动时,执行该方法
28     # 所有客户端消息均只记录在一个日志文件中
29     def startFactory(self):
30         print("打开文件:%s" % self.file)
31         self.fp = open(self.file, 'a+', encoding="utf-8")
32 
33     # 当服务器端关闭时,执行该方法
34     def stopFactory(self):
35         print("-------- 日志文件即将关闭,以下为截止目前的日志内容 --------")
36         self.fp.seek(0,0)
37         print(self.fp.read())
38         self.fp.close()
39 
40 
41 if __name__ == '__main__':
42     # 工厂类的实例,参数是一个文件,文件用来存储客户端的消息以作为日志信息
43     FACTORY = LogfileFactory("e:\\a.txt") 
44     reactor.listenTCP(8007, FACTORY)  # 监听8007端口
45     print ("监听中...")
46     reactor.run()  # 开始运行server端

客户端

 1 if __name__ == '__main__':
 2     import socket
 3     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 4     sock.connect(('localhost', 8007))  # 创建连接
 5     import time
 6     for i in range(3):
 7         content = input("请输入发送消息:")
 8         # 首次发送,触发服务器端startFactory,然后开始执行LoggingProtocol
 9         sock.send(('%s\r\n' % content).encode("utf-8"))  
10         print("收到服务器端消息:", sock.recv(1020).decode("utf-8"))
11     sock.close()  # 触发stopFactory,关闭文件

执行效果

 

5.5 Twisted+Select 应用示例(限Linux):广播聊天室

服务器端

server_socket.setsockopt(level,optname,value)

  • level 定义哪个选项将被使用,一般都是使用 SOL_SOCKET,意思是正在使用的 socket 选项。

server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

  • SO_REUSEADDR 用于对 TCP 套接字处于 TIME_WAIT 状态下的 socket,才可以重复绑定使用。Server 程序总是应该在调用 bind() 之前设置 SO_REUSEADDR 套接字选项。
  • 先调用 close() 的一方会进入 TIME_WAIT 状态,所以不设置此参数,需要系统等待一段事件才可以使用。
 1 import socket, select
 2 import traceback
 3 
 4 
 5 # 广播的函数,第一个参数:发送消息的socket对象,第二个参数:需要广播的消息
 6 def broadcast_data(sock, message):
 7     # Do not send the message to master socket and the client who has send us the message
 8     for socket in CONNECTION_LIST:  # 遍历每一个监听列表的对象
 9         # 当前的socket对象不是服务器端的socket,也不是当前发送广播消息的socket连接并且是有效
10         if socket != server_socket and socket != sock:
11             try:
12                 socket.send(message.encode("utf-8"))  # 就给这个客户端发送广播消息
13             except:
14                 # broken socket connection may be, chat client pressed ctrl+c for example
15                 socket.close()
16                 CONNECTION_LIST.remove(socket)
17 
18 
19 if __name__ == "__main__":
20 
21     # List to keep track of socket descriptors
22     CONNECTION_LIST = []  # 存储连接对象列表
23     RECV_BUFFER = 4096  # 设定缓冲区大小,一次最多收个4096字节
24     PORT = 6001  # 设定了server的监听端口
25 
26     server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 实例化server端的socket对象
27     # this has no effect, why ?
28     server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 套接字关闭之后端口号可以直接被重用。
29     server_socket.bind(("0.0.0.0", PORT))  # 监听本机所有的ip地址
30     server_socket.listen(10)  # 开始监听,最多10个链接
31 
32     # Add server socket to the list of readable connections
33     CONNECTION_LIST.append(server_socket)  # server端自己的socket对象添加到CONNECTION_LIST
34 
35     print ("Chat server started on port " + str(PORT))  # 打印正在连接得端口号
36 
37     while 1:
38         # 多路io复用,单线程轮训,阻塞方式,等有连接或者消息发来了,才会继续执行后面的代码。此处死等有事件发生
39         # 只监听读的动作:1)建立socket连接  2)连接后客户端发来消息
40         read_sockets, write_sockets, error_sockets = select.select(CONNECTION_LIST, [], [])
41 
42         # read_sockets:存储发生事件的socket对象:1)server端socket(建立连接时候触发)2)连接后的socket对象收到聊天消息了
43         for sock in read_sockets:
44             # 新建连接的分支,server_socket对象只会负责建立新的连接
45             if sock == server_socket:  # 判断是否是当前的连接,如果是就开始建立连接,每次建立新连接,都会执行一次此分支。
46                 # 建立新的连接
47                 sockfd, addr = server_socket.accept()
48                 CONNECTION_LIST.append(sockfd)#把新的连接放到监听的里列表中
49                 print ("Client (%s, %s) connected" % addr)
50                 print("broadcast0 now!")
51                 broadcast_data(sockfd, "[%s:%s] entered room\n" % addr)#函数用于给所有客户端socket发送广播消息的函数
52 
53             # Some incoming message from a client,当客户端发送数据的时候,会执行else分支
54             else:
55                 # 当客户端发送数据的时候,那么连接对象socket对象会产生读的事件,触发else逻辑
56                 try:
57                     # window系统中TCP连接可能会突然关闭导致出现连接复位得异常
58                     data = sock.recv(RECV_BUFFER)  # 收客户端发来的消息
59                     print("got data:", data)  # 打印一下收到的消息
60                     if data:  # 如果数据不为空
61                         print("broadcast2 now!")
62                         #调用广播函数给所有的客户端连接发送广播消息
63                         broadcast_data(sock, "\r" + '<' + str(sock.getpeername()) + '> ' + data.decode("utf-8"))
64                 except:
65                     print(traceback.print_exc())
66                     print("broadcast1 now!")
67                     broadcast_data(sock, "Client (%s, %s) is offline" % addr)  # 连接已退出后做处理
68                     print ("Client (%s, %s) is offline" % addr)
69                     sock.close()
70                     CONNECTION_LIST.remove(sock)
71                     continue
72 
73     server_socket.close()

客户端

 1 import socket, select, string, sys
 2 
 3 
 4 def prompt():
 5     sys.stdout.write('<You> ')  # 输出消息得格式(结合print分析)
 6     sys.stdout.flush()
 7 
 8 
 9 # main function
10 if __name__ == "__main__":
11 
12     if (len(sys.argv) < 3):  # 传入内容是否准确
13         print ('Usage : python telnet.py hostname port')
14         sys.exit()
15 
16     host = sys.argv[1]
17     port = int(sys.argv[2])
18 
19     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
20     s.settimeout(6)
21 
22     # connect to remote host
23     try:
24         s.connect((host, port))  # 创建连接
25     except:
26         print ('Unable to connect')  # 连接失败操作这块
27         sys.exit()
28 
29     print ('Connected to remote host. Start sending messages')
30     prompt()  # 给出格式开始接收信息
31 
32     while 1:
33         rlist = [sys.stdin, s]
34         # s.connect((host, port))
35         # 获取列表可读套接字
36         read_list, write_list, error_list = select.select(rlist, [], [])
37 
38         for sock in read_list:
39             # 远程服务端传入消息
40             if sock == s:  # 如果是正在建立通信,然后开始接收服务端返回信息
41                 data = sock.recv(4096)
42                 if not data:
43                     pass
44                 else:
45                     # print data
46                     sys.stdout.write(data.decode("utf-8"))  # 打印空内容
47                     prompt()
48             # 用户输入消息发送给服务端
49             else:
50                 msg = sys.stdin.readline()
51                 s.send(msg.encode("utf-8"))
52                 prompt()

执行效果

 

 

posted @ 2020-11-29 00:26  Juno3550  阅读(2605)  评论(0编辑  收藏  举报