We unite Websockets, Lisp and functional programming. But how?
With Clojure.
On Habré there are enough articles - examples of applications using
Web sockets ( WebSocket , RFC ) implemented using popular languages and technologies. Today I would like to show an example of a simple web application using less popular, but no less good, technologies and a small (~ 90kB JAR with zero dependencies and ~ 3k lines of (mostly Java) code) http / client library / server kit.
A possible side effect is (not the goal) dispelling the myth of the complexity of writing modern applications using Lisp and functional programming.
This article is not the answer to other technologies, and not their comparison. This attempt at writing is dictated solely by my personal attachment to Clojure and by a long-standing desire to try writing.
Meet the friendly company:
I would not like to make an excursion into Clojure and Lisp, the stack and the toolkit, I’d rather make short remarks and leave comments in the code, so let's start:
lein new ws-clojure-sample
Remark: leiningen allows you to use templates for creating a project, its structure and setting up start-up “settings” or connecting basic libraries. For the lazy: you can create a project using one of these templates like this: lein new compojure ws-clojure-sample
where compojure is a library for routing (routing) working with Ring. We will do it manually (our team also implements / uses a template called, default)
As a result, the project will be generated, having the following structure:
In the future, to build the project and manage dependencies, leiningen is guided by a file in the project project project root.
At the moment, he took the following form with us:
(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 immediately add the dependencies we need to the dependencies section
Remark: keyword (clojure keyword): dependencies.
and specify the entry point (namespace) in our application : main
(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"] ;; http-kit [compojure "1.6.0"] ;; compojure (/) [ring/ring-defaults "0.3.1"] ;; middleware [org.clojure/data.json "0.2.6"]] ;; JSON :profiles ;; lein with-profile < > {:dev ;; {:dependencies [[javax.servlet/servlet-api "2.5"] ;; ring/ring-core [ring/ring-devel "1.6.2"]]}} ;; :main ws-clojure-sample.core) ;; -main( )
Remark: middleware ring-defaults
Let's move, actually, to the entry point in the application. Open the core.clj file
(ns ws-clojure-sample.core) (defn foo "I don't do a whole lot." [x] (println x "Hello, World!"))
and replace the generated function foo with a more understandable and generally accepted -main. Next, we import the components we need into the current namespace. Actually, we need, first of all, a server, further routes, and our middleware. In the role of the server we have the http-kit and its run-server function.
(ns ws-clojure-sample.core (:require [org.httpkit.server :refer [run-server]] ;; http-kit server [compojure.core :refer [defroutes GET POST DELETE ANY]] ;; defroutes, [compojure.route :refer [resources files not-found]] ;; , not-found [ring.middleware.defaults :refer :all])) ;; middleware
Remark: this code is a completely valid Clojure code, and at the same time data structures of the language itself. This property of the language is called homoiconical
Reading, in my opinion, is also simple, and does not require special explanations.
The server, as an argument, must pass the handler function and server parameters to the function
like that:
(run-server <(handler)> {:port 5000})
This handler will be a function (actually a macro) of the router defroutes to which we will give a name, and which in turn will call, immediately depending on the route, the immediate handler. And all this we can still wrap up and spice up our middleware.
Remark: middleware behaves like a query decorator .
(ns ws-clojure-sample.core (:require [org.httpkit.server :refer [run-server]] ;; http-kit server [compojure.core :refer [defroutes GET POST DELETE ANY]] ;; defroutes, [compojure.route :refer [resources files not-found]] ;; , not-found [ring.middleware.defaults :refer :all])) ;; middleware (defroutes app-routes (GET "/" [] index-page) ;; (GET "/ws" [] ws-handler) ;; "" -. . (resources "/") ;; (files "/static/") ;; `public` (not-found "<h3> </h3>")) ;; , 404) (defn -main " " [] (run-server (wrap-defaults #'app-routes site-defaults) {:port 5000}))
So now we have an entry point to the application that starts the server that has the routing. We lack here two functions of request handlers:
Let's start with the index-page .
To do this, in the ws_clojure_sample
directory ws_clojure_sample
create a views
folder and in it an index.clj
file. We specify the resulting namespace,
and create our index-page header page :
(ns ws-clojure-sample.views.index) (def index-page "")
On this one could finish . In fact, here you can set a regular HTML page in a string. But it is ugly. What are the options? It would be nice to use any template engine at all. No problems. For example, you can use Selmer . This is a fast template engine, inspired by the Django template engine. In this case, the views will differ little from those in the Django project. Twig fans, or Blade, too, will be familiar.
I will go the other way, and choose Clojure. I will write HTML on Clojure . What does this mean - now we will see.
For this we need a small (this applies to most Clojure libraries) hiccup library. In the project.clj
file in :dependencies
add [hiccup "1.0.5"]
.
Remark: by the way, the author of the compojure and hiccup libraries , and many other key libraries in the Clojure ecosystem, is the same, his name is James Reeves, for which he thanks a lot.
After we add the dependency to the project, we need to import its contents into the namespace of our view src/ws_clojure_sample/views/index.clj
and write our HTML code. In order to speed up the process, I immediately bring the contents of views/index.clj
entirely
(and you wonder what is it watch):
(ns ws-clojure-sample.views.index (:use [hiccup.page :only (html5 include-css include-js)])) ;; hiccup ;; 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 presentation is ready, and I think does not need comments. We created the usual <input name="message" type="text"/>
and the Send
button. With this simple form we will send messages to chat channel. It remains to remember to import the index-page
into the core
namespace. To do this, go back to src/ws_clojure_sample/core.clj
and src/ws_clojure_sample/core.clj
directive :require
line [ws-clojure-sample.views.index :refer [index-page]]
.
At the same time, let's write the main handler ws-handler
, which we need to create next.
... [ws-clojure-sample.views.index :refer [index-page]] ;; index-page [ws-clojure-sample.handler :refer [ws-handler]])) ;; ws-handler (defroutes app-routes (GET "/" [] index-page) (GET "/ws" [] ws-handler) ;; handler.clj
Most of the methods and abstractions for working with web sockets / long-polling / stream are provided by our http-kit server, possible examples and variations are easily found on the library's website. In order not to make a fuss, I took one of such examples and simplified it a bit. Create the src/ws_clojure_sample/handler.clj
, set the namespace and import the with-channel, on-receive, on-close
methods from the htpp-kit :
(ns ws-clojure-sample.handler (:require [org.httpkit.server :refer [with-channel on-receive on-close]] ;; http-kit [ws-clojure-sample.receiver :refer [receiver clients]])) ;; ;; (handler) (defn ws-handler "Main WebSocket handler" [request] ;; (with-channel request channel ;; (swap! clients assoc channel true) ;; clients true (println channel "Connection established") (on-close channel (fn [status] (println "channel closed: " status))) ;; (on-receive channel (get receiver :chat)))) ;; ( )
swap! clients
swap! clients
- changes the state of the clients atom, writes there the channel identifier as the key and the flag as the value. Let's set further.with-channel
- gets channelon-close
- Sets a handler when closing a channelon-receive
- Installs the data handler from the channel (get receiver :chat)
- this is what we will have to do.Let's define a handler to receive data from the on-receive
channel and our clients
. Create src/ws_clojure_sample/receiver.clj
, as usual, we specify our namespace.
(ns ws-clojure-sample.receiver) (def clients (atom {})) ;;
Since we need a visual example, and there can be several handlers, first I will show by the example of a chat, and call it chat-receiver
.
(defn chat-receiver) [data] ;; ( *input*) (doseq [client (keys @clients)] ;; ( alias client) (send! client (json/write-str {:key "chat" :data data}))) ;; json- "chat" "data"
send!
and json/write-str
must be imported into the current namespace.
(ns ws-clojure-sample.receiver (:require [clojure.data.json :as json] [org.httpkit.server :refer [send!]]))
What if we don't want a chat? Or not just chat, but for example, to receive data from an external source and send it to sockets? I came up with a keeper of handlers, well, oh, very complicated.
(def receiver {:chat chat-receiver})
For example, I made such a "receiver" for sending and receiving data so that you can play not only with the chat, so we will add an example data-receiver
to the handler keeper. Let it be.
(def receiver {:chat chat-receiver :data data-receiver})
Just give his 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)] ;; ( ) urls (doall (map (fn [resp] ;; (doseq [client (keys @clients)] ;; - (send! client @resp))) responses)))) ;; -
Now we can choose which of them to run when receiving data from the channel, and how the application will work, just by changing the key:
(on-receive channel (get receiver :chat :data)) ;; :data , :chat .
With server part everything.
Remained client. And on the client, in the presentation code, you suddenly noticed how I connected the file ws-client.js
which lives in the resources/public/js/ws-client.js
(include-js "js/ws-client.js")
It is he who is responsible for the client part. Since this is plain javascript, I’ll just provide the code.
Remark: I can not help but note that the client code, instead of javascript, could be written in Clojure. More specifically, ClojureScript. If you go further, you can do the frontend, 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 project root using leiningen with the lein run
command,
the project should compile, and if you go to http: // localhost: 5000 , you can see
the same <input>
and the Send
button. If you open two such tabs and send a message in each, you can make sure that the simplest chat works. When closing a tab, our on-close
method works. Similarly, you can play with the data. They should simply be displayed in the browser in the console.
The result was a simple, minimalist application (62 lines of code along with imports), giving an idea of how to write web applications in the modern Lisp dialect, while you can easily write asynchronous code, parallelize tasks and use light, modern, simple solutions web. And all this is done by my 62 poor lines of code!
Good-bye is an interesting fact: I don’t know whether you paid attention, but when libraries were connected to a clojure project, most of them have “low” versioning, which is so unusual for good stable projects, 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 commonplace. This is primarily due to the high stability of the code written in Clojure. Believe it, as they say, you want not.
And a little more about the http-kit:
http-kit is not only a server, the library also provides the http-client API. Both the client and the server are easy to use, minimalist, and at the same time they have good capabilities ( 600k concurrent HTTP connections, with Clojure & http-kit ).
All application code the giant available on github .
If you have questions - write, I will try to answer to the best of my modest knowledge. I accept comments, suggestions.
Thanks for attention!
Source: https://habr.com/ru/post/339628/
All Articles