一、前言
前段时间做Raspberry Pi的开发,正好借这个机会用Python写了几个socket的服务,之前开发都是照着网上的代码自己去改改弄弄,Python没有过开发经历,自然也是这么来起步。开发过程中用到了Python类的继承,并使用类作为对象进行传递的方法,虽然程序是码出来了,但是心有不甘,对于类继承和重写还是知之甚少,于是借此死磕一下,再深入理解一下什么是面向对象。
二、代码分析
扯的有点远了,直接进入正题。Python程序使用的是TCP/IP端口进行协议转换,程序既是server端,能够一对多,又是client端,将server端接收的信息进行转换后再输出。server端一对多通信在Python库里有封装好的socketserver模块,同时Python库里还有socket模块,用于一对一通信。这次主要探讨的就是socketserver模块,server端一对多通信需要用到socketserver模块中的两个类,BaseRequestHandler和ThreadingTCPServer,程序大致代码如下:
新建MyServer类,并继承BaseRequestHandler类,同时重写该类下handle方法,handle方法主要是为了对soket接收的数据进行处理;实例化ThreadingTCPServer,并传入IP/端口号,以及之前新建的MyServer类,使用serve_forever()开启服务,每当监听到一个连接,就新建一个单独的线程。短短这几行代码,就把TCP数据通信全搞定了,不用担心线程间数据传递的阻塞,不要担心连接的client数量,就是这么神奇,神奇到我刚开始完全不知道究竟发生了什么,因为这个类,封装的太好了,它帮我们把能想到的都放进去了。
2.1、BaseRequestHandler类
究竟里面发生了什么,我们只能按照给的两个线索,BaseRequestHandler和ThreadingTCPServer类去查,首先是BaseRequestHandler类,在socketserver中它的定义如下:
这个类其实啥都没干,它就是个基类,定义了handle,finish,setup这三个API,同时初始化了三个参数request,client_address,server,并在初始化时调用了handle和finish两个方法,这也就是为什么我们在继承这个基类的时候一定要重写handle方法,因为这个类在实例化时会执行handle函数。我们暂时记住这三个传入参数,因为还不知道它们如何传入,只能根据字面意思理解为“请求”,“客户端地址”,“服务端”。
2.2、ThreadingTCPServer、ThreadingMixIn、TCPServer
其次是ThreadingTCPServer类,它的定义就更简单:
就一行,pass是占位符,等于没有,但是它有同时继承了ThreadingMixIn和TCPServer两个类,ThreadingMixIn先继承,继续往前查询ThreadingMixIn类
这个类也不多,从解释上理解它的作用就是处理每次请求,并新建一个线程,就里面三个内参daemon_threads(线程保护,默认不开),_block_on_close(关闭服务时的线程阻塞等待,默认不开),_threads(线程);同时类里面还有三个方法:
a.process_request_thread
解释上面说类似于BaseServer,但是它是作为线程使用,同时内部定义了三个方法,finish_request,handle_request,shutdown_request,暂时不理解有什么用。
b.process_request
开启一个线程来处理请求,然后把该线程加入到_threads这个定义好的线程组里面去。request和client_address,就是BaseRequestHandler初始化时的需要传入的参数中的两个,但是这里没看到process_request方法的调用,估计后面会有解释。
c.server_close
首先会去继承父类中的server_close方法(不用管),然后关闭所有线程。
至此,ThreadingMixIn这个类查询完,继续往下查询TCPServer类
2.3、TCPServer、BaseServer
TCPServer继承了BaseServer这个基类,同时在初始化时在继承基类的基础上进行了一定的修改,BaseServer我们会在最后查询,先了解下TCPServer这个类。
TCPServer里前两个变量address_family和socket_type用于指定端口的连接方式,即TCP数据流方式的连接。
request_queue_size,指定线程间通信的队列长度,默认是5。
allow_reuse_address,看字面意思是允许复用地址,实际应用中没试过怎么用。
a.__init__
继承BaseServer初始化方法,同时引用server_bind,server_active,server_close三个内部方法,主要用作绑定并打开服务端监听。
b.server_bind
在初始化时被引用,用于绑定TCP服务端端口。
c.server_active
在初始化时被引用,用于打开TCP服务端监听。
d.server_close
在初始化时被引用,发生异常情况时断开连接。
e.fileno
返回TCP端口的文件名。
f.get_request
当有client端连接时,返回连接的请求对象以及连接设备的地址。
g.shutdown_request
关闭指定客户端的连接。
h.close_request
关闭所有客户端的连接。
由上述可见,TCPServer类也没用做太多的工作,它主要的目的是建立server端的绑定和监听,同时可以对server端的连接对象做一定的操作,至此我们还是不明白这里面的参数传递关系,只有继续往下看,再深入了解BaseServer这个基类。
BaseServer中变量只有一个timeout,默认是None,但定义了17个方法,__init__初始化的时候定义了两个外部传入参数server_address和RequestHandlerClass,BaseServer的子类TCPServer,其子类的子类ThreadingTCPServer,都包含这两个外部传入参数,并且没有被修改过。因此正好契合我们刚开始在实例化socketserver.ThreadingTCPServer时传入的两个参数“ip_port”,“MyServer”(具体请看开始部分),转了一圈,终于摸索到参数怎么传递进来了(即在BaseServer的子类TCPServer的子类ThreadingTCPServer初始化的时候)。
原来由于方法太多,且很多方法内都是一个空的API(方便重写),因此只截取了前面几个,本文的目的不在于去分析BaseServer里面每个方法的作用,但是有一个方法对于我们非常重要,因为在最开始的时候已经被引用过了,该方法就是serve_forever(在实例化socketserver.ThreadingTCPServer后调用的唯一一个方法),我们来详细分析一下serve_forever到底做了写什么,外部传入了连个参数ip_port已经在TCPServer类的初始化时被引用了,MyServer至此还没有被引用,那么可以推测它一定会在BaseServer中被引用到。
a._is_shut_down_
是线程之间通信等待的标志位,_is_shut_down.clear()表示标志位清零,不理解的话没关系,继续往下走。
b.实例化ServerSelector为selector对象
其实ServerSelector的基类就是BaseSelector,它的作用是实现IO的自动多路复用(说明白点就是socket server端口的一对多通信).
c.为每次IO请求注册一个实例
同个每隔0.5秒(poll_interval)去查询是否有新的socket请求到达,如果有新的请求的话,就执行handle_request_noblock方法。
d.service_actions是个空方法
可以被重写,每隔0.5秒去做一个操作。
e.handle_request_noblock方法
字面意思是不带阻塞的去执行请求。方法内部获取client的连接亲求,返回请求的对象(request)和client地址(client_address);verify_request默认返回是true,然后去执行process_request方法,如果发生错误,就去执行shutdown_request和handle_error,字面意思很好理解,我们只需要关注process_request会怎么操作。
f.process_request方法
这里解释提醒我们process_request已经被ThreadingMixIn给重写了,那我们回到ThreadingMixIn的类里继续查看process_request,变化不大,主要是去调用finish_request方法。
g.finish_request方法
终于要到最后一个方法了,它去调用RequestHandlerClass,这是在BaseServer在实例化时需要传入的对象之一,进而它的子类的子类,ThreadingTCPServer也沿用了这个对象传递,实例化RequestHandlerClass类,并传入request和client_address,这两个参数哪里来的?前面handle_request_noblock方法调用而产生的两个对象。这样也能解释为什么在MyServer类重写handle的过程中,有“conn=self.request”,request就是每次client请求获得的一个对象。
至此,sockserver模块中TCP server端一对多通信的整个流程算是连接成功了,整个是一个闭环,我们再梳理下大致流程。
1、实例化socketserver.ThreadingTCPServer,并传入IP端口地址以及MyServer类。
2、调用serve_forever方法,通过select方法实现IO多路复用,并调用handle_request_noblock来处理每次client端的连接请求。
3、handle_request_noblock会建立与client端连接,返回client请求对象和client地址,然后再将这两个参数交给MyServer。
4、MyServer再重写handle方法时,会用到request请求,来接收和发送client端的信息。
最后,回到本文刚开始做项目时的需求,如果我们还需要传入其他参数,比如说与外部server端建立连接的client对象,实现程序里既当服务端又当客户端,我们应该怎么做?其实很简单,重写serve_forever方法,继承原来方法的同时,加入新的参数定义,并重新定义对该参数的操作。
三、总结
总结,sockserver这个模块看下来,花了不少时间,但是过程还是很愉快的,举一反三,什么是面向对象的编程,我觉得不光是会用一个方法,会抄抄改改网上的代码这么简单而已,还应该自己花时间去思考,通过不断的继承、复用、重定义,来让方法变成自己的方法,这也是一个进步的过程,正所谓站在巨人的肩膀上,这也许就是面向对象编程的快乐了吧。