├── .gitignore ├── test └── nettyca │ └── core_test.clj ├── project.clj ├── resources └── logback.xml ├── src └── nettyca │ ├── cli.clj │ ├── core.clj │ └── netty.clj ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /logs 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | -------------------------------------------------------------------------------- /test/nettyca/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns nettyca.core-test 2 | (:require [clojure.test :refer :all] 3 | [nettyca.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject nettyca "0.1.0-SNAPSHOT" 2 | :description "Create simple socket servers using Netty and core.async" 3 | :url "http://github.com/marsmining/nettyca" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.7.0-alpha2"] 7 | [org.clojure/tools.logging "0.3.0"] 8 | [io.netty/netty-handler "5.0.0.Alpha1"] 9 | [org.clojure/core.async "0.1.338.0-5c5012-alpha"] 10 | [org.clojure/tools.cli "0.3.1"]] 11 | :profiles {:dev {:dependencies [[org.clojure/test.check "0.5.8"] 12 | [ch.qos.logback/logback-classic "1.1.2"]]}} 13 | :main nettyca.cli 14 | :aot [nettyca.cli] 15 | :jar-exclusions [#"logback.xml"]) 16 | -------------------------------------------------------------------------------- /resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %date %6level [%32thread] %-15logger{15} %msg%n 6 | 7 | 8 | 9 | 10 | logs/nettyca.log 11 | 12 | logs/old/nettyca.%d{yyyy-MM-dd}.log 13 | 3 14 | 15 | 16 | %date %6level [%32thread] %-15logger{15} %msg%n 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/nettyca/cli.clj: -------------------------------------------------------------------------------- 1 | (ns nettyca.cli 2 | "Basic cli support" 3 | (:require [clojure.tools.logging :as log] 4 | [clojure.tools.cli :refer [parse-opts]] 5 | [clojure.string :as string] 6 | [clojure.core.async :as async] 7 | [nettyca.core :as nc]) 8 | (:gen-class)) 9 | 10 | ;; cli stuff 11 | ;; 12 | 13 | (def cli-options 14 | [[nil "--help"] 15 | ["-s" "--server" "Create server not client" 16 | :default false] 17 | ["-n" "--name HOST" "Hostname to bind or connect to" 18 | :default "127.0.0.1"] 19 | ["-p" "--port PORT" "Port number" 20 | :default 9090 21 | :parse-fn #(Integer/parseInt %) 22 | :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]]) 23 | 24 | (defn usage [options-summary] 25 | (->> ["Nettyca examples program." 26 | "" 27 | "Usage: lein run [options]" 28 | "" 29 | "Options:" 30 | options-summary 31 | ""] 32 | (string/join \newline))) 33 | 34 | (defn error-msg [errors] 35 | (str "The following errors occurred while parsing your command:\n\n" 36 | (string/join \newline errors))) 37 | 38 | (defn exit [status msg] 39 | (println msg) 40 | (shutdown-agents) 41 | (System/exit status)) 42 | 43 | (defn -main [& args] 44 | (log/info "starting..") 45 | (let [{:keys [options arguments errors summary]} 46 | (parse-opts args cli-options) 47 | host (:name options) 48 | port (:port options) 49 | type (if (:server options) :server :client)] 50 | (cond 51 | (:help options) (exit 0 (usage summary)) 52 | (not= (count arguments) 0) (exit 1 (usage summary)) 53 | errors (exit 1 (error-msg errors))) 54 | (log/info "starting netty" type "on" host "and" port) 55 | (let [sys (if (= type :server) 56 | (nc/start host port nc/echo-server-timeout :server) 57 | (nc/start host port nc/echo-client-test :client))] 58 | (async/! close!] :as async])) 7 | 8 | ;; three echo server impls 9 | ;; 10 | 11 | (defn echo-server-simple [r w] 12 | (async/pipe r w)) 13 | 14 | (defn echo-server-newline [r w] 15 | (async/pipeline 1 w (map #(str % "\r\n")) r)) 16 | 17 | (defn echo-server-timeout 18 | "An echo server, loop inside go macro, close chan if timeout" 19 | [r w] 20 | (go-loop [] 21 | (if-let [msg (first (alts! [r (timeout 5000)]))] 22 | (do (log/info "echo: got msg:" msg) 23 | (>! w (str msg "\r\n")) 24 | (recur)) 25 | (do (log/info "echo: got timeout or closed chan") 26 | (close! r) (close! w))))) 27 | 28 | ;; clients receive a 3rd arg, the connection channel 29 | ;; 30 | 31 | (defn echo-client-test 32 | "Client test, sends 42 then waits for response" 33 | [r w c] 34 | (go (let [[v _] (alts! [[w "42\r\n"] (timeout 1000)]) 35 | _ (log/info "client-test: wrote:" v) 36 | [v _] (alts! [r (timeout 1000)]) 37 | _ (log/info "client-test: read:" v)] 38 | (log/info "client-test: result:" (= v "42")) 39 | (close! r) (close! w) (close! c)))) 40 | 41 | ;; start/stop 42 | ;; 43 | 44 | (defn start [host port handler type] 45 | "Start a tcp client or server" 46 | (let [ch (async/chan)] 47 | {:conn-chan ch 48 | :go-chan (netty/start-netty-core-async 49 | ch host port handler type)})) 50 | 51 | (defn stop [sys] 52 | "Stop the system and clean-up" 53 | (async/close! (sys :conn-chan))) 54 | 55 | (comment 56 | 57 | ;; call from repl examples, server 58 | (def ss (start "127.0.0.1" 9090 echo-server-timeout :server)) 59 | (stop ss) 60 | 61 | ;; client connection 62 | (start "127.0.0.1" 9090 echo-client-test :client) 63 | ) 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nettyca 2 | 3 | A bit of code for starting up a [Netty](http://netty.io) tcp client or 4 | server, which delegates control for each connection to a 5 | protocol handler function you define. Your protocol function will be 6 | passed a pair of [core.async](https://github.com/clojure/core.async) 7 | channels, which you use in your handler for reading and writing to a 8 | client. This is not for production! Just a way to play with writing 9 | tcp client and servers in terms of core.async channels. It uses Netty 10 | 5 and Clojure 1.7 __alpha__ releases. 11 | 12 | ## Usage 13 | 14 | Add `nettyca` as a dependency in your `project.clj` file: 15 | 16 | ```clj 17 | (defproject example-project "x.y.z" 18 | :dependencies [[org.clojure/clojure "1.7.0-alpha2"] 19 | [nettyca "0.1.0-SNAPSHOT"] 20 | [ch.qos.logback/logback-classic "1.1.2"]]) 21 | ``` 22 | 23 | Notice the Clojure version is alpha, this is required in order to 24 | operate on channels with 25 | [transducers](http://blog.cognitect.com/blog/2014/8/6/transducers-are-coming). 26 | Also notice, in the example above, included is a Java logging 27 | implementation. If your project already has one, leave the 28 | [logback](http://logback.qos.ch/) dependency out. 29 | 30 | ### Quick Start 31 | 32 | To start an echo server and client in the repl, follow these steps: 33 | 34 | ```clj 35 | (use 'nettyca.core) 36 | ;; start netty tcp server, passing a fn which accepts two args, r and w chans 37 | (def sys (start "127.0.0.1" 9090 echo-server-timeout :server)) 38 | ;; try telnet 127.0.0.1 9090 now 39 | ;; or run an echo tcp client provided as an example 40 | (start "127.0.0.1" 9090 echo-client-test :client) 41 | (stop sys) 42 | ``` 43 | 44 | See the [core namespace](src/nettyca/core.clj) for other examples of a simple echo protocol. 45 | 46 | ### Why? 47 | 48 | There already exists a robust and feature complete set of libraries which 49 | implement a channel abstraction in conjuction with Netty, it is called 50 | [Aleph](https://github.com/ztellman/aleph). This library, `nettyca` is 51 | tiny and incompletely emulates just one or two specific cases of Aleph's 52 | functionality. Therefore this library is only for someone who might want 53 | to play with the very latest Netty and core.async libraries, without 54 | re-writing the Java interop code to wire them together. 55 | 56 | ### Server Detail 57 | 58 | To start an echo server in the repl, follow these steps: 59 | 60 | First load and refer in the `nettyca/core` ns: 61 | 62 | user=> (use 'nettyca.core) 63 | ... log messages omitted ... 64 | 65 | Next define an echo server as a function which accepts two arguments, 66 | the read channel and write channel respectively. 67 | 68 | user=> (defn echo [r w] (clojure.core.async/pipe r w)) 69 | #'user/echo 70 | 71 | In this simplest implementation, we just use core.async `pipe` 72 | function to send values from the read channel to the write channel. 73 | You'll notice that we could simply pass the `pipe` function directly 74 | because of the similar arguments expected, but for any protocol less 75 | trivial, you'll be implementing your own function, as shown here. 76 | 77 | Next, start a Netty server listening on a port: 78 | 79 | user=> (def sys (start 9090 echo-server-timeout)) 80 | ... log messages omitted ... 81 | 82 | Now you can telnet to port 9090: 83 | 84 | $ telnet localhost 9090 85 | Trying 127.0.0.1... 86 | Connected to localhost. 87 | Escape character is '^]'. 88 | 56 89 | 56 90 | Connection closed by foreign host. 91 | 92 | Finally, stop the server: 93 | 94 | user=> (stop sys) 95 | ... log messages omitted ... 96 | 97 | That's it! 98 | 99 | ### Client Detail 100 | 101 | With the `nettyca/core` ns loaded and referred: 102 | 103 | ```clj 104 | (start "127.0.0.1" 9090 echo-client-test :client) 105 | ``` 106 | 107 | You'll notice there is no corresponding stop function for client 108 | connections. The handler for clients is passed 3 channels, read, write 109 | and "connection" respectively. Closing the "connection" channel closes 110 | and cleans up resources associated with the connection. 111 | 112 | ### Example 113 | 114 | An example of a program to check existence of mailbox using SMTP can 115 | be found [here](https://github.com/marsmining/evalid/blob/6df45f16c17f1e82f3b860f139e24319b4c53159/src/evalid/core.clj). 116 | 117 | ### Command Line Interface 118 | 119 | See the [cli namespace](src/nettyca/cli.clj) for examples of starting 120 | from the cli versus repl. 121 | 122 | ## License 123 | 124 | Copyright © 2014 Brandon van Beekum 125 | 126 | Distributed under the Eclipse Public License either version 1.0 or (at 127 | your option) any later version. 128 | -------------------------------------------------------------------------------- /src/nettyca/netty.clj: -------------------------------------------------------------------------------- 1 | (ns nettyca.netty 2 | (:require [clojure.tools.logging :as log] 3 | [clojure.core.async :refer [chan timeout go go-loop alts! 4 | ! close!] :as async]) 5 | (:import (io.netty.bootstrap Bootstrap ServerBootstrap) 6 | (io.netty.channel ChannelHandlerAdapter 7 | ChannelInitializer ChannelOption) 8 | (io.netty.channel.nio NioEventLoopGroup) 9 | (io.netty.channel.socket SocketChannel) 10 | (io.netty.channel.socket.nio NioSocketChannel NioServerSocketChannel) 11 | (io.netty.handler.codec.string StringEncoder StringDecoder) 12 | (io.netty.handler.codec LineBasedFrameDecoder))) 13 | 14 | (defn mk-handler-core-async 15 | "Netty `ChannelHandler` which plugs into core async" 16 | [conn-chan] 17 | (log/info "handler: mk-handler-core-async") 18 | (let [rw {:r (chan) :w (chan)}] 19 | (proxy [ChannelHandlerAdapter] [] 20 | (channelActive [ctx] 21 | (log/info "handler: channelActive") 22 | (go 23 | (>! conn-chan rw) 24 | (loop [] 25 | (if-let [msg (! (rw :r) msg))) 34 | (exceptionCaught [ctx cause] 35 | (log/error cause "handler: error") 36 | (.close ctx))))) 37 | 38 | (defn mk-initializer 39 | "Line based pipeline" 40 | [handler-fn] 41 | (proxy [ChannelInitializer] [] 42 | (initChannel [^SocketChannel ch] 43 | (-> (.pipeline ch) 44 | (.addLast "frameDecoder" (LineBasedFrameDecoder. (int 1024))) 45 | (.addLast "stringDecoder" (StringDecoder.)) 46 | (.addLast "stringEncoder" (StringEncoder.)) 47 | (.addLast "myHandler" (handler-fn)))))) 48 | 49 | (defn start-netty-client 50 | "Start Netty client" 51 | [group host port pipeline] 52 | (try 53 | (log/info "client: starting netty client on port:" port) 54 | (let [b (doto (Bootstrap.) 55 | (.group group) 56 | (.channel NioSocketChannel) 57 | (.option ChannelOption/SO_KEEPALIVE true) 58 | (.handler pipeline)) 59 | f (-> b (.connect host (int port)) .sync)] 60 | (-> f .channel .closeFuture .sync)) 61 | (finally 62 | (log/info "client: in finally clause..") 63 | (.shutdownGracefully group)))) 64 | 65 | (defn start-netty-server 66 | "Start Netty server, blocking this thread until shutdown" 67 | [group host port pipeline] 68 | (try 69 | (log/info "server: starting netty on port:" port) 70 | (let [b (doto (ServerBootstrap.) 71 | (.group group) 72 | (.channel NioServerSocketChannel) 73 | (.childHandler pipeline) 74 | (.option ChannelOption/SO_BACKLOG (int 128)) 75 | (.childOption ChannelOption/SO_KEEPALIVE true)) 76 | f (-> b (.bind host (int port)) .sync)] 77 | (-> f .channel .closeFuture .sync)) 78 | (finally 79 | (log/info "server: in finally clause..") 80 | (.shutdownGracefully group)))) 81 | 82 | (defn start-netty-off-thread 83 | "Start Netty on another thread, return map with handles to shutdown" 84 | [host port pipeline type] 85 | (let [group (NioEventLoopGroup.)] 86 | {:group group 87 | :server (future (if (= type :server) 88 | (start-netty-server group host port pipeline) 89 | (start-netty-client group host port pipeline))) 90 | :shutdown-fn #(.shutdownGracefully group)})) 91 | 92 | (defn start-netty-core-async 93 | "Start Netty server, new connections send r/w channel pair on conn-chan" 94 | [conn-chan host port handler type] 95 | (let [pre (str (name type) ":") 96 | pipeline (mk-initializer #(mk-handler-core-async conn-chan)) 97 | sys (start-netty-off-thread host port pipeline type)] 98 | (go-loop [clients []] 99 | (if-let [rw (