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實現結果
個人微信公共號,感興趣的關注下,獲取更多技術文章