SocketServer源码分析

一、简介

        +----------------+
        | BaseServer |
        +----------------+
              |
              v
        +---------------+   
        | TCPServer |
        +---------------+   
              |
              v
        +---------------+    
        | UDPServer |
        +---------------+ 

-- 如何处理多请求:
   - 同步 (一次只能处理一个请求)
   - forking (fork一个新的进程来处理一个请求)
   - threading (创建一个新的线程来处理一个请求)

  forking和threading 可以被创建用于ForkingMixIn和TreadingMixIn mix-in类。例如: threading UDP server类会被如下方式创建:
 class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
  Mix-in 这个类必须首先实现,因为它重写了定义UDPServer的方法。设置不同的成员变量也改变了基本的服务器构造方法。 
  为了实现一个服务,你必须从基类BaseRequestHandler中重新定义它的handle方法。然后通过把服务类与你重写的Handle方法类结合,以此运行新的服务类。 
  请求处理类的TCP和UDP的方式是不同的,这个可以通过使用请求处理的子类StreamRequestHandler或者DatagramRequestHandler来隐藏。 
当然,你还可以思考其他的方法。
    例如,如果服务中包含请求修改的内存的状态,那么使用forking server没有任何意义(因为在子进程中修改将不对父进程的初始化状态有影响,父进程也不会把这个修改的参数传递给其他子进程)。
这种情况下,你可以使用threading server,而且你更有可能需要用到“锁”,以此来避免两个请求同时到达而使服务器状态产生冲突。
    此外,如果你在搭建如HTTP服务器等,所有的数据都会存储在外部(如文件系统中),当客户端的一项请求被处理时,并且客户端的读取数据的速度很慢,synchronous class将会使服务不做出响应,这可能需要维持很长时间。
    在一些情况下,请求同步可能需要恰当的方法,但是为了在子进程中完成请求要受到请求数据的影响。这可以通过使用同步服务器来实现,并且在请求处理类中的Handle方法中明确指定fork的进程。
    另一种处理多个同时发生的请求的方法是维系一张明确的完成请求的表单,使用select()方法来判定哪个请求应该在接下来做出响应(或者判断是否要处理新到来的请求),
当每一个客户端需要建立很长时间的连接时,这对于stream services来说非常重要。(前提是不使用线程和子进程的方法)

二、用到的所有的类方法

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类,用于处理数据相关的操作。
并且提供两个MixIn 类,用于扩展 Server,实现多进程或多线程。
在构建网络服务的时候,Server 和 RequestHandler 并不是分开的,RequestHandler的实例对象在Server 内配合 Server工作。
 
 3.1 BaseServer分析:
  BaseServer可供外部调用的方法:
- __init__(server_address, RequestHandlerClass)
- serve_forever(poll_interval=0.5)
- shutdown()
- handle_request()  # if you do not use serve_forever()
- fileno() -> int   # for select()
即我们可以通过init初始化,对外提供serve_forever()和handle_request()方法
 
3.1.1 init初始化
    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
__init__的主要作用就是创建server对象,并初始化server_address和RequestHandlerClass。
server_address就是包含主机地址和端口的元组。
3.1.2 serve_forever
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进行验证,通常会被子类重写。

process_request需要注意一下,它被ForkingMixIn 和 ThreadingMixIn重写,因此是mixin的入口,ForkingMixIn和ThreadingMixIn分别进行多进程和多线程的配置,并且调用finish_request()完成请求,调用shutdown_request()结束请求。
handle_error也可以被子类重写,打印错误的信息和客户端地址。
finish_request()处理完毕请求,在__init__中创建requestHandler对象,并通过requestHandler做出具体的处理
 
3.1.4 handle_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()方法,处理所有的请求。

总结:构建一个网络服务,需要一个BaseServer用于处理网络IO,同时在内部创建requestHandler对象,对所有具体的请求做处理。

四、各种子类 

4.1 由BaseServer衍生的子类 
4.1.1 TCPServer
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()

 

在BaseServer基础上,增加了一个TCP的socket连接,

使用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简单一些。

4.2 由BaseRequestHandler衍生的子类
 
4.2.1 StreamRequestHandler
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)    收藏  举报

导航