/* * Copyright (C) 2015 SoftIndex LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.datakernel.http; import io.datakernel.async.ResultCallback; import io.datakernel.bytebuf.ByteBuf; import io.datakernel.bytebuf.ByteBufPool; import io.datakernel.eventloop.Eventloop; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Random; import static io.datakernel.bytebuf.ByteBufPool.*; import static io.datakernel.bytebuf.ByteBufStrings.decodeAscii; import static io.datakernel.bytebuf.ByteBufStrings.encodeAscii; import static io.datakernel.eventloop.FatalErrorHandlers.rethrowOnAnyError; import static io.datakernel.http.TestUtils.readFully; import static io.datakernel.http.TestUtils.toByteArray; import static java.lang.Math.min; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class AsyncHttpServerTest { @Before public void before() { ByteBufPool.clear(); ByteBufPool.setSizes(0, Integer.MAX_VALUE); } public static AsyncHttpServer blockingHttpServer(Eventloop primaryEventloop, int port) { AsyncServlet servlet = new AsyncServlet() { @Override public void serve(HttpRequest request, ResultCallback<HttpResponse> callback) { HttpResponse content = HttpResponse.ok200().withBody(encodeAscii(request.getUrl().getPathAndQuery())); callback.setResult(content); } }; return AsyncHttpServer.create(primaryEventloop, servlet).withListenPort(port); } public static AsyncHttpServer asyncHttpServer(final Eventloop primaryEventloop, int port) { AsyncServlet servlet = new AsyncServlet() { @Override public void serve(final HttpRequest request, final ResultCallback<HttpResponse> callback) { final HttpResponse content = HttpResponse.ok200().withBody(encodeAscii(request.getUrl().getPathAndQuery())); primaryEventloop.post(new Runnable() { @Override public void run() { callback.setResult(content); } }); } }; return AsyncHttpServer.create(primaryEventloop, servlet).withListenPort(port); } public static AsyncHttpServer delayedHttpServer(final Eventloop primaryEventloop, int port) { final Random random = new Random(); AsyncServlet servlet = new AsyncServlet() { @Override public void serve(final HttpRequest request, final ResultCallback<HttpResponse> callback) { final HttpResponse content = HttpResponse.ok200().withBody(encodeAscii(request.getUrl().getPathAndQuery())); primaryEventloop.schedule(primaryEventloop.currentTimeMillis() + random.nextInt(3), new Runnable() { @Override public void run() { callback.setResult(content); } }); } }; return AsyncHttpServer.create(primaryEventloop, servlet).withListenPort(port); } public static void writeByRandomParts(Socket socket, String string) throws IOException { ByteBuf buf = ByteBuf.wrapForReading(encodeAscii(string)); Random random = new Random(); while (buf.canRead()) { int count = min(1 + random.nextInt(5), buf.readRemaining()); socket.getOutputStream().write(buf.array(), buf.readPosition(), count); buf.moveReadPosition(count); } } public static void readAndAssert(InputStream is, String expected) throws IOException { byte[] bytes = new byte[expected.length()]; readFully(is, bytes); Assert.assertEquals(expected, decodeAscii(bytes)); } @Test public void testKeepAlive_Http_1_0() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); doTestKeepAlive_Http_1_0(eventloop, blockingHttpServer(eventloop, port), port); doTestKeepAlive_Http_1_0(eventloop, asyncHttpServer(eventloop, port), port); doTestKeepAlive_Http_1_0(eventloop, delayedHttpServer(eventloop, port), port); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } private void doTestKeepAlive_Http_1_0(Eventloop eventloop, AsyncHttpServer server, int port) throws Exception { server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.setTcpNoDelay(true); socket.connect(new InetSocketAddress(port)); for (int i = 0; i < 200; i++) { writeByRandomParts(socket, "GET /abc HTTP1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\n/abc"); } writeByRandomParts(socket, "GET /abc HTTP1.1\r\nHost: localhost\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\n/abc"); // ? assertTrue(toByteArray(socket.getInputStream()).length == 0); assertTrue(socket.isClosed()); socket.close(); server.closeFuture().get(); thread.join(); } @Test public void testKeepAlive_Http_1_1() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); doTestKeepAlive_Http_1_1(eventloop, blockingHttpServer(eventloop, port), port); doTestKeepAlive_Http_1_1(eventloop, asyncHttpServer(eventloop, port), port); doTestKeepAlive_Http_1_1(eventloop, delayedHttpServer(eventloop, port), port); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } private void doTestKeepAlive_Http_1_1(Eventloop eventloop, AsyncHttpServer server, int port) throws Exception { server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.setTcpNoDelay(true); socket.connect(new InetSocketAddress(port)); for (int i = 0; i < 200; i++) { writeByRandomParts(socket, "GET /abc HTTP/1.1\r\nHost: localhost\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\n/abc"); } writeByRandomParts(socket, "GET /abc HTTP1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\n/abc"); // ? assertTrue(toByteArray(socket.getInputStream()).length == 0); assertTrue(socket.isClosed()); socket.close(); server.closeFuture().get(); thread.join(); } @Test public void testClosed() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); AsyncHttpServer server = blockingHttpServer(eventloop, port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.connect(new InetSocketAddress(port)); writeByRandomParts(socket, "GET /abc HTTP1.1\r\nHost: localhost\r\n"); socket.close(); server.closeFuture().get(); thread.join(); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } @Test public void testNoKeepAlive_Http_1_0() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); AsyncHttpServer server = blockingHttpServer(eventloop, port); server.withListenPort(port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.connect(new InetSocketAddress(port)); writeByRandomParts(socket, "GET /abc HTTP/1.0\r\nHost: localhost\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\n/abc"); assertTrue(toByteArray(socket.getInputStream()).length == 0); socket.close(); server.closeFuture().get(); thread.join(); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } @Test public void testNoKeepAlive_Http_1_1() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); AsyncHttpServer server = blockingHttpServer(eventloop, port); server.withListenPort(port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.connect(new InetSocketAddress(port)); writeByRandomParts(socket, "GET /abc HTTP/1.1\r\nConnection: close\r\nHost: localhost\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\n/abc"); assertTrue(toByteArray(socket.getInputStream()).length == 0); socket.close(); server.closeFuture().get(); thread.join(); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } @Test public void testPipelining() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); // doTestPipelining(eventloop, blockingHttpServer(eventloop)); // doTestPipelining(eventloop, asyncHttpServer(eventloop)); doTestPipelining(eventloop, delayedHttpServer(eventloop, port), port); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } private void doTestPipelining(Eventloop eventloop, AsyncHttpServer server, int port) throws Exception { server.withListenPort(port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.connect(new InetSocketAddress(port)); for (int i = 0; i < 100; i++) { writeByRandomParts(socket, "GET /abc HTTP/1.1\r\nConnection: Keep-Alive\r\nHost: localhost\r\n\r\n" + "GET /123456 HTTP/1.1\r\nHost: localhost\r\n\r\n" + "POST /post1 HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/json\r\n\r\n" + "{\"at\":2}" + "POST /post2 HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/json\r\n\r\n" + "{\"at\":2}" + ""); } for (int i = 0; i < 100; i++) { readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\n/abc"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 7\r\n\r\n/123456"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 6\r\n\r\n/post1"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 6\r\n\r\n/post2"); } server.closeFuture().get(); thread.join(); } // @Test public void testPipelining2() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); // doTestPipelining(eventloop, blockingHttpServer(eventloop)); // doTestPipelining(eventloop, asyncHttpServer(eventloop)); doTestPipelining2(eventloop, delayedHttpServer(eventloop, port), port); assertEquals(getPoolItemsString(), ByteBufPool.getCreatedItems(), ByteBufPool.getPoolItems()); } private void doTestPipelining2(Eventloop eventloop, AsyncHttpServer server, int port) throws Exception { server.withListenPort(port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.connect(new InetSocketAddress(port)); for (int i = 0; i < 100; i++) { writeByRandomParts(socket, "GET /abc HTTP/1.0\r\nHost: localhost\r\n\r\n" + "GET /123456 HTTP/1.1\r\nHost: localhost\r\n\r\n" + "POST /post1 HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/json\r\n\r\n" + "{\"at\":2}" + "POST /post2 HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 8\r\n" + "Content-Type: application/json\r\n\r\n" + "{\"at\":2}" + ""); } for (int i = 0; i < 100; i++) { readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\n/abc"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 7\r\n\r\n/123456"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 6\r\n\r\n/post1"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 6\r\n\r\n/post2"); } server.closeFuture().get(); thread.join(); } @Test public void testBigHttpMessage() throws Exception { int port = (int) (System.currentTimeMillis() % 1000 + 40000); final Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); final ByteBuf buf = HttpRequest.post("http://127.0.0.1:" + port) .withBody(ByteBuf.wrapForReading(encodeAscii("Test big HTTP message body"))) .toByteBuf(); AsyncServlet servlet = new AsyncServlet() { @Override public void serve(final HttpRequest request, final ResultCallback<HttpResponse> callback) { final HttpResponse content = HttpResponse.ok200().withBody(encodeAscii(request.getUrl().getPathAndQuery())); eventloop.post(new Runnable() { @Override public void run() { callback.setResult(content); } }); } }; final AsyncHttpServer server = AsyncHttpServer.create(eventloop, servlet) .withMaxHttpMessageSize(25) .withListenPort(port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(port)); socket.getOutputStream().write(buf.array(), buf.readPosition(), buf.readRemaining()); buf.recycle(); Thread.sleep(100); } server.closeFuture().get(); thread.join(); // assertEquals(1, server.getStats().getHttpErrors().getTotal()); // assertEquals(AbstractHttpConnection.TOO_BIG_HTTP_MESSAGE, // server.getStats().getHttpErrors().getLastException()); assertEquals(getPoolItemsString(), getCreatedItems(), getPoolItems()); } @Test public void testExpectContinue() throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); int port = (int) (System.currentTimeMillis() % 1000 + 40000); AsyncHttpServer server = AsyncHttpServer.create(eventloop, new AsyncServlet() { @Override public void serve(HttpRequest request, ResultCallback<HttpResponse> callback) { callback.setResult(HttpResponse.ok200().withBody(request.detachBody())); } }).withListenPort(port); server.listen(); Thread thread = new Thread(eventloop); thread.start(); Socket socket = new Socket(); socket.setTcpNoDelay(true); socket.connect(new InetSocketAddress(port)); writeByRandomParts(socket, "POST /abc HTTP/1.0\r\nHost: localhost\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n"); readAndAssert(socket.getInputStream(), "HTTP/1.1 100 Continue\r\n\r\n"); writeByRandomParts(socket, "abcde"); readAndAssert(socket.getInputStream(), "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 5\r\n\r\nabcde"); assertTrue(toByteArray(socket.getInputStream()).length == 0); assertTrue(socket.isClosed()); socket.close(); server.closeFuture().get(); thread.join(); } public static void main(String[] args) throws Exception { Eventloop eventloop = Eventloop.create().withFatalErrorHandler(rethrowOnAnyError()); AsyncHttpServer server = blockingHttpServer(eventloop, 8888); server.listen(); eventloop.run(); } }