Java tutorial
/* * Copyright (c) 2012 Intellectual Reserve, Inc. All rights reserved. * * 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 cf.component.http; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.CharsetUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.net.SocketAddress; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Provides a simple embeddable HTTP server for handling simple web service end-points and things like publishing * /varz and /healthz information and Cloud Controller REST calls to service gateways. * * @author "Mike Heath" */ public class SimpleHttpServer implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHttpServer.class); // Access must be synchronized on self private final List<RequestHandle> requestHandles = new ArrayList<>(); private final NioEventLoopGroup parentGroup; private final NioEventLoopGroup childGroup; private final Executor executor; public SimpleHttpServer(SocketAddress localAddress) { parentGroup = new NioEventLoopGroup(); childGroup = new NioEventLoopGroup(); bootstrapServer(localAddress, parentGroup, childGroup); executor = createLocalThreadExecutor(); } public SimpleHttpServer(SocketAddress localAddress, NioEventLoopGroup parentGroup, NioEventLoopGroup childGroup, Executor executor) { this.parentGroup = null; this.childGroup = null; bootstrapServer(localAddress, parentGroup, childGroup); if (executor == null) { this.executor = createLocalThreadExecutor(); } else { this.executor = executor; } } private Executor createLocalThreadExecutor() { return new Executor() { @Override public void execute(Runnable command) { command.run(); } }; } private void bootstrapServer(SocketAddress localAddress, NioEventLoopGroup parentGroup, NioEventLoopGroup childGroup) { new ServerBootstrap().group(parentGroup, childGroup).channel(NioServerSocketChannel.class) .localAddress(localAddress).childHandler(new SimpleHttpServerInitializer()).bind() .awaitUninterruptibly(); // Make sure the server is bound before the constructor returns. LOGGER.info("Server listening on {}", localAddress); } @Override public void close() { if (parentGroup != null) { parentGroup.shutdownGracefully(); } if (childGroup != null) { childGroup.shutdownGracefully(); } } public void addHandler(Pattern uriPattern, RequestHandler requestHandler) { LOGGER.debug("Registering pattern {}", uriPattern.pattern()); synchronized (requestHandles) { requestHandles.add(new RequestHandle(uriPattern, requestHandler)); } } private class SimpleHttpServerInitializer extends ChannelInitializer<SocketChannel> { @Override public void initChannel(SocketChannel ch) throws Exception { final ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("decoder", new HttpRequestDecoder()); pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); pipeline.addLast("encoder", new HttpResponseEncoder()); pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); pipeline.addLast("handler", new SimpleHttpServerHandler()); } } private class SimpleHttpServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(final ChannelHandlerContext context, final Object message) throws Exception { final FullHttpRequest request = (FullHttpRequest) message; if (LOGGER.isDebugEnabled()) { LOGGER.debug("Method: {}", request.getMethod()); LOGGER.debug("URI: " + request.getUri()); for (String name : request.headers().names()) { for (String value : request.headers().getAll(name)) { LOGGER.debug("{}: {}", name, value); } } LOGGER.debug("Request body: {}", request.content().toString(CharsetUtil.UTF_8)); LOGGER.debug("=== End of Request ==============="); } if (!request.getDecoderResult().isSuccess()) { sendError(context, HttpResponseStatus.BAD_REQUEST, "Bad request"); return; } final String uri = request.getUri(); final RequestHandler requestHandler; final Matcher uriMatcher; lock: synchronized (requestHandles) { for (RequestHandle handle : requestHandles) { final Matcher matcher = handle.uriPattern.matcher(uri); if (matcher.matches()) { requestHandler = handle.handler; uriMatcher = matcher; break lock; } } requestHandler = null; uriMatcher = null; } if (requestHandler != null) { // Copy buffer to make sure it's accessible if request is handled by another thread. final ByteBuf content = Unpooled.copiedBuffer(request.content()); executor.execute(new Runnable() { @Override public void run() { try { final HttpResponse httpResponse = requestHandler.handleRequest(request, uriMatcher, content); // Close the connection as soon as the message is sent. context.write(httpResponse).addListener(ChannelFutureListener.CLOSE); context.flush(); } catch (Exception e) { exceptionCaught(context, e); } } }); } else { LOGGER.debug("Returning 404"); sendError(context, HttpResponseStatus.NOT_FOUND, "Not found"); } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { LOGGER.debug("Received connection from {}", ctx.channel().remoteAddress()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { LOGGER.debug("Connection to {} closed", ctx.channel().remoteAddress()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { LOGGER.error(cause.getMessage(), cause); if (ctx.channel().isOpen()) { if (cause instanceof RequestException) { sendError(ctx, ((RequestException) cause).getStatus(), cause.getMessage()); } else { sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, cause.getMessage()); } } } private void sendError(ChannelHandlerContext context, HttpResponseStatus status, String message) { final ByteBuf buf = Unpooled.copiedBuffer("Failure: " + message + "\r\n", CharsetUtil.UTF_8); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buf); response.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8"); // Close the connection as soon as the error message is sent. context.write(response).addListener(ChannelFutureListener.CLOSE); context.flush(); } } private class RequestHandle { private final Pattern uriPattern; private final RequestHandler handler; private RequestHandle(Pattern uriPattern, RequestHandler handler) { this.uriPattern = uriPattern; this.handler = handler; } } }