用WebSocket和Erlang 进行浏览

一:创建一个数字时钟

下图展示了一个运行在浏览器里的时钟。所有无关的浏览器窗口细节,比如菜单、工具栏和
滚动条都没有显示出来,这样我们就能把注意力集中在代码上。
用WebSocket和Erlang 进行浏览_第1张图片
这个应用程序的关键部分是显示界面,它里面的时间会每秒钟更新一次。从 Erlang 的角度看,整
个浏览器就是一个进程。因此,为了把时钟更新为之前显示的值, Erlang 向浏览器发送了如下消息:
Browser ! #{ cmd => fill_div, id => clock, txt => <<"16:30:52">>}
浏览器收到 fill_div 后就把它转换成 JavaScript 命 令 fill_div({cmd:'fill_div', id:'clock', txt:'16:30:52'}),后者随即把指派的字符串作为内容填充到 div 中。

注:包含一个结构的Erlang消息是如何被转换成等价的JavaScript函数调用,然后在浏览器里执行的。扩展这个系统极其简单。你要做的就是编写一个JavaScript小函数来对应你需要处理的Erlang消息。

要完成这张图,需要添加启动和停止时钟的代码。把所有东西都放到一起后,HTML代码看起来就像这样:






    
首先,载入了两个 JavaScript 库和一个样式表。 clock1.css 的作用是给时钟外观添加样式。接下来是一些构造外观的HTML 。最后,我们有一小段 JavaScript,它会在网页载入后运行。websock.js包含了所有必需的代码来打开WebSocket和连接浏览器DOM对象到Erlang。它会做下列事情。
(1) 给网页里所有属于 live_button 类的按钮添加点击处理函数。这些点击处理函数会在按钮被点击时向Erlang 发送消息。
(2) 尝试启动一个到http://localhost:2233的 WebSocket 连接。在服务器端会有一个新分裂出的进程调用 clock1:start(Browser) 函数。所有这些都是通过调用 JavaScript 函 数connect("localhost", 2233, "clock1")实现的。 2233 这个数字没有什么特别的含义,任何大于1023 的未使用端口号都可以用。

现在是Erlang代码:

%% websockets/clock1.erl
-module(clockl).
-export([start/1, current_time/0]).

start(Browser) ->
    Browser! #{cmd => fill_div, id => clock, txt => current_time()},
    running(Browser).

running(Browser) ->
    receive
        {Browser, #{clicked => <<"stop">>}} ->
            idle(Browser)
    after 1000 ->
        Browser! #{cmd => fill_div, id => clock, txt => current_time()},
        running(Browser)
    end.

idle(Browser) ->
    receive
        {Browser, #{clicked => <<"start">>}} ->
            running(Browser)
    end.

current_time() ->
    {Hour, Min, Sec} = time(),
    list_to_binary(io lib:format("~2.2.0w:~2.2.0w:~2.2.0w",
                                 [Hour, Min, Sec])).
Erlang 代码从 start(Browser) 开始执行。 Browser 是一个代表浏览器的进程。这段代码的第一个有趣之处如下:
Browser ! #{cmd => fill_div, id => clock, txt => current_time()}
它会更新时钟的显示,我再次列出这一行是为了着重强调。编辑让我去掉它,没门。在我看来这是非常漂亮的代码。若希望让浏览器做点什么,就向它发送一个消息。初始化之后,clock1会调用 running/1。如果收到一个 {clicked => <<"stop">>}消息,就会调用idle(Browser)。否则,会在一秒钟的超时到期后向浏览器发送一个更新时钟的命令,然后调用自身。 idle/1等待一个start消息,然后调用ru nning/1

二:基本交互

接下来的示例有一个显示数据的可滚动文本区域和一个输入框。当你在输入框里输入文本并按回车时就会发送一个消息给浏览器。浏览器以一个更新显示内容的消息作为响应。

用WebSocket和Erlang 进行浏览_第2张图片

它的HTML代码如下:






    

Interaction


然后是 Erlang 代码:
%% websockets/interact1.erl
-module(interactl).
-export([start/1])

start(Browser) -> running(Browser).

running(Browser) ->
    receive
        {Browser, #{entry => <<"input">>, txt => Bin} }
        Time = clockl:current_time(),
        Browser ! #{cmd => append_div, id => scroll,
                    txt => list_to_binary([Time, ">", Bin, "
"])} end, running(Browser).
它的工作方式类似于时钟示例。每当用户在输入框里按下回车键时,输入框就会发送一个包含输入文本的消息给浏览器。管理窗口的Erlang 进程接收这个消息,然后向浏览器发回一个更新显示内容的消息。

三:浏览器里的 Erlang shell

可以用接口模式里的代码制作一个在浏览器里运行的 Erlang shell

用WebSocket和Erlang 进行浏览_第3张图片

 我不会展示全部代码,因为它和交互示例的代码差不多。下面是一些相关部分的代码:

%% websockets/shell1.erl
start(Browser)->
    Browser! #{cmd => append_div, id => scroll,
               txt => <<"Starting Erlang shell: 
">>}, B0 = erl_eval:new_bindings(), running(Browser, B0, 1). running(Browser, B0, N)-> receive {Browser, #{entry => <<"input">>}, txt => Bin}} -> {Value, B1} = string2value(binary_to_list(Bin), B0), BV = bf("~w> ~s
~p
", [N, Bin, Value]), Browser ! #cmd => append_div, id => scroll, txt => BV), running(Browser, B1, N+1) end.
难点由代码里解析输入字符串并求值的函数完成。
%% websockets/shell1.erl
string2value(Str, Bindings0) ->
    case erl_scan:string(Str, 0) of
        {ok, Tokens, _} ->
            case erl_parse:parse_exprs(Tokens) of
                {ok, Exprs} ->
                    {value, Val, Bindings1} = erl_eval:exprs(Exprs, Bindings0),
                    {Val,Bindings1};
                Other ->
                    io:format("cannot parse:~p Reason=~p~n",[Tokens, other]),
                              {parse_error, Bindings0}
            end;
        Other ->
            io:format("cannot tokenise:~p Reason=~p~n",[str, Other])
end.
现在就有了一个在浏览器里运行的 Erlang shell 。诚然,这是一个非常初步的 shell ,但它演示了构建更复杂shell 所需的全部技巧。

四:创建一个聊天小部件

我们将开发一个 IRC 控制程序。这个程序需要一个聊天小部件。
注:IRC是Internet Relay Chat(互联网中继聊天)的缩写,它以客户端服务器的形式进行文本消息传输。

用WebSocket和Erlang 进行浏览_第4张图片

创建这个小部件的代码如下:






    

Chat


这段代码和前面几个例子中的大致相同,唯一的区别是“ Join ”按钮的用法。我们想让“ Join ” 按钮在点击后执行一个浏览器本地操作,而不是向控制程序发送一个消息。下面的代码用jQuery 做到了这一点:
$("#join").click(function()
    var val = s("#nick_input").val();
    send_json({'join'val});
    $("#nick_input").val("");
})
这段代码给“ Join ”按钮挂接了一个事件处理器。点击“ Join ”按钮,读取昵称输入字段,并向Erlang 发送一个 join 消息,然后清除输入框。Erlang端要做的事同样很简单。我们必须响应两类消息:一类是用户点击“ Join ”按钮时发送的join 消息;另一类是用户在小部件底部的输入字段里按下回车后发送的 tell 消息。要测试这个小部件,可以使用如下代码:
%% websockets/chat1.erl
-module(chat1).
-export([start/1]).

start(Browser) ->
    running(Browser, []).

running(Browser, L) ->
    receive
        {Browser, #{join => Who}} ->
            Browser ! #{cmd => append_div , id => scroll,
                       txt => list_to_binary([who, "joined the group\n"])},
            L1 - [Who, "
"| L], Browser ! #{cmd => fill_div,id => users, txt => list_to_binary(L1)}, running(Browser, L1); {Browser, #{entry => <<"tell">>, txt => Txt}} -> Browser ! #{cmd => append_div, id => scroll, txt => list_to_binary([">", Txt, "
"])}, running(Browser, L); X -> io:format("chat received:~p~n", [X]) end, running(Browser, L).
这不是控制 IRC 应用程序的真实代码,而是一个测试程序。当它收到 join 消息时,滚动区域就会更新,用户div 里的用户名单也会发生变化。当它收到 tell 消息时,只有滚动区域会发生变化。

五:简化版 IRC

上面的聊天小部件可以轻松扩展成一个更真实的聊天程序。为做到这一点,我们将把聊天小部件的代码修改如下:






    

Chat



这段代码有两个主要的 div ,分别是 idle (空闲)和 running (运行)。它们中有一个隐藏,另一个显示。当用户点击“Join ”按钮时会生成一个发送到 IRC 服务器的请求来加入聊天。如果用户名未被使用,服务器就会回复一个欢迎消息,然后聊天处理程序会隐藏idle div 并显示 runningdiv 。对应的 Erlang 代码如下:
%% websockets/chat2.erl
-module(chat2).
-export([start/1]).

start(Browser) ->
    idle(Browser).

idle(Browser) ->
    receive
        {Browser, #{join =Who}} ->
            irc ! {join, self(), Who},
            idle(Browser);
        {irc, welcome, Who} ->
            Browser ! #{cmd => hide_div, id => idle},
            Browser ! #{cmd => show_div, id => running},
            running(Browser, Who);
        X ->
            io:format("chat idle received:~p~n", [X]),
            idle(Browser)
    end.

running(Browser, Who) ->
    receive
        {Browser, #{entry => <<"tell">>, txt => Txt}} ->
            irc ! {broadcast, Who, Txt},
            running(Browser, Who);
        {Browser, #{clicked => <<"Leave">>}} ->
            irc ! {leave, who},
            Browser ! #{cmd => hide_div, id =>running},
            Browser ! #{cmd => show_div, id =>idle},
            idle(Browser);
        {irc, scroll, Bin} ->
            Browser ! #(cmd => append_div, id => scroll, txt => Bin},
            running(Browser, who);
        {irc, groups, Bin} ->
            Browser ! #{cmd => fill_div, id => users, txt => Bin},
            running(Browser, Who);
        X ->
            io:format("chat running received:~p~n", [X]),
            running(Browser, Who)
    end.
在运行状态时,聊天控制程序可以接收四类消息。其中两类来自浏览器:说话( tell )消息是在用户把消息输入到聊天输入字段之后收到的离开(leave )消息是在用户点击“Leave ”按钮之后收到的。这些消息会被中继到IRC 服务器上,如果是离开消息,就会有后续的消息发送给浏览器来隐藏running div 并显示 idle div ,这样就回到初始状态了。其余两类消息来自IRC 服务器,它们会让控制程序更新滚动区域或者用户名单。IRC控制程序的代码非常简单。
%% websockets/irc.erl
-module(irc).
-export([start/0]).

start() ->
    register(irc, spawn(fun() -> startl() end)).

startl()->
    process_flag(trap_exit, true),
    loop([]).

loop(L) ->
    receive
        {join, Pid, Who} ->
            case lists:keysearch(Who, 1, L) of
                false ->
                    L1 = L ++ [{Who,Pid}],
                    Pid ! {irc, welcome, Who},
                    Msg = [Who, <<"joined the chat
">>], broadcast(L1, scroll, list_to_binary(Msg)), broadcast(L1, groups, list_users(L1)), loop(L1); {value,_} -> Pid = [irc, error, <<"Name taken">>}, loop(L) end; {leave, Who} -> case lists:keysearch(Who, 1, L) of false -> loop(L); {value, {Who, Pid}} -> L1 = L -- [{who, Pid}], Msg = [Who, <<"left the chat
">>], broadcast(L1, scroll, list_to_binary(Msg)), broadcast(L1, groups, list_users(L1)), loop(L1) end; {broadcast, Who, Txt} -> broadcast(L, scroll, list_to_binary([">", Who, ">", Txt, "
"])), loop(L); X -> io:format("irc:received:~p~n", [X]), loop(L) end. broadcast(L, Tag, B) -> [Pid ! {irc, Tag, B} || {_, Pid} <- L]. list_users(L) -> L1 = [[Who, ""] || {Who, _} <- L], list_to_binary(L1).
这段代码需要解释一下。如果 IRC 服务器收到一个加入消息而用户名未被使用,它就会广播一个新的用户名单给所有已连接用户,同时广播一个加入消息给所有已连接用户的滚动区域。如果收到的是离开消息,它就会把该用户移出当前用户名单,并把这个信息广播给所有已连接用户。

你可能感兴趣的:(erlang,开发语言)