nginx发送静态文件,速度极快,Nginx中的x-sendfile机制需要依靠 X-Accel-Redirect 特性实现,不过经过我的测试,不能满足我的需求,我 要用lua来处理业务逻辑, 然后发送文件内容,一开始用如下方式来实现, 这种方式如果文件小, 到无所谓, 但是当文件很大时, 对性能的影响非常大。

local file = io.open(filePath, "rb")


local size = file:seek("end")
ngx.header["Content-Length"] = size
file:seek("set", 0) 
data = file:read("*a")

ngx.print(data)
ngx.flush(true)

file:close()



众所周知, linux内核里有sendfile函数,可以0拷贝发送文件,于是在网上找资料,找到的都是同样的一个文章,都是介绍Nginx的 X-Accel-Redirect 如何使用的, 经过测试 X-Accel-Redirect ,不能满足我的需求。

介绍 X-Accel-Redirect 的官方文章地址为 : http://wiki.nginx.org/XSendfile


最后没办法, 只能从源码入手了。


参考了 ngx_http_static_module.c  这个发送静态文件的模块源码, 决定实现一个lua的接口, 可以让lua直接调用sendfile函数, 发送文件内容。


print 函数在 ngx_http_lua_log.c 中实现, 我也把sendfile函数放这里吧, 直接用 ngx_lua的框架。

void
ngx_http_lua_inject_log_api(lua_State *L)
{
    ngx_http_lua_inject_log_consts(L);

    lua_pushcfunction(L, ngx_http_lua_ngx_log);
    lua_setfield(L, -2, "log");

    lua_pushcfunction(L, ngx_http_lua_print);
    lua_setglobal(L, "print");

    lua_pushcfunction(L, ngx_http_lua_sendfile); //添加的内容
    lua_setglobal(L, "sendfile");//添加的内容
}



上面的代码里 lua_pushcfunction 就是把函数的指针压入堆栈,lua_setglobal 就是用来把 "sendfile"压入堆栈的, 并且设置的是全局函数,全局函数的话, 在lua里调用就是直接 sendfile(),  如果是用 lua_setfield 来压入堆栈的话, 那在lua里就得用  ngx.sendfile () 这样的形式来调用了。  反正是两种都可以, 随便你了。


下面贴出 ngx_http_lua_sendfile 函数的实现 :

static int ngx_http_lua_sendfile(lua_State *L)
{
    u_char                    *last, *location;
    size_t                     root, len;
    ngx_http_request_t        *r;
    ngx_str_t                  path;
    ngx_int_t                  rc;
    ngx_uint_t                 level;
    ngx_log_t                 *log;
    ngx_buf_t                 *b;
    ngx_chain_t                out;
    ngx_open_file_info_t       of;
    ngx_http_core_loc_conf_t  *clcf;
	int                        offset;
	int                        bytes;
	char                      *filename;
	int                        nargs;

    lua_pushlightuserdata(L, &ngx_http_lua_request_key);
    lua_rawget(L, LUA_GLOBALSINDEX);
    r = lua_touserdata(L, -1);
    lua_pop(L, 1);

    if (r == NULL) 
    {
        luaL_error(L, "no request object found");
	return 1;
    }

 
    nargs = lua_gettop(L);

	filename = (char *) lua_tolstring(L, 1, &len);
	offset   = lua_tonumber(L, 2);
	bytes    = lua_tonumber(L, 3);

    log = r->connection->log;

    path.len = ngx_strlen(filename);

    path.data = ngx_pnalloc(r->pool, path.len + 1);
    if (path.data == NULL) {
        return 0;
    }

    (void) ngx_cpystrn(path.data, (u_char *) filename, path.len + 1);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "ngx send lua filename: \"%s\"", filename);

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

    ngx_memzero(&of, sizeof(ngx_open_file_info_t));

    of.read_ahead = clcf->read_ahead;
    of.directio = clcf->directio;
    of.valid = clcf->open_file_cache_valid;
    of.min_uses = clcf->open_file_cache_min_uses;
    of.errors = clcf->open_file_cache_errors;
    of.events = clcf->open_file_cache_events;

    if (ngx_http_set_disable_symlinks(r, clcf, &path, &of) != NGX_OK) 
    {
        return 0;//NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool) != NGX_OK)
    {
        switch (of.err) 
	{

        case 0:
            return 0;//NGX_HTTP_INTERNAL_SERVER_ERROR;

        case NGX_ENOENT:
        case NGX_ENOTDIR:
        case NGX_ENAMETOOLONG:

            level = NGX_LOG_ERR;
            rc = NGX_HTTP_NOT_FOUND;
            break;

        case NGX_EACCES:
#if (NGX_HAVE_OPENAT)
        case NGX_EMLINK:
        case NGX_ELOOP:
#endif
            level = NGX_LOG_ERR;
            rc = NGX_HTTP_FORBIDDEN;
            break;

        default:

            level = NGX_LOG_CRIT;
            rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
            break;
        }

        if (rc != NGX_HTTP_NOT_FOUND || clcf->log_not_found) 
	{
            ngx_log_error(level, log, of.err, "%s \"%s\" failed", of.failed, path.data);
        }

        return 0;//rc;
    }

    r->root_tested = !r->error_page;

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "http static fd: %d", of.fd);

    if (offset < 0) {
        offset = 0;
    }

    if (bytes <= 0) {
        bytes = of.size - offset;
    }


#if !(NGX_WIN32) /* the not regular files are probably Unix specific */

    if (!of.is_file) 
    {
        ngx_log_error(NGX_LOG_CRIT, log, 0, "\"%s\" is not a regular file", path.data);

        return 0;//NGX_HTTP_NOT_FOUND;
    }

#endif

    if (r->method & NGX_HTTP_POST) 
    {
        return 0;//NGX_HTTP_NOT_ALLOWED;
    }

    rc = ngx_http_discard_request_body(r);

    if (rc != NGX_OK) 
    {
        return 0;//rc;
    }

    log->action = "sending response to client";

    len = (offset + bytes) >= of.size ? of.size : (offset + bytes);

    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = len - offset;
    r->headers_out.last_modified_time = of.mtime;

    if (ngx_http_set_content_type(r) != NGX_OK) 
    {
        return 0;//NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    if (r != r->main && of.size == 0) 
    {
         ngx_http_send_header(r);
		 return 0;//
    }

    r->allow_ranges = 1;

    /* we need to allocate all before the header would be sent */

    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL) 
    {
        return 0;//NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
    if (b->file == NULL) 
    {
        return 0;//NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    rc = ngx_http_send_header(r);

    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) 
    {
        return 0;//rc;
    }

    b->file_pos = offset;
    b->file_last = (offset + bytes) >= of.size ? of.size : (offset + bytes);

    b->in_file = 1;
    b->last_buf = (r == r->main) ? 1: 0;
    b->last_in_chain = 1;

    b->file->fd = of.fd;
    b->file->name = path;
    b->file->log = log;
    b->file->directio = of.is_directio;

    out.buf = b;
    out.next = NULL;

    ngx_http_output_filter(r, &out);
    return 0;//
}

sendfile函数的参数共有三个, 第一个参数为文件名filename。 第二个参数为文件偏移量offset, offset<0代表从文件头开始发送。 第三个参数为bytes, 要发送的字节数,如果bytes<0, 代表发送到文件尾。


这样在 lua 脚本里就可以这样调用了:

sendfile("/opt/f1.ts", -1,-1) 发送整个文件

或者

sendfile("/opt/f1.ts", 104857600,104857600)  从100MB开始的地方发送100MB的数据


经过测试, 速度和直接nginx发送静态文件的速度一致。

在添加该功能时,碰到过一些小问题,记录下来。

ngx_lua里的C函数返回值代表压入堆栈的返回值的个数, 看我上面的代码, 基本都返回0, 代表我没有给lua的堆栈压入任何参数, 而下面的代码

luaL_error(L, "no request object found");
return 1;

就代表压入了一个参数, 所以返回值为1, 要不然会出现段错误。