SocketServer源码分析
一、简介
+----------------+
| BaseServer |
+----------------+
|
v
+---------------+
| TCPServer |
+---------------+
|
v
+---------------+
| UDPServer |
+---------------+
-- 如何处理多请求:
- 同步 (一次只能处理一个请求)
- forking (fork一个新的进程来处理一个请求)
- threading (创建一个新的线程来处理一个请求)
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
例如,如果服务中包含请求修改的内存的状态,那么使用forking server没有任何意义(因为在子进程中修改将不对父进程的初始化状态有影响,父进程也不会把这个修改的参数传递给其他子进程)。
此外,如果你在搭建如HTTP服务器等,所有的数据都会存储在外部(如文件系统中),当客户端的一项请求被处理时,并且客户端的读取数据的速度很慢,synchronous class将会使服务不做出响应,这可能需要维持很长时间。
在一些情况下,请求同步可能需要恰当的方法,但是为了在子进程中完成请求要受到请求数据的影响。这可以通过使用同步服务器来实现,并且在请求处理类中的Handle方法中明确指定fork的进程。
另一种处理多个同时发生的请求的方法是维系一张明确的完成请求的表单,使用select()方法来判定哪个请求应该在接下来做出响应(或者判断是否要处理新到来的请求),
二、用到的所有的类方法
import socket import select import sys import os import errno try: import threading except ImportError: import dummy_threading as threading __all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer", "ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler", "StreamRequestHandler","DatagramRequestHandler", "ThreadingMixIn", "ForkingMixIn"]
三、BaseServer和BaseRequestHandler
Python把网络服务抽象成两个主要的类,一个是Server类,用于处理连接相关的网络操作,另外一个则是RequestHandler类,用于处理数据相关的操作。- serve_forever(poll_interval=0.5)
- shutdown()
- handle_request() # if you do not use serve_forever()
- fileno() -> int # for select()
timeout = None """ self.socket、self.get_request()本类中未实现+由子类TCPServer实现, RequestHandlerClass本类中未实现+由用户实现""" def __init__(self, server_address, RequestHandlerClass): self.server_address = server_address self.RequestHandlerClass = RequestHandlerClass self.__is_shutdown = threading.Event() self.__shutdown_request = False
def _eintr_retry(func, *args): while True: try: return func(*args) except (OSError, select.error) as e: if e.args[0] != errno.EINTR: raise
def server_forever(self, poll_interval=0.5): self.__is_shutdown.clear() try: while self.__shutdown_request is False: # select(rlist, wlist, xlist, timeout=None) r, w, e = _eintr_retry(select.select, [self], [], [], poll_interval) if self in r: self._handle_request_noblock() finally: self.__is_shutdown.set() self.__shutdown_request = False
这里用到了select()函数,即server_forever接受了一个poll_interval=0.5的参数传入,这表示用于select轮询的时间,然后进入一个无限循环中,由_eintr_retry实现。
在这个循环中,select每隔poll_interval秒轮询一次(阻塞于此),以此来进行网络IO的监听。一旦有新的网络连接请求到来,则会调用_handle_request_noblock()方法处理新的连接。
3.1.3 _handle_request_noblock()
def _handle_request_noblock(self): try: """返回客户端连接和客户端地址""" request, client_address = self.get_request() except socket.error: return if self.verify_request(request, client_address): try: self.process_request(request, client_address) except: self.handle_error(request, client_address) self.shutdown_request(request) else: self.shutdown_request(request)
该方法处理的是一个非阻塞请求,首先通过get_request()方法获取连接,具体实现在其子类TCPServer。
一旦获取了连接,立即调用verify_request认证连接信息。
通过认证,则调用process_request()方法处理请求,如果中途出现错误,则调用handle_error处理错误,同时,调用shutdown_request()方法结束这个连接。
其它调用函数:
def verify_request(self, request, client_address): return True def process_request(self, request, client_address): self.finish_request(request, client_address) self.shutdown_request(request) def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self) def shutdown_request(self, request): self.close_request(request) def close_request(self, request): pass def handle_error(self, request, client_address): print '-'*40 print 'Exception happened during processing of request from', print client_address import traceback traceback.print_exc() # XXX But this goes to stderr! print '-'*40 def shutdown(self): self.__shutdown_request = True self.__is_shutdown.wait() def server_close(self): pass def handle_timeout(self): pass
verify_request()方法对request进行验证,通常会被子类重写。
def handle_request(self): """未使用""" timeout = self.socket.gettimeout() if timeout is None: timeout = self.timeout elif self.timeout is not None: timeout = min(timeout, self.timeout) fd_sets = _eintr_retry(select.select, [self], [], [], timeout) if not fd_sets[0]: self.handle_timeout() return self._handle_request_noblock()
文档中说明,如果你没有用到server_forever()方法,说明你希望使用的是阻塞请求来处理连接。
如英文描述所说,该方法只是处理一个阻塞的请求,仍然使用select()方法轮询来监听网络连接,但是需要考虑时间超时影响。
一旦超时,调用handle_timeout()方法处理超时,一般在子类重写该方法;
如果在超时之前监听到了网络的连接请求,则同server_forever一样,调用_handle_request_noblock()方法,完成对新的连接的请求处理。
3.2 BaseRequestHandler分析
class BaseRequestHandler: def __init__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() finally: self.finish() # 以下方法由子类实现 def setup(self): pass def handle(self): pass def finish(self): pass
所有requestHandler都继承BaseRequestHandler基类,该类会处理每一个请求。
在__init__中初始化实例变量request、client_address、server,然后调用handle()方法完成请求处理。
那么,我们唯一需要做的就是重写好Handle()方法,处理所有的请求。
四、各种子类
class TCPServer(BaseServer): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM request_queue_size = 5 allow_reuse_address = False def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True): """Constructor. May be extended, do not override.""" BaseServer.__init__(self, server_address, RequestHandlerClass) self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: # True:TCP;False:UDP try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): if self.allow_reuse_address: """地址复用""" self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) "返回套接字自己的地址。通常是一个元组(ipaddr,port)" self.server_address = self.socket.getsockname() def server_activate(self): self.socket.listen(self.request_queue_size) def server_close(self): self.socket.close() def get_request(self): return self.socket.accept() def shutdown_request(self, request): """SERVER代码中: 客户端连接先shutdown,再close; 服务端socket,只需close""" try: request.shutdown(socket.SHUT_WR) except socket.error: pass #some platforms may raise ENOTCONN here self.close_request(request) "实现父类close_request方法" def close_request(self, request): request.close() def fileno(self): return self.socket.fileno()
使用server_bind、server_activate、server_close处理TCP启停等操作,
同时增加了get_request、shutdown_request、close_request处理客户端请求。
4.1.2 UDPServer
class UDPServer(TCPServer): socket_type = socket.SOCK_DGRAM allow_reuse_address = False max_packet_size = 8192 """未写__init__方法,则调用父类的__init__方法""" "重写UDP与TCPServer不一样的地方" def get_request(self): """get_request方法: 在TCPServer中,通过request, address = self.socket.accept() 获取; 在UDPServer中,因为不需要客户端与服务端先建立连接,它是面向无连接的,所以没有listen监听TCP和accept阻塞接受连接的功能和方法, 因此,它不能接收连接con(这里的request)。 只能在接收客户端数据时得到客户端地址address,而得不到连接, 即通过data, address = self.socket.recvfrom(self.max_packet_size)获取data和address""" data, client_addr = self.socket.recvfrom(self.max_packet_size) return (data, self.socket), client_addr def server_activate(self): # No need to call listen() for UDP. pass def shutdown_request(self, request): """在TCPSERVER中,可以关闭客户端连接(con.shutdown和con.close); 但在UDPSERVER中,是没有连接的,因此它并需要关闭连接。 但还是要重写此方法,以覆盖父类方法""" # No need to shutdown anything. self.close_request(request) def close_request(self, request): # No need to close anything. pass
继承自TCPServer,将socket改为了SOCK_DGRAM型,
并修改了get_request,用于从SOCK_DGRAM中获取request。
同时server_activate、shutdown_request、close_request都改成了空(UDP不需要),比TCP简单一些。
class StreamRequestHandler(BaseRequestHandler): rbufsize = -1 wbufsize = 0 timeout = None disable_nagle_algorithm = False def setup(self): self.connection = self.request if self.timeout is not None: self.connection.settimeout(self.timeout) if self.disable_nagle_algorithm: self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) self.rfile = self.connection.makefile('rb', self.rbufsize) self.wfile = self.connection.makefile('wb', self.wbufsize) def finish(self): if not self.wfile.closed: try: self.wfile.flush() except socket.error: # A final socket error may have occurred here, such as # the local error ECONNABORTED. pass self.wfile.close() self.rfile.close()
最主要的功能是根据socket生成了读写socket用的两个文件对象(可以理解为句柄)rfile和wfile
4.2.2 DatagramRequestHandler
class DatagramRequestHandler(BaseRequestHandler): def setup(self): try: from cStringIO import StringIO except ImportError: from StringIO import StringIO self.packet, self.socket = self.request self.rfile = StringIO(self.packet) self.wfile = StringIO() def finish(self): self.socket.sendto(self.wfile.getvalue(), self.client_address)
同样是生成rfile和wfile,但UDP不直接关联socket。这里的rfile是直接由从UDP中读取的数据生成的,wfile则是新建了一个StringIO,用于写数据。
五、Mix-In混合类
BaseServer和BaseRequestHandler两个基类,它们只用与派生,它们派生的子类TCPServer和StreamRequestHandler、DataGramRequestHandler。
混合类ForkingMix-In 和 ThreadingMix-In,两者分别实现了核心的进程化和线程化的功能,
如前面简介中所提,作为混合类,它们与服务器类一并使用以提供一些异步特性。
Mix-in 这个类必须首先实现,因为它重写了定义UDPServer的方法。注意,它们不会被直接实例化。
BaseRequestHandler和BaseServer也不会实例化对象。
通常使用以下四个类之一来实例化对象,创建服务器:
class ForkingUDPServer(ForkingMixIn, UDPServer): pass class ForkingTCPServer(ForkingMixIn, TCPServer): pass class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
5.1 ForkingMixIn
该类针对每一个监听到来的新的连接请求都会fork一个新的子进程,在子进程中完成请求处理。
这里需要注意,由于windows对fork支持的不完整,所以无法在windows环境下运行该模块的forkingMixIn类,只能在linux或Mac环境中测试运行。
class ForkingMixIn(object): timeout = 300 # 超时时间 active_children = None max_children = 40 #最大子进程数 def collect_children(self): """子进程回收方法""" """使用此钩子时,必须重定认active_children""" if self.active_children is None: return while len(self.active_children) >= self.max_children: # 如果满足条件,主进程将在此阻塞,直到最大子进程数小于max_children try: """os.waitpid:如果pid为-1,则等待当前进程的任何子进程,返回子进程ID和退出码""" pid, _ = os.waitpid(-1, 0) """如果在集合中存在元素pid, 则删除;不存在,不抛出异常 """ self.active_children.discard(pid) except OSError as e: if e.errno == errno.ECHILD: # we don't have any children, we're done self.active_children.clear() elif e.errno != errno.EINTR: break for pid in self.active_children.copy(): try: pid, _ = os.waitpid(pid, os.WNOHANG) self.active_children.discard(pid) except OSError as e: if e.errno == errno.ECHILD: self.active_children.discard(pid) def handle_timeout(self): self.collect_children() def process_request(self, request, client_address): """Fork a new subprocess to process the request.""" self.collect_children() pid = os.fork() # 创建子进程,返回值为0表示子进程,大于0表示父进程pid if pid: if self.active_children is None: self.active_children = set() self.active_children.add(pid) self.close_request(request) #如果是TCP,关闭客户端连接 return else: # Child process. # This must never return, hence os._exit()! try: self.finish_request(request, client_address) self.shutdown_request(request) os._exit(0) except: try: self.handle_error(request, client_address) self.shutdown_request(request) finally: os._exit(1)
collect_children()方法用于判断当前的子进程数是否超过阈值,以保证程序的稳定运行。
如果当前的fork的子进程数超过阈值40,我们把主进程阻塞住。
使os.waitpid(-1),直到有子进程处理完成请求并且断开连接,等待总体的正在运行的子进程数降到阈值以下;
与此同时,该方法也会通过分配出去的pid遍历所有fork的子进程,查看它们是否在正常工作。
如果发现僵死进程或者不存在的子进程,主进程则会调用discard()方法将子进程占用的资源回收,以便分配给其他新到来的请求。
process_request()方法为主进程监听连接请求。
一旦发现了连接请求,首先调用上面的collect_children()方法,查看set()这个资源池中的子进程数是否达到阈值。
如果没有,则为新到来的请求fork一个子进程,分配一个pid放入set()资源池中,
然后在子进程中处理到来的请求。
注意,子进程中不能有return值,只能用os._exit()退出子进程。
为了便于理解,在centos上写了个简单的server和client,当有多个连接请求时,我们 ps -ef | grep sock ,发现server端有多个子进程。如下图:
很明显,主进程fork了三个子进程处理连接请求,而我也恰好开了三个TCP的连接。此时,我们把一个客户端断开,再 ps -ef | grep sock ,如下图:
此时,我们发现了僵死进程(19385)<defunct>,然而当我把断开的客户端重新启动时,就恢复了3个活跃的子进程,并且进程号增加,说明父进程在新的连接请求到来时清理了set()资源池,把僵死的子进程干掉,并为新的请求分配了新的pid,放入set()资源池中。
5.2 ThreadingMixIn
该类会针对每一个新到来的连接请求分配一个新的线程来处理,这里面有用到python的thread和threading模块
class ThreadingMixIn(object): daemon_threads = False def process_request_thread(self, request, client_address): u"""此方法雷同BaseServer中的process_request方法""" try: # 此处的几个方法,都是在BaseServer中实现的,此类却没有继承BaseServer # 那么此处,它与BaseServer没任何关联。 # 它必须在继承它的类中,获得BaseServer中这几个方法,因此它是钩子Mix-in self.finish_request(request, client_address) self.shutdown_request(request) except: self.handle_error(request, client_address) self.shutdown_request(request) def process_request(self, request, client_address): """重写BaseServer中的process_request方法。当使用此钩子时,将覆盖BaseServer中的process_request方法 为什么要重写呢? 其一: 1.因为BaseServer必须使用process_request方法! 2.而多线程时,每一个线程都要使用到原来的process_request方法,因此必须重写,以替代! 且将原来的process_request方法复制一份,重命名,给多线程调用! 其二: 1.BaseServer中process_request和错误处理是分开的; 2.多线程时,需要将BaseServer中process_request和错误处理合在一起为每个线程使用!""" t = threading.Thread(target = self.process_request_thread, args = (request, client_address)) t.daemon = self.daemon_threads t.start()
六、应用举例
以上提到的所有的类,我们最好不要直接实例化,简介里面提到过:
1.我们最好使用它们的混合来使我们的server更加简单强大。
2.我们需要做的就是按照我们的需求重写Handle方法,然后调用以下四种方法之一就可以了:
class ForkingUDPServer(ForkingMixIn, UDPServer): pass class ForkingTCPServer(ForkingMixIn, TCPServer): pass class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
下面是服务器端调用ThreadingTCPServer类实例化的一个简单例子
服务端:
import SocketServer ''''' 多线程并发实现TCP连接 ''' class MyTCPHandler(SocketServer.BaseRequestHandler): def handle(self): while True: try: self.data = self.request.recv(1024).strip() print "{} wrote:".format(self.client_address) # client_address[0]:客户端IP # client_address[1]:客户端端口号 print self.data self.request.sendall(self.data.upper()) except Exception: print 'A client has left!!!' break if __name__ == "__main__": # 把类实例化,把自己写的类绑定到ThreadingTCPServer上 server = SocketServer.ForkingTCPServer(("localhost", 9999), MyTCPHandler) ''''' Handle one request at a time until shutdown. Polls for shutdown every poll_interval seconds. Ignores self.timeout. If you need to do periodic tasks, do them in another thread. ''' print 'waiting for connection........ ' server.serve_forever()
客户端:
# coding=utf8 import socket import time class Client(object): # 注释掉的部分为UDPClient def __init__(self, host='127.0.0.1', port=8000, bufsize=1024): self.BUFFSIZE = bufsize self.ADDR = (host, port) def run(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(self.ADDR) # s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) while True: try: data = raw_input('>>>') if not data or data == 'exit': s.send('bye!!') # s.sendto('bye!!', self.ADDR) time.sleep(0.5) break try: s.send(data) receive_data = s.recv(self.BUFFSIZE) # s.sendto(data, self.ADDR) # receive_data, addr = s.recvfrom(self.BUFFSIZE) except (socket.error, Exception) as e: print e break if not receive_data or receive_data == 'exit': break print receive_data except KeyboardInterrupt: break s.close() if __name__ == '__main__': Client().run()
posted on 2018-06-02 15:07 myworldworld 阅读(196) 评论(0) 收藏 举报