打造mvc框架之sendfile使静态文件高效传输

    以前我写过一篇关于nginx sendfile的文章,讲的是sendfile零拷贝理论方面,这次主要是讲实现的细节。 http://xiaorui.cc/?p=1673

    sendfile的流程示意图 :

        

     nginx有个sendfile参数,我想大家一般都是开启的。 简单说sendfile可以高效传输静态文件,尤其针对静态文件下载的逻辑。 sendfile的好处主要是两点,  普通的sendall流程是,kernel buffer — > user buffer — > kernel socket buffer –> 协议栈,对于sendfile来说,他的流程是kernel buffer –> kernel socket buffer –> 协议栈.  很明显减少copy的次数,另外这也减少了上线文切换的次数。

该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。http://xiaorui.cc/?p=3225

python是无法直接实现sendfile的,只能是借用PyObject + C语言实现。

讲解py库之前,先介绍下C语言的sendfile函数使用方法:

#xiaorui.cc
#include <sys/sendfile.h>  
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);  

sendfile在两个文件描术符之间直接传递数据,完全在内核中操作,从而避免了内核缓冲区到用户缓冲区的拷贝,因此效率很高,称为零拷贝。
out_fd参数是待写入描述符。

in_fd参数是待读出的描述符。

offset参数指定从读入文件流的哪个位置开始读,如果为空,则从文件流的默认超始位置读入。

count参数指定传输的字节数。

调用成功时返回传输的字节数,失败则为-1,并设置errno .

需要注意的是,in_fd必须是进程打开文件后产生的文件描述符,你不能直接传入文件名,不能是socket和管道;而out_fd必须是一个socket。可见,sendfile专为网络传输文件而生。

python sendfile已经有个意大利的同学实现了,我们直接套用该模块就可以了。模块名是 pysendfile . 

#xiaorui.cc
from sendfile import sendfile

file = open("xiaorui.cc", "rb")
blocksize = os.path.getsize("xiaorui.cc")
sock = socket.socket()
sock.connect(("127.0.0.1", 80))
offset = 0

while True:
    #	 	socket 文件描述符  #文件的文件描述符  #读取位置,seek   #传输文件的大小
    sent = sendfile(sock.fileno(), file.fileno(), offset, blocksize)
    if sent == 0:
        break  # EOF
    offset += sent

再观摩下他的sendfilemodule.c的源码实现,可以看到sendfile函数并不是一次性把数据塞入到socket buffer,而是用while循环的copy到kernel socket端.  另外还针对EAGAIN做了及时返回。避免了阻塞读取写入的情况.  如果你有类似ioloop的调度实现,完全可以针对这事件进行io调度,当然如果是同步的逻辑,跟我上面的python实例一样,那么就无所谓了。

method_sendfile(PyObject *self, PyObject *args)
{
    int out_fd, in_fd;
    off_t offset;
    size_t nbytes;
    char *hdr=NULL, *trail=NULL;
    int hdrsize, trailsize;
    ssize_t sts=0;
    struct sf_parms sf_iobuf;
    int rc;

    if (!PyArg_ParseTuple(args, "iiLk|s#s#",
                          &out_fd, &in_fd, &offset, &nbytes, &hdr, &hdrsize,
                          &trail, &trailsize))
        return NULL;

    if(hdr != NULL) {
        sf_iobuf.header_data = hdr;
        sf_iobuf.header_length = hdrsize;
    }
    else {
        sf_iobuf.header_data = NULL;
        sf_iobuf.header_length = 0;
    }
    if(trail != NULL) {
        sf_iobuf.trailer_data = trail;
        sf_iobuf.trailer_length = trailsize;
    }
    else {
        sf_iobuf.trailer_data = NULL;
        sf_iobuf.trailer_length = 0;
    }
    sf_iobuf.file_descriptor = in_fd;
    sf_iobuf.file_offset = offset;
    sf_iobuf.file_bytes = nbytes;

    Py_BEGIN_ALLOW_THREADS;
    do {
            sf_iobuf.bytes_sent = 0; /* Really needed? */
            rc = send_file(&out_fd, &sf_iobuf, SF_DONT_CACHE);
            sts += sf_iobuf.bytes_sent;
    } while( rc == 1 || (rc == -1 && errno == EINTR) );
    Py_END_ALLOW_THREADS;

    offset = sf_iobuf.file_offset;

    if (rc == -1) {
        PyErr_SetFromErrno(PyExc_OSError);
        return NULL;
    }
    else {
    #if defined(HAVE_LARGEFILE_SUPPORT)
        return Py_BuildValue("L", sts);
    #else
        return Py_BuildValue("l", sts);
    #endif
    }
}


    if (ret < 0) {
        if ((errno == EAGAIN) || (errno == EBUSY) || (errno == EWOULDBLOCK)) {
            if (sent != 0) {
                // some data has been sent
                errno = 0;
                goto done;
            }
            else {
                // no data has been sent; upper application is supposed
                // to retry on EAGAIN / EBUSY / EWOULDBLOCK
                PyErr_SetFromErrno(PyExc_OSError);
                return NULL;
            }
        }
        PyErr_SetFromErrno(PyExc_OSError);
        return NULL;
    }

sendfile对于静态文件的处理是很高效的,也是web服务器必有的模式.  在python的主流web框架(tornado,flask,django)里默认是没有sendfile模块,原因是大多数的架构都是让nginx顶在python web框架前面的,原因不用废话。  按照实际应用的场景来描述,python sendfile更适合于client的角色,那么我写这篇关于web框架之python sendfile的用意是什么?学习性质的造轮子 !

END.


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

2 Responses

  1. 在路上*2涛* 2016年4月24日 / 下午8:15

    关于sendfile至今看到讲的最清楚的一个

发表评论

邮箱地址不会被公开。 必填项已用*标注