Long Polling長輪詢實(shí)現(xiàn)進(jìn)階
簡書 滌生。
轉(zhuǎn)載請注明原創(chuàng)出處,謝謝!
如果讀完覺得有收獲的話,歡迎點(diǎn)贊加關(guān)注。
介紹
由于Long Polling長輪詢詳解 這篇文章中的code實(shí)現(xiàn)較為簡單,尤其是服務(wù)端處理較為粗暴,有一些同學(xué)反饋希望服務(wù)端處理阻塞這塊內(nèi)容進(jìn)行更深入討論等等,所以這里專門補(bǔ)一篇實(shí)現(xiàn)進(jìn)階,讓大家對長輪詢有更加深刻的理解。
疑問
對上篇文章,同學(xué)反饋有兩個(gè)疑問。
服務(wù)端實(shí)現(xiàn)使用的是同步servlet,性能比較差,能支撐的連接數(shù)比較少?
同步servlet來hold請求,確實(shí)會(huì)導(dǎo)致后續(xù)請求得不到及時(shí)處理,servlet3.0開始支持異步處理,可以更高效的處理請求。服務(wù)端如何去hold住請求,sleep好嗎?
同步servlet hold住請求的處理邏輯必須在servlet的doGet方法中,一般先fetch數(shù)據(jù),準(zhǔn)備好了,就返回,沒準(zhǔn)備好,就sleep片刻,再來重復(fù)。
異步servlet hold住請求比較簡單,只要開啟異步,執(zhí)行完doGet方法后,不會(huì)自動(dòng)返回此次請求,需要等到請求的context被complete,這樣很巧妙的請求就自動(dòng)hold住了。
實(shí)現(xiàn)
- 客戶端實(shí)現(xiàn)
客戶端實(shí)現(xiàn)基本和上篇差不多,沒什么改變。
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() {
//循環(huán)執(zhí)行,保證每次longpolling結(jié)束,再次發(fā)起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作為長輪詢超時(shí)時(shí)間
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("結(jié)果 " + 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();
//發(fā)起longpolling
bootstrap.poll();
System.in.read();
}
}
- 服務(wù)端實(shí)現(xiàn)
長輪詢服務(wù)端同步servlet處理
服務(wù)端同步servlet和上篇差不多,沒什么改動(dòng),增加了相關(guān)注釋。
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");
//由于客戶端設(shè)置的超時(shí)時(shí)間是50s,
//為了更好的展示長輪詢,這邊random 100,模擬服務(wù)端hold住大于50和小于50的情況。
//再具體場景中,這塊在具體實(shí)現(xiàn)上,
//對于同步servlet,首先這里必須阻塞,因?yàn)橐坏ヾoGet方法走完,容器就認(rèn)為可以結(jié)束這次請求,返回結(jié)果給客戶端。
//所以一般實(shí)現(xiàn)如下:
// while(結(jié)束){ //結(jié)束條件,超時(shí)或者拿到數(shù)據(jù)
// 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();
}
}
長輪詢服務(wù)端異步servlet處理
由于同步servlet,性能較差,所有的請求操作必須在doGet方法中完成,包括等待數(shù)據(jù),占用了容器的處理線程,會(huì)導(dǎo)致后續(xù)的請求阻塞,來不及處理。servlet 3.0支持異步處理,使用異步處理doGet方法執(zhí)行完成后,結(jié)果也不會(huì)返回到客戶端,會(huì)等到請求的context被complete才會(huì)寫回客戶端,這樣一來,容器的處理線程不會(huì)受阻,請求結(jié)果可由另外的業(yè)務(wù)線程進(jìn)行寫回,也就輕松實(shí)現(xiàn)了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");
//設(shè)置request異步處理
AsyncContext asyncContext = request.startAsync();
//異步處理超時(shí)時(shí)間,這里需要注意,jetty容器默認(rèn)的這個(gè)值設(shè)置的是30s,
//如果超時(shí),異步處理沒有完成(通過是否asyncContext.complete()來進(jìn)行判斷),將會(huì)重試(會(huì)再次調(diào)用doGet方法)。
//這里由于客戶端long polling設(shè)置的是50s,所以這里如果小于50,會(huì)導(dǎo)致重試。
asyncContext.setTimeout(51000);
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
}
//超時(shí)處理,注意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 {
}
});
//提交線程池異步寫會(huì)結(jié)果
//具體場景中可以有具體的策略進(jìn)行操作
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 {
//數(shù)據(jù)寫回客戶端
asyncContext.complete();
}
}
}
}
結(jié)果
- 同步servlet實(shí)現(xiàn)結(jié)果


- 異步servlet實(shí)現(xiàn)結(jié)果


個(gè)人微信公共號(hào),感興趣的關(guān)注下,獲取更多技術(shù)文章
