• goahead webserver源码分析


    1、一个txt文本架构图

    main()

          |

          |--websOpenServer()

          |             |-- websOpenListen()

          |                           |--socketOpenConnection()

          |                                           |--打开webServer服务器

          |                                           |--初化socket_t结构(注册websAccept()回调函数(socket_t sp->accept= websAccept)等)

          |                                           |--把socket_t结构加入数组socketList    

          |

          |--websUrlHandlerDefine()

          |                |--初始化websUrlHandlerType结构的websUrlHandler数组

          |                |--将urlPrefix和回调函数绑定在websUrlHandler[websUrlHandlerMax]中

          |

          |--websUrlHandlerDefine(websDefaultHandler)

          |                |--初始化websUrlHandlerType结构的websUrlHandler数组

          |                |--将urlPrefix和回调函数绑定在websUrlHandler[websUrlHandlerMax]中

          |         

          |--websFormDefine()

          |               |--初始化symbol table结构sym_t,把名字和回调函数名放进sym_t结构

          |               |--把sym_t结构放进hash表中

          |

          |--websAspDefine()

          |               |--初始化symbol table结构sym_t,把名字和回调函数名放进sym_t结构

          |               |--把sym_t结构放进hash表中

          |

          |--(main loop)

        |  |--socketReady(-1) || socketSelect(-1, 1000)

        |      |          |--轮询socketList        |--轮询socketList中的handlerMask

          |      |         |--中的几个变量        |--改变socketList中的currentEvents

          |    |

        |    |--socketProcess()

          |               |--轮询socketList[]

          |               |--socketReady()

          |               |--socketDoEvent()

          |                                |--如果有新的连接(来自listenfd)就调用socketAccept()

          |                                |                    |--调用socketAlloc()初始化socket_t结构

          |                                |                    |--把socket_t结构加入 socketList数组

          |                                |                    |--调用socket_t sp->accept()回调函数

          |                                |

          |                                |--如果不是新的连接就查找socketList数组调用socket_t sp->handler()回调函数

          |

       --|

    websAccept()

         |--做一些检查

         |--socketCreateHandler(sid, SOCKET_READABLE, websSocketEvent, (int) wp)

         |            |--把sid注册为读事件,初始化socket_t sp->handler = websSocketEvent等, 更新对应的socketList数组(handlerMask值等)

        

    websSocketEvent()

         |--判断读写操作

         |--读websReadEvent()

         |                |--websUrlHandlerRequest()

         |                                     |--查找wbsUrlHandler数组,调用和urlPrefix对应的回调函数(websFormHandler(),websDefaultHandler()等)

         |

         |--写,调用(wp->writeSocket)回调函数

    websFormHandler()

        |--跟据formName查找hash表,调用用户定义的函数

    websDefaultHandler()

        |--处理默认的URL请求,包括asp页面

        |--websSetRequestSocketHandler()

        |                                              |--注册默认的写事件函数wp->writeSocket = websDefaultWriteEvent

        |                                              |--socketCreateHandler(wp->sid, SOCKET_WRITABLE, websSocketEvent, (int) wp)

        |                                                          |--把sid注册为写事件,初始化socket_t sp->handler = websSocketEvent等, 更新对应的socketList数组

        

    websDefaultWriteEvent()

        |

        |--写数据,不包括asp页面

    2、跟着main走

    Main函数很简短,所以可以对他的代码进行一行一行注释,如下:

    /*
     *    Main -- entry point from LINUX
     */
    
    int main(int argc, char** argv)
    {
    /*
     *    Initialize the memory allocator. Allow use of malloc and start 
     *    with a 60K heap.  For each page request approx 8KB is allocated.
     *    60KB allows for several concurrent page requests.  If more space
     *    is required, malloc will be used for the overflow.
     */
        bopen(NULL, (60 * 1024), B_USE_MALLOC);
        signal(SIGPIPE, SIG_IGN);
    
    
        printf("**************88
    ");
    
    /*
     *    Initialize the web server
     */
        if (initWebs() < 0) {
            return -1;
        }
    
    #ifdef WEBS_SSL_SUPPORT
        websSSLOpen();
    #endif
    
    /*
     *    Basic event loop. SocketReady returns true when a socket is ready for
     *    service. SocketSelect will block until an event occurs. SocketProcess
     *    will actually do the servicing.
     */
        while (!finished) {
            if (socketReady(-1) || socketSelect(-1, 100)) {
                socketProcess(-1);
            }
            websCgiCleanup();
            emfSchedProcess();
        }
    
    #ifdef WEBS_SSL_SUPPORT
        websSSLClose();
    #endif
    
    #ifdef USER_MANAGEMENT_SUPPORT
        umClose();
    #endif
    
    /*
     *    Close the socket module, report memory leaks and close the memory allocator
     */
        websCloseServer();
        socketClose();
    #ifdef B_STATS
        memLeaks();
    #endif
        bclose();
        return 0;
    }

    3.一些想法

    1,  找出他们共同的数据结构

    2,  找出对这些数据结构维护(操作)的函数

    3,  从http的get或者是post流程来看程序

    4,  整体架构如何掌握

    5,  分模块,从全局的角度看各个模块的功能

    6,  从main函数起,按树型结构一层层分析下去

    选择第五种方法:

    1,  sock模块,专门处理网络链接这一块,有这么几个文件:

    sock.c和sockGen.c,sock.c是(维护)处理链接的socket_t数据结构,sockGen.c是(维护)处理链接的。

    2,  对http协议数据进行操作(读取和分析),webc.c文件

    3,  对具体数据的操作(asp,form…),handler.c文件

    选择第三种方法来看程序:

    假设有个http请求:从这个http请求到服务器的处理,然后返回这样一个过程来看goahead是怎么操作的?

    1,写一个http请求的url和一个head

    1,  写一个http请求的post的head

    注:因为这次要看通整个goahead代码,所以一下子不知道以什么思路来看。上面是一些想法,不知道从哪里开始分析一个项目的代码,也不知道取舍哪些进行程序结构和功能方面的分析。后来的结果是写出了下面的文字。

    4.goahead mainloop源码分析

    4.1 socketReady(-1)函数分析

    socketReady函数检查已建立连接的socket中是否有以下事件,如果检查到一个,就返回1,如果没有检查到,就返回零。

      (1)sp->flags & SOCKET_CONNRESET,如果该socket的flag标志为SOCKET_CONNRESET(该标志在哪里设置(初始化)的?),则调用函数socketCloseConnection(该函数后面会解释)关闭该socket连接,然后返回0;

      (2)sp->currentEvents & sp->handlerMask,如果该socket当前的事件和他要处理的事件相同,就返回1,告诉调用socketReady的函数有socket准备好被处理了;

      (3)sp->handlerMask & SOCKET_READABLE && socketInputBuffered(sid) > 0,如果该socket要处理的事件是SOCKET_READABLE并且该socket的缓存中有可读的数据,则调用socketSelect函数(为什么在这里要调用这个函数,看了下socketSelect,应该是为了设置sp->currentEvents |= SOCKET_READABLE,所以这里应可以优化),然后返回1,告诉调用socketReady的函数有socket准备好被处理了;

      (4) socketReady函数根据传入的参数sid决定是检查id为sid的socket(当sid大于0),还是遍历整个socketList(当sid小于0),如果以上3个条件中没有一个满足,则返回0。

    4.2 socketSelect(-1, 1000)函数分析

    socketSelect函数是系统调用select的外包函数,该函数的主要功能就是监听(?)注册的socket事件集合,然后修改sp->currentEvents变量。

    流程如下:

     转载goahead webserver源码分析 - 一鸿秋水 - 一鸿秋水的博客

    在主函数中,对socketSelect的调用是这样的:

    if (socketReady(-1) || socketSelect(-1, 1000)),这样做并没有对socketSelect的返回值进行检查,也就是说当socketSelect返回-1时,该条件也会满足,从而程序也会往下走,所以,这个地方也是可以优化的。

    4.3 socketProcess(-1)函数分析

    socketProcess处理到达的socket事件,如果传入的参数是小于0,则会处理所有的socket的事件,如果大于0,则会处理指定的socket的事件。下面是主要过程:

    /*
     *    Process socket events
     */
    
    void socketProcess(int sid)
    {
        socket_t    *sp;
        int            all;
    
        all = 0;
        if (sid < 0) {
            all = 1;
            sid = 0;
        }
    /*
     *     Process each socket
     */
        for (; sid < socketMax; sid++) {
            if ((sp = socketList[sid]) == NULL) {
                if (! all) {
                    break;
                } else {
                    continue;
                }
            }
            if (socketReady(sid)) {
                socketDoEvent(sp);
            }
            if (! all) {
                break;
            }
        }
    }

    socketReady()函数请看上面的解释,但不明白这里为什么还要用到这个函数,应该也是个可以优化的地方,我现在想到一个过程,应该是这样的:

    if(socketSelect ()){
      socketProcess();
    }

    后注:走完websGetInput()函数的分析后,因为这时仔细看到了更多的代码,上面的这个优化是不行的,因为socketReady(int sid)函数中,是sp->currentEvents,sp->handlerMask这几个标志位来判断是否有数据读写。socketDoEvent()是对已连接的socket通过改变sp->currentEvents和sp->handlerMask来分阶段的去处理数据,

    并不是一路执行到底直到把这个连接关闭的。socketSelect ()是主要还是用在有新连接到来的时候,有新连接到来才会使这个函数返回真。socketDoEvent大致分两个阶段去处理一个连接,1是READ阶段,READ处理成功,便会设置状态到WRITE阶段,却不执行WRITE动作,2是WRITE阶段,WRITE执行完后才会结束这个连接。当第一次主循环时,socketDoEvent()执行的是READ,所以,如果按上一个代码段,第二次执行循环时,如socketSelect ()中没有新连接或数据到来,就不会往下执行了,而已有数据的连接将得不到立即的处理。socketReady(sid)可以检查已有连接是否有数据准备好读写,所以在这里优化是错误的。

    下面看看socketDoEvent函数的实现:

    转载goahead webserver源码分析 - 一鸿秋水 - 一鸿秋水的博客

    (1)socketDoEvent函数首先对socket的当前事件进行检查,如果是读事件并且是服务器监听socket上的读事件,说明有新连接到来,于是调用socketAccept()欢迎新连接,并使currentEvents为0,然后马上返回。

    (2)如果当前不是读事件但是该socket原感兴趣的是读事件并且socket缓存中确有数据可读,那就置currentEvents为可读,这一步在socketReady函数中有做过,所以这里应该是可以去掉的。

    (3)如果当前是写事件,那就看看该socket的写缓存中有没有数据,如果有并且有SOCKET_FLUSHING标志就全部输出该写缓存,这是为新的写事件做清理工作。

    (4)调用事件处理函数sp->handler,该函数指针分别在两个地方进行初始化:

      1,在websDefaultHandler()函数中注册写事件,该函数在什么时候被调?

      2,在websAccept()函数中注册读事件

      两处都指向websSocketEvent()函数。等下解释这个函数。

    (5)把currentEvents置为0。

    4.4 socketAccept()函数分析

    socketAccept()函数接收一个新的连接,并且调用用户注册的接收函数,这一个过程后,就把对socket_t结构的处理转换到了webs_t结构。

    转载goahead webserver源码分析 - 一鸿秋水 - 一鸿秋水的博客

    Sp->accept函数指针在socketOpenConnection()函数中调用socketAlloc()函数注册给监听socket的socket_t数据结构,当socketAccept()函数处理新连接时,就会把自已的Sp->accept指针及其他几个属性通过调用socketAlloc(sp->host, sp->port, sp->accept, sp->flags)函数又给了新的连接,注意,调用完这个后,会更新新的nsp->flags &= ~SOCKET_LISTENING,把该连接和监听连接区别开。

    然后,监听socket用自已的Sp->accept调用到websAccept()函数,但是传递的第一个参数是新连接的id:nid。这也应该是个技巧吧。

    websAccept()函数功能:

     转载goahead webserver源码分析 - 一鸿秋水 - 一鸿秋水的博客

    经过websAccept()函数后,将走出socket层,来到webs_t结构层,以后对读和写的操作都通过操作webs_t这个数据结构来完成。

    4.5 websSocketEvent()函数分析

    websSocketEvent()函数处理socket的读和写事件:

     转载goahead webserver源码分析 - 一鸿秋水 - 一鸿秋水的博客

    (1)websSocketEvent()函数根据mask决定调用读还是写函数,wp->writeSocket函数指针在websDefaultHandler()中通过调用websSetRequestSocketHandler()进行注册,指向websDefaultWriteEvent()函数。

    (2)websReadEvent()函数:

    websReadEvent()函数处理读事件,我们怀疑这个函数有问题,会导致web服务器宕机,因此要重点分析。

    转载goahead webserver源码分析 - 一鸿秋水 - 一鸿秋水的博客

    我们来看看该函数中用到的wp结构的四个状态:

    先给出一个http的post头,看起来形象点:

    POST /goform/formTest HTTP/1.1
    
    Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, **
    
    Accept-Language: zh-cn
    
    Accept-Encoding: gzip, deflate
    
    User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; CIBA)
    
    Host: 192.168.90.50
    
    Connection: Keep-Alive

    程序不进入①是因为wp->flags & WEBS_POST_REQUEST这个条件不成立,所以我给出上面的一个GET头,这时wp->flags中应该在WEBS_BEGIN状态机下的websParseFirst()函数中没有被设置为WEBS_POST_REQUEST,这里不得不插入一段websParseFirst()函数中的代码说明情况:

    /*
     *    Parse the first line of a HTTP request
     */
    
    static int websParseFirst(webs_t wp, char_t *text)
    {
        char_t     *op, *proto, *protoVer, *url, *host, *query, *path, *port, *ext;
        char_t    *buf;
        int        testPort;
    
        a_assert(websValid(wp));
        a_assert(text && *text);
    
    /*
     *    Determine the request type: GET, HEAD or POST
     */
        op = gstrtok(text, T(" 	"));
        if (op == NULL || *op == '') {
            websError(wp, 400, T("Bad HTTP request"));
            return -1;
        }
        if (gstrcmp(op, T("GET")) != 0) {
            if (gstrcmp(op, T("POST")) == 0) {
                wp->flags |= WEBS_POST_REQUEST;
            } else if (gstrcmp(op, T("HEAD")) == 0) {
                wp->flags |= WEBS_HEAD_REQUEST;
            } else {
                websError(wp, 400, T("Bad request type"));
                return -1;
            }
        }
    
    /*
     *    Store result in the form (CGI) variable store
     */
        websSetVar(wp, T("REQUEST_METHOD"), op);
    
        url = gstrtok(NULL, T(" 	
    "));
        if (url == NULL || *url == '') {
            websError(wp, 400, T("Bad HTTP request"));
            return -1;
        }
        protoVer = gstrtok(NULL, T(" 	
    "));
    
    /*
     *    Parse the URL and store all the various URL components. websUrlParse
     *    returns an allocated buffer in buf which we must free. We support both
     *    proxied and non-proxied requests. Proxied requests will have http://host/
     *    at the start of the URL. Non-proxied will just be local path names.
     */
        host = path = port = proto = query = ext = NULL;
        if (websUrlParse(url, &buf, &host, &path, &port, &query, &proto, 
                NULL, &ext) < 0) {
            websError(wp, 400, T("Bad URL format"));
            return -1;
        }
    
        wp->url = bstrdup(B_L, url);
    
    #ifndef __NO_CGI_BIN
        if (gstrstr(url, CGI_BIN) != NULL) {
            wp->flags |= WEBS_CGI_REQUEST;
            if (wp->flags & WEBS_POST_REQUEST) {
                wp->cgiStdin = websGetCgiCommName();
            }
        }
    #endif
    
        wp->query = bstrdup(B_L, query);
        wp->host = bstrdup(B_L, host);
        wp->path = bstrdup(B_L, path);
        wp->protocol = bstrdup(B_L, proto);
        wp->protoVersion = bstrdup(B_L, protoVer);
        
        if ((testPort = socketGetPort(wp->listenSid)) >= 0) {
            wp->port = testPort;
        } else {
            wp->port = gatoi(port);
        }
    
        if (gstrcmp(ext, T(".asp")) == 0) {
            wp->flags |= WEBS_ASP;
        }
        bfree(B_L, buf);
    
        websUrlType(url, wp->type, TSZ(wp->type));
    
    #ifdef WEBS_PROXY_SUPPORT
    /*
     *    Determine if this is a request for local webs data. If it is not a proxied 
     *    request from the browser, we won't see the "http://" or the system name, so
     *    we assume it must be talking to us directly for local webs data.
     *    Note: not fully implemented yet.
     */
        if (gstrstr(wp->url, T("http://")) == NULL || 
            ((gstrcmp(wp->host, T("localhost")) == 0 || 
                gstrcmp(wp->host, websHost) == 0) && (wp->port == websPort))) {
            wp->flags |= WEBS_LOCAL_PAGE;
            if (gstrcmp(wp->path, T("/")) == 0) {
                wp->flags |= WEBS_HOME_PAGE;
            }
        }
    #endif
    
        ringqFlush(&wp->header);
        return 0;
    }

    看来,对于GET头,wp->flags并不需要设置一个WEBS_GET_XXX什么的标志位。

    原因说清了,按上图,程序进入websUrlHandlerRequest(wp)函数,websUrlHandlerRequest(wp)函数会怎么处理上面给出的这个GET头,websUrlHandlerRequest(wp)根据urlPrefix来调用注册好的回调函数,先说urlPrefix,他是指/goform/xxx,/cgi_bin/yyy中的/goform和/cgi_bin这些东西,如果一个URL是这样的:/forms.asp那么他的urlPrefix为””,在主main()函数中,注册了对这个urlPrefix的回调函数为:

    websUrlHandlerDefine(T(""), NULL, 0, websDefaultHandler, WEBS_HANDLER_LAST);

    即websDefaultHandler()函数。websUrlHandlerRequest(wp)函数通过查找websUrlHandler[]数组会得到urlPrefix和回调函数的对应关系,然后调用回调函数,现在我们进到websDefaultHandler(),在socketProcess(-1)函数分析的第(4)点中有个疑问,到这里就不再是疑问了。websDefaultHandler()函数的最后调用:

    websSetRequestSocketHandler(wp, SOCKET_WRITABLE, websDefaultWriteEvent);

    注册该wp连接写事件的回调函数,并把wp的感兴趣的事件handlerMask改为SOCKET_WRITABLE。这样当第二次执行主循环时,想想回到websSocketEvent()函数,就会调用到写事件的函数websDefaultWriteEvent(),通过wp->writeSocket指针。

    4.6 websCgiCleanup()函数分析

    该函数清除执行完的CGI进程。

    4.7 emfSchedProcess()函数分析

    emfSchedProcess()函数检查超时的连接,如果有超时的连接就会把这个连接清理掉,这里要注意程序中是怎么设置超时的起起始时间的。

    在websReadEvent()函数中,调用websSetTimeMark(wp)为该连接设置一个时间戳,超时就是相对于这个时间的,但是请想下如果该连接一直没有数据到来的话,仅完成三次握手(不了解内核会不会对这样的连接有个超时机制,如果有,我下面就是白说),因为不可能执行到websReadEvent()函数,那么超时机制将对该连接无效,可以想象有很多这样的恶意连接没有被清除将会浪费系统资源,所以当accept这个连接的时候,就用websSetTimeMark(wp)为该连接设置一个时间戳,这个动作可以放在websAlloc ()函数中,这个函数被websAccept()函数调用,作用是初始化一个新的webs_t结构的连接。如果在超时前有数据来,这个时间戳将在websReadEvent()函数更改成新的;如果没有新的数据,那超时之后,服务器将断开这个连接,这样并不会影响系统运行,因此是可行的。

    可以用telnet 192.168.0.50 80这样连接上服务器但是不发任何字符到服务器进行测试。

  • 相关阅读:
    [知乎]20世纪初的军阀.
    Clover的简单使用
    影像工作站的数据库安装错误之Win7系统下pg服务无法启动
    屏蔽各大视频网站播放前15秒30秒广告
    电脑双显示器主分屏,巨鲨显示器不亮
    move 和 CopyMemory的区别
    The CompilerVersion constant identifies the internal version number of the Delphi compiler.
    Firemonkey的旁门左道[六]
    电够动力足——认识主板上的CPU供电模块
    delphi 枚举类型
  • 原文地址:https://www.cnblogs.com/cslunatic/p/3658880.html
Copyright © 2020-2023  润新知