4_webflux服務端開發

weixin_34007291發表於2018-07-22

一 初識Web Flux

  1. 概念
    Web Flux是Spring5.0提出的新的開發web的技術棧,它是一種非阻塞的開發模式,它執行在Netty或者Servlet 3.1的容器上邊,支援非常高的併發量,也就是說我們現在開發Web應用又有了一種新的開發模式,可以使用之前的mvc開發模式也可以使用web Flux開發模式;
  1. Web Flux 與 MVC的區別
    2.1 開發方式的不同:Web Flux是非同步非阻塞的開發模式,MVC同步的阻塞的I/O開發模式;
    2.2 執行環境的不同:MVC的執行環境是基於Servlet API的,所以它必須執行在Servlet容器上邊,而Web Flux的開發模式是基於Reactive Stream流的方式,它可以執行在Netty或者Servlet 3.1及以後版本的容器上邊;其實Spring預設的容器就是Netty;
    2.3 資料庫型別的不同:關係型資料庫(mysql等)暫時不支援響應式的開發模式;
  1. Web Flux的優勢
    支援非常高的併發量;

二 非同步Servlet

  1. 為什麼要使用非同步Servlet?
    因為使用非同步Servlet不會阻塞Tomcat的Servlet執行緒,所以非同步Servlet可以達到非常高的吞吐量,可以處理非常高的併發;
  1. 同步Servlet阻塞了什麼?
    2.1 其實阻塞的是Tomcat容器的Servlet執行緒,當有網路請求傳送到Tomcat以後,Tomcat會為每一個請求分配一個執行緒去處理,執行緒會呼叫對應的Servlet容器去處理,在這個處理的過程中,相關的業務程式碼所花費的時間就是Servlet執行緒等待的時間,這就是同步Servlet的阻塞;

程式碼演示:

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class SyncServlet
 */
@WebServlet("/SyncServlet")
public class SyncServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public SyncServlet() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        long t1 = System.currentTimeMillis();

        // 執行業務程式碼
        doSomeThing(request, response);

        System.out.println("sync use:" + (System.currentTimeMillis() - t1));
    }

    private void doSomeThing(HttpServletRequest request,
            HttpServletResponse response) throws IOException {

        // 模擬耗時操作
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
        }

        response.getWriter().append("done");
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
}
  1. 非同步Servlet是怎麼工作的?
    因為使用非同步Servlet不會阻塞Tomcat的Servlet執行緒,它會將請求放在獨立的執行緒池中(特別是對於比較耗時的操作),結果會立馬被返回,結果返回後就接著處理下一個請求,所以非同步Servlet可以達到非常高的吞吐量;

程式碼演示:

import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class AsyncServlet
 */
@WebServlet(asyncSupported = true, urlPatterns = { "/AsyncServlet" })
public class AsyncServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public AsyncServlet() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        long t1 = System.currentTimeMillis();

        // 開啟非同步
        AsyncContext asyncContext = request.startAsync();

        // ִ執行業務程式碼
        CompletableFuture.runAsync(() -> doSomeThing(asyncContext,
                asyncContext.getRequest(), asyncContext.getResponse()));

        System.out.println("async use:" + (System.currentTimeMillis() - t1));
    }

    private void doSomeThing(AsyncContext asyncContext,
            ServletRequest servletRequest, ServletResponse servletResponse) {

        // 模擬耗時操作
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
        }

        //
        try {
            servletResponse.getWriter().append("done");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 業務程式碼處理完畢,通知結束
        asyncContext.complete();
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
}

三 Web Fulx開發效率對比

package com.imooc;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@Slf4j
public class TestController {

    @GetMapping("/1")
    private String get1() {
        log.info("get1 start");
        String result = createStr();
        log.info("get1 end.");
        return result;
    }

    @GetMapping("/2")
    private Mono<String> get2() {
        log.info("get2 start");
        Mono<String> result = Mono.fromSupplier(() -> createStr());
        log.info("get2 end.");
        return result;
    }

    /**
     * Flux : 返回0-n個元素
     * 
     * @return
     */
    @GetMapping(value = "/3", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    private Flux<String> flux() {
        Flux<String> result = Flux
                .fromStream(IntStream.range(1, 5).mapToObj(i -> {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                    }
                    return "flux data--" + I;
                }));
        return result;
    }

    private String createStr() {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
        }
        return "some string";
    }
}
12420747-3791a138e379d6ba.png
普通方式呼叫
12420747-526b2d725d6efd33.png
Web Flux呼叫
12420747-82d0309e0170a217.png
執行效率結果展示
從上邊可以看出,get1(正常模式)執行時間為5秒,get2(Web Flux模式)執行基本沒有時間損耗,效能更高;關於原因已經在上邊進行了闡述這裡就不在進行說明了;
12420747-bad872e172aeeb05.png
Flux的使用展示

四 SSE(server-sent events)

  1. SSE的本質
    伺服器向客戶端宣告,接下來要傳送的是流資訊(streaming),也就是說,傳送的不是一次性的資料包,而是一個資料流,會連續不斷地傳送過來。這時,客戶端不會關閉連線,會一直等著伺服器發過來的新的資料流,視訊播放就是這樣的例子。
  1. SSE的服務端實現
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class SSE
 */
@WebServlet("/SSE")
public class SSE extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public SSE() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // 實現SSE必須要設定的內容
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");

        for (int i = 0; i < 5; i++) {
            // 指定事件標識
            response.getWriter().write("event:me\n");
            // 格式: data: + 資料 + 兩個回車
            response.getWriter().write("data:" + i + "\n\n");
            response.getWriter().flush();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        }

    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
}
  1. SSE的前端實現(基於H5)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <script type="text/javascript">
        // 初始化, 引數為url
        // 依賴H5
        var sse = new EventSource("SSE");
        sse.onmessage = function(e) {
            console.log("message", e.data, e);
        }

        // 監聽指定事件, (就不會進入onmessage了)
        sse.addEventListener("me", function(e) {
            console.log("me event", e.data);
            // 如果不關閉,會自動重連
            if (e.data == 3) {
                sse.close();
            }
        });
    </script>
</body>
</html>
  1. 效果展示
    12420747-016578960663513b.png
    SSE實現效果展示1
    12420747-4d01b21bba0d6960.png
    SSE實現效果展示2

    我們可以自定義我們的訊息返回,請看圖示
    12420747-a07dece1787686ec.png
    SSE實現效果展示3
    但是此時我們發現一個問題,就是如果不主動關閉的話,瀏覽器會一直的接受這個流,所以我們需要手動關閉,當執行到4的時候又從頭開始接受了,所以我們限制一下讓他接受到3的時候就關閉這個動作
    12420747-a516168aa54af55b.png
    SSE實現效果展示4
  1. 更多關於SSE的問題請參考阮一峰的網路日誌

相關文章