wifidog认证源码分析Lighttpd1.4.20源码分析之插件系统(2)---插件的加载和初始化(1)

前面讲了lighttpd插件系统的接口,下面我们来看看插件是怎么加载 和初始化的。
lighttpd的插件是以动态链接库的形式存在的。在服务器启动的时候,在初始化阶段将所有插件都加载进来。在server.c中的main函数中,加载插件是调用plugins_load函数:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->1 if (plugins_load(srv))
{
    log_error_write(srv, __FILE__, __LINE__, "s","loading plugins finally failed");
    plugins_free(srv);
    server_free(srv);
    return -1;
}

请读者注意一下这个函数调用的位置。这个函数是在服务器的初始化阶段进行调用的,并且该函数就在这调用了一次,其他地方没有再被调用过。虽然插件是以动态链接库的形式存在,但这些库是在服务器启动阶段一次性加载完毕,如果想再增加插件,只能配置好配置文件后重新启动服务器。
下面看一看plugins_load函数的实现:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 #ifdef LIGHTTPD_STATIC
 int plugins_load(server * srv)
{
    plugin *p;
 #define PLUGIN_INIT(x)\
    p = plugin_init(); \
    if (x ## _plugin_init(p)) { \
        log_error_write(srv, __FILE__, __LINE__, "ss",#x, "plugin init failed" ); \
        plugin_free(p); \
        return -1;\
    }\
    plugins_register(srv, p);
#include "plugin-static.h"
    return 0;
}
 #else
 //动态链接
 int plugins_load(server * srv)
{
    plugin *p;
    int (*init) (plugin * pl);
    const char *error;
    size_t i;
    for (i = 0; i < srv->srvconf.modules->used; i++)
    {
        //获得动态链接库的名称。
         data_string *d = (data_string *) srv->srvconf.modules->data[i];
        char *modules = d->value->ptr;
        //库所在目录
         buffer_copy_string_buffer(srv->tmp_buf, srv->srvconf.modules_dir);

        buffer_append_string_len(srv->tmp_buf,     CONST_STR_LEN("/"));
        //拼接库的名称。
         buffer_append_string(srv->tmp_buf, modules);
        buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN(".so"));
        p = plugin_init();
                //linux调用函数dlopen加载动态库
         if (NULL == ( p->lib = dlopen(srv->tmp_buf->ptr, RTLD_NOW | RTLD_GLOBAL)))
        {
            log_error_write(srv, __FILE__, __LINE__, "sbs", "dlopen() failed for:", srv->tmp_buf, dlerror());
            plugin_free(p);
            return -1;
        }
        //调用动态库中的XXX_plugin_init函数。
        //XXX是库的名称
         buffer_reset(srv->tmp_buf);
        buffer_copy_string(srv->tmp_buf, modules);
        buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN("_plugin_init"));
 #if 1
        //调用dlsym函数获得XXX_plugin_init函数的地址。
         init = (int (*)(plugin *)) (intptr_t) dlsym(p->lib, srv->tmp_buf->ptr);
 #else
        //这句没有用
         *(void **) (&init) = dlsym(p->lib, srv->tmp_buf->ptr);
 #endif
        if ((error = dlerror()) != NULL)
        {
            log_error_write(srv, __FILE__, __LINE__, "s", error);
            plugin_free(p);
            return -1;
        }
        //初始化插件
    //在初始化的过程中,模块将自己所有的对外接口函数的入口地址都存入到p中。
         if ((*init) (p))
        {
            log_error_write(srv, __FILE__, __LINE__, "ss", modules, "plugin init failed");
            plugin_free(p);
            return -1;
        }
 #if 0
        log_error_write(srv, __FILE__, __LINE__, "ss", modules,
                        "plugin loaded");
 #endif
        plugins_register(srv, p);
    }
    return 0;
}
#endif //end of #ifdef LIGHTTPD_STATIC

上面的函数中删除了处理在windows下加载动态链接库的部分。有兴趣的读者可以自行查看源代码。下面全部讨论在linux下的实现。
这个函数作者编写了两个版本,从宏LIGHTTPD_STATIC可以看出,作者貌似是想提供一个加载静态链接库的版本。但是,该版本的函数并没有什么实质性的实现(在最新的版本中(1.4.26),该函数依然没有实现)。我们主要来看看后面的动态链接库的版本。
加载的过程如下:
(1)从配置文件中读取到动态链接库所在的文件夹和动态库的名称。
(2)创建一个plugin结构体的实例。
(3)调用dlopen函数加载动态库。在plugin结构体中保存返回的句柄。
(4)通过dlsym函数获得XXXXXX_plugin_init函数的地址。其中XXXXXX是配置文件中定义的这个插件的内容。
(5)调用XXXXXX_plugin_init函数。
(6)调用plugins_register函数注册插件。
XXXXXX_plugin_init函数在插件中定义,对这个插件进行初始化。其中,最重要的部分就是对plugin结构体中那一系列的函数指针进行赋值。如,mod_cgi模块中:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 int mod_cgi_plugin_init(plugin * p)
{
    p->version = LIGHTTPD_VERSION_ID;
    p->name = buffer_init_string("cgi");
    p->connection_reset = cgi_connection_close_callback;
    p->handle_subrequest_start = cgi_is_handled;
    p->handle_subrequest = mod_cgi_handle_subrequest;
#if 0
    p->handle_fdevent = cgi_handle_fdevent;
#endif
    p->handle_trigger = cgi_trigger;
    p->init = mod_cgi_init;
    p->cleanup = mod_cgi_free;
    p->set_defaults = mod_fastcgi_set_defaults;
    p->data = NULL;
    return 0;
}

plugins_register函数是将plugin结构体的指针存放在server结构体的plugins数组中。
插件加载完毕之后,main函数有处理了一些初始化的工作,然后,调用plugins_call_init函数对所有插件进行初始化:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->1 if (HANDLER_GO_ON != plugins_call_init(srv)) 
{
    log_error_write(srv, __FILE__, __LINE__, "s",
            "Initialization of plugins failed. Going down.");
    plugins_free(srv);
    network_close(srv);
    server_free(srv);
    return -1;
}

本文章由 http://www.wifidog.pro/2015/04/17/wifidog%E8%AE%A4%E8%AF%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90lighttpd%E5%8A%A0%E8%BD%BD%E5%92%8C%E5%88%9D%E5%A7%8B%E5%8C%96-1.html 整理编辑,转载请注明出处

wifidog认证源码分析Lighttpd1.4.20源码分析之插件系统(1)---plugin结构体和插件接口

在lighttpd中,使用插件的形式来增加服务的功能。同时,lighttpd提供了一个插件的公共接口给开发者,方便第三方提供额外的插件。Lighttpd的插件接口主要提供在plugin.h文件中。其中,plugin结构体是最核心的部分。
plugin结构体的定义如下:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 
typedef struct 
{
    size_t version;

    buffer *name;                /* name of the plugin */

    void *(*init) ();
    handler_t(*set_defaults) (server * srv, void *p_d);
    handler_t(*cleanup) (server * srv, void *p_d);

    /*
     * is called ... 纯虚函数,在子类中要予以赋值。
     */
    handler_t(*handle_trigger) (server * srv, void *p_d);    /* once a second */
    handler_t(*handle_sighup) (server * srv, void *p_d);    /* at a signup */
    handler_t(*handle_uri_raw) (server * srv, connection * con, void *p_d); /* after uri_raw is set */
    handler_t(*handle_uri_clean) (server * srv, connection * con, void *p_d);/* after uri is set */
    handler_t(*handle_docroot) (server * srv, connection * con, void *p_d);    /* getting the document-root */
    handler_t(*handle_physical) (server * srv, connection * con, void *p_d);    /* mapping url to physical path */
    handler_t(*handle_request_done) (server * srv, connection * con, void *p_d);    /* at the end of a request */
    handler_t(*handle_connection_close) (server * srv, connection * con, void *p_d);    /* at the end of a connection */
    handler_t(*handle_joblist) (server * srv, connection * con, void *p_d);    /* after all events are handled */
    handler_t(*handle_subrequest_start) (server * srv, connection * con, void *p_d);

    /*
     * when a handler for the request has to be found 
     */
    handler_t(*handle_subrequest) (server * srv, connection * con, void *p_d);    /* */
    handler_t(*connection_reset) (server * srv, connection * con, void *p_d);    /* */
    void *data;

    /*
     * dlopen handle 
     */
    void *lib;
} plugin;

可以看出,在结构体plugin的设计中,作者使用了面向对象的思想。plugin结构体就是一个虚基类,其中的数据成员,如name,version等都是子类公有的。而随后的一系列函数指针则是虚函数,这些函数指针在plugin结构体中并没有进行赋值,要求所有的子类必须对其进行赋值。不同的子类对这些函数指针赋不同的值,在进行函数调用的时候就可以实现多态。
另外,c语言毕竟不支持面向对象,因此,在通过c实现面向对象的时候大多情况先是要靠人的理解,而不是语言上的约束。如,这里说plugin结构体是一个虚基类,实际上所有的子类都是这个结构体的实例,而子类的实例只有一个,也就是他自己。这就和C++中的子类不同了。
在plugin结构体中,version成员比较重要。很明显,这个成员标记这个插件的版本。在plugin结构体中定义的那一系列函数指针是插件的对外接口,也就是插件对lighttpd的接口,lighttpd只知道这些接口,通过调用这些接口来完成工作。随着lighttpd的不断改进,这些接口可能满足不了服务器的要求,因此要对其进行改进,这样就有可能造成以前开发的插件无法使用。通过version成员,在加载插件的时候判断这个插件是否符合当前服务器的版本,也就是接口是否相符。如果不相符,则不加载插件,这样就可以避免由于接口的不相符造成服务器的崩溃等问题。
这些函数指针在lighttpd的文档中被称作'hooks'。分为serverwide hooks和connectionwide hooks,serverwide hooks是有服务器调用的,主要处理一些初始化等辅助的工作,包括:init,cleanup, set_defaults, handle_trigger和handle_sighup。connectionwide hooks主要是面向连接的,在处理连接的时候调用这些hooks完成相应的工作。这些hooks大部分在函数http_response_prepare()中被调用。
至于这些hooks是在哪被调用,都完成哪些功能,在后面分析具体的插件的时候会详细介绍。有兴趣的读者可以阅读lighttpd源码包中doc文件夹下的plugins文件。
在plugin.h中,plugin结构体的定义后面还有一堆的函数声明:

int plugins_load(server * srv);
void plugins_free(server * srv);

这两个很明显是加载和释放插件函数。

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 handler_t plugins_call_handle_uri_raw(server * srv, connection * con);
handler_t plugins_call_handle_uri_clean(server * srv, connection * con);
handler_t plugins_call_handle_subrequest_start(server * srv, connection * con);
handler_t plugins_call_handle_subrequest(server * srv, connection * con);
handler_t plugins_call_handle_request_done(server * srv, connection * con);
handler_t plugins_call_handle_docroot(server * srv, connection * con);
handler_t plugins_call_handle_physical(server * srv, connection * con);
handler_t plugins_call_handle_connection_close(server * srv, connection * con);
handler_t plugins_call_handle_joblist(server * srv, connection * con);
handler_t plugins_call_connection_reset(server * srv, connection * con);

handler_t plugins_call_handle_trigger(server * srv);
handler_t plugins_call_handle_sighup(server * srv);

handler_t plugins_call_init(server * srv);
handler_t plugins_call_set_defaults(server * srv);
handler_t plugins_call_cleanup(server * srv);

这一系列的plugins_call_XXXXX函数则是插件对外的接口。也就是说,lighttpd服务器通过这些函数,调用插件进行工作。lighttpd在调用插件的时候并不知道到底调用的是哪些插件,而仅仅调用上面的函数。这些函数再调用相应的插件的函数,从而完成工作。具体怎么调用插件的函数,放在后面的文章中介绍。
最后面的config_XXXXXX函数是处理一些配置问题,暂不讨论。
在plugin.h文件中还定义了一些宏:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 #define SERVER_FUNC(x) \
        static handler_t x(server *srv, void *p_d)
#define CONNECTION_FUNC(x) \
    static handler_t x(server *srv, connection *con, void *p_d)
#define INIT_FUNC(x)  static void *x()

#define FREE_FUNC          SERVER_FUNC
#define TRIGGER_FUNC       SERVER_FUNC
#define SETDEFAULTS_FUNC   SERVER_FUNC
#define SIGHUP_FUNC        SERVER_FUNC
#define SUBREQUEST_FUNC    CONNECTION_FUNC
#define JOBLIST_FUNC       CONNECTION_FUNC
#define PHYSICALPATH_FUNC  CONNECTION_FUNC
#define REQUESTDONE_FUNC   CONNECTION_FUNC
#define URIHANDLER_FUNC    CONNECTION_FUNC

前面的三个宏(SERVER_FUNC, CONNECTION_FUNC和INIT_FUNC)定义了函数签名的模板。后面的一系列宏和plugin结构体中的函数指针对应,确定这些函数指针所对应的函数签名。
在进行插件开发的时候,插件中的函数签名要使用上面的宏来生成。这样可以保证接口的统一。
  最后,还要提一下plugin.c文件中的结构体:

typedef struct 
{
    PLUGIN_DATA;
} plugin_data;

PLUGIN_DATA是一个宏,定义为:#define PLUGIN_DATA size_t id。
这个结构体用来存放插件所需要使用的数据,这个结构体作为plugin结构体中函数指针的最后一个参数:void p_d传入对应的函数中。在plugin.c结构体中plugin_data的定义很简单,仅仅包含一个数据成员id。在mod_.c/h文件中,同样也包含有plugin_data结构体的定义。如:mod_cgi.c中,

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->1 typedef struct {
    PLUGIN_DATA;
    buffer_pid_t cgi_pid;
    buffer *tmp_buf;
    buffer *parse_response;
    plugin_config **config_storage;
    plugin_config conf;
} plugin_data;

在mod_cml.h中:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->1 typedef struct {
    PLUGIN_DATA;
    buffer *basedir;
    buffer *baseurl;
    buffer *trigger_handler;
    plugin_config **config_storage;
    plugin_config conf;
} plugin_data;

等等。
这些定义有一个共通的特点,那就是第一个成员都是PLUGIN_DATA。这又是一个技巧。所有这些plugin_data相当于是plugin.c中plugin_data的子类。这些子类开始的部分和父类相同,这就允许子类的指针转换成父类指针,然后再转换回去,并保证数据不会丢失。这样,lighttpd所面对的插件数据接口是plugin.c中定义的plugin_data,当lighttpd在调用插件中的函数,并把数据传进去的时候,插件可以再把数据的类型还原回去。这样,对于lighttpd,所面对的数据接口就只有一个,插件所需要的数据可以不对lighttpd公开,这就很好的隐藏了数据。同时也简化了lighttpd的复杂度,提高了程序的扩展性。

  下一篇中,将解释lighttpd中插件的加载和初始化。

本文章由 http://www.wifidog.pro/2015/04/16/wifidog%E8%AE%A4%E8%AF%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90Lighttpd-plugin%E7%BB%93%E6%9E%84%E4%BD%93%E5%92%8C%E6%8F%92%E4%BB%B6%E6%8E%A5%E5%8F%A3-1.html 整理编辑,转载请注明出处

wifidog认证源码分析lighttpd1.4.20源码分析 -----工作模型

lighttpd的工作模型很简单──一个主进程加多个工作进程的多进程模型,也就是所谓的watcher-worker模型。
  整个程序的入口(main函数)在server.c文件中。在main函数的开始部分必然是处理参数和各种繁杂的初始化工作。其中有两个地方要重点看一起。第一个是下面的语句:

if (test_config) //没有进行任何测试。。。
{
     printf("Syntax OK\n");
}

这个If语句是为了判断配置文件的语法是否合法。但是,明显,没有进行任何的测试,而仅仅是输出了一句话。
  第二个是函数daemonize()。这个函数的作用是使程序进入daemon。函数的详细内容如下:

static void daemonize(void)
{
        /*
         * 忽略和终端读写有关的信号
         */
#ifdef SIGTTOU
        signal(SIGTTOU, SIG_IGN);
#endif    
#ifdef SIGTTIN
        signal(SIGTTIN, SIG_IGN);
#endif
#ifdef SIGTSTP
        signal(SIGTSTP, SIG_IGN);
#endif
        if (0 != fork()) /* 产生子进程,父进程退出 */
            exit(0);
        if (-1 == setsid())/* 设置子进程的设置ID */
            exit(0);
        signal(SIGHUP, SIG_IGN);/* 忽略SIGHUP信号 */
        if (0 != fork())/* 再次产生子进程,父进程退出 */
            exit(0);
        if (0 != chdir("/"))/* 更改工作目录为根目录 */
            exit(0);
}

  这里作者使用了标准的产生*nix daemon的方法。两次调用fork并退出父进程。具体的原因读者可以参阅《Unix环境高级编程》(APUE)中有关daemon的讲解部分。
  顺着main函数继续向下走,沿途的各种初始化工作尽可忽略。下面的语句是本文的重点!

* 下面程序将产生多个子进程。这些子进程成为worker,
     * 也就是用于接受处理用户的连接的进程。而当前的主进程将
     * 成为watcher,主要工作就是监视workers的工作状态,
     * 当有worker因为意外而退出时,产生新的worker。
     * 在程序退出时,watcher负责停止所有的workers并清理资源。
     */
        int child = 0; //用来标记这个进程是子进程还是父进程。
           //当子进程返回到这个while循环的开始的时候,由于标记
        //进程是子进程,流程直接跳出while循环执行后面的程序。
        //而对于父进程,则继续产生子进程。
        while (!child && !srv_shutdown && !graceful_shutdown)
        {
            if (num_childs > 0) //watcher继续产生worker
            {
                switch (fork())
                {
                case -1:    //出错
                    return -1;
                case 0:     //子进程进入这个case
                    child = 1;
                    break;
                default:    //父进程进入这个case
                    num_childs--;
                    break;
                }
            } 
            else         //watcher
            {
                /**
                 * 当产生了足够的worker时,watcher就在这个while
                 * 中不断的循环。
                 * 一但发现有worker退出(进程死亡),立即产生新的worker。
                 * 如果发生错误并接受到SIGHUP信号,向所有的进程
                 *(父进程及其子进程)包括自己发送SIGHUP信号。
                 * 并退出。
                 */
                int status;

                if (-1 != wait(&status))
                {
                    /** 
                     * one of our workers went away 
                     */
                    num_childs++;
                } 
                else
                {
                    switch (errno)
                    {
                    case EINTR:
                        /**
                         * if we receive a SIGHUP we have to close our 
                         * logs ourself as we don't 
                         * have the mainloop who can help us here
                         */
                        if (handle_sig_hup)
                        {
                            handle_sig_hup = 0;
                            log_error_cycle(srv);
                            /**
                             * forward to all procs in the process-group
                             * 向所有进程发送SIGHUP信号。(父进程及其子进程)
                             * we also send it ourself
                             */
                            if (!forwarded_sig_hup)
                            {
                                forwarded_sig_hup = 1;
                                kill(0, SIGHUP);
                            }
                        }
                        break;
                    default:
                        break;
                    }end of switch (errno)...
                }//end of if (-1 != wait(&status)) ...
            }//end of if (num_childs > 0)...
        }// end of while(!child...

在正常的运行过程中,watcher进程是不会退出上面的while循环。一旦退出了这个循环,那么也就意为着整个程序退出了。
另外,woker的数量可以在配置文件中进行配置。
子进程,也就是worker退出了上面的while循环后就开始处理连接请求等各种工作。
在子进程的一开始,还是各种初始化工作,包括fd时间处理器的初始化(fdevent_init(srv->max_fds + 1, srv->event_handler)),stat cache初始化(stat_cache_init())等。子进程工作在一个大while循环中。
while的工作流程如下:
1、判断连接是否断开。如果断开,则调用处理程序进行处理并重新开始新一轮的日志记录。
2、判断是否接受到了alarm函数发出的信号。接受到信号后,判断服务器记录的时间是否和当前时间相同。如果相同,说明时间还没有过一秒,继续处理连接请求。如果不相同,则时间已经过了一秒。那么,服务器则触发插件,清理超时连接,清理stat-cache缓存。这理里面最重要的是处理超时连接。程序中通过一个for循环查询所有的连接,比较其idle的时间和允许的最大idle时间来判断连接是否超时。如果连接超时,则让连接进入出错的状态(connection_set_state(srv, con, CON_STATE_ERROR);)。
3、判断服务器socket连接是否失效。如果失效了,则在不是服务器过载的情况下将所有连接重新加入的fdevent中。为什么服务器socket会失效呢?可以看到,在后面判断出服务器过载后,即标记了socket连接失效。srv->sockets_disabled = 1;
4、如果socket没有失效,判断服务器是否过载。如果过载了,则关闭所有连接,清理服务器并退出服务器。
5、分配文件描述符。
6、启动事件轮询。等待各种IO时间的发生。包括文件读写,socket请求等。
7、一旦有事件发生,调用相应的处理函数进行处理。
8、最后,检查joblist中是否有未处理的job并处理之。
至此,一次循环结束了。然后,从头开始继续循环直到服务器关闭。

在处理IO事件的时候,程序进入下面的循环:

do
{
        fdevent_handler handler; //事件处理函数指针
        void *context;
        handler_t r;
        //获得下一个事件的标号
        fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx);
        //获得这个事件的具体类型。
revents = fdevent_event_get_revent(srv->ev, fd_ndx);
        //获得事件对应的文件描述符
fd = fdevent_event_get_fd(srv->ev, fd_ndx);
        //获得事件处理函数的指针
handler = fdevent_get_handler(srv->ev, fd);
        //获得事件的环境
context = fdevent_get_context(srv->ev, fd);
        /**
         * 这里,调用请求的处理函数handler处理请求!
         * 这才是重点中的重点!!
         */
        switch (r = (*handler) (srv, context, revents))
        {
        case HANDLER_FINISHED:
        case HANDLER_GO_ON:
        case HANDLER_WAIT_FOR_EVENT:
        case HANDLER_WAIT_FOR_FD:
            break;
        case HANDLER_ERROR:
            /*
             * should never happen 
             */
            SEGFAULT();
            break;
        default:
            log_error_write(srv, __FILE__, __LINE__, "d", r);
            break;
        }
    }while (—n > 0);

这个循环是worker进程的核心部分。这里由于作者对IO系统的出色的封装。我们不须要理解这些函数的作用就可知道连接的处理流程。在程序中,作者使用回调函数轻松的解决掉了处理工种事件时判断处理函数的问题。代码优雅而高效。在lighttpd中,作者使用了大量的回调函数,这使得代码清晰易懂,效率也很高。
  有一点值得注意。程序在处理连接超时的时候是每一秒中轮询所有的连接,判断其是否超时。这必然会降低服务器的效率。在实际的运行中,确实会对lighttpd的效率造成一定的影响。
  lighttpd使用的watcher-worker模型虽然简单,但是在实际的运行过程中也非常的高效。有的时候,简单并不一定就不高效。

本文章由 http://www.wifidog.pro/2015/04/16/wifidog%E8%AE%A4%E8%AF%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90lighttpd%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%9E%8B.html 整理编辑,转载请注明出处

wifidog认证源码分析Lighttpd1.4.20源码分析之etag.c(h) -------HTTP/1.1中的Etag域

etag的全称是entity tag(标记实体值),在RFC2616中关于etag的定义如下:
The ETag response-header field provides the current value of the entity tag for the requested variant. The headers used with entity tags are described in sections 14.24, 14.26 and 14.44. The entity tag MAY be used for comparison with other entities from the same resource(see section 13.3.3).
ETag = "ETag" ":" entity-tag
Examples:
ETag: "xyzzy"
ETag: W/"xyzzy" (前面的W/表示这个是个弱Etag)
ETag: ""
巨长的RFC2616对Etag的描述就上面这么多。意思就是说Etag域提供了请求变体的一个实体标记值。这个值可以和If-Match和If-No-Match一起使用。RFC2616中对Etag的唯一要求就是它是一个双引号包围的字符串,至于怎么生成这个字符串以及怎么使用,由应用程序决定。
下面说一说在服务器程序中,一般是怎么使用Etag的(这个东西用好了还是很不错的。。。):
把Last-Modified和ETags请求的http报头一起使用,这样可利用客户端(例如浏览器)的缓存。
因为服务器首先产生Last-Modified/Etag标记,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
过程如下:
1.客户端请求一个页面(A)。
2.服务器返回页面A,并在给A加上一个Last-Modified/ETag。
3.客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
4.客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。
5.服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304和一个空的响应体。
工作原理:
Etag由服务器端生成,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改。常见的是使用If-None-Match.
请求一个文件的流程可能如下:
====第一次请求===
1.客户端发起 HTTP GET 请求一个文件;
2.服务器处理请求,返回文件内容和一堆Header,当然包括Etag(例如"2e681a-6-5d044840")(假设服务器支持Etag生成和已经开启了Etag).状态码200。
====第二次请求===
1.客户端发起 HTTP GET 请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头的内容就是第一次请求时服务器返回的Etag:2e681a-6-5d044840
2.服务器判断发送过来的Etag和计算出来的Etag匹配,因此If-None-Match为False,不返回200,返回304,客户端继续使用本地缓存;

流程很简单,问题是,如果服务器又设置了Cache-Control:max-age和Expires呢,怎么办?答案是同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304.
另外,使用Etag比使用Last-Modified接合If-Modified-Sience更有优势。如果一些文件经常被修改的不是文件的内容,而是文件的属性,如:文件的修改时间等。那么就没有必要重新发送文件,此时,Last-Modified不能判断其内容是否修改,所以只会重新发送。而使用Etag,可以通过检验如文件的i节点号,大小等来判断是否重传。
那么,在Lighttpd中,Etag到底是个什么东东呢?且听我慢慢道来。。。
在头文件Etag.h中定义了一个枚举类型

typedef enum 
{ 
    ETAG_USE_INODE = 1,      //包含文件的i节点号。
    ETAG_USE_MTIME = 2,      //包含文件最后一次修改的时间。
    ETAG_USE_SIZE = 4        //包含文件的byte数。
} etag_flags_t;

这个枚举类型决定了Etag中所包含的东西。注意,三个枚举量被定义为1,2,4.这样可以通过或的方式来包含多个内容。如::ETAG_USE_INODE | ETAG_USE_SIZE,表示Etag中既包含文件的i节点号,也包含文件的大小。
在头文件etag.h中,只声明了三个函数:
1、int etag_is_equal(buffer * etag, const char *matches);
这个是判断etag的内容是否和matches相同。相同返回1,不同返回0.
2、int etag_create(buffer * etag, struct stat *st, etag_flags_t flags);
这个是根据flags和st生成一个etag,存放在etag中传出。st是struct stat,在文件sys/stat.h中定义,用来表示文件的状态(目录也是文件哦)。可以使用stat函数获取一个文件的文件状态。具体操作请读者自己查阅。对于flags的设置,Lighttpd通过读取配置文件信息,设置flags。
3、int etag_mutate(buffer * mut, buffer * etag);
这个函数个etag生成一个哈希值,存放在mut中,并用双引号包围。里面使用的哈希方法是上一个结果左移五位异或上一个结果右移27位异或下一个字符,得到下一个结果。在实际应用中,这个哈希方法是很可靠的,基本上不会得到两个不同的字符串得到相同的结果,也就是基本不会出现碰撞。

三个函数的具体实现如下:

Code
int etag_is_equal(buffer * etag, const char *matches)
{
    if (etag && !buffer_is_empty(etag) && 0 == strcmp(etag->ptr, matches))
    {
        return 1;
    }
    return 0;
}

int etag_create(buffer * etag, struct stat *st, etag_flags_t flags)
{
    if (0 == flags)
        return 0;
    buffer_reset(etag);
    if (flags & ETAG_USE_INODE) //i节点号(serial number)
    {
        buffer_append_off_t(etag, st->st_ino);
        buffer_append_string_len(etag, CONST_STR_LEN("-"));
    }
    if (flags & ETAG_USE_SIZE) //普通文件的byte数
    {
        buffer_append_off_t(etag, st->st_size);
        buffer_append_string_len(etag, CONST_STR_LEN("-"));
    }
    if (flags & ETAG_USE_MTIME) //文件最后一次修改的时间。
    {
        buffer_append_long(etag, st->st_mtime);
    }
    return 0;
}

int etag_mutate(buffer * mut, buffer * etag)
{
    size_t i;
    uint32_t h;
    //计算哈希值。
    for (h = 0, i = 0; i < etag->used; ++i)
    {
        h = (h << 5) ^ (h >> 27) ^ (etag->ptr[i]);
    }
    buffer_reset(mut);
    buffer_copy_string_len(mut, CONST_STR_LEN("\""));
    buffer_append_long(mut, h);
    buffer_append_string_len(mut, CONST_STR_LEN("\""));
    return 0;
}

本文章由 http://www.wifidog.pro/2015/04/16/wifidog%E8%AE%A4%E8%AF%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html 整理编辑,转载请注明出处