Описание проблемы
Servlet-3.0 API позволяет отделить контекст запроса/ответа и ответить на него позже.
Однако, если я попытаюсь написать большой объем данных, что-то вроде:
AsyncContext ac = getWaitingContext() ;
ServletOutputStream out = ac.getResponse().getOutputStream();
out.print(some_big_data);
out.flush()
Он может блокировать - и он блокирует тривиальные тестовые примеры - как для Tomcat 7, так и для Jetty 8. Учебники рекомендуют создать пул потоков, который обрабатывать такую настройку - ведьма обычно является контрположительной для традиционной архитектуры 10K.
Однако, если у меня есть 10 000 открытых подключений и пул потоков, пусть говорят 10 потоков, достаточно даже для 1% клиентов, имеющих низкоскоростные соединения или просто заблокированные соединение для блокировки пула потоков и полностью блокировать реакцию кометы или значительно замедлить его.
Ожидаемая практика заключается в получении уведомления о готовности к записи или уведомлении о завершении ввода-вывода и чем продолжать нажимать данные.
Как это можно сделать с помощью API Servlet-3.0, то есть как я могу получить:
- Уведомление о асинхронном завершении операции ввода-вывода.
- Получите неблокирующий ввод-вывод с уведомлением о готовности.
Если это не поддерживается API-интерфейсом Servlet-3.0, существуют ли какие-либо API-интерфейсы для веб-сервера (например, Jetty Continuation или Tomcat CometEvent), которые позволяют обрабатывать такие события по-настоящему асинхронно, не создавая асинхронный ввод-вывод с использованием пула потоков.
Кто-нибудь знает?
И если это невозможно, вы можете подтвердить его ссылкой на документацию?
Демонстрация проблемы в примерном коде
Я добавил код ниже, который эмулирует поток событий.
Примечания:
- он использует
ServletOutputStream
, который бросаетIOException
для обнаружения отключенных клиентов - отправляет сообщения
keep-alive
, чтобы убедиться, что клиенты все еще там - Я создал пул потоков для "эмулирования" асинхронных операций.
В таком примере я явно определил пул потоков размером 1, чтобы показать проблему:
- Запустить приложение
- Запуск от двух терминалов
curl http://localhost:8080/path/to/app
(дважды) - Теперь отправьте данные с помощью
curd -d m=message http://localhost:8080/path/to/app
- Оба клиента получили данные
- Теперь приостановите действие одного из клиентов (Ctrl + Z) и отправьте сообщение еще раз
curd -d m=message http://localhost:8080/path/to/app
- Обратите внимание, что другой не приостановленный клиент либо ничего не получил, либо после того, как сообщение было передано, перестали получать запросы keep-alive, потому что другой поток заблокирован.
Я хочу решить такую проблему без использования пула потоков, потому что с 1000-5000 открытыми соединения. Я могу очень быстро исчерпать поток.
Пример кода ниже.
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
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 javax.servlet.ServletOutputStream;
@WebServlet(urlPatterns = "", asyncSupported = true)
public class HugeStreamWithThreads extends HttpServlet {
private long id = 0;
private String message = "";
private final ThreadPoolExecutor pool =
new ThreadPoolExecutor(1, 1, 50000L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
// it is explicitly small for demonstration purpose
private final Thread timer = new Thread(new Runnable() {
public void run()
{
try {
while(true) {
Thread.sleep(1000);
sendKeepAlive();
}
}
catch(InterruptedException e) {
// exit
}
}
});
class RunJob implements Runnable {
volatile long lastUpdate = System.nanoTime();
long id = 0;
AsyncContext ac;
RunJob(AsyncContext ac)
{
this.ac = ac;
}
public void keepAlive()
{
if(System.nanoTime() - lastUpdate > 1000000000L)
pool.submit(this);
}
String formatMessage(String msg)
{
StringBuilder sb = new StringBuilder();
sb.append("id");
sb.append(id);
for(int i=0;i<100000;i++) {
sb.append("data:");
sb.append(msg);
sb.append("\n");
}
sb.append("\n");
return sb.toString();
}
public void run()
{
String message = null;
synchronized(HugeStreamWithThreads.this) {
if(this.id != HugeStreamWithThreads.this.id) {
this.id = HugeStreamWithThreads.this.id;
message = HugeStreamWithThreads.this.message;
}
}
if(message == null)
message = ":keep-alive\n\n";
else
message = formatMessage(message);
if(!sendMessage(message))
return;
boolean once_again = false;
synchronized(HugeStreamWithThreads.this) {
if(this.id != HugeStreamWithThreads.this.id)
once_again = true;
}
if(once_again)
pool.submit(this);
}
boolean sendMessage(String message)
{
try {
ServletOutputStream out = ac.getResponse().getOutputStream();
out.print(message);
out.flush();
lastUpdate = System.nanoTime();
return true;
}
catch(IOException e) {
ac.complete();
removeContext(this);
return false;
}
}
};
private HashSet<RunJob> asyncContexts = new HashSet<RunJob>();
@Override
public void init(ServletConfig config) throws ServletException
{
super.init(config);
timer.start();
}
@Override
public void destroy()
{
for(;;){
try {
timer.interrupt();
timer.join();
break;
}
catch(InterruptedException e) {
continue;
}
}
pool.shutdown();
super.destroy();
}
protected synchronized void removeContext(RunJob ac)
{
asyncContexts.remove(ac);
}
// GET method is used to establish a stream connection
@Override
protected synchronized void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Content-Type header
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");
// Access-Control-Allow-Origin header
response.setHeader("Access-Control-Allow-Origin", "*");
final AsyncContext ac = request.startAsync();
ac.setTimeout(0);
RunJob job = new RunJob(ac);
asyncContexts.add(job);
if(id!=0) {
pool.submit(job);
}
}
private synchronized void sendKeepAlive()
{
for(RunJob job : asyncContexts) {
job.keepAlive();
}
}
// POST method is used to communicate with the server
@Override
protected synchronized void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
request.setCharacterEncoding("utf-8");
id++;
message = request.getParameter("m");
for(RunJob job : asyncContexts) {
pool.submit(job);
}
}
}
В приведенном выше примере используются потоки для предотвращения блокировки... Однако, если количество блокирующих клиентов больше, чем размер пула потоков, он блокирует.
Как это можно реализовать без блокировки?