goahead webserver源碼分析

一、一個txt文本架構圖web

main()數組

      |緩存

      |--websOpenServer()服務器

      |             |-- websOpenListen()網絡

      |                           |--socketOpenConnection()數據結構

      |                                           |--打開webServer服務器架構

      |                                           |--初化socket_t結構(註冊websAccept()回調函數(socket_t sp->accept= websAccept)等)app

      |                                           |--把socket_t結構加入數組socketList    socket

      |函數

      |--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頁面

 

二、跟着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\n");

/*
 *    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(" \t"));
    if (op == NULL || *op == '\0') {
        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(" \t\n"));
    if (url == NULL || *url == '\0') {
        websError(wp, 400, T("Bad HTTP request"));
        return -1;
    }
    protoVer = gstrtok(NULL, T(" \t\n"));

/*
 *    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這樣鏈接上服務器可是不發任何字符到服務器進行測試。

相關文章
相關標籤/搜索