星星博客 »  > 

好玩的sendfile---探索Node.js中更快的数据传输方式

在Node.js中,当我们给前端返回一个静态文件的时候,我们通常会把文件先读进内容,然后通过socket接口写到底层,从而返回给前端。无论是一次性读取到内存还是使用流式的方式,都不可避免地要把数据从内核复制到用户层,再把数据复制到内核,这是一种低效的方式,因为多了无效的复制。在nginx中,可以通过sendfile指令提供效率。Node.js的copyFile底层使用了sendfile系统调用,但是网络IO的时候,没有使用该API。因为Node.js通过队列的方式,控制数据的写入。那么是否可以实现sendfile的方式来提供这网络IO的效率。首先我们看一下sendfile的好处是什么。

sendfile() copies data between one file descriptor and another.
Because this copying is done within the kernel, sendfile() is
more efficient than the combination of read(2) and write(2),
which would require transferring data to and from user space.

我们看到sendfile通过把内核完成数据的传输,减少了内核和用户层的数据复制,从而提高了效率。下面我们通过napi写一个addon来实现这个功能。

#include <sys/sendfile.h> 
#include <stdio.h> 
#include <unistd.h>
#include <fcntl.h>
#include <node_api.h>

static napi_value copyFile(napi_env env, napi_callback_info info) {
  size_t argc = 3;
  napi_value args[3];
  // 拿到js层的入参,这里是三个
  napi_get_cb_info(env, info, &argc, args, NULL, NULL);
  int fd1;
  int fd2;
  int len;
  // js传入的是一个数字,v8转成了对象,这里再次把入参转成int型
  napi_get_value_int32(env, args[0], &fd1);
  napi_get_value_int32(env, args[1], &fd2);
  napi_get_value_int32(env, args[2], &len);
  int writed = sendfile(fd2, fd1, 0,len);
  napi_value ret;
  napi_create_int32(env, writed, &ret);
  return ret;
}

napi_value Init(napi_env env, napi_value exports) {
  napi_value func;
  // 创建一个函数并且设置为exports对象的getArray属性的值
  napi_create_function(env,
                      NULL,
                      NAPI_AUTO_LENGTH,
                      copyFile,
                      NULL,
                      &func);
  napi_set_named_property(env, exports, "copyFile", func);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

下面我们看看怎么使用。首先用这个addon来复制文件,类似Node.js的copyyFile

const fs= require('fs');
const { copyFile } = require('./build/Release/sendfile.node');
const {
  O_WRONLY,
  O_CREAT,
} = fs.constants;
async function test() {
  const [fd1, fd2] = await Promise.all([openFile('1.txt', 'r'), openFile('2.txt', O_WRONLY | O_CREAT)]);
  const { size } = await getFileInfo(fd1);
  console.log(copyFile(fd1, fd2, size));
  fs.close(fd1, () => {});
  fs.close(fd2, () => {});
}
function openFile(filename, mode) {
  return new Promise((resolve, reject) => {
    fs.open(filename, mode, (err, fd) => {
      if (err) {
        reject(err);
      } else {
        resolve(fd);
      }
    });
  })
}

function getFileInfo(fd) {
  return new Promise((resolve, reject) => {
    fs.fstat(fd, (err, stat) => {
      if (err) {
        reject(err)
      }else {
        resolve(stat);
      }
    });
  })
}

test();

执行上面代码,我们可以看到文件会成功复制2.txt。接着我们再来试一下网络IO的场景。

const fs= require('fs');
const http = require('http');
const { copyFile } = require('./build/Release/sendfile.node');
const server = http.createServer(async (req, res) => {
  const fd = await openFile('1.txt', 'r');
  const { size } = await getFileInfo(fd);
  const ret = copyFile(fd, res.socket._handle.fd, size);
  res.socket.end();
}).listen(8002);

const {
  O_WRONLY,
  O_CREAT,
} = fs.constants;

function openFile(filename, mode) {
  return new Promise((resolve, reject) => {
    fs.open(filename, mode, (err, fd) => {
      if (err) {
        reject(err);
      } else {
        resolve(fd);
      }
    });
  })
}

function getFileInfo(fd) {
  return new Promise((resolve, reject) => {
    fs.fstat(fd, (err, stat) => {
      if (err) {
        reject(err)
      }else {
        resolve(stat);
      }
    });
  })
}

以上代码首先启动一个http服务器,然后收到请求的时候,通过addon调用sendfile给前端返回对应的内容,最后关闭连接。结果如下。

sendfile似乎在网络IO中可以应用了,但只是一个demo的思路,后续有时间继续研究分析。

相关文章