goahead是一个成熟的嵌入式WEB服务器,在未来物联网行业中,相信可以发挥更大的作用。我们刚买路由器的时候,通常需要连接网线登陆一个页面配置路由器IP,WIFI密码,这个就是嵌入式WEB服务器的一个应用。
对于嵌入式的设备来说,配备了WEB服务器,用户便可以轻松实现查阅设备信息、配置设备参数等操作。
如果能熟悉此代码,利用本框架为公司的嵌入式设备搭建一个WEB服务器, 便是可以独当一面的一种体现。goahead作为一个HTTP服务器,实现了基本的功能,又不过与繁杂,不像nginx或者apache一样代码量大,作为HTTP服务器的入门是一个很好的选择。
主函数主要流程是载入配置文件、申请必要数据结构、对服务器进行监听。内容不长,大家可结合注释看。
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;
}
作为一个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 */
}
}