How to write a Client-Server application using Sockets

With the release of IDL 8.5, the socket procedure supports two new keywords: Listen and Accept. The Listen keyword instructs a socket to listen on a specified port, while the Accept keyword instructs the socket to accept communication over a specified logical unit number (LUN).  In this post, I will demonstrate how to use these keywords to write a simple and practical IDL client-server application that can send data from a client to a server and execute a remote data operation on the server. The application behaves much like a Remote Procedure Call (RPC), but written entirely in IDL.

Let’s start with the server application SockServer that will listen for client connections:

pro SockServer,port
if n_elements(port) eq 0 then port=21038
socket, ListenerLUN, port, /listen, /get_lun
ID = Timer.Set (.1, "ListenerCallback", ListenerLUN)  
return & end

The SockServer procedure opens a unit number ListenerLUN on a specified port. I use a default port of 21038, but be sure not to use a port that is already allocated for other applications such as HTTP (80) or FTP (21). The Listen keyword instructs the socket to listen for connections. These connections will be handled by a callback function ListenerCallback which is invoked by calling IDL’s timer object using the syntax:

ID = Timer.Set( Time, Callback , UserData)

The Timer object creates an asynchronous timer that calls the callback function after a specified time with an optional user input argument. In SockServer, the ListenerCallback function is called after 0.1 seconds with the socket’s ListenerLUN as input. Note that a requirement for a callback function is that its first argument be the ID of the Timer object set method.

Let’s look at ListenerCallback:

pro ListenerCallback,ID,ListenerLUN
status = File_Poll_Input(ListenerLUN, Timeout = .1d)
if status then begin
 socket, ClientLUN, accept = ListenerLUN, /get_lun
 message,'Client connection established on LUN '+strtrim(clientlun,2),/info
 ID = Timer.Set(.1, "ServerCallback", ClientLUN)
ID = Timer.Set(.1, "ListenerCallback", ListenerLUN)
return & end

The Listener callback function literally listens on ListenerLUN for a client connection by using the file_poll_input function:

status = File_Poll_Input(ListenerLUN, Timeout=value) 

When a client attempts to connect to the server, file_poll_input returns a status of true which then triggers another call to socket. This socket call accepts the connection and assigns a ClientLUN unit number to the client. All subsequent data communication between the client and server occurs over this ClientLUN. Once this handshaking is complete, the Listener callback function calls another callback function ServerCallback (via the Timer object) which performs the actual data processing. If there is no connection during a specified Timeout period, file_poll_input returns a status of false and the Listener callback function is called again by the Timer object.

Let’s look at ServerCallback:

pro ServerCallback,ID,ClientLUN
status=File_Poll_Input(ClientLUN, Timeout = .01)
if status then begin
 readu,ClientLun,dsize                  ;-- read data size and type
 data=make_array(size=dsize)            ;-- reconstruct data type
 readu,ClientLun,data                   ;-- read the data
 readf,Clientlun,command                ;-- read the command to execute on the data
ID=Timer.Set(.1, "ServerCallback", ClientLUN)
return & end

The Server callback function behaves in a similar fashion to ListenerCallback. It uses file_poll_input to listen for data sent from the client on ClienLUN. When a true status is signaled, the callback first reads the size and type of data using readu, and prepares a data variable using make_array into which the data will be read. Lastly, the callback reads the string variable command using readf which is passed to the execute function to operate on the data.

Let’s finish with the client application SockClient that will establish the connection to the server:

pro SockClient, ID, port,server, lun=ServerLUN
if n_elements(port) eq 0 then port = 21038
if n_elements(server) eq 0  then server="localhost"
socket, ServerLUN, server, port, /get_lun, error=error
if error ne 0 then ID=timer.set(.1,"SockClient",port)
return & end

As with SockServer, I use a default port of 21038 for the connection. In principle, I can connect to a SockServer application running on a different IP address but security restrictions may prevent that. Hence, for this test example, I default to using localhost such that server and client are running on the same system. The physical connection is made by calling socket with the server and port as input arguments. If the connection is successful (i.e. the server accepted the connection), the socket returns an error value of 0 and the client is ready to send data over the assigned ServerLUN unit number. If not successful, I call SockClient again (via a Timer) and try reconnecting.

To demonstrate the socket client-server application, try the following sample program SockTest which will send a JPEG image from a client to a server where it will be displayed:

pro SockTest,serverLUN
read_jpeg,'stereo.jpg',data,/true          ;-- read JPEG image into data array
writeu, ServerLUN,size(data)               ;-- send the image size and type to the server
writeu, ServerLUN,data                     ;-- send the image data to the server
command="a=image(data,/no_tool)"           ;-- create the command to execute on the server
printf,serverLUN,command                   ;-- send the command to the server
return & end

You will need to download SockClient, SockServer, SockTest, and the JPEG image into your local directory. The programs and image are available at GitHub. Run the test as follows: start two separate IDL sessions; type SockServer in the first session; and type SockClient in the second followed by SockTest. If the connection is successful, you will see the following messages:

IDL> SockServer               ;-- start socket server
% LISTENERCALLBACK: Client connection established on LUN 101
IDL> SockClient               ;-- start socket client
% SOCKCLIENT: Server connection established on LUN 100

IDL> SockTest,100             ;-- send data and command to server via LUN 100

If all goes well, you should see this image in the server’s IDL session:


Leave a Reply

Your email address will not be published. Required fields are marked *