From e68434b77bf6639cbda79e8aa8b96e55270f2a61 Mon Sep 17 00:00:00 2001 From: Abhinav Sarkar Date: Mon, 13 May 2013 02:24:14 +0530 Subject: [PATCH] Initial commit, created a basic server based on netty --- .gitignore | 42 +++++++ pom.xml | 110 ++++++++++++++++++ .../ircsearch/HttpRequestHandler.scala | 71 +++++++++++ .../ircsearch/HttpRequestRouter.scala | 34 ++++++ .../net/abhinavsarkar/ircsearch/Server.scala | 81 +++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestHandler.scala create mode 100644 src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestRouter.scala create mode 100644 src/main/scala/net/abhinavsarkar/ircsearch/Server.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8559669 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# use glob syntax. +syntax: glob +*.ser +*.class +*~ +*.bak +#*.off +*.old + +# eclipse conf file +.settings +.classpath +.project +.manager +.scala_dependencies +.cache + +# idea +.idea +*.iml + +# building +target +build +null +tmp* +temp* +dist +test-output +build.log + +# other scm +.svn +.CVS +.hg* + +# switch to regexp syntax. +# syntax: regexp +# ^\.pc/ + +#SHITTY output not in target directory +build.log diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..00824b3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,110 @@ + + 4.0.0 + net.abhinavsarkar.ircsearch + irc-search + 1.0-SNAPSHOT + ${project.artifactId} + + + 1.6 + 1.6 + UTF-8 + 2.10.1 + ${project.build.directory}/dependency + + + + + org.scala-lang + scala-library + ${scala.version} + + + org.scala-lang + scala-reflect + ${scala.version} + + + io.netty + netty + 4.0.0.Alpha5 + compile + + + com.typesafe + scalalogging-slf4j_2.10 + 1.0.1 + + + ch.qos.logback + logback-classic + 1.0.0 + runtime + + + + + + + src/main/scala + src/test/scala + + + org.apache.maven.plugins + maven-dependency-plugin + 2.3 + + + copy-dependencies + package + + copy-dependencies + + + ${project.dependencyDir} + runtime + provided + pom + + + + + + org.scala-tools + maven-scala-plugin + 2.15.0 + + + + compile + testCompile + + + + -make:transitive + -dependencyfile + ${project.build.directory}/.scala_dependencies + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.6 + + false + true + + + + **/*Test.* + **/*Suite.* + + + + + + diff --git a/src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestHandler.scala b/src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestHandler.scala new file mode 100644 index 0000000..ebecaba --- /dev/null +++ b/src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestHandler.scala @@ -0,0 +1,71 @@ +package net.abhinavsarkar.ircsearch + +import io.netty.channel.ChannelInboundMessageHandlerAdapter +import io.netty.handler.codec.http.HttpRequest +import com.typesafe.scalalogging.slf4j.Logging +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.http.HttpResponse +import io.netty.channel.ChannelFuture +import io.netty.handler.codec.http.HttpHeaders.isKeepAlive +import io.netty.handler.codec.http.HttpHeaders.Names._ +import io.netty.handler.codec.http.HttpHeaders +import io.netty.channel.ChannelFutureListener +import io.netty.handler.codec.http.DefaultHttpResponse +import io.netty.handler.codec.http.HttpResponseStatus._ +import io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import io.netty.handler.codec.http.HttpVersion +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.buffer.Unpooled + + +trait HttpRequestHandler extends ChannelInboundMessageHandlerAdapter[HttpRequest] with Logging { + + protected def sendDefaultResponse(ctx : ChannelHandlerContext, request : HttpRequest) : HttpResponse = { + val response = new DefaultHttpResponse( + HTTP_1_1, if (request.getDecoderResult.isSuccess) OK else BAD_REQUEST) + writeResponse(ctx, request, response) + response + } + + protected def sendNotFound(ctx : ChannelHandlerContext, request : HttpRequest) : HttpResponse = { + val response = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND) + writeResponse(ctx, request, response) + response + } + + protected def sendSuccess(ctx : ChannelHandlerContext, request : HttpRequest, body : String) : HttpResponse = { + val response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) + response.setContent(Unpooled.copiedBuffer(body.getBytes)) + writeResponse(ctx, request, response) + response + } + + protected def writeResponse( + ctx : ChannelHandlerContext, request : HttpRequest, response : HttpResponse) { + response.setHeader(CONTENT_LENGTH, response.getContent().readableBytes()) + + if (isKeepAlive(request)) { + response.setHeader(CONNECTION, HttpHeaders.Values.KEEP_ALIVE) + } + + val future = ctx.write(response) + + if (!isKeepAlive(request) || response.getStatus().getCode() != 200) { + future.addListener(ChannelFutureListener.CLOSE) + } + } + + protected def logRequest(ctx: ChannelHandlerContext, request: HttpRequest, response: HttpResponse) { + logger.info("{} {} {} {}", + response.getStatus().getCode() : Integer, + request.getMethod(), + request.getUri(), + ctx.channel().remoteAddress()) + } + + override def exceptionCaught(ctx : ChannelHandlerContext, cause : Throwable) { + logger.warn("Exception in handling request", cause) + ctx.close() + } + +} \ No newline at end of file diff --git a/src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestRouter.scala b/src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestRouter.scala new file mode 100644 index 0000000..fc5efc7 --- /dev/null +++ b/src/main/scala/net/abhinavsarkar/ircsearch/HttpRequestRouter.scala @@ -0,0 +1,34 @@ +package net.abhinavsarkar.ircsearch + +import io.netty.channel.ChannelHandler.Sharable +import java.util.regex.Pattern +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.http.HttpRequest + +@Sharable +abstract class HttpRequestRouter extends HttpRequestHandler { + + def route : PartialFunction[String, HttpRequestHandler] + + override def messageReceived(ctx: ChannelHandlerContext, request: HttpRequest) { + if (request.getDecoderResult.isSuccess) { + val uri = request.getUri + if (route.isDefinedAt(uri)) { + val routeHandler = route.apply(uri) + ctx.pipeline.addLast("handler", routeHandler) + try { + ctx.nextInboundMessageBuffer.add(request) + ctx.fireInboundBufferUpdated + } finally { + ctx.pipeline.remove("handler") + } + } else { + logRequest(ctx, request, sendNotFound(ctx, request)) + } + } else { + logger.warn("Could not decode request: {}", request) + logRequest(ctx, request, sendDefaultResponse(ctx, request)) + } + } + +} \ No newline at end of file diff --git a/src/main/scala/net/abhinavsarkar/ircsearch/Server.scala b/src/main/scala/net/abhinavsarkar/ircsearch/Server.scala new file mode 100644 index 0000000..2853a85 --- /dev/null +++ b/src/main/scala/net/abhinavsarkar/ircsearch/Server.scala @@ -0,0 +1,81 @@ +package net.abhinavsarkar.ircsearch + +import java.net.InetSocketAddress +import com.typesafe.scalalogging.slf4j.Logging +import io.netty.bootstrap.ServerBootstrap +import io.netty.channel.ChannelHandler.Sharable +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioEventLoopGroup +import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.handler.codec.http.HttpChunkAggregator +import io.netty.handler.codec.http.HttpContentCompressor +import io.netty.handler.codec.http.HttpRequest +import io.netty.handler.codec.http.HttpRequestDecoder +import io.netty.handler.codec.http.HttpResponseEncoder +import io.netty.handler.codec.http.DefaultHttpResponse +import io.netty.handler.codec.http.HttpVersion +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.buffer.Unpooled +import java.nio.charset.Charset + +object Server extends App with Logging { + + if (args.isEmpty) { + println("Please specify port to run the server on") + System.exit(1) + } else { + val port = args(0).toInt + logger.info("Starting server at port {}", port: Integer) + + val httpRequestRouter = new HttpRequestRouter { + val Echo = "^/echo$".r + def route = { + case Echo() => new HttpRequestHandler { + override def messageReceived(ctx: ChannelHandlerContext, request: HttpRequest) { + val content = request.getContent().toString(Charset.forName("UTF-8")) + logRequest(ctx, request, sendSuccess(ctx, request, content)) + } + } + } + } + + val server = (new ServerBootstrap) + .group(new NioEventLoopGroup(1), new NioEventLoopGroup(1)) + .channel(classOf[NioServerSocketChannel]) + .childHandler(new ChannelInitializer[SocketChannel] { + def initChannel(ch: SocketChannel) { + val p = ch.pipeline + .addLast("decoder", new HttpRequestDecoder) + .addLast("aggregator", new HttpChunkAggregator(1048576)) + .addLast("encoder", new HttpResponseEncoder) + .addLast("compressor", new HttpContentCompressor) + .addLast("router", httpRequestRouter) + }}) + .localAddress(new InetSocketAddress(port)) + + Runtime.getRuntime.addShutdownHook( + new Thread("ShutdownHook") { + override def run { + stopServer(server); + } + }) + + try { + server.bind().sync.channel.closeFuture.sync + } catch { + case e : Exception => { + logger.error("Exception while running server. Stopping server", e) + stopServer(server); + } + } + } + + def stopServer(server : ServerBootstrap) { + logger.info("Stopping server") + server.shutdown + logger.info("Stopped server") + } + +} \ No newline at end of file