Skip to content

Commit

Permalink
Add images and Etude 11-3. Changed Etude 11-2 to use ?SERVER
Browse files Browse the repository at this point in the history
instead of manifest constant atom "weather".
  • Loading branch information
jdeisenberg committed Mar 1, 2013
1 parent a4a4c6d commit e9ad394
Show file tree
Hide file tree
Showing 14 changed files with 3,130 additions and 5 deletions.
55 changes: 52 additions & 3 deletions appendix-a.asciidoc
Expand Up @@ -445,7 +445,8 @@ include::code/ch11-01/weather_sup.erl[]
=== Solution 11-2
Here is a suggested solution for
<<CH11-ET02,Étude 11-2>>. Since the bulk of the code
is identical, the only code shown here is the revised +-export+ list
is identical to the code in the previous étude,
the only code shown here is the revised +-export+ list
and the added functions.

==== +weather.erl+
Expand All @@ -456,10 +457,58 @@ and the added functions.
%% Wrapper to hide internal details when getting a weather report
report(Station) ->
gen_server:call(weather, Station).
gen_server:call(?SERVER, Station).
%% Wrapper to hide internal details when getting a list of recently used
%% stations.
recent() ->
gen_server:cast(weather, "").
gen_server:cast(?SERVER, "").
----

[[SOLUTION11-ET03]]
=== Solution 11-3
Here is a suggested solution for
<<CH11-ET03,Étude 11-3>>. Since the bulk of the code
is identical to the previous étude,
the only code shown here is the added and revised code.

[source, erlang]
----
%% @doc Connect to a named server
connect(ServerName) ->
Result = net_adm:ping(ServerName),
case Result of
pong -> io:format("Connected to server.~n");
pang -> io:format("Cannot connect to ~p.~n", [ServerName])
end.
%% Wrapper to hide internal details when getting a weather report
report(Station) ->
gen_server:call({global, weather}, Station).
%% Wrapper to hide internal details when getting a list of recently used
%% stations.
recent() ->
gen_server:call({global,weather}, recent).
%%% convenience method for startup
start_link() ->
gen_server:start_link({global, ?SERVER}, ?MODULE, [], []).
%%% gen_server callbacks
init([]) ->
inets:start(),
{ok, []}.
handle_call(recent, _From, State) ->
{reply, State, State};
handle_call(Request, _From, State) ->
{Reply, NewState} = get_weather(Request, State),
{reply, Reply, NewState}.
handle_cast(_Message, State) ->
io:format("Most recent requests: ~p\n", [State]),
{noreply, State}.
----


197 changes: 197 additions & 0 deletions ch11-otp.asciidoc
@@ -1,6 +1,28 @@
[[OTP]]
Getting Started with OTP
------------------------
In order to help me understand how the +gen_server+ behavior works,
I drew the diagram shown in <<FIG1101>>.

[[FIG1101]]
.Processing a call in +gen_server+
image::images/eter_1101.png[float="true"]

The client does a +gen_server::call(Server, Request)+. The server will
then call the +handle_call/3+ function that you have provided in the
+Module+ that you told +gen_server+ to use. +gen_server+ will send your
module the client's +Request+, an identifier telling who the request is
+From+, and the server's current +State+.

Your +handle_call/3+ function will fulfill the client's +Request+ and
send a +{reply, Reply, NewState}+ tuple back to the server. It, in turn,
will send the +Reply+ back to the client, and use the +NewState+ to update
its state.

In _Introducing Erlang_ and in the next two études,
the client is you, using the shell. The module that handles the
client's call is contained within the same module as the +gen_server+
framework, but, as the preceding diagram shows, it does not have to be.

[[CH11-ET01]]
Étude 11-1: Get the Weather
Expand Down Expand Up @@ -300,3 +322,178 @@ Most recent requests: ["KCMI","KSJC"]
------
<<SOLUTION11-ET02,See a suggested solution in Appendix A.>>
[[CH11-ET03]]
Étude 11-3: Independent Server and Client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the previous études, the client and server have been running in
the same shell. In this étude, you will make the server available to
clients running in other shells.
To make a node available to other nodes, you need to name the node by using
the +-name+ option when starting +erl+. It looks like this:
[source, erl]
----
marco@localhost $ erl -name serverNode
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(serverNode@localhost.gateway.2wire.net)1>
----
This is a _long name_. You can also set up a node with a short name by using
the +-sname+ option:
[source, erl]
----
marco@localhost $ erl -sname serverNode
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(serverNode@localhost)1>
----
WARNING: If you set up a node in this way, _any_ other node can connect
to it and do any shell commands at all. In order to prevent this,
you may use the +-setcookie _Cookie_+ when starting +erl+. Then,
only nodes that have the same _Cookie_ (which is an atom) can
connect to your node.
To connect to a node, use the +net_adm:ping/1+ function, and give it
the name of the server you want to connect to as its argument. If you
connect succesfully, the function will return the atom +pong+; otherwise,
it will return +pang+.
Here is an example. First, start a shell with a (very bad) secret
cookie:
[source, erl]
----
marco@localhost $ erl -sname serverNode -setcookie chocolateChip
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(serverNode@localhost)1>
----
Now, open another terminal window, start a shell with a different
cookie, and try to connect to the server node. I have purposely used
a different user name to show that this works too.
[source, erl]
----
jasper@localhost $ erl -sname clientNode -setcookie oatmealRaisin
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(clientNode@localhost)1> net_adm:ping(serverNode@localhost).
pang
----
The server node will detect this attempt and let you know about it:
[source, erl]
----
=ERROR REPORT==== 28-Feb-2013::22:41:38 ===
** Connection attempt from disallowed node clientNode@localhost **
----
Quit the client shell, and restart it with a matching cookie, and
all will be well.
[source, erl]
----
jasper@localhost erltest $ erl -sname clientNode -setcookie chocolateChip
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(clientNode@localhost)1> net_adm:ping(serverNode@localhost).
pong
----
To make your weather report server available to other nodes, you
need to do these things:
* In the +start_link/0+ convenience method, set the first argument to
+gen_server:start_link/4+ to +{global, ?SERVER}+ instead of
+{local, ?SERVER}+
* In calls to +gen_server:call/2+ and +gen_server:cast/2+, replace the
module name +weather+ with +{global, weather}+
* Add a +connect/1+ function that takes the server node name as its
argument. This function will use +net_adm:ping/1+ to attempt to contact
the server. It provides appropriate feedback when it succeeds or fails.
Here is what it looks like when one user starts the server in a shell.
[source, erl]
----
marco@localhost $ erl -sname serverNode -setcookie meteorology
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(serverNode@localhost)1> weather:start_link().
{ok,<0.39.0>}
----
And here's another user in a different shell, calling upon the server.
[source, erl]
----
jasper@localhost $ erl -sname clientNode -setcookie meteorology
Erlang R15B02 (erts-5.9.2) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]
Eshell V5.9.2 (abort with ^G)
(clientNode@localhost)1> weather:connect(serverNode@localhost).
Connected to server.
ok
(clientNode@localhost)2> weather:report("KSJC").
{ok,[{location,"San Jose International Airport, CA"},
{observation_time_rfc822,"Thu, 28 Feb 2013 21:53:00 -0800"},
{weather,"Fair"},
{temperature_string,"52.0 F (11.1 C)"}]}
(clientNode@localhost)3> weather:report("KITH").
{ok,[{location,"Ithaca / Tompkins County, NY"},
{observation_time_rfc822,"Fri, 01 Mar 2013 00:56:00 -0500"},
{weather,"Light Snow"},
{temperature_string,"31.0 F (-0.5 C)"}]}
(clientNode@localhost)4> weather:recent().
ok
----
Whoa! What happened to the output from that last call?
The problem is that the +weather:recent/0+ call does
an +io:format/3+ call; that output will go to the server shell, since the
server is running that code, not the client. Bonus points if you fix this
problem by changing +weather:recent/0+ from using
+gen_server:cast/2+ to use +gen_server:call/2+ instead to return
the recently reported weather stations as its reply.
There's one more question that went through my mind after I implemented
my solution: how did I know that the client was calling the +weather+ code
running on the server and not the +weather+ code in its own shell? It was
easy to find out: I stopped the server.
[source, erl]
----
(serverNode@localhost)2>
User switch command
--> q
marco@localhost $
----
Then I had the client try to get a weather report.
[source, erl]
----
(clientNode@localhost)5> weather:report("KSJC").
** exception exit: {noproc,{gen_server,call,[{global,weather},"KSJC"]}}
in function gen_server:call/2 (gen_server.erl, line 180)
----
The fact that it failed told me that yes, indeed, the client was getting its
information from the server.
<<SOLUTION11-ET03,See a suggested solution in Appendix A.>>
4 changes: 2 additions & 2 deletions code/ch11-02/weather.erl
Expand Up @@ -15,12 +15,12 @@

%% Wrapper to hide internal details when getting a weather report
report(Station) ->
gen_server:call(weather, Station).
gen_server:call(?SERVER, Station).

%% Wrapper to hide internal details when getting a list of recently used
%% stations.
recent() ->
gen_server:cast(weather, "").
gen_server:cast(?SERVER, "").

%%% convenience method for startup
start_link() ->
Expand Down

0 comments on commit e9ad394

Please sign in to comment.