Combine Websockets Lisp and functional programming
Combine Websockets Lisp and functional programming. But how?
With Clojure.
On Habre there are enough articles are examples of applications that use
websocket (WebSocket, RFC), implemented using popular languages and technologies. Today I would like to show an example of a simple web application using a less popular but no less good, technology, and small (~90kB JAR with zero dependencies and ~3k lines of (mostly Java) code) client/server library for http-kit.
the Possible side effect (not a goal) razvijanje myth of the difficulty of writing modern applications using Lisp and functional programming.
This article is not a response to other technologies, and not their comparison. This attempt at writing dictated solely by my personal attachment to Clojure and long-time desire to write.
Meet the friendly company:
the
- Genre: FP (Functional programming) the
- Client/server: the http-kit the
- Tools: lein (leiningen) — utility to build(build tool), a dependency Manager. the
- and others
starring Clojure the
I would not want to do an excursion into Clojure and Lisp, the stack and Toolkit, the better I will do a short remark, and leave comments in the code, so let's start:
lein new ws-clojure-sample
Note: leiningen allows you to use templates to create the project, its structure and tasks starter "settings" or connect the base libraries. For the lazy: you can create a project using one of these templates:
lein new compojure ws-clojure-sample
where compojure — library for routing(routing) running Ring. We'll be doing this manually (our team also implements/uses template called, default)
the output is a generated project that has the following structure:
In the future, to build the project and manage dependencies, leiningen follows the file in the root of the project project.clj.
the moment he took the following form:
project.clj
the
(defproject ws-clojure-sample "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]])
Let's just add the corresponding dependency in the dependencies
Note: a keyword(clojure keyword) :dependencies.
specifying the entry point(the namespace) in our application main
project.clj
the
(defproject ws-clojure-sample "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]
[http-kit "2.2.0"] ;; Plug-in http-kit
[compojure "1.6.0"] ;; Plug-in compojure (routing/route)
[ring/ring-defaults "0.3.1"] ;; Gentleman's set of middleware by default
[org.clojure/data.json "0.2.6"]] ;; Useful for working with JSON
:profiles ; Profiles that we can run lein with-profile <profile name>
{:dev ;; Profile development
{:dependencies [[javax.servlet/servlet-api "2.5"] ;; will be useful if you will install the ring/ring-core
[ring/ring-devel "1.6.2"]]}} ;; useful for hot reboot
:main ws-clojure-sample.core) ;; the namespace which is the main function(the entry point into the app)
note: the middleware ring-defaults
I come, in fact, to the entry point into the application. Open the file core.clj
core.clj
the
(ns, ws-clojure-sample.core)
(defn foo
"I don't do a whole lot."
[x]
(println x "Hello, World!"))
core.clj
the
(ns, ws-clojure-sample.core
(:require [org.httpkit.server :refer [run-server]] ;; the http-kit server
[compojure.core :refer [defroutes GET POST DELETE ANY]] ;; defroutes, and methods
[compojure.route :refer [files resources not-found]] ;; routes for statics and also page not-found
[ring.middleware.defaults :refer :all])) ;; middleware
Note: this code is completely valid code in Clojure, and at the same time data structures of the language itself. This property of language is called gameconnect
To read, in my opinion, too easy and does not require special explanation.
the Server as the argument, you must pass a function handler and server settings
like this:
the
(run-server <Handler(handler)> {:port 5000})
this handler will be the function(actually a macro) router defroutes which we give the name, and which in turn will cause, depending on the route, is a direct handler. And all this we can still wrap and season our middleware.
the Remark: middleware behaves like a decorator query.
core.clj
the
(ns, ws-clojure-sample.core
(:require [org.httpkit.server :refer [run-server]] ;; the http-kit server
[compojure.core :refer [defroutes GET POST DELETE ANY]] ;; defroutes, and methods
[compojure.route :refer [files resources not-found]] ;; routes for the static, and not-found
[ring.middleware.defaults :refer :all])) ;; middleware
(defroutes app-routes
(GET "/" [] index-page) ;; We need to be home to demonstrate
(GET "/ws" [] ws-handler) ;; here will "catch" the web sockets. Handler.
(resources "/") ;; directory of resources
(files "/static/") ;; prefix for static files in the folder `public`
(not-found "<h3>Page not found</h3>")) ;; all other, return 404)
(defn -main
"The entry point into the application"
[]
(run-server (wrap-defaults #'app-routes site-defaults) {:port 5000}))
so now we have the entry point to the application that starts the server, which has routing. We lack here the two functions of listeners:
the
-
the
- index-page the
- ws-handler
let's Start with the index page.
To do this in the directory ws_clojure_sample
will create directory views
and index.clj
. Specify the namespace
and let's create our main page index page:
views/index.clj
the
(ns, ws-clojure-sample.views.index)
(def index-page "Home")
this can be finished. In essence then, you can string to specify a regular HTML page. But it's ugly. What are the options? It was a good idea in General to use any template engine. No problem. For example you can use Selmer. This is a fast templating engine inspired by the Django templating engine. In this case, performance will be little different from those in Django project. Fans of Twig or Blade will be all too familiar.
I will go the other way and choose Clojure. I will write HTML for the Clojure. What does it mean now we'll see.
For this we need a small (this applies to most Clojure library) library hiccup. In the file project.clj
in :dependencies
add the [hiccup "1.0.5"]
.
Note: the author, from library compojure and hiccup, and many other key libraries in the Clojure ecosystem, one and the same, his name is James Reeves, for which he thank you very much.
After we have added the dependency in the project, you must import its contents into the namespace of our representation src/ws_clojure_sample/views/index.clj
and write our HTML code. In order to speed up the process I will immediately bring the contents of the views/index.clj
as a whole
(and you be surprised that it's watch)
views/index.clj
the
(ns, ws-clojure-sample.views.index
(:use [hiccup.page :only (html5 include-css include-js)])) ;; Import the needed functions of a hiccup in the current namespace
;; Index page
(def index-page
(html5
[:head
(include-css "https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css")]
[:body {:style "padding-top: 50px;"}
[:div.container
[:div.form-group [:input#message.form-control {:name "message" :type "text"}]]
[:button.btn.btn-primary {:name "send-btn"} "Send"]]
[hr]
[:div.container
[:div#chat]]
(include-js "js/ws-client.js")
(include-js "https://unpkg.com/jquery@3.2.1/dist/jquery.min.js")
(include-js "https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js")]))
Our idea ready, and I think needs no comment. Created the usual <input name="message" type="text"/>
and Send
. With this simple form, we'll send soobshenia in im channel. Left to remember to import index-page
in the namespace core
. This is returned in src/ws_clojure_sample/core.clj
and adding the Directive :require
the line [ws-clojure-sample.views.index :refer [index-page]]
.
At the same time let the primary handler of ws-handler
will adjust that next we need to create.
core.clj
the
...
[ws-clojure-sample.views.index :refer [index-page]] ;; Add the index view page
[ws-clojure-sample.handler :refer [ws-handler]])) ;; Have to create ws-handler
(defroutes app-routes
(GET "/" [] index-page)
(GET "/ws" [] ws-handler) ;; Create the handler.clj
most of the methods and abstractions to work with web sockets/long polling/stream, provides our http-kit server, possible examples and variations are easy to find on the library website. In order not to make a fuss, I took one of such examples is somewhat simplified. Create a file src/ws_clojure_sample/handler.clj
, set the namespace methods and import the with-channel on-receive on-close
of htpp-kit:
handler.clj
the
(ns, ws-clojure-sample.handler
(:require [org.httpkit.server :refer [with-channel on-receive on-close]] ;; Import from http-kit
[ws-clojure-sample.receiver :refer [clients receiver]])) ;; Have to create
;; Main processor (handler)
(defn ws-handler
"Main WebSocket handler"
[request] ;; Accepts the request
(with-channel request channel ;; Receives the channel
(swap! clients assoc channel true) ;; Maintain a pool of clients connected to an atom of clients and set the flag to true
(println channel "Connection established")
(on-close channel (fn [status] (println "channel closed:" status))) ;; Sets the handler when closing the channel
(on-receive channel (get receiver :chat)))) ;; Ustanavlivaet data processor of the channel (it'll create next)
the
-
the
swap! clients
— changes the state of an atom clients, then writes the channel ID as the key and the flag value. We define next.
the with-channel
— gets channel
the on-close — Sets the handler when closing channel
the on receive
— Ustanavlivaet data processor from the channel(get receiver :chat)
to.
Let's define a handler for receive data from channel on receive
and our clients
. Create a src/ws_clojure_sample/receiver.clj
, as usual, we specify our namespace.
receiver.clj
the
(ns, ws-clojure-sample.receiver)
(def clients (atom {})) ;; our customers
As an example needs, and handlers may be a few, first I'll show an example of the chat, and call its chat-receiver
.
the
(defn chat-receiver)
[data] ;; Accepts the data (for chat this message from *input*)
(doseq [client (keys @clients)] ;; each client (performs for each element of the sequence and gives him the alias client)
(send! client (json/write-str {:key "chat" :data data}))) ;; sends a json string with key "chat" and data "data" which was received
send!
and json/write-str
should be imported in the current namespace.
receiver.clj
the
(ns, ws-clojure-sample.receiver
(:require [clojure.data.json :as json]
[org.httpkit.server :refer [send!]]))
what if we want to not chat? Or just chat, and for example receive data from an external source and send it to the sockets? I invented the Keeper of the handlers, well, Oh, very complicated.
the
(def receiver {:chat chat-receiver})
For example, I made a "receiver" to send-receive data, so you can play not only with chat, so add in the guardian handlers example data-receiver
. Let it be.
data data-receiver})
Just give the code:
(def urls ["https://now.httpbin.org" "https://httpbin.org/ip" "https://httpbin.org/stream/2"])
(defn data-receiver
"Data receiver"
[data]
(let [responses (map #(future (slurp %)) urls)] ;; send requests (in separate threads) on the list of urls
(doall (map (fn [resp] ;; run all the answers
(doseq [client (keys @clients)] ;; run all the socket clients
(send! client @resp))) responses)))) ;; and send this data to all the socket-clients
Now we can choose which one to run when data is received from the channel, and how to work the app, just changing the key:
the
(on-receive channel (get receiver :chat :data)) ;; can be swapped to :data or add as option in if :chat will not be found.
With the server part.
Left to the client. And on the client, in code views, suddenly you notice how I connected the file ws-client.js
which lives in the directory resources/public/js/ws-client.js
the
(include-js "js/ws-client.js")
he also is responsible for the client part. Because it is an ordinary JavaScript, I will just give you a code.
Note: I can not mention that client-side code instead of javascript you could write something in Clojure. If to speak more precisely, on ClojureScript. If you go further, the frontend can be done, for example, using Reagent.
let msg = document.getElementById('message');
let btn = document.getElementsByName('send-btn')[0];
let chat = document.getElementById('chat');
const sendMessage = () => {
console.log('Sending...');
socket.send(msg.value);
}
const socket = new WebSocket('ws://localhost:5000/ws?foo=clojure');
msg.addEventListener("keyup", (event) = > {
event.preventDefault();
if (event.keyCode == 13) {
sendMessage();
}
});
btn.onclick = () => sendMessage();
socket.onopen = (event) => console.log('Connection established...');
socket.onmessage = (event) = > {
let response = JSON.parse(event.data);
if (response.key == 'chat') {
var p = document.createElement('p');
p.innerHTML = new Date().toLocaleString() + ": "+ response.data;
chat.appendChild(p);
}
}
socket.onclose = (event) = > {
if (event.wasClean) {
console.log('Connection closed. Clean exit.')
} else {
console.log(`Code: ${event.code}, Reason: ${event.reason}`);
}
}
socket.onerror = (event) = > {
console.log(`Error: ${event.message}`);
socket.close();
}
If you run this code from the root of the project using the leiningen with lein run
,
the project should compile, and having at http://localhost:5000, you see
the same <input>
and Send
. If you open two tabs and in each to send a message, you can be sure that a simple chat. When closing a tab, triggered our method on-close
. Similarly, you can play with the data. They should just appear in the browser console.
the result is a simple, minimalistic app (62 lines of code with imports), giving an idea on how to write web applications on a modern dialect of Lisp, it is easy possible to write asynchronous code, parallelize tasks and to use light, modern, simple solutions for the web. the And make it all my 62 miserable line of code!
goodbye interesting fact: I don't know if you paid attention, but when you connect to the project clojure libraries, most of them have a "low" version, so unusual for a good stable project, for example [ring/ring-defaults "0.3.1"]
or [org.clojure/data.json "0.2.6"]
. Moreover, both libraries are used almost everywhere. But for the Clojure ecosystem such versioning is quite common. This is primarily due to high stability of the code written in Clojure. Believe it or not, as they say, want not.
And a little bit about http-kit:
http-kit is not only a server, the library provides and the http client API. Both the client and the server easy to use, minimalistic, and yet have a good opportunity (600k concurrent HTTP connections, with Clojure &http-kit).
All of the application code giant is available at Github.
Thank you!
Комментарии
Отправить комментарий