欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

文件上传和下载原理:http 协议分析与实现

最编程 2024-03-10 21:04:24
...

我们现在用得非常多互联网下载文件,非常直观。有一个下载按钮,然后我点击了下载,然后文件慢慢就下载到本地了。就好像是一个复制的过程。

而既然是互联网,那么必然会是使用网络进行传输的。那么到底是怎样传输的呢?

当然,下载文件有两种方式:一是直接针对某个文件资源进行下载,无需应用开发代码;二是应用代码临时生成需要的内容文件,然后输出给到下载端。

其中,直接下载资源文件的场景给我们感觉是下载就是针对这个文件本身的一个操作,和复制一样没有什么疑义。而由应用代码进行下载文件时,又当如何处理呢?

 

1. 上传下载文件demo

在网上你可以非常容易地找到相应的模板代码,然后处理掉。基本的样子就是设置几个头信息,然后将数据写入到response中。

demo1. 服务端接收文件上传,并同时输出文件到客户端

    @PostMapping("fileUpDownTest")
    @ResponseBody
    public Object fileUpDownTest(@ModelAttribute EncSingleDocFileReqModel reqModel,
                              MultipartFile file,
                              HttpServletResponse response) {
        // 做两件事:1. 接收上传的文件; 2. 将文件下载给到上传端; 
        // 即向双向文件的传输,下载的文件可以是你处理之后的任意文件。
        String tmpPath = saveMultipartToLocalFile(file);
        outputEncFileStream(tmpPath, response);
        System.out.println("path:" + tmpPath);
        return null;
    }
    /**
     * 保存文件到本地路径
     *
     * @param file 文件流
     * @return 本地存储路径
     */
    private String saveMultipartToLocalFile(MultipartFile file) {
        try (InputStream inputStream = file.getInputStream()){
            // 往临时目录写文件
            String fileSuffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.'));
            File tmpFile = File.createTempFile(file.getName(), ".tmp" + fileSuffix);
            FileUtils.copyInputStreamToFile(inputStream, tmpFile);
            return tmpFile.getCanonicalPath();
        }
        catch (Exception e){
            log.error("【加密文件】文件流处理失败:" + file.getName(), e);
            throw new EncryptSysException("0011110", "文件接收失败");
        }
    }
    
    /**
     * 输出文件流数据
     *
     * @param encFileLocalPath 文件所在路径
     * @param response servlet io 流
     */
    private void outputEncFileStream(String encFileLocalPath, HttpServletResponse response) {
        File outFile = new File(encFileLocalPath);
        OutputStream os = null;
        InputStream inputStream = null;
        try {
            response.reset();
            response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
//            response.setHeader("Content-Length", file.getContentLength()+"");
            String outputFileName = encFileLocalPath.substring(encFileLocalPath.lastIndexOf('/') + 1);
            response.setHeader("Content-Disposition", String.format("attachment; filename=%s", URLEncoder.encode(outputFileName, "UTF-8")));
            response.setContentType("application/octet-stream; charset=utf-8");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Expires", "0");
            inputStream = new FileInputStream(outFile);
            //写入信息
            os = CommonUtil.readInputStream(inputStream, response.getOutputStream());
        }
        catch (Exception re) {
            log.error("输出文件流失败,", re);
            throw new RuntimeException("0011113: 输出加密后的文件失败");
        }
        finally {
            if (os != null) {
                try {
                    os.flush();
                    os.close();
                }
                catch (IOException e) {
                    log.error("输出流文件失败", e);
                }
            }
            if(inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    log.error("加密文件输入流关闭失败", e);
                }
            }
        }
    }

我们在做开发时,面对的仅仅是 Request, Response 这种什么都有对象,直接问其要相关信息即可。给我们提供方便的同时,也失去了了解真相的机会。

demo2.  服务端转发文件到另一个服务端,并同接收处理响应回来的文件流数据

    /**
     * 使用本地文件,向加密服务器请求加密文件,并输出到用户端
     *
     * @param localFilePath 想要下载的文件
     * @return 文件流
     */
    @GetMapping("transLocalFileToEnc")
    public Object transLocalFileToEnc(@ModelAttribute EncSingleDocFileReqModel reqModel,
                                      @RequestParam String localFilePath,
                                      HttpServletResponse response) {
        File localFileIns = new File(localFilePath);
        if(!localFileIns.exists()) {
            return ResponseInfoBuilderUtil.fail("指定文件未找到");
        }

        try(InputStream sourceFileInputStream = new FileInputStream(localFileIns);) {

            //这个url是要上传到另一个服务器上接口, 此处模拟向本机发起加密请求
            String url = "http://localhost:8082/encrypt/testEnc";
            int lastFileSeparatorIndex = localFilePath.lastIndexOf('/');
            String filename = lastFileSeparatorIndex == -1
                                    ? localFilePath.substring(localFilePath.lastIndexOf('\\'))
                                    : localFilePath.substring(lastFileSeparatorIndex);
            Object object = null;
            // 创建HttpClients实体类
            CloseableHttpClient aDefault = HttpClients.createDefault();
            try  {
                HttpPost httpPost = new HttpPost(url);
                MultipartEntityBuilder builder = MultipartEntityBuilder.create();
                //使用这个,另一个服务就可以接收到这个file文件了
                builder.addBinaryBody("file", sourceFileInputStream, ContentType.create("multipart/form-data"), URLEncoder.encode(filename, "utf-8"));
                builder.addTextBody("systemCode", "self");
                String encOutputFilename = filename;
                builder.addTextBody("encOutputFileName", encOutputFilename);
                HttpEntity entity  =  builder.build();
                httpPost.setEntity(entity);
                ResponseHandler<Object> rh = new ResponseHandler<Object>() {
                    @Override
                    public  Object handleResponse(HttpResponse re) throws IOException {
                        HttpEntity entity = re.getEntity();
                        if(entity.getContentType().toString().contains("application/json")) {
                            // 通过判断响应类型来判断是否输出文件流,非严谨的做法
                            String retMsg = EntityUtils.toString(entity,  "UTF-8");
                            return JSONObject.parseObject(retMsg, ResponseInfo.class);
                        }
                        InputStream input = entity.getContent();
//                        String result = EntityUtils.toString(entity,  "UTF-8");
                        // 写入响应流信息
                        OutputStream os = null;
                        try {
                            response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
                            // response.setHeader("Content-Length", file.getContentLength()+"");

                            response.setHeader("Content-Disposition", String.format("attachment; filename=%s", URLEncoder.encode(filename, "UTF-8")));
                            response.setContentType("application/octet-stream; charset=utf-8");
                            response.setHeader("Pragma", "no-cache");
                            response.setHeader("Expires", "0");

                            // 往临时目录写文件
                            File tmpFile = File.createTempFile(filename, "");
                            FileUtils.copyInputStreamToFile(input, tmpFile);
                            String encFilePathTmp = tmpFile.getCanonicalPath();
                            File encFileIns = new File(encFilePathTmp);
                            if(encFileIns.exists()) {
                                FileInputStream zipStream = new FileInputStream(encFileIns);
                                os = CommonUtil.readInputStream(zipStream, response.getOutputStream());
                            }

                        }
                        finally {
                            if(os != null) {
                                os.flush();
                                os.close();
                            }
                        }
                        // 已向客户端输出文件流
                        return Boolean.TRUE;
                    }
                };
                object = aDefault.execute(httpPost, rh);

                return object == Boolean.TRUE
                            ? "加密成功,下载文件去!"
                            : object;
            }
            catch (Exception e) {
                log.error("", e);
            }
            finally  {
                try {
                    aDefault.close();
                } catch (IOException e) {
                    log.error("关闭错误", e);
                }
            }
        }
        catch (FileNotFoundException e) {
            log.error("要加密的文件不存在", e);
        }
        catch (IOException e) {
            log.error("要加密的文件不存在", e);
        }
        return "处理失败";
    }

    // 抽出写socket流的逻辑,方便统一控制
    /**
     * 从输入流中获取字节数组
     *
     * @param inputStream 输入流
     * @return 输出流,超过5000行数据,刷写一次网络
     * @throws IOException
     */
    public static OutputStream readInputStream(InputStream inputStream, OutputStream os) throws IOException {
        byte[] bytes = new byte[2048];
        int i = 0;
        int read = 0;
        //按字节逐个写入,避免内存占用过高
        while ((read = inputStream.read(bytes)) != -1) {
            os.write(bytes, 0, read);
            i++;
            // 每5000行
            if (i % 5000 == 0) {
                os.flush();
            }
        }
        inputStream.close();
        return os;
    }

此处仅是使用后端代码展现了前端的一人 form 提交过程,并无技巧可言。不过,这里说明了一个问题:文件流同样可以任意在各服务器间流转。只要按照协议规范实现即可。(注意以上代码可能需要引入pom依赖: org.apache.httpcomponents:httpclient:4.5.6,org.apache.httpcomponents:httpmime:4.5.6)

 

2. http 协议之文件处理

一般地,我们应对的互联网上的整个上传下载文件,基本都是基于http协议的。所以,要从根本上理解上传下载文件的原理,来看看http协议就好了。

我们可以通过上面的demo看下上传时候的数据样子,我们通过 fiddler进行抓包查看数据即可得如下:

POST http://localhost:8082/test/fileUpDownTest?systemCode=1111&outputFileName=111 HTTP/1.1
Host: localhost:8082
Connection: keep-alive
Content-Length: 197
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36 OPR/68.0.3618.63
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryen2ZJyNfx7WhA3yO
Origin: http://localhost:8082
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8082/swagger-ui.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: JSESSIONID=40832A6766FB11E105717690AEF826AA

------WebKitFormBoundaryen2ZJyNfx7WhA3yO
Content-Disposition: form-data; name="file"; filename="123.txt"
Content-Type: text/plain

123content
over
------WebKitFormBoundaryen2ZJyNfx7WhA3yO     
Content-Disposition: form-data; name="file2"; filename="123-2.txt"
Content-Type: text/plain

2222content
2over
------WebKitFormBoundaryen2ZJyNfx7WhA3yO--

因为fiddler会做解码操作,且http是一种基于字符串的传输协议,所以,我们看到的都是可读的文件信息。我这里模拟是使用一个 123.txt 的文件,里面输入了少量字符:“123content\nover”;

我们知道,http协议是每行作为一个header的,其中前三是固定的,不必多说。

与我们相关的有:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryen2ZJyNfx7WhA3yO
Content-Type是个重要的标识字段,当我们用文件上传时,multipart/form-data代表了这是一个多部分上传的文件类型请求,即此处的文件上传请求。 后面的 boundary 代表在上传的实际多个部分内容时的分界线,该值应是在每次请求时随机生成且避免与业务数据的冲突。
Content-Length: 197.
这个值是由浏览器主动计算出来的负载内容长度,服务端收到该信息后,只会读取这么多的长度即认为传输完成。
http协议的包体是从遇到第一个两个连续的换行符开始的。(所以,如果在header中包含了此特征时,需要自行编码后再请求,否则将发生协议冲突。)
每个part部分的内容,以boundary作为分界线。part部分的内容可以是文件、流、或者纯粹的key-value。

根据以上数据格式,服务端作出相应的反向解析就可以得到相应的内容了。

 

如果服务响应的结果是一个文件下载,那么对于响应的结果示例如下:

HTTP/1.1 200
Cache-Control: no-cache, no-store, must-revalidate
Content-Disposition: attachment; filename=file5983940017135638617.tmp.txt
Pragma: no-cache
Expires: 0
Content-Type: application/octet-stream;charset=utf-8
Transfer-Encoding: chunked
Date: Sun, 17 May 2020 05:30:57 GMT

10
123content
over
0

重要字段说明:

Content-Disposition: attachment; filename=file5983940017135638617.tmp.txt
该字段说明本次响应的值应该作为一个附件形式下载保存到本地,这会被几乎所有浏览器支持。但如果你自己写代码接收,那就随你意好了,它只是一个标识而已;其中 filename 是用作用户下载时的默认保存名称,如果本地已存在一般会被添加(xxx)的后缀以避免下载覆盖。
Content-Type: application/octet-stream;charset=utf-8
代表这是一个二进制的文件,也就是说,浏览器一般无法作出相应的处理。当然,这也只是一个建议,至于你输出的是啥也无所谓了,反正只要追加到文件之后,就可以还原文件内容了。

同样,遇到第一个连续的换行之后,代表正式的文件内容开始了。
如上的输出中,并没有 Content-Length 字段,所以无法直接推断出下载的数据大小,所以会在前后加一些字符器,用于判定结束。这样做可能导致浏览器上无法判定已下载的数据量占比,即无法展示进度条。虽然不影响最终下载数据,但是一般别这么干。

如下,我们加下content-length之后的响应如下:

HTTP/1.1 200
Cache-Control: no-cache, no-store, must-revalidate
Content-Disposition: attachment; filename=file4383190990004865558.tmp.txt
Pragma: no-cache
Expires: 0
Content-Type: application/octet-stream;charset=utf-8
Content-Length: 16
Date: Sun, 17 May 2020 07:26:47 GMT

123content
over
    

如上,就是http协议对于文件的处理方式了,只要你按照协议规定进行请求时,对端就能接受你的文件上传。只要服务按照协议规定输出响应数据,浏览器端就可以进行相应文件下载。

http协议头更多信息可以参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers

 

3. http协议上传下载的背后,还有什么?

我们知道,http协议是基于tcp协议上实现的一个应用层协议。上一节我们说到的,如何进行上传下载文件,也是基于应用层去说的。说直接点就是,如果把网络比作黑盒,那么我们认为这个黑盒会给我们正确的数据。我们只要基于这些数据,就可以解析相应的文件信息了。

实际上,tcp协议是一种可靠的传输协议。至于如何可靠,额,这么说吧:网络上的信息是非常复杂和无序的,你从一个端点发送数据到另一个网络站点,会使用IP协议通过网络传送出去,而这些传输是单向的,多包的。它会受到外部复杂环境的影响,可能有的包丢失,可能有的包后发先到等等。如果不能处理好它们的这些丢包、乱序,重复等问题,那么网络发过来的数据将是无法使用的。(基本就是数据损坏这个结论)

tcp则是专门为处理这些问题而设计的,具体嘛,就很复杂了。总之一句话,使用了tcp协议后,你就无需关注复杂的网络环境了,你可以无条件相信你从操作系统tcp层给你的数据就是有序的完整的数据。你可以去看书,或者查看更多网上资料。(书更可靠些,只是更费时间精力)可以参考这篇文章: http://www.ruanyifeng.com/blog/2017/06/tcp-protocol.html

 

4. java中对于文件上传的处理实现?

虽然前面我们解读完成http协议对于文件的上传处理方式,但是,到具体如何实现,又当如何呢?如果给你一个socket的入口lib,你又如何去处理这些http请求呢?

可以大概这么思考: 1. 接收到头信息,判断出是文件类型的上传;2. 取出 boundary, 取出content-length, 备用;3. 继续读取后续的网络流数据,当发现传输的是key-value数据时,将其放入内存缓冲中存起来,当发现是文件类型的数据时,创建一个临时文件,将读取到的数据写入其中,直到该部分文件传输完成,并存储临时文件信息;4. 读取完整个http协议指定的数据后,封装相应的请求给到应用代码,待应用处理完成后响应给客户端;

以tomcat为例,它会依次解析各个参数值。

有兴趣的的同学可以先看看它是如何接入http请求的吧:(基于nio socket)大概流程为(下图为其线程模型):Accepter -> Pollor -> SocketProcessor 。

        // org.apache.tomcat.util.net.NioEndpoint.Acceptor
        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        // Nio 的 ServerSocketChannelImpl, 阻塞等待socket accept 事件
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        // We didn't get a socket
                        countDownConnection();
                        if (running) {
                            // Introduce delay if necessary
                            errorDelay = handleExceptionWithDelay(errorDelay);
                            // re-throw
                            throw ioe;
                        } else {
                            break;
                        }
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    if (running && !paused) {
                        // setSocketOptions() will hand the socket off to
                        // an appropriate processor if successful
                        // 处理socket事件
                        if (!setSocketOptions(socket)) {
                            closeSocket(socket);
                        }
                    } else {
                        closeSocket(socket);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }
    /**
     * Process the specified connection.
     * @param socket The socket channel
     * @return <code>true</code> if the socket was correctly configured
     *  and processing may continue, <code>false</code> if the socket needs to be
     *  close immediately
     */
    protected boolean setSocketOptions(SocketChannel socket) {
        // Process the connection
        try {
            //disable blocking, APR style, we are gonna be polling it
            // 组装channel,交给 Pollor
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);

            NioChannel channel = nioChannels.pop();
            if (channel == null) {
                SocketBufferHandler bufhandler = new SocketBufferHandler(
                        socketProperties.getAppReadBufSize(),
                        socketProperties.getAppWriteBufSize(),
                        socketProperties.getDirectBuffer());
                if (isSSLEnabled()) {
                    channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                } else {
                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                channel.setIOChannel(socket);
                channel.reset();
            }
            // 添加到 Pollor 队列中,Poller 的获取使用轮询方式获取
            getPoller0().register(channel);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            try {
                log.error("",t);
            } catch (Throwable tt) {
                ExceptionUtils.handleThrowable(tt);
            }
            // Tell to close the socket
            return false;
        }
        return true;
    }    
    /**
     * Return an available poller in true round robin fashion.
     *
     * @return The next poller in sequence
     */
    public Poller getPoller0() {
        // 第1次取1,第2次取2,第3次取1... 轮询
        int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
        return pollers[idx];
    }
        // org.apache.tomcat.util.net.NioEndpoint.Poller#register
        /**
         * Registers a newly created socket with the poller.
         *
         * @param socket    The newly created socket
         */
        public void register(final NioChannel socket) {
            socket.setPoller(this);
            NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
            socket.setSocketWrapper(ka);
            ka.setPoller(this);
            ka.setReadTimeout(getSocketProperties().getSoTimeout());
            ka.setWriteTimeout(getSocketProperties().getSoTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());
            ka.setReadTimeout(getConnectionTimeout());
            ka.setWriteTimeout(getConnectionTimeout());
            PollerEvent r = eventCache.pop();
            // 注册OP_READ事件,给selector使用
            ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
            // 将socket信息添加到 PollerEvent 中
            if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
            else r.reset(socket,ka,OP_REGISTER);
            addEvent(r);
        }
        // 添加事件并唤醒selector
        // org.apache.tomcat.util.net.NioEndpoint.Poller#addEvent
        private void addEvent(PollerEvent event) {
            events.offer(event);
            // 正在select()阻塞中的 selector, wakeupCounter=-1, 即可被唤醒状态
            if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
        }
        
        // step2. Poller 使用selector池处理读就绪事件
        /**
         * The background thread that adds sockets to the Poller, checks the
         * poller for triggered events and hands the associated socket off to an
         * appropriate processor as events occur.
         */
        @Override
        public void run() {
            // Loop until destroy() is called
            while (true) {

                boolean hasEvents = false;

                try {
                    if (!close) {
                        // events() 会检查是否有acceptor提交过来的 PollerEvent, 如果有,会先初始化event
                        // 向selector注册读事件等等,以便后续 select() 生效
                        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 {
                            keyCount = selector.select(selectorTimeout);
                        }
                        wakeupCounter.set(0);
                    }
                    if (close) {
                        events();
                        timeout(0, false);
                        try {
                            selector.close();
                        } catch (IOException ioe) {
                            log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
                        }
                        break;
                    }
                } catch (Throwable x) {
                    ExceptionUtils.handleThrowable(x);
                    log.error("",x);
                    continue;
                }
                //either we timed out or we woke up, process events first
                if ( keyCount == 0 ) hasEvents = (hasEvents | events());

                Iterator<SelectionKey> iterator =
                    keyCount > 0 ? selector.selectedKeys().iterator() : null;
                // Walk through the collection of ready keys and dispatch
                // any active event.
                while (iterator != null && iterator.hasNext()) {
                    SelectionKey sk = iterator.next();
                    NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
                    // Attachment may be null if another thread has called
                    // cancelledKey()
                    if (attachment == null) {
                        iterator.remove();
                    } else {
                        // 把key监听移除,然后去处理具体key, 网络接入成功
                        iterator.remove();
                        processKey(sk, attachment);
                    }
                }//while

                //process timeouts
                timeout(keyCount,hasEvents);
            }//while

            getStopLatch().countDown();
        }
        // org.apache.tomcat.util.net.NioEndpoint.Poller#processKey
        protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
            try {
                if ( close ) {
                    cancelledKey(sk);
                } else if ( sk.isValid() && attachment != null ) {
                    if (sk.isReadable() || sk.isWritable() ) {
                        // sendfile
                        if ( attachment.getSendfileData() != null ) {
                            processSendfile(sk,attachment, false);
                        } else {
                            // 取消事件监听,那么后续如何读数据呢?
                            // 这意味着当前socket将会从epoll的表中移除掉,不再被其管理,但并不影响后续的read
                            // 后续的read() 操作将以bio等式展开
                            unreg(sk, attachment, sk.readyOps());
                            boolean closeSocket = false;
                            // Read goes before write
                            // 优先处理读事件,再处理写事件
                            if (sk.isReadable()) {
                                if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
                                    closeSocket = true;
                                }
                            }
                            if (!closeSocket && sk.isWritable()) {
                                if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
                                    closeSocket = true;
                                }
                            }
                            if (closeSocket) {
                                cancelledKey(sk);
                            }
                        }
                    }
                } else {
                    //invalid key
                    cancelledKey(sk);
                }
            } catch ( CancelledKeyException ckx ) {
                cancelledKey(sk);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error("",t);
            }
        }
    // org.apache.tomcat.util.net.AbstractEndpoint#processSocket
    /**
     * Process the given SocketWrapper with the given status. Used to trigger
     * processing as if the Poller (for those endpoints that have one)
     * selected the socket.
     *
     * @param socketWrapper The socket wrapper to process
     * @param event         The socket event to be processed
     * @param dispatch      Should the processing be performed on a new
     *                          container thread
     *
     * @return if processing was triggered successfully
     */
    public boolean processSocket(SocketWrapperBase<S> socketWrapper,
            SocketEvent event, boolean dispatch) {
        try {
            if (socketWrapper == null) {
                return false;
            }
            // 使用线程池处理单个读事件
            SocketProcessorBase<S> sc = processorCache.pop();
            if (sc == null) {
                sc = createSocketProcessor(socketWrapper, event);
            } else {
                sc.reset(socketWrapper, event);
            }
            // 线程池默认10个核心线程
            // 此处的线程池并非原生jdk的线程池ThreadPoolExecutor,而是经过tomcat继承过来的 org.apache.tomcat.util.threads.ThreadPoolExecutor, 主要用于做一次统计类工作
            // 最终的socket处理将会由 SocketProcessor 进行统一调度具体的Handler处理
            Executor executor = getExecutor();
            if (dispatch && executor != null) {
                executor.execute(sc);
            } else {
                sc.run();
            }
        } 
																				
															

推荐阅读