├── README.md ├── example ├── hello_world.clj ├── linted.clj ├── public │ └── clojure.png └── wrapping.clj ├── project.clj └── src └── ring └── adapter ├── netty.clj └── plumbing.clj /README.md: -------------------------------------------------------------------------------- 1 | # No longer developed 2 | 3 | As an alternative, please see [aleph](https://github.com/ztellman/aleph) 4 | 5 | 6 | ## Ring-Netty-Adapter 7 | 8 | This repo adds (experimental/alpha) Netty support to Ring 9 | 10 | (use 'ring.adapter.netty) 11 | 12 | (defn app [req] 13 | {:status 200 14 | :headers {"Content-Type" "text/html"} 15 | :body "Hello World from Ring-Netty"}) 16 | 17 | (run-netty app {:port 8080}) 18 | 19 | 20 | You can try out the demos; they are the same as the ones in the Ring repository, except here they use netty as the backend server instead of jetty. 21 | 22 | $ lein jar 23 | 24 | $ java -cp "lib/*:*" clojure.main example/hello_world.clj 25 | 26 | $ java -cp "lib/*:*" clojure.main example/wrapping.clj 27 | 28 | $ java -cp "lib/*:*" clojure.main example/linted.clj 29 | 30 | 31 | Currently there are 2 branches: master (clojure 1.1) and compat-1.2 for clojure 1.2 support. The only difference is proxy vs reify (reify performs better). 32 | 33 | I'm getting roughly 7k req/s with 1.1 and 9.5k req/s with 1.2 using an unscientific benchmark on my not-quite-so-new machine. 34 | 35 | ### Leiningen 36 | 37 | To use the netty backend, include ring-netty-adapter in your project.clj's :dependencies 38 | 39 | [ring-netty-adapter "0.0.3"] 40 | 41 | 42 | Next steps: 43 | 44 | * squash any bugs that pop up 45 | * ssl support 46 | * websockets, comet 47 | -------------------------------------------------------------------------------- /example/hello_world.clj: -------------------------------------------------------------------------------- 1 | ; A very simple Ring application. 2 | 3 | (ns ring.example.hello-world 4 | (:use ring.adapter.netty) 5 | (:import java.util.Date java.text.SimpleDateFormat)) 6 | 7 | (defn app 8 | [req] 9 | {:status 200 10 | :headers {"Content-Type" "text/html"} 11 | :body (str "

Hello World from Ring and Netty

" 12 | "

The current time is " 13 | (.format (SimpleDateFormat. "HH:mm:ss") (Date.)) 14 | ".

")}) 15 | 16 | (run-netty app {:port 8080}) 17 | -------------------------------------------------------------------------------- /example/linted.clj: -------------------------------------------------------------------------------- 1 | ; An example of inserting the linter between each component to ensure 2 | ; compliance to the Ring spec. 3 | 4 | (ns ring.example.linted 5 | (:use (ring.handler dump) 6 | (ring.middleware stacktrace file file-info reload lint) 7 | (ring.adapter netty))) 8 | 9 | (def app 10 | (-> handle-dump 11 | wrap-lint 12 | wrap-stacktrace 13 | wrap-lint 14 | wrap-file-info 15 | wrap-lint 16 | (wrap-file "example/public") 17 | wrap-lint 18 | (wrap-reload '(ring.handler.dump)) 19 | wrap-lint)) 20 | 21 | (run-netty app {:port 8080}) 22 | -------------------------------------------------------------------------------- /example/public/clojure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datskos/ring-netty-adapter/1f49d499909853b912e54d660e6049c6113745b8/example/public/clojure.png -------------------------------------------------------------------------------- /example/wrapping.clj: -------------------------------------------------------------------------------- 1 | ; A example of modular construction of Ring apps. 2 | 3 | (ns ring.example.wrapping 4 | (:use (ring.handler dump) 5 | (ring.middleware stacktrace file-info file) 6 | (ring.adapter netty) 7 | (clojure.contrib except))) 8 | 9 | (defn wrap-error [app] 10 | (fn [req] 11 | (if (= "/error" (:uri req)) 12 | (throwf "Demonstrating ring.middleware.stacktrace") 13 | (app req)))) 14 | 15 | (def app 16 | (-> handle-dump 17 | wrap-error 18 | (wrap-file "example/public") 19 | wrap-file-info 20 | wrap-stacktrace)) 21 | 22 | (run-netty app {:port 8080}) 23 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring-netty-adapter "0.0.3" 2 | :repositories [["JBoss" "http://repository.jboss.org/nexus/content/groups/public/"]] 3 | :description "Ring Netty adapter" 4 | :dependencies [[org.clojure/clojure "1.1.0"] 5 | [org.clojure/clojure-contrib "1.1.0"] 6 | [org.jboss.netty/netty "3.2.1.Final"] 7 | [ring "0.2.5"]] 8 | :dev-dependencies [[swank-clojure "1.2.1"]] 9 | :namespaces [ring.adapter.netty ring.adapter.plumbing]) 10 | -------------------------------------------------------------------------------- /src/ring/adapter/netty.clj: -------------------------------------------------------------------------------- 1 | (ns ring.adapter.netty 2 | (:use ring.adapter.plumbing) 3 | (:import (java.net InetSocketAddress) 4 | (java.util.concurrent Executors) 5 | (java.io ByteArrayInputStream) 6 | (org.jboss.netty.bootstrap ServerBootstrap) 7 | (org.jboss.netty.channel ChannelPipeline ChannelPipelineFactory Channels 8 | SimpleChannelUpstreamHandler) 9 | (org.jboss.netty.channel.socket.nio NioServerSocketChannelFactory) 10 | (org.jboss.netty.handler.stream ChunkedWriteHandler) 11 | (org.jboss.netty.handler.codec.http HttpContentCompressor HttpRequestDecoder 12 | HttpResponseEncoder HttpChunkAggregator))) 13 | 14 | (defn- make-handler [handler zerocopy] 15 | (proxy [SimpleChannelUpstreamHandler] [] 16 | (messageReceived [ctx evt] 17 | (let [request-map (build-request-map ctx (.getMessage evt)) 18 | ring-response (handler request-map)] 19 | (when ring-response 20 | (write-response ctx zerocopy (request-map :keep-alive) ring-response)))) 21 | (exceptionCaught [ctx evt] 22 | ;(-> evt .getCause .printStackTrace) 23 | (-> evt .getChannel .close)))) 24 | 25 | (defn- make-pipeline [options handler] 26 | (let [pipeline (Channels/pipeline) 27 | pipeline (doto pipeline 28 | (.addLast "decoder" (HttpRequestDecoder.)) 29 | (.addLast "aggregator" (HttpChunkAggregator. 65636)) 30 | (.addLast "encoder" (HttpResponseEncoder.)) 31 | (.addLast "chunkedWriter" (ChunkedWriteHandler.)) 32 | ;(.addLast "deflater" (HttpContentCompressor.)) 33 | (.addLast "handler" (make-handler handler (or (:zerocopy options) false))))] 34 | pipeline)) 35 | 36 | (defn- pipeline-factory [options handler] 37 | (proxy [ChannelPipelineFactory] [] 38 | (getPipeline [] (make-pipeline options handler)))) 39 | 40 | (defn- create-server [options handler] 41 | (let [bootstrap (ServerBootstrap. (NioServerSocketChannelFactory. 42 | (Executors/newCachedThreadPool) 43 | (Executors/newCachedThreadPool))) 44 | bootstrap (doto bootstrap 45 | (.setPipelineFactory (pipeline-factory options handler)) 46 | (.setOption "child.tcpNoDelay" true) 47 | (.setOption "child.keepAlive" true))] 48 | bootstrap)) 49 | 50 | (defn- bind [bs port] 51 | (.bind bs (InetSocketAddress. port))) 52 | 53 | (defn run-netty [handler options] 54 | (let [bootstrap (create-server options handler) 55 | port (options :port 80)] 56 | (println "Running server on port:" port) 57 | (bind bootstrap port))) 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/ring/adapter/plumbing.clj: -------------------------------------------------------------------------------- 1 | (ns ring.adapter.plumbing 2 | (:use [clojure.contrib.except :only (throwf)]) 3 | (:import (java.io InputStream File RandomAccessFile FileInputStream) 4 | (java.net URLConnection) 5 | (org.jboss.netty.channel ChannelFutureListener DefaultFileRegion ChannelFutureProgressListener) 6 | (org.jboss.netty.buffer ChannelBufferInputStream ChannelBuffers) 7 | (org.jboss.netty.handler.stream ChunkedStream ChunkedFile) 8 | (org.jboss.netty.handler.codec.http HttpHeaders HttpVersion HttpMethod 9 | HttpResponseStatus DefaultHttpResponse))) 10 | 11 | (defn- remote-address [ctx] 12 | (-> ctx .getChannel .getRemoteAddress .toString (.split ":") first (subs 1))) 13 | 14 | (defn- get-meth [req] 15 | (-> req .getMethod .getName .toLowerCase keyword)) 16 | 17 | (defn- get-body [req] 18 | (ChannelBufferInputStream. (.getContent req))) 19 | 20 | (defn- get-headers [req] 21 | (reduce (fn [headers name] 22 | (assoc headers (.toLowerCase name) (.getHeader req name))) 23 | {} 24 | (.getHeaderNames req))) 25 | 26 | (defn- content-type [headers] 27 | (if-let [ct (headers "content-type")] 28 | (-> ct (.split ";") first .trim .toLowerCase))) 29 | 30 | 31 | (defn- uri-query [req] 32 | (let [uri (.getUri req) 33 | idx (.indexOf uri "?")] 34 | (if (= idx -1) 35 | [uri nil] 36 | [(subs uri 0 idx) (subs uri (inc idx))]))) 37 | 38 | (defn- keep-alive? [headers msg] 39 | (let [version (.getProtocolVersion msg) 40 | minor (.getMinorVersion version) 41 | major (.getMajorVersion version)] 42 | (not (or (= (headers "connection") "close") 43 | (and (and (= major 1) (= minor 0)) 44 | (= (headers "connection") "keep-alive")))))) 45 | 46 | (defn build-request-map 47 | "Converts a netty request into a ring request map" 48 | [ctx netty-request] 49 | (let [headers (get-headers netty-request) 50 | socket-address (-> ctx .getChannel .getLocalAddress) 51 | [uri query-string] (uri-query netty-request)] 52 | {:server-port (.getPort socket-address) 53 | :server-name (.getHostName socket-address) 54 | :remote-addr (remote-address ctx) 55 | :uri uri 56 | :query-string query-string 57 | :scheme (keyword (headers "x-scheme" "http")) 58 | :request-method (get-meth netty-request) 59 | :headers headers 60 | :content-type (content-type headers) 61 | :content-length (headers "content-length") 62 | :character-encoding (headers "content-encoding") 63 | :body (get-body netty-request) 64 | :keep-alive (keep-alive? headers netty-request)})) 65 | 66 | (defn- set-headers [response headers] 67 | (doseq [[key val-or-vals] headers] 68 | (if (string? val-or-vals) 69 | (.setHeader response key val-or-vals) 70 | (doseq [val val-or-vals] 71 | (.addHeader response key val))))) 72 | 73 | (defn- set-content-length [msg length] 74 | (HttpHeaders/setContentLength msg length)) 75 | 76 | (defn- write-content [ch response content keep-alive] 77 | (.setContent response (ChannelBuffers/copiedBuffer (.getBytes content))) 78 | (if keep-alive 79 | (do (set-content-length response (count content)) 80 | (.write ch response)) 81 | (-> ch (.write response) (.addListener ChannelFutureListener/CLOSE)))) 82 | 83 | (defn- write-file [ch response file keep-alive zero-copy] 84 | (let [raf (RandomAccessFile. file "r") 85 | len (.length raf) 86 | region (if zero-copy 87 | (DefaultFileRegion. (.getChannel raf) 0 len) 88 | (ChunkedFile. raf 0 len 8192))] 89 | (.setHeader response "Content-Type" (URLConnection/guessContentTypeFromName (.getName file))) 90 | (set-content-length response len) 91 | (.write ch response) ;write initial line and header 92 | (let [write-future (.write ch region)] 93 | (if zero-copy 94 | (.addListener write-future 95 | (proxy [ChannelFutureProgressListener] [] 96 | (operationComplete [fut] 97 | (.releaseExternalResources region))))) 98 | (if not keep-alive 99 | (.addListener write-future ChannelFutureListener/CLOSE))))) 100 | 101 | (defn write-response [ctx zerocopy keep-alive {:keys [status headers body]}] 102 | (let [ch (.getChannel ctx) 103 | netty-response (DefaultHttpResponse. HttpVersion/HTTP_1_1 (HttpResponseStatus/valueOf status))] 104 | (set-headers netty-response headers) 105 | (cond (string? body) 106 | (write-content ch netty-response body keep-alive) 107 | (seq? body) 108 | (write-content ch netty-response (apply str body) keep-alive) 109 | (instance? InputStream body) 110 | (do 111 | (.write ch netty-response) 112 | (-> (.write ch (ChunkedStream. body)) 113 | (.addListener (proxy [ChannelFutureListener] [] 114 | (operationComplete [fut] 115 | (.close body) 116 | (-> fut .getChannel .close)))))) 117 | (instance? File body) 118 | (write-file ch netty-response body keep-alive zerocopy) 119 | (nil? body) 120 | nil 121 | :else 122 | (throwf "Unrecognized body: %s" body)))) 123 | 124 | --------------------------------------------------------------------------------