1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
|
\section[A Basic Client/Server]{A Basic Client/Server}
\label{sec:cs}
\addcontentsline{toc}{section}{\thesection. A Basic Client/Server}
In this section, we will develop a more complicated and realistic example using
\pkg{pbdZMQ}. The example will show the construction of a basic client and
server. To do so, we will (eventually) use the Request/Reply pattern, where a
message is passed from client to server, executed on the server, and then the
result is passed back to the client as a message.
All server code is meant to be executed in
batch; though it can be used from an interactive R session, we feel this
somewhat misses the point. To do this, save the server code as, say,
\code{server.r} and start the server by running
\begin{Command}
Rscript server.r
\end{Command}
from a terminal. One could use \code{R CMD BATCH} in place of \code{Rscript},
though by default it will suppress some messages on the server that we will
want to see. Finally, the client should be run inside of an interactive
\proglang{R} session. This can be from RStudio, the Windows/Mac \proglang{R}
guis, or by running the command \code{R} at the terminal --- the way you are
used to using \proglang{R}.
Throughout our examples here, we will be using the \pkg{rzmq}-like bindings
available in \pkg{pbdZMQ}. The other interfaces are easy enough to figure out
once you understand one of them; and the \pkg{rzmq} ones are arguably the most
\proglang{R}-like of the three.
The source code for the final example can be found in the
\code{inst/examples/reqrep/} subtree of the \pkg{pbdZMQ} source, or under
\code{examples/reqrep/} of the binary installation. The same directory
contains versions of this example using the other \pkg{pbdZMQ} interfaces.
\subsection{Our First Client/Server}
\paragraph{The Server}
Our first server will be very humble. It will receive one command from the
client, print that command, and send back a success message before terminating
--- nothing more. Save the following in a file, say \code{server.r}, and
execute it by running \code{Rscript server.r} from a terminal:
\begin{lstlisting}[language=R,title=Server]
library(pbdZMQ)
ctxt <- init.context()
socket <- init.socket(ctxt, "ZMQ_REP")
bind.socket(socket, "tcp://*:55555")
cat("Client command: ")
msg <- receive.socket(socket)
cat(msg, "\n")
send.socket(socket, "Message received!")
\end{lstlisting}
Unfortunately, the first 4 lines are just boilerplate; see the package manual
for an explanation. The good news is that this is about as complicated as
it gets on the ZeroMQ side; everything beyond this is just \proglang{R}
programming.
\paragraph{The Client}
From and interactive R session (\emph{not} in batch!), enter the
following:
\begin{lstlisting}[language=R,title=Client]
library(pbdZMQ)
ctxt <- init.context()
socket <- init.socket(ctxt, "ZMQ_REQ")
connect.socket(socket, "tcp://localhost:55555")
send.socket(socket, "1+1")
receive.socket(socket)
\end{lstlisting}
If all goes well, your message (namely, \code{"1+1"}) should be sent from the
client to the server, and the response \code{"Message received!"} should be
sent from server to client. Afterwards, the server will terminate and you are
free to exit your interactive \proglang{R} session (i.e., the client).
This example is deliberately as basic as can be, and lacks 2 crucial
features: server persistence, and remote execution of commands. We will
develop examples with these features in the remainder of this section.
\subsection{A Persistent Server}
\paragraph{The Server}
Next, we make the server \emph{persistent}, in the sense that it will not
immediately die after receiving its first command. This is trivial, as all
we need to do is encapsulate the receive/send piece inside a \code{while} loop.
As before, save the following to a file and execute in batch via \code{Rscript}:
\begin{lstlisting}[language=R,title=Server]
library(pbdZMQ)
ctxt <- init.context()
socket <- init.socket(ctxt, "ZMQ_REP")
bind.socket(socket, "tcp://*:55555")
while(TRUE)
{
cat("Client command: ")
msg <- receive.socket(socket)
cat(msg, "\n")
send.socket(socket, "Message received!")
}
\end{lstlisting}
The \code{receive.socket()} command does not use \emph{busy waiting}. You can
verify this by starting up the server and then looking at a process monitor for
your operating system; you should see no elevated activity.
\paragraph{The Client}
Set up the client as above (everything but the \code{send.socket()} line is
necessary) in an interaction \proglang{R} session. Now that we have a
persistent server, we can make a shorthand function that encapsulates
sending a message (from client to server) and receiving a response (from
server to client):
\begin{lstlisting}[language=R,title=Client Send/Receive]
sendrecv <- function(socket, data)
{
send.socket(socket, data)
receive.socket(socket)
}
\end{lstlisting}
This assumes that the various optional arguments in \code{send.socket()} and
\code{receive.socket()} are acceptable; and for the purposes of this
demonstration they are. But the reader is encouraged to consult the
\pkg{pbdZMQ} manual for more details about these two functions.
Now, with the convenience function, we can simply execute:
\begin{lstlisting}[language=R,title=Client Usage]
sendrecv(socket, "1+1")
sendrecv(socket, "rnorm(10)")
\end{lstlisting}
or any other valid \proglang{R} command.
\subsection{More Than Messaging}\label{fullcs}
The final piece is to actually execute commands that are sent to the server,
and to pass the result back to the client. This is very easy, and only
requires a slight modification to the server code. Modify the server piece
above to do the following just after receiving (and printing) the client's
message:
\begin{lstlisting}[language=R,title=Server Modification]
result <- eval(parse(text=msg))
send.socket(socket, result)
\end{lstlisting}
Of course, you will also need to remove the original \code{send.socket()} line
with the one here. A nasty source of bugs in client/server programming is
sending when you should receive or vice-versa, leading to deadlocks.
One additional thing the observant reader may have already realized is that our
client/server framework leaves the server running perpetually, with no
reasonable way for the client to terminate it. This just requires basic
filtering of incoming messages (from the client, on the server). So for
example, we might want the message \code{"EXIT"} to terminate the server.
Modifying the server to handle this is trivial, and we present the full server
below:
\begin{lstlisting}[language=R,title=Full Server]
library(pbdZMQ)
ctxt <- init.context()
socket <- init.socket(ctxt, "ZMQ_REP")
bind.socket(socket, "tcp://*:55555")
while(TRUE)
{
cat("Client command: ")
msg <- receive.socket(socket)
cat(msg, "\n")
if (msg == "EXIT")
break
result <- eval(parse(text=msg))
send.socket(socket, result)
}
send.socket(socket, "shutting down!")
\end{lstlisting}
Notice that we essentially added just a few lines. The first and more obvious
is the check on \code{msg} for the magic word \code{"EXIT"}. The addition
of the final \code{send.socket()} line at the end, which returns the string
\code{"shutting down!"} to the client is necessary to prevent the client from
hanging after the server shuts down. Recall, the client expects a response
from the server for every message the client sends!
\subsection{Other Issues}
The above examples are all very basic, but should illustrate how one could
proceed to a more complex client/server design using ZeroMQ from R. Of course,
there are a host of issues that we have not gone into here that are very
important. For example, we perform no scrubbing of inputs that are to be
executed on the server. This could be more or less important depending on the
application.
Another important issue we have not addressed is error and warning handling.
The reader is encouraged to return to the example in Subsection~\ref{fullcs}
and try executing something like
\code{sendrecv(socket, "object_does_not_exist")}
or
\code{sendrecv(socket, "warning('uh oh')")}
to see what happens.
Additionally, each time the client wanted to send a message,
the user had to manually pass it as an argument to the function
\code{sendrecv()}. It is possible --- though complicated --- to create a
custom REPL which will automatically handle the client/server
send-evaluate-receive pattern as though the user were at a standard \proglang{R}
terminal.
Finally, we have not addressed the important issue of logging user commands
sent to the server. Although anyone comfortable with \proglang{R} should
see the path forward.
For a more detailed example illustrating these points, see the \pkg{pbdCS}
package.
|