Long Polling長輪詢實現進階

Long Polling長輪詢實現進階

簡書 滌生
轉載請注明原創出處,謝謝!
如果讀完覺得有收獲的話,歡迎點贊加關注。

介紹

由于Long Polling長輪詢詳解 這篇文章中的code實現較為簡單,尤其是服務端處理較為粗暴,有一些同學反饋希望服務端處理阻塞這塊內容進行更深入討論等等,所以這里專門補一篇實現進階,讓大家對長輪詢有更加深刻的理解。

疑問

對上篇文章,同學反饋有兩個疑問。

  • 服務端實現使用的是同步servlet,性能比較差,能支撐的連接數比較少?
    同步servlet來hold請求,確實會導致后續請求得不到及時處理,servlet3.0開始支持異步處理,可以更高效的處理請求。

  • 服務端如何去hold住請求,sleep好嗎?
    同步servlet hold住請求的處理邏輯必須在servlet的doGet方法中,一般先fetch數據,準備好了,就返回,沒準備好,就sleep片刻,再來重復。
    異步servlet hold住請求比較簡單,只要開啟異步,執行完doGet方法后,不會自動返回此次請求,需要等到請求的context被complete,這樣很巧妙的請求就自動hold住了。

實現

  • 客戶端實現
    客戶端實現基本和上篇差不多,沒什么改變。
package com.andy.example.longpolling.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by andy on 17/7/8.
 */
public class AbstractBootstrap {
    
    //同步URL
    protected static final String URL = "http://localhost:8080/long-polling";
    //異步URL
    protected static final String ASYNC_URL = "http://localhost:8080/long-polling-async";

    private final AtomicLong sequence = new AtomicLong();

    protected void poll() {
        //循環執行,保證每次longpolling結束,再次發起longpolling
        while (!Thread.interrupted()) {
            doPoll();
        }
    }

    protected void doPoll() {
        System.out.println("第" + (sequence.incrementAndGet()) + "次 longpolling");

        long startMillis = System.currentTimeMillis();

        HttpURLConnection connection = null;
        try {
            URL getUrl = new URL(URL);
            connection = (HttpURLConnection) getUrl.openConnection();

            //50s作為長輪詢超時時間
            connection.setReadTimeout(50000);
            connection.setConnectTimeout(3000);
            connection.setRequestMethod("GET");
            connection.setUseCaches(false);
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
            connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");
            connection.connect();

            if (200 == connection.getResponseCode()) {
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
                    StringBuilder result = new StringBuilder(256);
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        result.append(line);
                    }

                    System.out.println("結果 " + result);

                } finally {
                    if (reader != null) {
                        reader.close();
                    }
                }
            }
        } catch (IOException e) {
            System.out.println("request failed");
        } finally {
            long elapsed = (System.currentTimeMillis() - startMillis) / 1000;
            System.out.println("connection close" + "     " + "elapse " + elapsed + "s");
            if (connection != null) {
                connection.disconnect();
            }
            System.out.println();
        }
    }

}


package com.andy.example.longpolling.client;

import java.io.IOException;

/**
 * Created by andy on 17/7/6.
 */
public class ClientBootstrap extends AbstractBootstrap {


    public static void main(String[] args) throws IOException {
        ClientBootstrap bootstrap = new ClientBootstrap();
        //發起longpolling 
        bootstrap.poll();

        System.in.read();
    }

}

  • 服務端實現

長輪詢服務端同步servlet處理

服務端同步servlet和上篇差不多,沒什么改動,增加了相關注釋。

package com.andy.example.longpolling.server;

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.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by andy on 17/7/6.
 */
@WebServlet(urlPatterns = "/long-polling")
public class LongPollingServlet extends HttpServlet {

    private final Random random = new Random();

    private final AtomicLong sequence = new AtomicLong();

    private final AtomicLong value = new AtomicLong();


    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println();
        final long currentSequence = sequence.incrementAndGet();
        System.out.println("第" + (currentSequence) + "次 longpolling");

        //由于客戶端設置的超時時間是50s,
        //為了更好的展示長輪詢,這邊random 100,模擬服務端hold住大于50和小于50的情況。
        //再具體場景中,這塊在具體實現上,
        //對于同步servlet,首先這里必須阻塞,因為一旦doGet方法走完,容器就認為可以結束這次請求,返回結果給客戶端。
        //所以一般實現如下:
        // while(結束){ //結束條件,超時或者拿到數據
        //    data = fetchData();
        //    if(data == null){
        //       sleep();
        //    }
        // }
    
        int sleepSecends = random.nextInt(100);

        System.out.println(currentSequence + " wait " + sleepSecends + " second");

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

        }

        PrintWriter out = response.getWriter();
        long result = value.getAndIncrement();
        out.write(Long.toString(result));
        out.flush();
    }

}

長輪詢服務端異步servlet處理

由于同步servlet,性能較差,所有的請求操作必須在doGet方法中完成,包括等待數據,占用了容器的處理線程,會導致后續的請求阻塞,來不及處理。servlet 3.0支持異步處理,使用異步處理doGet方法執行完成后,結果也不會返回到客戶端,會等到請求的context被complete才會寫回客戶端,這樣一來,容器的處理線程不會受阻,請求結果可由另外的業務線程進行寫回,也就輕松實現了hold操作。

package com.andy.example.longpolling.server;

import javax.servlet.*;
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.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by andy on 17/7/7.
 */

/**
 * 開啟異步servlet,asyncSupported = true
 */
@WebServlet(urlPatterns = "/long-polling-async", asyncSupported = true)
public class LongPollingAsyncServlet extends HttpServlet {

    private Random random = new Random();

    private final AtomicLong sequence = new AtomicLong();

    private final AtomicLong value = new AtomicLong();

    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L,
            TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100));

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println();
        final long currentSequence = sequence.incrementAndGet();
        System.out.println("第" + (currentSequence) + "次 longpolling async");
        //設置request異步處理
        AsyncContext asyncContext = request.startAsync();

        //異步處理超時時間,這里需要注意,jetty容器默認的這個值設置的是30s,
        //如果超時,異步處理沒有完成(通過是否asyncContext.complete()來進行判斷),將會重試(會再次調用doGet方法)。
        //這里由于客戶端long polling設置的是50s,所以這里如果小于50,會導致重試。
        asyncContext.setTimeout(51000);
        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {

            }

            //超時處理,注意asyncContext.complete();,表示請求處理完成
            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                AsyncContext asyncContext = event.getAsyncContext();
                asyncContext.complete();
            }

            @Override
            public void onError(AsyncEvent event) throws IOException {

            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {

            }
        });
        
        //提交線程池異步寫會結果
        //具體場景中可以有具體的策略進行操作
        executor.submit(new HandlePollingTask(currentSequence, asyncContext));

    }

    class HandlePollingTask implements Runnable {

        private AsyncContext asyncContext;

        private long sequense;

        public HandlePollingTask(long sequense, AsyncContext asyncContext) {
            this.sequense = sequense;
            this.asyncContext = asyncContext;
        }

        @Override
        public void run() {
            try {
                //通過asyncContext拿到response
                PrintWriter out = asyncContext.getResponse().getWriter();
                int sleepSecends = random.nextInt(100);


                System.out.println(sequense + " wait " + sleepSecends + " second");

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

                }

                long result = value.getAndIncrement();

                out.write(Long.toString(result));

            } catch (Exception e) {
                System.out.println(sequense + "handle polling failed");
            } finally {
                //數據寫回客戶端
                asyncContext.complete();
            }
        }
    }

}

結果

  • 同步servlet實現結果
同步servlet客戶端結果
同步servlet服務端結果
  • 異步servlet實現結果
異步servlet客戶端結果
異步servlet服務端結果

個人微信公共號,感興趣的關注下,獲取更多技術文章

滌生-微信公共號
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • 基于HTTP的長連接,是一種通過長輪詢方式實現"服務器推"的技術,它彌補了HTTP簡單的請求應答模式的不足,極大地...
    永遠的冷冽閱讀 1,167評論 0 6
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,356評論 11 349
  • 一. Java基礎部分.................................................
    wy_sure閱讀 3,833評論 0 11
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,754評論 18 399