1、普通的发送文件方式
想要服务端发送文件,浏览器端下载文件,一般的写法是创建一个buffer不断读取文件,写到response里面,像这样
@Controller
public class TestController {
@RequestMapping("/index")
public void index(HttpServletRequest request, HttpServletResponse response) throws IOException {
String filename = "111.txt";
String filePath = "src/main/resources/" + filename;
File file = new File(filePath);
FileInputStream fileInputStream = new FileInputStream(file);
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setHeader("Content-Length", String.valueOf(file.length()));
ServletOutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[1024];
int cnt;
while ((cnt = fileInputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, cnt);
}
}
}
但是这样文件的发送是比较低效的,这里面涉及到两次上下文切换(用户态、内核态之间切换),CPU参与复制数据两次(从文件缓冲区复制到buffer,从buffer复制到socket缓冲区)。这篇文章介绍的比较好https://blog.youkuaiyun.com/qq_26323323/article/details/120244493
2、零拷贝的发送文件方式
实际上java NIO提供了比较高效的文件传输方式,FileChannel#transferTo,底层利用了linux sendfile优化,可以做到零拷贝,提高传输效率。tomcat利用了FileChannel#transferTo来提高文件传输效率,在spring里只需要这样
@Controller
public class TestController {
@RequestMapping("/index")
public void index(HttpServletRequest request, HttpServletResponse response) {
String filename = "111.txt";
String filePath = "src/main/resources/" + filename;
File file = new File(filePath);
// 设置文件下载的header。这里本身跟文件传输无关,只是这样写能够通过浏览器下载这个文件
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setHeader("Content-Length", String.valueOf(file.length()));
// 设置attrible,由tomcat处理文件传输
request.setAttribute(Constants.SENDFILE_FILENAME_ATTR, filePath);
request.setAttribute(Constants.SENDFILE_FILE_START_ATTR, 0L);
request.setAttribute(Constants.SENDFILE_FILE_END_ATTR, file.length());
}
}
尽管此种方式能够做到零拷贝,但是底层tomcat的实现原因,是不能完全支持Range语法的
3、tomcat实现方式
tomcat是怎么实现的呢?
Poller
在tomcat NioEndpoint里,Poller处理所有的客户端socket连接,通过nio的方式获取socket就绪事件并处理就绪事件。处理的过程中会检查是否存在sendFileData,存在的话会处理文件的发送
1、这个是poller线程主要逻辑,processKey处理每个就绪的socket
public void run() {
// Loop until destroy() is called
while (true) {
boolean hasEvents = false;
try {
if (!close) {
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// If we are here, means we have other stuff to do
// Do a non blocking select
keyCount = selector.selectNow();
} else {