title: Goahead源碼解析(轉)
date: 2019/12/21 15:24:47
toc: true
源碼解析
Goahead源碼解析(轉)
https://blog.csdn.net/chenlonglong2014
1. 從主函數到I/O事件循環
一、主函數
主函數主要流程是載入配置文件、申請必要數據結構、對服務器進行監聽。內容不長,大家可結合注釋看。
MAIN(goahead, int argc, char **argv, char **envp)
{
char *argp, *home, *documents, *endpoints, *endpoint, *route, *auth, *tok, *lspec;
int argind;
#if WINDOWS
if (windowsInit() < 0) {
return 0;
}
#endif
route = "route.txt"; //路徑文件,類似權限
auth = "auth.txt"; //權限文件
for (argind = 1; argind < argc; argind++) {
argp = argv[argind];
if (*argp != '-') {
break;
} else if (smatch(argp, "--auth") || smatch(argp, "-a")) {
if (argind >= argc) usage();
auth = argv[++argind];
#if ME_UNIX_LIKE && !MACOSX
} else if (smatch(argp, "--background") || smatch(argp, "-b")) {
websSetBackground(1);
#endif
} else if (smatch(argp, "--debugger") || smatch(argp, "-d") || smatch(argp, "-D")) {
websSetDebug(1);
} else if (smatch(argp, "--home")) {
if (argind >= argc) usage();
home = argv[++argind];
if (chdir(home) < 0) {
error("Cannot change directory to %s", home);
exit(-1);
}
} else if (smatch(argp, "--log") || smatch(argp, "-l")) {
if (argind >= argc) usage();
logSetPath(argv[++argind]);
} else if (smatch(argp, "--verbose") || smatch(argp, "-v")) {
logSetPath("stdout:2");
} else if (smatch(argp, "--route") || smatch(argp, "-r")) {
route = argv[++argind];
} else if (smatch(argp, "--version") || smatch(argp, "-V")) {
printf("%s\n", ME_VERSION);
exit(0);
} else if (*argp == '-' && isdigit((uchar) argp[1])) {
lspec = sfmt("stdout:%s", &argp[1]);
logSetPath(lspec);
wfree(lspec);
} else {
usage();
}
}
//截止到這里是程序運行時根據入參來配置功能,實際改造時不會要求輸入這么多參數,都是事先配置好
documents = ME_GOAHEAD_DOCUMENTS;//存放web頁面的位置
if (argc > argind) {
documents = argv[argind++];
}
initPlatform(); //定義了信號處理的行為,收到SIGTERM信號后,調用sigHandler,講finished變量設置為1,退出服務器監聽事件循環
if (websOpen(documents, route) < 0) {//初始化變量以及相關函數行為對應的handler
error("Cannot initialize server. Exiting.");
return -1;
}
#if ME_GOAHEAD_AUTH
if (websLoad(auth) < 0) {//載入權限文件
error("Cannot load %s", auth);
return -1;
}
#endif
logHeader();
if (argind < argc) {
while (argind < argc) {
endpoint = argv[argind++];
if (websListen(endpoint) < 0) {
return -1;
}
}
} else {
endpoints = sclone(ME_GOAHEAD_LISTEN);
for (endpoint = stok(endpoints, ", \t", &tok); endpoint; endpoint = stok(NULL, ", \t,", &tok)) {
#if !ME_COM_SSL
if (strstr(endpoint, "https")) continue;
#endif
if (websListen(endpoint) < 0) {//將IP:PORT設置為監聽套接字,打開監聽端口
wfree(endpoints);
return -1;
}
}
wfree(endpoints);
}
#if ME_ROM && KEEP
/*
If not using a route/auth config files, then manually create the routes like this:
If custom matching is required, use websSetRouteMatch. If authentication is required, use websSetRouteAuth.
*/
websAddRoute("/", "file", 0);
#endif
#ifdef GOAHEAD_INIT
/*
Define your init function in main.me goahead.init, or
configure with DFLAGS=GOAHEAD_INIT=myInitFunction
*/
{
extern int GOAHEAD_INIT();
if (GOAHEAD_INIT() < 0) {
exit(1);
}
}
#endif
#if ME_UNIX_LIKE && !MACOSX
/*
Service events till terminated
*/
if (websGetBackground()) {//后台運行
if (daemon(0, 0) < 0) {
error("Cannot run as daemon");
return -1;
}
}
#endif
websServiceEvents(&finished);//里面調用select監聽套接字,同時處理I/O事件循環,正常情況下不會退出此循環。直到收到SIGTERM信號,finished = 1,退出此循環,服務器優雅退出,清理資源。
logmsg(1, "Instructed to exit");
websClose();
#if WINDOWS
windowsClose();
#endif
return 0;
}
二、I/O事件循環
作為一個HTTP服務器,該代碼最重要的就是socket事件循環,也就是websServiceEvents(&finished);函數,下來對這個函數展開。
PUBLIC void websServiceEvents(int *finished)
{
int delay, nextEvent;
if (finished) {
*finished = 0;
}
delay = 0;
while (!finished || !*finished) {//主程序進入此循環,進行I/O監聽
if (socketSelect(-1, delay)) {//如果select監聽有返回個數,就針對套接字進行I/O處理
socketProcess();
}
#if ME_GOAHEAD_CGI
delay = websCgiPoll();//決定select的超市時間,實際上為什么這個時間要動態變化,還在研究中
#else
delay = MAXINT;
#endif
nextEvent = websRunEvents();
delay = min(delay, nextEvent);
}
}
下面我們來看看select函數是怎么寫的:
PUBLIC int socketSelect(int sid, int timeout)
{
struct timeval tv;
WebsSocket *sp;
fd_set readFds, writeFds, exceptFds;
int nEvents;
int all, socketHighestFd; /* Highest socket fd opened */
FD_ZERO(&readFds);
FD_ZERO(&writeFds);
FD_ZERO(&exceptFds);
socketHighestFd = -1;
tv.tv_sec = (long) (timeout / 1000);
tv.tv_usec = (DWORD) (timeout % 1000) * 1000;
/*
Set the select event masks for events to watch
*/
all = nEvents = 0;
if (sid < 0) {
all++;
sid = 0;
}
for (; sid < socketMax; sid++) {
if ((sp = socketList[sid]) == NULL) {
continue;
}
assert(sp);
/*
Set the appropriate bit in the ready masks for the sp->sock.
*/
if (sp->handlerMask & SOCKET_READABLE) {//套接字需要監聽的事件放到監聽事件組中
FD_SET(sp->sock, &readFds);
nEvents++;
}
if (sp->handlerMask & SOCKET_WRITABLE) {
FD_SET(sp->sock, &writeFds);
nEvents++;
}
if (sp->handlerMask & SOCKET_EXCEPTION) {
FD_SET(sp->sock, &exceptFds);
nEvents++;
}
if (sp->flags & SOCKET_RESERVICE) {
tv.tv_sec = 0;
tv.tv_usec = 0;
}
if (! all) {
break;
}
}
/*
Windows select() fails if no descriptors are set, instead of just sleeping like other, nice select() calls.
So, if WINDOWS, sleep.
*/
if (nEvents == 0) {
Sleep((DWORD) timeout);
return 0;
}
/*
Wait for the event or a timeout
*/
nEvents = select(socketHighestFd + 1, &readFds, &writeFds, &exceptFds, &tv);
if (all) {
sid = 0;
}
for (; sid < socketMax; sid++) {
if ((sp = socketList[sid]) == NULL) {
continue;
}
if (sp->flags & SOCKET_RESERVICE) {
if (sp->handlerMask & SOCKET_READABLE) {
sp->currentEvents |= SOCKET_READABLE;
}
if (sp->handlerMask & SOCKET_WRITABLE) {
sp->currentEvents |= SOCKET_WRITABLE;
}
sp->flags &= ~SOCKET_RESERVICE;
nEvents++;
}//如果套接字在監聽返回的事件組中,就將sp->currentEvents設置成對應的事件,供后續socketProcess處理
if (FD_ISSET(sp->sock, &readFds)) {
sp->currentEvents |= SOCKET_READABLE;
}
if (FD_ISSET(sp->sock, &writeFds)) {
sp->currentEvents |= SOCKET_WRITABLE;
}
if (FD_ISSET(sp->sock, &exceptFds)) {
sp->currentEvents |= SOCKET_EXCEPTION;
}
if (! all) {
break;
}
}
return nEvents;
}
#else /* !ME_WIN_LIKE */
三、服務器與客戶端建立連接
假設瀏覽器有HTTP請求發往服務器,我們相應的流程是怎樣的呢?
通過socketSelect監聽,發現監聽套接字有讀事件,先調用socketAccep創建新的套接字,同時調用websAccept函數,為這個連接創建必要的數據結構,保存傳輸過程中需要的WEB數據結構
Webs *wp。 創建好套接字之后,再為這個套接字注冊一個讀事件,從而進行請求的讀取。函數實現如下:
PUBLIC int websAccept(int sid, cchar *ipaddr, int port, int listenSid)
{
Webs *wp;
WebsSocket *lp;
struct sockaddr_storage ifAddr;
int wid, len;
assert(sid >= 0);
assert(ipaddr && *ipaddr);
assert(listenSid >= 0);
assert(port >= 0);
/*
Allocate a new handle for this accepted connection. This will allocate a Webs structure in the webs[] list
*/
if ((wid = websAlloc(sid)) < 0) {
return -1;
}
wp = webs[wid];
assert(wp);
wp->listenSid = listenSid;
strncpy(wp->ipaddr, ipaddr, min(sizeof(wp->ipaddr) - 1, strlen(ipaddr)));
/*
Get the ip address of the interface that accept the connection.
*/
len = sizeof(ifAddr);
if (getsockname(socketPtr(sid)->sock, (struct sockaddr*) &ifAddr, (Socklen*) &len) < 0) {
error("Cannot get sockname");
websFree(wp);
return -1;
}
socketAddress((struct sockaddr*) &ifAddr, (int) len, wp->ifaddr, sizeof(wp->ifaddr), NULL);
#if ME_GOAHEAD_LEGACY
/*
Check if this is a request from a browser on this system. This is useful to know for permitting administrative
operations only for local access
*/
if (strcmp(wp->ipaddr, "127.0.0.1") == 0 || strcmp(wp->ipaddr, websIpAddr) == 0 ||
strcmp(wp->ipaddr, websHost) == 0) {
wp->flags |= WEBS_LOCAL;
}
#endif
/*
Arrange for socketEvent to be called when read data is available
*/
lp = socketPtr(listenSid);
trace(4, "New connection from %s:%d to %s:%d", ipaddr, port, wp->ifaddr, lp->port);
#if ME_COM_SSL
if (lp->secure) {
wp->flags |= WEBS_SECURE;
trace(4, "Upgrade connection to TLS");
if (sslUpgrade(wp) < 0) {
error("Cannot upgrade to TLS");
websFree(wp);
return -1;
}
}
#endif
assert(wp->timeout == -1);
wp->timeout = websStartEvent(PARSE_TIMEOUT, checkTimeout, (void*) wp);
socketEvent(sid, SOCKET_READABLE, wp);//給這個已連接套接字注冊一個讀事件,從而調用事件處理函數,發出讀HTTP請求。
return 0;
}
socketEvent 此函數我認為是HTTP連接中最關鍵的函數,里面進行I/O處理。作為HTTP服務器,其中的讀寫都遵循HTTP協議,根據請求的不同類型,做出不同的響應。
理解了這個事件中的readEvent, writeEvent兩個函數,就可以理解HTTP協議的大概脈絡。這兩個函數對應的HTTP處理流程,后續專題講述。
static void socketEvent(int sid, int mask, void *wptr)
{
Webs *wp;
wp = (Webs*) wptr;
assert(wp);
assert(websValid(wp));
if (! websValid(wp)) {
return;
}
if (mask & SOCKET_READABLE) {
readEvent(wp);
}
if (mask & SOCKET_WRITABLE) {
writeEvent(wp);
}
if (wp->flags & WEBS_CLOSED) {
websFree(wp);
/* WARNING: wp not valid here */
}
}
2. 讀取HTTP請求
一、讀取HTTP請求
瀏覽器與服務器建立好連接之后,會調用readEvent接口來讀取從瀏覽器來的請求數據。HTTP請求的結束符是"\r\n\r\n",服務器調用readEvent,通過websRead讀取緩沖區(內容長度不超過2048字節)。
The webs read handler. This is the primary read event loop. It uses a state machine to track progress while parsing
the HTTP request. Note: we never block as the socket is always in non-blocking mode.
*/
static void readEvent(Webs *wp)
{
WebsBuf *rxbuf;
WebsSocket *sp;
ssize nbytes;
assert(wp);
assert(websValid(wp));
if (!websValid(wp)) {
return;
}
websNoteRequestActivity(wp);
rxbuf = &wp->rxbuf;//緩沖區的數據結構看定義,寫得很清楚
if (bufRoom(rxbuf) < (ME_GOAHEAD_LIMIT_BUFFER + 1)) {//緩沖區不夠了增加緩沖區的大小
if (!bufGrow(rxbuf, ME_GOAHEAD_LIMIT_BUFFER + 1)) {
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot grow rxbuf");
websPump(wp);
return;
}
}
if ((nbytes = websRead(wp, (char*) rxbuf->endp, ME_GOAHEAD_LIMIT_BUFFER)) > 0) {//調用socketRead,讀HTTP請求.rxbuf->endp是上一次的數據尾,每次讀之后接上
wp->lastRead = nbytes;//一次讀了多少字節
bufAdjustEnd(rxbuf, nbytes);//讀了多少字節,數據的尾指針就加多少字節
bufAddNull(rxbuf);//寫字符串結束符
}
if (nbytes > 0 || wp->state > WEBS_BEGIN) {//讀到數據了,進來處理
websPump(wp);
}
if (wp->flags & WEBS_CLOSED) {
return;//通過websPump處理完請求,需要關閉連接,return返回readEvent.數據結構依然保留。如果是非keep alive 什么時候清除本鏈接的數據結構?
} else if (nbytes < 0 && socketEof(wp->sid)) {
/* EOF or error. Allow running requests to continue. */
if (wp->state < WEBS_READY) {
if (wp->state > WEBS_BEGIN) {
websError(wp, HTTP_CODE_COMMS_ERROR, "Read error: connection lost");
websPump(wp);
} else {
complete(wp, 0);
}
} else {
socketDeleteHandler(wp->sid);
}
} else if (wp->state < WEBS_READY) {//如果是keep alive的請求,繼續監聽。
sp = socketPtr(wp->sid);
socketCreateHandler(wp->sid, sp->handlerMask | SOCKET_READABLE, socketEvent, wp);
}
二、解析HTTP請求
websPump是處理WEB請求的主要函數,里面根據不同狀態機來處理HTTP請求。HTTP請求的解析,響應,完成對應狀態機中的幾個狀態。
PUBLIC void websPump(Webs *wp)
{
bool canProceed;
for (canProceed = 1; canProceed; ) {//只到conProceed = 0 ,才退出循環,否則按狀態順序循環執行
switch (wp->state) {
case WEBS_BEGIN://最初都是BEGIN狀態
canProceed = parseIncoming(wp);
break;
case WEBS_CONTENT:
canProceed = processContent(wp);//除了請求頭之外有額外的數據輸入到服務器
break;
case WEBS_READY:
if (!websRunRequest(wp)) {//接受數據已經完成,開始響應HTTP請求。調用注冊的各個handler,有jstHandler,fileHandler,actionHandler等。默認是fileHandler,即普通的文檔傳輸。handler執行過程中將state置為COMPLETE
/* Reroute if the handler re-wrote the request */
websRouteRequest(wp);
wp->state = WEBS_READY;
canProceed = 1;
continue;
}
canProceed = (wp->state != WEBS_RUNNING);
break;
case WEBS_RUNNING:
/* Nothing to do until websDone is called */
return;
case WEBS_COMPLETE:
canProceed = complete(wp, 1);//此處退出webPump,最終退出readEvent,等待select下一次返回
break;
}
}
}
parseIncoming() 解析HTTP頭的內容,確定是何種請求,從而才能知道怎么去響應:
static bool parseIncoming(Webs *wp)
{
WebsBuf *rxbuf;
char *end, c;
rxbuf = &wp->rxbuf;
while (*rxbuf->servp == '\r' || *rxbuf->servp == '\n') {
if (bufGetc(rxbuf) < 0) {
break;
}
}//找到非\r\n的第一個字節
if ((end = strstr((char*) wp->rxbuf.servp, "\r\n\r\n")) == 0) {//“\r\n\r\n”是協議規定請求頭的結束符,實際上就是兩個連續換行
if (bufLen(&wp->rxbuf) >= ME_GOAHEAD_LIMIT_HEADER) {//沒讀完請求的話,繼續讀,但是也不能讀太長
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Header too large");
return 1;
}
return 0;
}
trace(3 | WEBS_RAW_MSG, "\n<<< Request\n");
c = *end;
*end = '\0';
trace(3 | WEBS_RAW_MSG, "%s\n", wp->rxbuf.servp);
*end = c;
//讀完了請求了,開始解析
/*
Parse the first line of the Http header
*/
parseFirstLine(wp);//解析第一行信息
if (wp->state == WEBS_COMPLETE) {
return 1;
}
parseHeaders(wp);//解析整個請求,把請求每一個屬性記錄下來,存在WP中
if (wp->state == WEBS_COMPLETE) {
return 1;
}
wp->state = (wp->rxChunkState || wp->rxLen > 0) ? WEBS_CONTENT : WEBS_READY;//解析頭來判斷是不是有內容,是不是有輸入。
websRouteRequest(wp);//route的意思是將這個wp與route.txt中每一行相匹配,如果能匹配,wp-route = route
if (wp->state == WEBS_COMPLETE) {
return 1;
}
#if ME_GOAHEAD_CGI
if (wp->route && wp->route->handler && wp->route->handler->service == cgiHandler) {
if (smatch(wp->method, "POST")) {
wp->cgiStdin = websGetCgiCommName();
if ((wp->cgifd = open(wp->cgiStdin, O_CREAT | O_WRONLY | O_BINARY | O_TRUNC, 0666)) < 0) {
websError(wp, HTTP_CODE_NOT_FOUND | WEBS_CLOSE, "Cannot open CGI file");
return 1;
}
}
}
#endif
#if !ME_ROM
if (smatch(wp->method, "PUT")) {
WebsStat sbuf;
wp->code = (stat(wp->filename, &sbuf) == 0 && sbuf.st_mode & S_IFDIR) ? HTTP_CODE_NO_CONTENT : HTTP_CODE_CREATED;
wfree(wp->putname);
wp->putname = websTempFile(ME_GOAHEAD_PUT_DIR, "put");
if ((wp->putfd = open(wp->putname, O_BINARY | O_WRONLY | O_CREAT | O_BINARY, 0644)) < 0) {
error("Cannot create PUT filename %s", wp->putname);
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot create the put URI");
wfree(wp->putname);
return 1;
}
}
#endif
return 1;
}
3. 響應HTTP請求
一、如何響應HTTP請求
websPump中若前面兩步解析請求行請求頭成功,wp->state置為READY時,調用websRunRequest響應請求。
此時,websRunRequest中將wp->state置為RUNNING,之后調用route對應的service回調函數,也就是websDefineHandler中定義的各種handler。
PUBLIC void websPump(Webs *wp)//如何判斷不同類型,去調用不同類型的handler呢。
{
bool canProceed;
for (canProceed = 1; canProceed; ) {
switch (wp->state) {
case WEBS_BEGIN://最初都是BEGIN狀態
canProceed = parseIncoming(wp);
break;
case WEBS_CONTENT:
canProceed = processContent(wp);
break;
case WEBS_READY:
if (!websRunRequest(wp)) {
/* Reroute if the handler re-wrote the request */
websRouteRequest(wp);
wp->state = WEBS_READY;
canProceed = 1;
continue;
}
canProceed = (wp->state != WEBS_RUNNING);
break;
case WEBS_RUNNING:
/* Nothing to do until websDone is called */
return;
case WEBS_COMPLETE:
canProceed = complete(wp, 1);
break;
}
}
}
二、響應HTTP請求handler的類型
根據route中的定義,響應類型具體有actionHandler(post請求),jstHandler(動態頁面),fileHandler(默認靜態頁面),cgiHandler(調用外部程序)等。在這些handler中將數據返回給客戶端。extensions就是后綴名,如果請求的文件后綴是.jst就會調用jstHandler。
2.1 actionHandler
actionHandler比較簡單,就是通過hash表,將actionName與對應websDefineAction定義的函數回調匹配上,去回調自己定義的回調函數即可,入參wp。用戶定義action的行為中,要自己返回客戶端action的結果。
/*
Process an action request. Returns 1 always to indicate it handled the URL
Return true to indicate the request was handled, even for errors.
*/
static bool actionHandler(Webs *wp)
{
WebsKey *sp;
char actionBuf[ME_GOAHEAD_LIMIT_URI + 1];
char *cp, *actionName;
WebsAction fn;
assert(websValid(wp));
assert(actionTable >= 0);
/*
Extract the action name
*/
scopy(actionBuf, sizeof(actionBuf), wp->path);
if ((actionName = strchr(&actionBuf[1], '/')) == NULL) {
websError(wp, HTTP_CODE_NOT_FOUND, "Missing action name");
return 1;
}
actionName++;
if ((cp = strchr(actionName, '/')) != NULL) {
*cp = '\0';
}
/*
Lookup the C action function first and then try tcl (no javascript support yet).
*/
sp = hashLookup(actionTable, actionName);
if (sp == NULL) {
websError(wp, HTTP_CODE_NOT_FOUND, "Action %s is not defined", actionName);
} else {
fn = (WebsAction) sp->content.value.symbol;
assert(fn);
if (fn) {
#if ME_GOAHEAD_LEGACY
(*((WebsProc) fn))((void*) wp, actionName, wp->query);
#else
(*fn)((void*) wp);
#endif
}
}
return 1;
}
2.2 jstHandler
jstHandler處理流程是先將page讀取到內存中,從第一個字節開始,依次發送給客戶端,遇到<%
%>之后,回調綁定的C函數,將函數返回結果替換<% %>返回客戶端,直到頁面的所有內容都發完。
這種技術可以使得頁面可以動態根據服務器執行C函數的結果來響應內容。也就是動態頁面。
/*
Process requests and expand all scripting commands. We read the entire web page into memory and then process. If
you have really big documents, it is better to make them plain HTML files rather than Javascript web pages.
Return true to indicate the request was handled, even for errors.
*/
//動態頁面響應肯定比靜態頁面要慢
static bool jstHandler(Webs *wp)
{
WebsFileInfo sbuf;
char *lang, *token, *result, *ep, *cp, *buf, *nextp, *last;
ssize len;
int rc, jid;
assert(websValid(wp));
assert(wp->filename && *wp->filename);
assert(wp->ext && *wp->ext);
buf = 0;
if ((jid = jsOpenEngine(wp->vars, websJstFunctions)) < 0) {
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot create JavaScript engine");
goto done;
}
jsSetUserHandle(jid, wp);
if (websPageStat(wp, &sbuf) < 0) {
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot stat %s", wp->filename);
goto done;
}
if (websPageOpen(wp, O_RDONLY | O_BINARY, 0666) < 0) {
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot open URL: %s", wp->filename);
goto done;
}
/*
Create a buffer to hold the web page in-memory
*/
len = sbuf.size;
if ((buf = walloc(len + 1)) == NULL) {
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot get memory");
goto done;
}
buf[len] = '\0';
if (websPageReadData(wp, buf, len) != len) {
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot read %s", wp->filename);
goto done;
}
websPageClose(wp);
websWriteHeaders(wp, (ssize) -1, 0);
websWriteHeader(wp, "Pragma", "no-cache");
websWriteHeader(wp, "Cache-Control", "no-cache");
websWriteEndHeaders(wp);
/*
Scan for the next "<%"
*/
last = buf;
for (rc = 0; rc == 0 && *last && ((nextp = strstr(last, "<%")) != NULL); ) {//循環到最后一個<%
websWriteBlock(wp, last, (nextp - last));//先發送<%前的一塊數據給客戶端
nextp = skipWhite(nextp + 2);
/*
Decode the language
*/
token = "language";
if ((lang = strtokcmp(nextp, token)) != NULL) {
if ((cp = strtokcmp(lang, "=javascript")) != NULL) {
/* Ignore */;
} else {
cp = nextp;
}
nextp = cp;
}
/*
Find tailing bracket and then evaluate the script
*/
if ((ep = strstr(nextp, "%>")) != NULL) {
*ep = '\0';
last = ep + 2;
nextp = skipWhite(nextp);
/*
Handle backquoted newlines
*/
for (cp = nextp; *cp; ) {
if (*cp == '\\' && (cp[1] == '\r' || cp[1] == '\n')) {
*cp++ = ' ';
while (*cp == '\r' || *cp == '\n') {
*cp++ = ' ';
}
} else {
cp++;
}
}
if (*nextp) {
result = NULL;
if (jsEval(jid, nextp, &result) == 0) {
/*
On an error, discard all output accumulated so far and store the error in the result buffer.
Be careful if the user has called websError() already.
*/
rc = -1;
if (websValid(wp)) {
if (result) {
websWrite(wp, "<h2><b>Javascript Error: %s</b></h2>\n", result);
websWrite(wp, "<pre>%s</pre>", nextp);
wfree(result);
} else {
websWrite(wp, "<h2><b>Javascript Error</b></h2>\n%s\n", nextp);
}
websWrite(wp, "</body></html>\n");
rc = 0;
}
goto done;
}
}
} else {
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Unterminated script in %s: \n", wp->filename);
goto done;
}
}
/*
Output any trailing HTML page text
*/
if (last && *last && rc == 0) {
websWriteBlock(wp, last, strlen(last));
}
/*
Common exit and cleanup
*/
done:
if (websValid(wp)) {
websPageClose(wp);
if (jid >= 0) {
jsCloseEngine(jid);
}
}
websDone(wp);
wfree(buf);
return 1;
}
2.3 fileHandler
fileHandler就是普通靜態文件傳輸
/*
Serve static files
Return true to indicate the request was handled, even for errors.
*/
static bool fileHandler(Webs *wp)
{
WebsFileInfo info;
char *tmp, *date;
ssize nchars;
int code;
assert(websValid(wp));
assert(wp->method);
assert(wp->filename && wp->filename[0]);
#if !ME_ROM
if (smatch(wp->method, "DELETE")) {
if (unlink(wp->filename) < 0) {
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot delete the URI");
} else {
/* No content */
websResponse(wp, 204, 0);
}
} else if (smatch(wp->method, "PUT")) {
/* Code is already set for us by processContent() */
websResponse(wp, wp->code, 0);
} else
#endif /* !ME_ROM */
{
/*
If the file is a directory, redirect using the nominated default page
*/
if (websPageIsDirectory(wp)) {
nchars = strlen(wp->path);
if (wp->path[nchars - 1] == '/' || wp->path[nchars - 1] == '\\') {
wp->path[--nchars] = '\0';
}
tmp = sfmt("%s/%s", wp->path, websIndex);
websRedirect(wp, tmp);
wfree(tmp);
return 1;
}
if (websPageOpen(wp, O_RDONLY | O_BINARY, 0666) < 0) {
#if ME_DEBUG
if (wp->referrer) {
trace(1, "From %s", wp->referrer);
}
#endif
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot open document for: %s", wp->path);
return 1;
}
if (websPageStat(wp, &info) < 0) {
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot stat page for URL");
return 1;
}
code = 200;
if (wp->since && info.mtime <= wp->since) {
code = 304;
info.size = 0;
}
websSetStatus(wp, code);
websWriteHeaders(wp, info.size, 0);
if ((date = websGetDateString(&info)) != NULL) {
websWriteHeader(wp, "Last-Modified", "%s", date);
wfree(date);
}
websWriteEndHeaders(wp);
/*
All done if the browser did a HEAD request
*/
if (smatch(wp->method, "HEAD")) {
websDone(wp);
return 1;
}
if (info.size > 0) {
websSetBackgroundWriter(wp, fileWriteEvent);
} else {
websDone(wp);
}
}
return 1;
}
2.4 cgiHandler
調用外部的程序執行,從字面上理解如果是調用外部程序,還需要考慮到進程間通信。在我接觸的項目中沒有用到這個功能,不去研究。實際上嵌入式的WEB服務器不一定要用到這個。
/*
Process a form request.
Return true to indicate the request was handled, even for errors.
*/
PUBLIC bool cgiHandler(Webs *wp)//那么復雜的話不需要這樣用到這個模塊。
{
Cgi *cgip;
WebsKey *s;
char cgiPrefix[ME_GOAHEAD_LIMIT_FILENAME], *stdIn, *stdOut, cwd[ME_GOAHEAD_LIMIT_FILENAME];
char *cp, *cgiName, *cgiPath, **argp, **envp, **ep, *tok, *query, *dir, *extraPath, *exe, *vp;
CgiPid pHandle;
int n, envpsize, argpsize, cid;
assert(websValid(wp));
websSetEnv(wp);
/*
Extract the form name and then build the full path name. The form name will follow the first '/' in path.
*/
scopy(cgiPrefix, sizeof(cgiPrefix), wp->path);
if ((cgiName = strchr(&cgiPrefix[1], '/')) == NULL) {
websError(wp, HTTP_CODE_NOT_FOUND, "Missing CGI name");
return 1;
}
*cgiName++ = '\0';
getcwd(cwd, ME_GOAHEAD_LIMIT_FILENAME);
dir = wp->route->dir ? wp->route->dir : cwd;
chdir(dir);
extraPath = 0;
if ((cp = strchr(cgiName, '/')) != NULL) {
extraPath = sclone(cp);
*cp = '\0';
websSetVar(wp, "PATH_INFO", extraPath);
websSetVarFmt(wp, "PATH_TRANSLATED", "%s%s%s", dir, cgiPrefix, extraPath);
wfree(extraPath);
} else {
websSetVar(wp, "PATH_INFO", "");
websSetVar(wp, "PATH_TRANSLATED", "");
}
cgiPath = sfmt("%s%s/%s", dir, cgiPrefix, cgiName);
websSetVarFmt(wp, "SCRIPT_NAME", "%s/%s", cgiPrefix, cgiName);
websSetVar(wp, "SCRIPT_FILENAME", cgiPath);
/*
See if the file exists and is executable. If not error out. Don't do this step for VxWorks, since the module
may already be part of the OS image, rather than in the file system.
*/
#if !VXWORKS
{
WebsStat sbuf;
if (stat(cgiPath, &sbuf) != 0 || (sbuf.st_mode & S_IFREG) == 0) {
exe = sfmt("%s.exe", cgiPath);
if (stat(exe, &sbuf) == 0 && (sbuf.st_mode & S_IFREG)) {
wfree(cgiPath);
cgiPath = exe;
} else {
error("Cannot find CGI program: ", cgiPath);
websError(wp, HTTP_CODE_NOT_FOUND | WEBS_NOLOG, "CGI program file does not exist");
wfree(cgiPath);
return 1;
}
}
#if ME_WIN_LIKE
if (strstr(cgiPath, ".exe") == NULL && strstr(cgiPath, ".bat") == NULL)//執行一個外部可執行程序。實際是否需要用到這種CGI?
#else
if (access(cgiPath, X_OK) != 0)
#endif
{
websError(wp, HTTP_CODE_NOT_FOUND, "CGI process file is not executable");
wfree(cgiPath);
return 1;
}
}
#endif /* ! VXWORKS */
/*
Build command line arguments. Only used if there is no non-encoded = character. This is indicative of a ISINDEX
query. POST separators are & and others are +. argp will point to a walloc'd array of pointers. Each pointer
will point to substring within the query string. This array of string pointers is how the spawn or exec routines
expect command line arguments to be passed. Since we don't know ahead of time how many individual items there are
in the query string, the for loop includes logic to grow the array size via wrealloc.
*/
argpsize = 10;
if ((argp = walloc(argpsize * sizeof(char *))) == 0) {
websError(wp, HTTP_CODE_NOT_FOUND, "Cannot allocate CGI args");
wfree(cgiPath);
return 1;
}
assert(argp);
*argp = cgiPath;
n = 1;
query = 0;
if (strchr(wp->query, '=') == NULL) {
query = sclone(wp->query);
websDecodeUrl(query, query, strlen(query));
for (cp = stok(query, " ", &tok); cp != NULL && argp != NULL; ) {
*(argp+n) = cp;
trace(5, "ARG[%d] %s", n, argp[n-1]);
n++;
if (n >= argpsize) {
argpsize *= 2;
if (argpsize > ME_GOAHEAD_LIMIT_CGI_ARGS) {
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE, "Too many arguments");
wfree(cgiPath);
return 1;
}
argp = wrealloc(argp, argpsize * sizeof(char *));
}
cp = stok(NULL, " ", &tok);
}
}
*(argp+n) = NULL;
/*
Add all CGI variables to the environment strings to be passed to the spawned CGI process.
This includes a few we don't already have in the symbol table, plus all those that are in
the vars symbol table. envp will point to a walloc'd array of pointers. Each pointer will
point to a walloc'd string containing the keyword value pair in the form keyword=value.
Since we don't know ahead of time how many environment strings there will be the for
loop includes logic to grow the array size via wrealloc.
*/
envpsize = 64;
envp = walloc(envpsize * sizeof(char*));
if (wp->vars) {
for (n = 0, s = hashFirst(wp->vars); s != NULL; s = hashNext(wp->vars, s)) {
if (s->content.valid && s->content.type == string) {
vp = strim(s->name.value.string, 0, WEBS_TRIM_START);
if (smatch(vp, "REMOTE_HOST") || smatch(vp, "HTTP_AUTHORIZATION") ||
smatch(vp, "IFS") || smatch(vp, "CDPATH") ||
smatch(vp, "PATH") || sstarts(vp, "LD_")) {
continue;
}
if (s->arg != 0 && *ME_GOAHEAD_CGI_VAR_PREFIX != '\0') {
envp[n++] = sfmt("%s%s=%s", ME_GOAHEAD_CGI_VAR_PREFIX, s->name.value.string,
s->content.value.string);
} else {
envp[n++] = sfmt("%s=%s", s->name.value.string, s->content.value.string);
}
trace(0, "Env[%d] %s", n, envp[n-1]);
if (n >= envpsize) {
envpsize *= 2;
envp = wrealloc(envp, envpsize * sizeof(char *));
}
}
}
}
*(envp+n) = NULL;
/*
Create temporary file name(s) for the child's stdin and stdout. For POST data the stdin temp file (and name)
should already exist.
*/
if (wp->cgiStdin == NULL) {
wp->cgiStdin = websGetCgiCommName();
}
stdIn = wp->cgiStdin;
stdOut = websGetCgiCommName();
if (wp->cgifd >= 0) {
close(wp->cgifd);
wp->cgifd = -1;
}
/*
Now launch the process. If not successful, do the cleanup of resources. If successful, the cleanup will be
done after the process completes.
*/
if ((pHandle = launchCgi(cgiPath, argp, envp, stdIn, stdOut)) == (CgiPid) -1) {
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "failed to spawn CGI task");
for (ep = envp; *ep != NULL; ep++) {
wfree(*ep);
}
wfree(cgiPath);
wfree(argp);
wfree(envp);
wfree(stdOut);
wfree(query);
} else {
/*
If the spawn was successful, put this wp on a queue to be checked for completion.
*/
cid = wallocObject(&cgiList, &cgiMax, sizeof(Cgi));
cgip = cgiList[cid];
cgip->handle = pHandle;
cgip->stdIn = stdIn;
cgip->stdOut = stdOut;
cgip->cgiPath = cgiPath;
cgip->argp = argp;
cgip->envp = envp;
cgip->wp = wp;
cgip->fplacemark = 0;
wfree(query);
}
/*
Restore the current working directory after spawning child CGI
*/
chdir(cwd);
return 1;
}
4.用戶登陸與權限認證
一、用戶登陸
1.1 用戶信息存儲
在goahead源碼實現了登陸功能,auth.txt中以文件的形式保存用戶信息。密碼是一串字符,由用戶名密碼和一個鑰匙利用MD5算法生成的。web初始化時載入這個文件時,就會載入用戶信息。
1.2 登陸頁面前台實現
前台頁面已經實現好了login.html
<html><head><title>login.html</title></head>
<body>
<p>Please log in</p>
<form name="details" method="post" action="/action/login">
Username <input type="text" name="username" value=''><br/>
Password <input type="password" name="password" value=''><br/>
<input type="submit" name="submit" value="OK">
</form>
</body>
</html>
1.3 后台實現
websOpenAuth中綁定了login的action函數, websDefineAction(“login”, loginServiceProc);
static void loginServiceProc(Webs *wp)
{
WebsRoute *route;
assert(wp);
route = wp->route;
assert(route);
if (websLoginUser(wp, websGetVar(wp, "username", ""), websGetVar(wp, "password", ""))) { //輸入用戶名和密碼,與auth.txt比較是否匹配,是的話認為校驗通過
/* If the application defines a referrer session var, redirect to that */
cchar *referrer;
if ((referrer = websGetSessionVar(wp, "referrer", 0)) != 0) {
websRedirect(wp, referrer);
} else {
websRedirectByStatus(wp, HTTP_CODE_OK);//網址重定向
}
websSetSessionVar(wp, "loginStatus", "ok");
} else {
if (route->askLogin) {
(route->askLogin)(wp);
}
websSetSessionVar(wp, "loginStatus", "failed");
websRedirectByStatus(wp, HTTP_CODE_UNAUTHORIZED);
}
}
1.4 redirect
websRedirectByStatus是重定向函數,在route.txt中可以定義redirect選項,意思就是,針對login,如果狀態碼是200,就跳轉到home.asp,如果是401,就跳轉到login.html
route uri=/action/login methods=POST handler=action redirect=200@/home.asp redirect=401@login.html
1.5 cookie與session
websLoginUser密碼校驗函數中,當密碼校驗成功,會為這個用戶創建一個session,並且將session id放在cookie中發給客戶端,以后客戶端的請求頭中就會帶有cookie,根據此cookie來校驗之后請求的身份。
WebsSession *websGetSession(Webs *wp, int create)
{
WebsKey *sym;
char *id;
assert(wp);
if (!wp->session) {
id = websGetSessionID(wp);
if ((sym = hashLookup(sessions, id)) == 0) {
if (!create) {
wfree(id);
return 0;
}
if (sessionCount >= ME_GOAHEAD_LIMIT_SESSION_COUNT) {
error("Too many sessions %d/%d", sessionCount, ME_GOAHEAD_LIMIT_SESSION_COUNT);
wfree(id);
return 0;
}
sessionCount++;
if ((wp->session = websAllocSession(wp, id, ME_GOAHEAD_LIMIT_SESSION_LIFE)) == 0) {
wfree(id);
return 0;
}
websSetCookie(wp, WEBS_SESSION, wp->session->id, "/", NULL, 0, 0);
} else {
wp->session = (WebsSession*) sym->content.value.symbol;
}
wfree(id);
}
if (wp->session) {
wp->session->expires = time(0) + wp->session->lifespan;
}
return wp->session;
}
1.6 基本認證與摘要認證
當route.txt中配置了auth屬性之后,websRouteRequest中會針對這條route進行認證。auth有basic和digest兩種方式,即基本認證和摘要認證。這樣的話每一條請求都會進行認證。
#if ME_GOAHEAD_AUTH
if (route->authType && !websAuthenticate(wp)) {
return;
}
if (route->abilities >= 0 && !websCan(wp, route->abilities)) {
return;
}
#endif
后記:假如利用了https協議,就沒必要用到basic或者digest這兩種認證模式了。基於https如何進行用戶和權限管理,后續需自己實現
5.實現文件導入和導出
對於一個完整的WEB服務器來說,應該支持WEB文件導入功能,例如導入業務的配置文件,導入軟件升級包進行升級等等。導出功能一般是導出用戶配置文件,導出log日志等。導入導出對於HTTP請求來說依然是POST和GET。文件導入和導出在goahead中已經原生實現了。
一、文件導入
1、在主函數中定義action函數;
websDefineAction("upload", uploadTest);
2、實現uploadTest;
upfile 路徑我修改了一下。這里只是做了回顯和修改文件名。實際文件傳輸是在接受請求過程websPump中processContent–>websProcessUploadData已經實現的。接受請求的過程中,如果是上傳文件,會把文件放在/tmp下。回調uploadTest前文件已經傳輸完畢了。
static void uploadTest(Webs *wp)
{
WebsKey *s;
WebsUpload *up;
char *upfile;
websSetStatus(wp, 200);
websWriteHeaders(wp, -1, 0);
websWriteHeader(wp, "Content-Type", "text/plain");
websWriteEndHeaders(wp);
if (scaselessmatch(wp->method, "POST")) {
for (s = hashFirst(wp->files); s; s = hashNext(wp->files, s)) {
up = s->content.value.symbol;
websWrite(wp, "FILE: %s\r\n", s->name.value.string);
websWrite(wp, "FILENAME=%s\r\n", up->filename);
websWrite(wp, "CLIENT=%s\r\n", up->clientFilename);
websWrite(wp, "TYPE=%s\r\n", up->contentType);
websWrite(wp, "SIZE=%d\r\n", up->size);
upfile = sfmt("/tmp/%s", up->clientFilename);
if (rename(up->filename, upfile) < 0) {
error("Cannot rename uploaded file: %s to %s, errno %d", up->filename, upfile, errno);
}
wfree(upfile);
}
websWrite(wp, "\r\nVARS:\r\n");
for (s = hashFirst(wp->vars); s; s = hashNext(wp->vars, s)) {
websWrite(wp, "%s=%s\r\n", s->name.value.string, s->content.value.string);
}
}
websDone(wp);
}
3、寫前端頁面 uploadFile.asp
<!DOCTYPE html>
<html>
<HEAD>
<meta charset="utf-8">
<title>上傳文件</title>
</HEAD>
<body>
<div>
<form action="/action/upload" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>請上傳文件</td>
<td><input name="file" type="file"></td>
<td><input type="submit" value="上傳"></td>
</tr>
</table>
</form>
</div>
</body>
</html>
4、http.c中修改大小限制,為了簡便,我先注釋了
/* if (wp->rxLen > ME_GOAHEAD_LIMIT_POST) {
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too big");
return;
}*/
5、實際效果
這樣就將windows本地文件傳到服務器/tmp目錄下了。
二、文件導出
文件導出用的是fileHandler,將目標文件放在web放頁面的目錄下,直接請求這個文件名,就可以將文件下載出來的。在真實項目中,需要把FLASH中的數據先拷貝到/tmp下,再去請求tmp下對應的文件,可以結合JS和AJAX的交互方式來請求這個文件。后續再來補充案例。
6.結合openssl實現https協議
http協議是不安全的,因此還需要結合openssl實現安全的https協議。
一、SSL讀函數
/*
Read from a connection. Return the number of bytes read if successful. This may be less than the requested "len" and
may be zero. Return -1 for errors or EOF. Distinguish between error and EOF via socketEof().
*/
static ssize websRead(Webs *wp, char *buf, ssize len)
{
assert(wp);
assert(buf);
assert(len > 0);
#if ME_COM_SSL
if (wp->flags & WEBS_SECURE) {//https對應的讀函數
return sslRead(wp, buf, len);
}
#endif
return socketRead(wp->sid, buf, len);
}
在websListen監聽的服務器地址,只要是帶https://格式的,自動轉換為啟用openssl的模式。
二、SSL寫函數
/*
Non-blocking write to socket.
Returns number of bytes written. Returns -1 on errors. May return short.
*/
PUBLIC ssize websWriteSocket(Webs *wp, cchar *buf, ssize size)
{
ssize written;
assert(wp);
assert(buf);
assert(size >= 0);
if (wp->flags & WEBS_CLOSED) {
return -1;
}
#if ME_COM_SSL
if (wp->flags & WEBS_SECURE) {
if ((written = sslWrite(wp, (void*) buf, size)) < 0) {
return written;
}
} else
#endif
if ((written = socketWrite(wp->sid, (void*) buf, size)) < 0) {
return written;
}
wp->written += written;
websNoteRequestActivity(wp);
return written;
}