网络编程(九):Python裸写异步非阻塞网络框架

网络编程(九):Python裸写异步非阻塞网络框架

auxtenauxten

裸写的意思就是不用任何第三方库,这样有助于网络编程入门的同学了解其中的奥秘。

相关的前置的网络编程的知识参见:

好了,直接上代码(之前的代码是没有太多注释的感谢@康存化 给代码加了详尽的注释):

# 从之前写好的守护进程类中导入Daemon
from daemon import Daemon
import socket
import select
import time
# 导入pdb,即Python debug模块,方便调试
import pdb

# 设定这个变量,可以控制本文件在被import *
# 的时候导入的变量、函数和类的范围
__all__ = ["nbNet"]
#DEBUG = True

# 这个文件的内容会在后面详述
from nbNetUtils import *

# 我们住程序的conn_state的类原型

class STATE:

    def __init__(self):
        self.state = "accept"  # 初始状态
        self.have_read = 0
        # 我们的程序在开始的时候总是要读取10个字节的头
        self.need_read = 5
        self.have_write = 0
        self.need_write = 0

        # 读写缓冲区
        self.buff_read = ""
        self.buff_write = ""
        # 用来后续存放sock对象
        self.sock_obj = ""

    def printState(self):
        """
        debug输出函数
        """
        if DEBUG:
            dbgPrint('\n - current state of fd: %d' % self.sock_obj.fileno())
            dbgPrint(" - - state: %s" % self.state)
            dbgPrint(" - - have_read: %s" % self.have_read)
            dbgPrint(" - - need_read: %s" % self.need_read)
            dbgPrint(" - - have_write: %s" % self.have_write)
            dbgPrint(" - - need_write: %s" % self.need_write)
            dbgPrint(" - - buff_write: %s" % self.buff_write)
            dbgPrint(" - - buff_read:  %s" % self.buff_read)
            dbgPrint(" - - sock_obj:   %s" % self.sock_obj)


class nbNetBase:
    '''
    non-blocking Net类,首先我们设定了一套通信的协议,以10个byte的
    ASCII码数字的头来表示,后续数据的长度,例如:
    0000000005HELLO
    0000000001a
    0000000012hello world\n
    这样做的好处在于,我们可以很容易的解析消息的结束位置。
    并且能够在这套框架下进行任何数据的传输,包括各种二进制数据,而不需要转义。
    '''

    def setFd(self, sock):
        """把sock放进全局的conn_state字典里"""
        dbgPrint("\n -- setFd start!")
        tmp_state = STATE()
        tmp_state.sock_obj = sock
        self.conn_state[sock.fileno()] = tmp_state
        self.conn_state[sock.fileno()].printState()
        dbgPrint("\n -- setFd end!")

    def accept(self, fd):
        """在fd上进行accept,并且把socket设置成非阻塞模式"""
        dbgPrint("\n -- accept start!")
        sock_state = self.conn_state[fd]
        sock = sock_state.sock_obj
        conn, addr = sock.accept()
        # 把socket设置成非阻塞模式
        conn.setblocking(0)
        return conn

    def close(self, fd):
        """关闭fd,从epoll中取消关注,清理conn_state里相关的数据"""
        try:
            # cancel of listen to event
            sock = self.conn_state[fd].sock_obj
            sock.close()
        except:
            dbgPrint("Close fd: %s abnormal" % fd)
        finally:
            self.epoll_sock.unregister(fd)
            self.conn_state.pop(fd)

    def read(self, fd):
        """
        读取fd中的数据(非阻塞模式)
        并且设置各个计数器的数值,以供后续处理
        返回值是个字符串,表示下一步需要进行的处理,如:
        “readcontent”、“process”、“readmore”
        """
        # pdb.set_trace()
        try:
            # 从conn_state字典中取出连接
            sock_state = self.conn_state[fd]
            conn = sock_state.sock_obj
            if sock_state.need_read <= 0:
                raise socket.error

            # 进行一次非阻塞的读取
            one_read = conn.recv(sock_state.need_read)
            dbgPrint("\tread func fd: %d, one_read: %s, need_read: %d" %
                     (fd, one_read, sock_state.need_read))

            # 如果什么都没有读到,那应该是socket出错了
            if len(one_read) == 0:
                raise socket.error
            # 将读到的数据放入buff_read,
            # 设定have_read(已经从socket中读取的数量)
            # 设定need_read(还需从socket中要读取的数量)
            sock_state.buff_read += one_read
            sock_state.have_read += len(one_read)
            sock_state.need_read -= len(one_read)
            sock_state.printState()

            # 如果已经读取的数据是10个byte,那么说明数据的10字节头已经读取完毕,
            # 我们可以解析判断后续的数据的长度了
            if sock_state.have_read == 5:
                # 由于是ASCII的数据头,我们需要用int()将它转化成数字
                header_said_need_read = int(sock_state.buff_read)
                if header_said_need_read <= 0:
                    raise socket.error
                sock_state.need_read += header_said_need_read
                sock_state.buff_read = ''

        # 为了方便大家理解,这里打印一些debug信息
                sock_state.printState()
                return "readcontent"
            elif sock_state.need_read == 0:
                # 所有数据已经读取完毕,转入业务逻辑处理“process”
                return "process"
            else:
                # 出去上述的所有情况,剩下的情况就是还需要读取更多的数据
                return "readmore"
        except (socket.error, ValueError), msg:
            # 进行一些异常处理
            try:
                # errno等于11,即“EAGAIN”。是表示,还可以尝试进行一次读取
                if msg.errno == 11:
                    dbgPrint("11 " + msg)
                    return "retry"
            except:
                pass
            # 除去上述的特殊情况,发生了任何错误,不要挣扎,直接把socket关闭
            return 'closing'

    def write(self, fd):
        """
        非阻塞的写数据到socket中,返回值的涵义和上述的read一致
        """
        # 还是取出fd对应的sock_state结构体
        sock_state = self.conn_state[fd]
        conn = sock_state.sock_obj
        last_have_send = sock_state.have_write
        try:
            # 非阻塞的发送数据,这里send的返回值是已经成功发送的数据量
            have_send = conn.send(sock_state.buff_write[last_have_send:])
            sock_state.have_write += have_send
            sock_state.need_write -= have_send
            if sock_state.need_write == 0 and sock_state.have_write != 0:
                # 如果已经全部发送成功,返回“writecomplete”
                sock_state.printState()
                dbgPrint('\n write data completed!')
                return "writecomplete"
            else:
                return "writemore"
        except socket.error, msg:
            # 发生错误,直接关闭socket
            return "closing"

    def run(self):
        """
        这个函数是装个状态机的主循环所在
        """
        while True:

            # 这部分就是我们上面多次提到的epoll
            # poll()返回的epoll_list就是有事件发生的fd的list
            # 需要在循环中按照event的类型分别处理,一般分为以下几种类型
            #  EPOLLIN :表示对应的文件描述符可以读;
            #  EPOLLOUT:表示对应的文件描述符可以写;
            #  EPOLLPRI:表示对应的文件描述符有紧急的数据可读;一般不需要特殊处理
            #  EPOLLERR:表示对应的文件描述符发生错误;后面这两种需要关闭socket
            #  EPOLLHUP:表示对应的文件描述符被挂断;
            epoll_list = self.epoll_sock.poll()
            for fd, events in epoll_list:
                sock_state = self.conn_state[fd]
                if select.EPOLLHUP & events:
                    dbgPrint("EPOLLHUP")
                    sock_state.state = "closing"
                elif select.EPOLLERR & events:
                    dbgPrint("EPOLLERR")
                    sock_state.state = "closing"
                # 调用状态机
                self.state_machine(fd)

    def state_machine(self, fd):
        """
        这里的逻辑十分的简单:“按照不同fd的state,调用不同的函数即可”
        具体的对应表见nbNet的__init__()
        """
        sock_state = self.conn_state[fd]
        self.sm[sock_state.state](fd)


class nbNet(nbNetBase):

    def __init__(self, addr, port, logic):
        dbgPrint('\n__init__: start!')
        # 初始化conn_state字典,这个字典将会保存每个连接的状态
        # 以及连接的读写内容。
        self.conn_state = {}
        # 初始化监听socket
        # socket.AF_INET指的是以太网
        # socket.SOCK_STREAM指的是TCP
        self.listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        # 开启SO_REUSEADDR,这样当监听端口处于各种xxx_WAIT的状态的时候
        # 也能正常的listen、bind
        self.listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 绑定在制定的IP和端口上
        self.listen_sock.bind((addr, port))
        # 指定backlog数,这里初学者可能会存在一个误区,需要解释一下:
        #   有些地方把listen的参数成为“积压值”或者backlog,
        #   最大连接数是最大能处理的连接数(accept了丢一边晾着是耍流氓)
        #   提高并发处理能力是门学问,不单是提高“最大连接数”,两点结论:
        #     1. backlog不能提高“最大连接数”
        #     2. backlog不宜设置过大
        #   举个栗子,假设我们的服务器是一个非常受欢迎的饭店:
        #   最大连接数就是这个饭店最大能容纳的顾客人数,backlog就是门外允许排队的最大长度。
        #   如果饭店点菜慢上菜也慢(服务器的处理不能满足要求),饭店将很快被被顾客坐满(最大连接数上限)
        #   ,门口排位如果无限排下去(backlog设置非常大),可能还不如告诉顾客现在人太多了,
        #   我们处理不过来,不过等会儿再来试试。
        #   想要提高服务质量只能通过提高翻桌率(服务器处理速度)来实现。
        self.listen_sock.listen(5)  # backlog
        # 将listen socket同样放入conn_state
        self.setFd(self.listen_sock)
        # 初始化epoll的fd
        self.epoll_sock = select.epoll()
        # 这里指定我们的listen socket只关注EPOLLIN,即connect过来的连接
        # LT 是这里的默认, 想要ET 需要改成'select.EPOLLIN | select.EPOLLET'
        self.epoll_sock.register(self.listen_sock.fileno(), select.EPOLLIN)
        # 业务逻辑处理函数
        self.logic = logic
        # 状态机的各个状态的处理函数,这里的self.sm是一个key是字符串,value是函数的字典
        self.sm = {
            "accept": self.accept2read,
            "read": self.read2process,
            "write": self.write2read,
            "process": self.process,
            "closing": self.close,
        }
        dbgPrint('\n__init__: end, register no: %s' %
                 self.listen_sock.fileno())

    def process(self, fd):
        """
        调用业务逻辑处理函数self.logic,然后将它返回的字符串当成是
        Server对Client的回应
        通过约定好调用的函数原型,就可以实现比较干净的业务逻辑和网络框架的分离
        """
        sock_state = self.conn_state[fd]
        response = self.logic(sock_state.buff_read)
        # 组装给Client回应的10字节协议头
        sock_state.buff_write = "%05d%s" % (len(response), response)
        sock_state.need_write = len(sock_state.buff_write)
        sock_state.state = "write"
        self.epoll_sock.modify(fd, select.EPOLLOUT)
        sock_state.printState()

    def accept2read(self, fd):
        """
        这个函数主要完成accept到等待数据读取的状态转换
        """
        # accept一个连接之后,需要注册,初始化state为read
        conn = self.accept(fd)
        self.epoll_sock.register(conn.fileno(), select.EPOLLIN)
        self.setFd(conn)
        self.conn_state[conn.fileno()].state = "read"
        # 现在accept 到 read的转换完成了
        # 需要明确的是,我们的listen socket还是处于等待连接到来
        # 的accept状态
        dbgPrint("\n -- accept end!")

    def read2process(self, fd):
        """
        这个函数主要完成read完所有请求到处理业务逻辑的状态转换
        """
        # pdb.set_trace()
        read_ret = ""
        try:
            read_ret = self.read(fd)
        except (Exception), msg:
            dbgPrint(msg)
            read_ret = "closing"
        if read_ret == "process":
            # 数据接收完成,转换到process阶段
            self.process(fd)
        # readcontent、readmore、retry 都不用改变socket的state
        elif read_ret == "readcontent":
            pass
        elif read_ret == "readmore":
            pass
        elif read_ret == "retry":
            pass
        elif read_ret == "closing":
            self.conn_state[fd].state = 'closing'
            # 发生错误直接关闭,做到快速失败
            self.state_machine(fd)
        else:
            raise Exception("impossible state returned by self.read")

    def write2read(self, fd):
        """
        这个函数主要完成write给client回应到等待数据读取的状态转换。
        这个情况就是我们经常听到的“长连接”
        """
        try:
            write_ret = self.write(fd)
        except socket.error, msg:
            write_ret = "closing"

        if write_ret == "writemore":
            pass
            # 写数据完成,重置各种计数器,开始等待新请求过来
        elif write_ret == "writecomplete":
            sock_state = self.conn_state[fd]
            conn = sock_state.sock_obj
            self.setFd(conn)
            self.conn_state[fd].state = "read"
            self.epoll_sock.modify(fd, select.EPOLLIN)
        elif write_ret == "closing":
            # 发生错误直接关闭,做到快速失败
            dbgPrint(msg)
            self.conn_state[fd].state = 'closing'
            # closing directly when error.
            self.state_machine(fd)


if __name__ == '__main__':
    # 这个是我们演示用的“业务逻辑”,做的事情就是将请求的数据反转
    # 例如:
    #   收到:0000000005HELLO
    #   回应:0000000005OLLEH
    def logic(d_in):
        return(d_in[::-1])

    # 监听在0.0.0.0:9076
    reverseD = nbNet('0.0.0.0', 9090, logic)

    # 状态机开始运行,除非被kill,否则永不退出
    reverseD.run()


# >>study:/home/kang>telnet 127.0.0.1 50009
# Trying 127.0.0.1...
# Connected to 127.0.0.1.
# Escape character is '^]'.
# 00005abcde
# 00005edcba


上面的代码里import 的deamon类是一个用Python纯手工实现的daemonlize类

import sys
import os
import time
import atexit
from signal import SIGTERM


class Daemon:
    """
    通用的Daemonlize类,能将一个程序变成守护进程
    使用方式:继承Daemon类,然后重写run()函数即可
    """

    def __init__(self, pidfile='nbMon.pid', stdin='/dev/null', stdout='nbMon.log', stderr='nbMon.log'):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile

    def daemonize(self):
        """
        双重fork,具体原因参见Stevens写的《UNIX环境高级编程》
        书籍链接参见:http://book.douban.com/subject/1788421/
        还有:
        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
        """
        try:
            pid = os.fork()
            if pid > 0:
                # 退出第一个“爷爷进程”,因为后面还要第二次fork
                # 所以这个进程辈分是“爷爷”
                sys.exit(0)
        except OSError, e:
            sys.stderr.write("fork #1 failed: %d (%s)\n" %
                             (e.errno, e.strerror))
            sys.exit(1)

        # 需要执行一些操作避免可能从父进程继承过来的影响守护进程的设定
        # 改变当前工作目录
        os.chdir("/")
        # 设置sid,成为session Leader
        os.setsid()
        # 重设umask
        os.umask(0)

        # 第二次fork
        try:
            pid = os.fork()
            if pid > 0:
                # 父进程依然退出
                sys.exit(0)
        except OSError, e:
            sys.stderr.write("fork #2 failed: %d (%s)\n" %
                             (e.errno, e.strerror))
            sys.exit(1)

        # 重定向0、1、2三个fd(依次为标准输入、标准输出、错误输出)
        # 这里需要注意,有些不讲究的程序或者文章,会直接将0、1、2关闭,
        # 这样会造成一定的隐患,可能会导致后续操作打开的文件句柄占用
        # 0、1、2这三个一般认为有特殊含义的句柄,会导致一些莫名其妙的问题发生
        # 所以这里最好的建议是,将这三个fd重新定向到/dev/null,或者相应的日志文件

        # 重新定向之前flush一次,确保该打印出来的文字已经输出
        sys.stdout.flush()
        sys.stderr.flush()
        si = file(self.stdin, 'r')
        so = file(self.stdout, 'a+')
        se = file(self.stderr, 'a+', 0)
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

        """
        写pid文件
        """
        # 这里设定一个程序退出时回调,但必须知道的是,这个回调
        # 在一些情况下并不保证一定会被执行,比如被kill -9
        atexit.register(self.delpid)
        pid = str(os.getpid())
        file(self.pidfile, 'w+').write("%s\n" % pid)

    def delpid(self):
        os.remove(self.pidfile)

    def start(self):
        """
        启动守护进程
        """
        # 检查pid文件是否存在,如果存在就认为程序还在运行
        try:
            pf = file(self.pidfile, 'r')
            pid = int(pf.read().strip())
            pf.close()
        except IOError:
            pid = None

        if pid:
            message = "pidfile %s already exist. Daemon already running?\n"
            sys.stderr.write(message % self.pidfile)
            sys.exit(1)

        # 开始变身守护进程,哈哈哈哈
        self.daemonize()
        self.run()

    def stop(self):
        """
        停止守护进程
        """
        # 从pid文件中获取进程id
        try:
            pf = file(self.pidfile, 'r')
            pid = int(pf.read().strip())
            pf.close()
        except IOError:
            pid = None

        if not pid:
            message = "pidfile %s does not exist. Daemon not running?\n"
            sys.stderr.write(message % self.pidfile)
            return

        # 开始尝试kill掉守护进程
        try:
            while 1:
                os.kill(pid, SIGTERM)
                time.sleep(0.1)
        except OSError, err:
            err = str(err)
            if err.find("No such process") > 0:
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                print str(err)
                sys.exit(1)

    def restart(self):
        """
        重启
        """
        self.stop()
        self.start()

    def run(self):
        """
        这个方法是空的,所以要想使用这个类,必须在子类中
        重写这个函数,这个函数应该写的是程序的主逻辑循环。
        后面这个函数将会在start()和restart()函数中被调用。
        """

服务端开发群:365534424,本文仅授权51 Reboot相关账号发布。

「真诚赞赏,手留余香」
2 人赞赏
蜗牛老湿
高少虾
文章被以下专栏收录
27 条评论
推荐阅读