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实现结果
个人微信公共号,感兴趣的关注下,获取更多技术文章