Spring Boot使用Servlet居然也可以实现长轮询,敲了5年代码,我居然不知道 - 第413篇

关历史文章(阅读本文前,您可能需要先看下之前的系列👇

国内最全的Spring Boot系列之四

享元模式:共享女友 - 第355篇

Spring Boot @ConditionalOnClass上的注解你了解多少-java元注解和注解 - 第404篇

SpringBoot 使用validation数据校验-超级详细超级多干货 - 第406篇

SpringBoot 使用validation数据校验之分组校验怎么玩?·分组还有这么多讲究 - 第408篇

SpringBoot 使用validation数据校验之自定义校验注解·源码分析+实例 - 第410篇

SpringBoot 使用validation数据校验之国际化问题怎么搞?满满的干货,值得收藏 - 第411篇

什么是轮询、长轮询、长连接一篇文章让你不在懵懂 - 第412篇

Spring Boot使用Servlet居然也可以实现长轮询,敲了5年代码,我居然不知道 - 第413篇

悟纤:师傅,上一节偷懒了,在评论区你被点名了。

师傅:纳尼,师傅太难了,这一节为师要拿出看家的本领了。

悟纤:师傅,那你加油ヾ(◍°∇°◍)ノ゙,我在旁边给你助威。不知道师傅今天要讲什么?

师傅:今天咱们就聊聊长轮询的实现方式一,使用Servlet进行长轮询的实现。

悟纤:那师傅咱们就赶紧吧,我已经迫不及待了。

 

导读

         在前面的小节中,我们对于长轮询有了一个基本的认知,那么具体长轮询在代码上怎么实现呢?这一节我们会使用Servlet的异步处理。

         具体点来说,我们会使用Spring Boot构建一个基本框架,然后使用Servlet的异步处理来实现长轮询。

         长轮询系列:

(1)✅《什么是轮询、长轮询、长连接一篇文章让你不在懵懂》

(2)✅《Spring Boot使用Servlet居然也可以实现长轮询》

(3)「待定」《Spring Boot使用DeferredResult实现长轮询》

(4)「待拟定」…

这一节我们先来看看《Spring Boot使用Servlet居然也可以实现长轮询》。

一、悟纤十万个疑问

悟纤:师傅,有点疑问,想问下。

师傅:有问题,不错,说明你思考了。

悟纤:同步的Servlet是否可以实现长轮询?

师傅:可以,我们在前一节讲过了长轮询就是hold住连接了。可能有些人对于hold住连接不是很理解。Hold的意思有持有、抓住、拿着… ,那么hold住连接,就是一直持有连接的意思了。也就是对于对于客户端和服务端一直保持着连接,至于如何一直保持连接呢?往下看就懂了。所以对于长轮询只要能够Hold住连接就可以了跟是否是异步实现没有关系。

悟纤:既然同步的Servlet可以实现长轮询,那为啥不使用同步的方式呐?

师傅:占用了容器的线程了,比如容器支持100个线程并发,那么都是长轮询最多就支持100的连接了,再加上我们还有其它的业务处理需要建立连接,要是这样子的系统基本不可用了。

悟纤:为什么用Servlet异步处理实现长轮询?

师傅:这样子就能够释放掉容器的线程,开启新的线程以此来提高Web容器的响应能力。

悟纤:最后一个问题哦,师傅看你这里要使用Spring Boot要使用来实现,这是Servlet的异步处理和Spring Boot关系有关系吗?

师傅:这里不要误解了哦,Servlet的异步处理是Servlet3.0本身就具备的处理能力,和使用什么框架无关哦,你使用java web构建一个普通的web项目也是可以实现的,这里使用Spring Boot是因为写例子的时候比较方便,比较快速。

悟纤:师傅,你是我的偶像呢,那我没啥问题了,可以开始我们的快乐之旅了。

二、为什么需要异步处理请求

         在Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由某一个线程从头到尾负责处理。如果一个请求需要进行IO操作,比如访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待IO操作完成, 而IO操作是非常慢的,所以此时的线程并不能及时地释放回线程池以供后续使用。在并发量越来越大的情况下,这将带来严重的性能问题。即便是像Spring、Struts这样的高层框架也脱离不了这样的桎梏,因为他们都是建立在Servlet之上的。为了解决这样的问题,Servlet 3.0引入了异步处理,然后在Servlet 3.1中又引入了非阻塞IO来进一步增强异步处理的性能。

Servlet 3.0 开始提供了AsyncContext用来支持异步处理请求,那么异步处理请求到底能够带来哪些好处?

         Web容器一般来说处理请求的方式是:为每个request分配一个thread。我们都知道thread的创建不是没有代价的,Web容器的thread pool都是有上限的

         那么一个很容易预见的问题就是,在高负载情况下,thread pool都被占着了,那么后续的request就只能等待,如果运气不好客户端会报等待超时的错误。

         在AsyncContext出现之前,解决这个问题的办法就是扩充Web容器的thread pool

         但是这样依然有一个问题,考虑以下场景:

         有一个web容器,线程池大小200。有一个web app,它有两个servlet,Servlet-A处理单个请求的时间是10s,Servlet-B处理单个请求的时间是1s。

         现在遇到了高负载,有超过200个request到Servlet-A,如果这个时候请求Servlet-B就会等待,因为所有HTTP thread都已经被Servlet-A占用了。

         这个时候工程师发现了问题,扩展了线程池大小到400,但是负载依然持续走高,现在有400个request到Servlet-A,Servlet-B依然无法响应。

         看到问题了没有,因为HTTP thread和Worker thread耦合在了一起(就是同一个thread),所以导致了当大量request到一个耗时操作时,就会将HTTP thread占满,导致整个Web容器就会无法响应。

         但是如果使用AsyncContext,我们就可以将耗时的操作交给另一个thread去做,这样HTTP thread就被释放出来了,可以去处理其他请求了。

三、Servlet异步处理请求

3.1 例子说明

         接下里我们会使用一个小栗子来演示使用Servelt的异步处理来实现长轮询。

         对于这个例子先总体的说明下:

(1)有一个页面会使用ajax定时的请求后台,5秒一请求,看是否有新的信息发布。

(2)后端接收到请求之后会使用Servlet的异步处理请求,如果此时没有新的信息,那么使用while(true)的方式hold住连接。

(3)打开新的一个窗口,调用发布新的消息的请求发布新消息。

(4)此时ajax定时请求的页面,应该会及时的显示新的信息。

3.2 环境说明

(1)OS:Mac OS

(2)开发工具:IntelliJ Idea

(3)JDK:1.8

(4)Spring Boot:2.6.1

3.3 开发步骤

(1)构建一个基本的Spring Boot框架

(2)构建一个Servlet

(3)构建一个发布请求的Controller

(4)构建一个页面定时请求后台的Servlet

(5)启动测试

3.4 开发实操

3.4.1构建一个基本的Spring Boot框架

         使用开发工具构建一个基本的Spring Boot项目,这一步没啥好说的,

         还需要操作的就是在启动类添加@ServletComponentScan注解以此来扫描我们自定义的Servlet:

@SpringBootApplication
@ServletComponentScan//这个就是扫描相应的Servlet包;

         对于Spring Boot使用自定义的Servlet有两种方式,可以看原来的文章:

20. Spring Boot Servlet【从零开始学Spring Boot】_悟空学院-CSDN博客

3.4.2构建一个Servlet

         构建一个Servlet,我们先看下源码然后在进行说明:

package com.kfit.springbootlongpollingdemo;


import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;

/**
 * (异步)长训轮servlet
 *
 * @author 悟纤「公众号SpringBoot」
 * @date 2022-1-6
 * @slogan 大道至简 悟在天成
 */
//【1】要设置为支持asyncSupported,以便支持通过异步线程返回HTTP 200 OK
@WebServlet(urlPatterns = {"/asyncLongPollingServlet"},asyncSupported = true)
public class AsyncLongPollingServlet extends HttpServlet {

    public  static String message = "welcome";



    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //【2】建立AsyncContext
        //AsyncContext asyncContext = req.startAsync(req,resp);
        AsyncContext asyncContext = req.startAsync();

        //【3】设置timeout,单位ms,如果在timeout时间之前,异步线程不能完成处理,则会抛出异常。
        long sleepTime = 4700;//定义超时时间(毫秒)
        //asyncContext.setTimeout(sleepTime-200);//设置超时时间.

        // 【4】设置Listner跟踪状态,一般情况不需要
        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                //调用了asyncContext.complete()之后调用
                System.out.println("AsyncLongPollingServlet.onComplete");
            }

            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                //超时了调用
                System.out.println("AsyncLongPollingServlet.onTimeout");
            }

            @Override
            public void onError(AsyncEvent event) throws IOException {
                System.out.println("AsyncLongPollingServlet.onError");
            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
                // 不会看到有相关的打印信息,根据注解
                // Notifies this AsyncListener that a new asynchronous cycle is being initiated via
                // a call to one of the ServletRequest.startAsync methods
                // 这是在前面调用的,已经是历史了
                System.out.println("AsyncLongPollingServlet.onStartAsync");
            }
        });

        //【5】启动异步线程进行处理
        asyncContext.start(new Runnable() {
            @Override
            public void run() {

                //【5.1】hold住连接直到超时了
                long curSleepTime = 0;
                boolean timeout = false;
                while(message == null){
                    curSleepTime +=  100;
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(curSleepTime>=(sleepTime)){
                        timeout=true;
                        break;
                    }
                }
                if(timeout){//超时了,直接返回即可.
                    asyncContext.complete();
                    return;
                }

                //【5.2】获取ServletResponse,返回信息
                PrintWriter pw = null;
                try {

                    pw = asyncContext.getResponse().getWriter();

                    if(message == null){
                        pw.write("");
                    }else{
                        pw.write(message);
                    }
                    pw.flush();

                    // 【5.3】必须明确通知AsyncContext已经完成
                    asyncContext.complete();
                    message = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

说明:

【1】异步要设置为支持asyncSupported,以便支持通过异步线程返回HTTP 200 OK。

【2】异步的处理类AsyncContext,通过request. startAsync()获取。

【3】设置超时时间asyncContext.setTimeout(sleepTime),避免前端已经超时了,后端还持有着,这里我们是下面的while(true)的逻辑中自己实现了一个超时结束。

【4】设置Listner跟踪状态,一般情况不需要,asyncContext.addListener()。

【5】启动异步线程进行处理:在这里可以处理比较长的业务逻辑,也可以使用while(true)的方式一直持有。当然这里的while true也写了一段代码以此来结束这个循环。

 

3.4.3构建一个发布请求的Controller

         我们在前面定义了一个静态变量,这个变量的值一改变,请求就会返回,前端接收到请求之后会再次发起请求:

package com.kfit.springbootlongpollingdemo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 控制层
 *
 * @author 悟纤「公众号SpringBoot」
 * @date 2022-1-6
 * @slogan 大道至简 悟在天成
 */
@Controller
public class IndexController {

    @RequestMapping({"/index"})
    public String index(){
        System.out.println("IndexController.index");
       return "index";
    }


    @RequestMapping({"/publishMsg"})
    @ResponseBody
    public String publishMsg(String message){
          AsyncLongPollingServlet.message  = message;
       return "OK";
    }
}

3.4.4构建一个页面定时请求后台的Servlet

         看下index.html的代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>长轮询</title>
</head>
<body>

    <b>长轮询小栗子</b>
    <div id="message"></div>

    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
    <script>

        $(function () {

            function getMessage() {
                $.ajax({
                    //  /longPollingServlet
                    url:"/asyncLongPollingServlet"
                    ,data:{}
                    ,type:"get"
                    ,timeout:5000 //定义超时时间为5秒
                    ,success:function(rs){
                        if(rs !=''){
                            $("#message").append("<p>"+rs+"</p>");
                        }
                    }
                    ,complete:function(rs){
                        console.log("重新发起");
                        getMessage();
                    }
                });
            }

            getMessage();
        });


    </script>

</body>
</html>

说明:

(1)使用了jquery的ajax请求后台请求。

(2)对于长轮询前端做了什么呢?其一就是请求返回之后再次发起请求以此hold连接;其二就是定义了一个超时时间timeout,超时之后也会再次发起请求。这里不管是请求成功了还是超时了,jquery的ajax都会执行complete方法。

3.4.5启动测试

         启动应用,然后访问地址:

http://127.0.0.1:8080/index

         一开始的效果图是这样子的:

         然后在执行一个打开新的tab,请求如下地址:

http://127.0.0.1:8080/publishMsg?message=love

悟纤小结

         最后总结下几个要点:

(1)servlet是在3.0才有的异步处理。

(2)异步处理的核心类是AsyncContext,可以通过req.startAsync()获得。

(3)后端hold住连接的核心逻辑就是while(true)直到超时。

(4)前端hold住连接的核心逻辑其一就是处理完请求之后紧接着发起请求,其二就是超时了在重新发起请求。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟纤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值