Статьи

Перекрестное адресное пространство core.async в Clojure / ClojureScript

Возиться с сервером Push

Хорошо … Я злодей. Я серверный толчок. Я сделал несколько удивительных семантики и реализации для push сервера в  Lift .

Я начал работать с Clojure через  Project Plugh . Первый шаг в моем создании набора инструментов для использования в Clojure-land.

Отличная вещь, которую я создал до сих пор, заключается в том, что вы можете запустить приложение чата, просто используя  каналы core.async .

Приложение чата

Таким образом, это приложение чат с помощью  AngularJS , модифицированный  лязг  код, и некоторые вещи , я был пивоварения в Plugh.

Вид

Во-первых, давайте посмотрим на представление … это стандартный код Angular:

<h2>Chat</h2>




<div ng-controller="Chatter">
<ul>
<li ng-repeat="n in chats">{{n}}</li>
</ul>




<input ng-model="line"><button ng-click="send()">Send</button>
</div>

Довольно просто

Клиентская сторона

Вот контроллер AngularJS, написанный на ClojureScript:

(def server-chan (pc/server-chan "The Chat Server"))

Определите канал, который на стороне сервера содержит наш чат-сервер.

(def.controller pc/m Chatter [$scope]
(assoc! $scope :chats (clj->js []))

Определите массив JavaScript для  chats.

 (assoc! $scope :line "")

И  line переменная.

 (defn.scope send [] 
(let [msg (:line $scope)]
(go 
(>! server-chan {:msg msg})))
(assoc! $scope :line ""))

Когда пользователь нажимает  Send кнопку, возьмите текущее значение  line и отправьте его как сообщение на сервер.

 (go
(let [rc (chan)]
(>! server-chan {:add rc})
(while true
(let [chats (<! rc)]
(in-scope (doseq [m chats] (.push (:chats $scope) m)))
))))
)

И зарегистрируйте себя в качестве слушателя на сервере, создав локальный канал  chan и отправив его в  :add сообщении на сервер.

Затем дождитесь сообщения от сервера и добавьте их в  chats массив в рамках Angular (чтобы Angular взял изменения и отобразил их).

Серверная сторона

Вот код на стороне сервера:

(go 
(let [server-chan (make-server-chan "The Chat Server")]

Определите названный канал на стороне сервера.

 (while true
(let [v (<! server-chan)]

Получите следующее сообщение.

 (when-let [n (:add v)]
(swap! listeners #(conj % n))
(>! n @chats))

Добавьте к слушателям.

 (when-let [chat-msg (:msg v)]
(swap! chats #(conj % chat-msg))
(doseq [ch @listeners] (>! ch [chat-msg])))))))

обработайте сообщение чата и повторно отправьте его всем слушателям.

Boom. Вот и все.

Я хочу свой HTTP

Итак, как мы отправляем сообщения через HTTP и имеем дело с вещами?

Автоканалы на стороне клиента

Первое, что мы делаем, это меняем сериализатор для канала:

(extend-protocol IPrintWithWriter
cljs.core.async.impl.channels/ManyToManyChannel
(-pr-writer [chan writer opts]
(let [guid (find-guid-for-chan chan)]
(-write writer "#guid-chan\"")
(-write writer guid)
(-write writer "\""))))

Приведенный выше код ищет  guid канал. Если нет  guid для канала, один создан. Итак, у нас есть текстовое представление для канала для отправки на сервер.

И когда мы читаем каналы назад, мы используем:

(defn ^:private guid-chan-reader
[s]
(find-chan-for-guid (str s)))




(cr/register-tag-parser! "guid-chan" guid-chan-reader)

Читатель канала находит соответствующий канал для  guid. Но если  guid не было замечено, мы создаем прокси обратно на сервер:

(defn find-chan-for-guid [guid]
(or (get @guid-to-chan guid)
(let [nc (chan)]
(register-chan nc guid)
(go
(while true
(let [msg (<! nc)]
(send-to-server (pr-str {:chan nc :msg msg}))
)))
nc)
))

И этот прокси ждет сообщений и затем отправляет сообщение на сервер с каналом guid.

Наконец, когда мы получаем сообщение от сервера обратно клиенту, процесс переворачивается:

(fn [me] 
(do
(let [info (.-data me)
parsed (cr/read-string info)
chan (:chan parsed)
msg (:msg parsed)]
(if (and chan msg)
(do 
(go (>! chan msg))) 
)))))

Мы ищем канал из сообщения  :chan guid и отправляем сообщение этому каналу.

Серверная сторона

На стороне сервера почти то же самое, за исключением того, что заклинания сериализации Clojure незначительно отличаются от заклинаний ClojureScript.

Вот наш писатель:

(defmethod print-method 
clojure.core.async.impl.channels.ManyToManyChannel
[chan, ^java.io.Writer w]
(let [guid (find-guid-for-chan chan)]
(.write w "#guid-chan\"")
(.write w guid)
(.write w "\"")))

И наш читатель (мы используем  edn ) код для чтения, потому что он не исполняет код с провода:

(edn/read-string {:readers {'guid-chan find-chan-for-guid}} data)

А вот полный код «читать с провода и отправить сообщение»:

(binding [*websocket* ch]
(let [thing (edn/read-string 
{:readers {'guid-chan find-chan-for-guid}} data)
chan (:chan thing)
msg (:msg thing)]
(if (and chan msg)
(go (>! chan msg))))))))

Наконец, наш прокси-код для клиента:

(go
(loop []
(let [msg (<! nc)]
(hs/send! socket {:body (pr-str {:chan nc :msg msg})})
)
(recur)))

Вот и все.

Делать

В коде еще многое предстоит сделать, включая работу с временными отбрасываниями веб-сокетов, распаковкой сообщений, необязательными сеансами, сборкой мусора прокси каналов при закрытии канала на другой стороне.

Но ключевые части кросс-адресного пространства core.async через HTTP находятся в Plugh.

Ура!

благодаря

Сообщество Clojure довольно удивительно. Выражаем благодарность  Дэвиду  Нолену ( David Nolen), который поддерживает материал ClojureScript core.async вместе с десятками других пакетов.