Sockets Connection


The Sockets Connection package provides Dolphin Smalltalk with an interface to TCP/IP Sockets.

You must agree to the license conditions when you install the software. Once installed, use the Package Browser to load Sockets Connection.pac into your Dolphin image.

Sockets provide a network communication infrastructure so that two computer processes may exchange data with one another. A Socket represents one end of a network connection. When two parties are connected there is a Socket at each end of the connection. These two end points may reside in the same machine, on separate machines on a LAN or on separate machines on a wide area network such as the Internet. Once a connection has been established, is listed data can be sent in either direction across the link.

Microsoft Windows implements Sockets so that they can be used with a number of underlying network protocols. However, Dolphin Sockets Connection is mainly concerned with using TCP/IP which is the standard Internet protocol.


Table of Contents

The Socket Connection Interface

Internet Addresses

Listening for a Connection with a ServerSocket

Establishing a Client Connection

Talking Bytes across a Socket Connection

Streaming over a Socket Connection

Talking Objects across a Socket Connection

Monitoring Blocking Calls

Using the Non-Blocking Calls

Exceptions

Additional Samples - Chat


The Socket Connection Interface

The Dolphin Sockets Connection package provides several levels of interface to the underlying Windows Sockets layer.

The lowest level of interface is provided by the WSockLibrary class, which is an ExternalLibrary subclass responsible for interface into the function calls in the Windows Sockets, WSOCK32.DLL library. We provide methods for most of the functions in this library. Most often, you will find that the higher levels of interface provided by Sockets Connection will be more than adequate for your networking needs but, if you find that you need to add facilities, you always can "kick down" and call methods in WSockLibrary when necessary.

Next up, Sockets Connection implements a number of classes that make it easy to set up a network link with a Socket at either end. At this basic level you can send and receive bytes and arrays of bytes across the link.

A level further up you'll find that each Socket can provide you with a read stream and a write stream for easy streaming of data using the standard Smalltalk Stream protocol. This is a very powerful level of interface and one that you will probably find yourself using most often.

At the highest level it is possible to connect a Dolphin STB filer onto either of the two Socket streams, thus allowing serializable objects to be easily passed back and forth across a connection.

This text is intended to provide a general user's guide on how to use Sockets Connection. We will not touch any further on the lowest level of interface; the WSockLibrary function calls. If you need more information on these then you should consult the Microsoft SDK documentation or a suitable book. We will run, however, through a number of scenarios for using the higher levels of interface. If you want reference information about each of the classes and the methods that they provide, then you should consult the code directly. All of the classes and methods are commented appropriately

Blocking

Many of the standard function calls in the WSockLibrary will block the operating system thread on which they are executing until an operation completes. At the time of writing, Dolphin Smalltalk does not support native operating system threads. Therefore the use of such thread blocking calls is, to say the least, somewhat inconvenient since they will hang up the entire user interface for the duration of the call. Fortunately, Microsoft has provided a number of asynchronous equivalents to these functions (usually with a WSA prefix) which allows the current thread to continue while the operation takes place. The caller is then notified by means of a Windows message when the function call completes its business.

Dolphin supports multiple Smalltalk processes for multitasking purposes but, in fact, these execute on a single operating system thread (apart from garbage collection and finalization). The Sockets Connection package makes extensive use of these asynchronous WSA calls to avoid blocking the Dolphin main thread but to allow the higher level calls to be able to block a Smalltalk process on a Semaphore. The result is that, to all intents and purposes, we are able to provide blocking socket operations in Dolphin.

Implementation note: the WinAsyncSocket subclass of View is used to handle the callbacks from the WSAxxx function calls. It has a singleton instance whose job it is to intercept the Windows notification messages and to redirect them to the appropriate Socket that made call. The handlers for these notifications are the #onAsyncXXX methods in SocketAbstract and its subclasses.

Blocking the User Interface Main Process

Tip: if you find the following explanation difficult to understand, don't worry; it is not necessary to do so in order to use Sockets effectively.

Since the asynchronous function calls that Sockets Connection makes use of, use Windows messages as their callback mechanism, it is important that the main message processing loop in Dolphin always continues to run. In order to guarantee this, if you execute a blocking Socket call in the user interface process (say by evaluating it in a workspace), then another temporary UI process will be started to take over as the first one is blocked. This allows the callback notification messages generated by the WSA calls to continue to be processed. When the operation completes, the blocking semaphore may then be signalled and the original process will be released. Eventually, when the original process runs to completion, it will be terminated and you'll be left with the same number of basic processes running in the system, it's just that the UI process will now be a different one.

We only mention this here, since it can create some slightly strange effects when evaluating blocking calls from within a workspace. You'll find, if you issue such a call, that it actually appears not to block since the user interface remains alive. This allows you to evaluate more expressions and perform other UI activities before the original call returns.

Non-Blocking

Although the blocking calls described above are probably the most common way of using Sockets in your application, they do demand that you execute multiple processes to handle them. For simple applications this can be an overly complex solution so we also provide a number of non-blocking calls as alternatives. For each of these, the call returns immediately and the actual completion of the operation is signalled by an event triggered off the Socket. Interested parties can register an interest in these events using #when:send:to:.

Examples

In the examples that follow we will assume that you have two workspace windows, one for each end of a connection. We'll call these the Server and Client workspaces. Ideally, to demonstrate the full networking potential, you'll have these workspaces on two different machines connected via a LAN. However, you can just as easily use two workspaces running in the same image on a single machine.

Persistency of Sockets

You should be aware that the validity of a Socket descriptor is not maintained when a Dolphin image is shut down and re-started. You'll find that, after re-starting an image, all Sockets will appear to have been closed and will have nil descriptors. You should bear this in mind when executing the examples since, if you work through them over several sessions, you will have to re-open previously opened Sockets before continuing.


Internet Addresses

If we want to connect one machine to another across a network, it is essential to be able to identify machines easily. The TCP/IP Internet Protocol identifies machines by means of their IP addresses. An IP address usually looks something like 192.169.0.1. Each machine on a LAN or connected to the Internet will have a unique IP address. However, these IP addresses are difficult to remember (and sometimes may change) so it is useful to have an alternative naming scheme. TCP/IP has the concept of host names where each computer may be given a textual name. There must therefore be a mapping mechanism of host names to IP addresses and vice versa. How this works is beyond the scope of this text but, if you intend to test Sockets Connection across a network, you should find out the hostname and IP address of the computers you'll be using. You may have to contact your network administrator to find out this information.

Sockets Connection uses the InternetAddress class to represent an Internet address, either by hostname or IP address. You can also use this class for mapping between IP addresses and hostnames as and when required.

Examples

"The local machine can always be identified by the hostname - localhost"
ia := InternetAddress host: 'localhost'.

"Display the IP address for this host"
ia ipAddress

In fact, the 'localhost' name and #[127 0 0 1] address are aliases present on all machines. You cannot use them for talking from one machine to another. You will have to substitute your own IP addresses and host names, where appropriate, in the example below.

"Display the real IP address of your machine"
(InternetAddress host: 'moll') ipAddress.

"Display a reverse lookup of host name from IP address"
(InternetAddress ipAddress: #[192 169 0 1]) host.

This latter operation can often be quite time-consuming.

In the examples that follow, we will assume that the server machine has an IP address of 192.169.0.1 and a host name of 'moll'.


Listening for a Connection with a ServerSocket

In order to set up a Socket connection, one end of the proposed conversation must request a connection with another. We call the requesting end the client and the other end the server. This nomenclature is only really relevant while the conversation is being set up. Subsequently, both ends of the connection have equal status and can send unsolicited data to each other as they see fit.

For a server to listen for a connection request from a client we use a ServerSocket set up on a particular TCP/IP port on the server machine.

Tip: Ports are represented by integers in the range 0-65,535 but normally those below 1024 are reserved for common services such as a web or FTP servers. Therefore, you should usually choose a port number above 1024 for your applications.

Examples

In the Server workspace:

"Create a ServerSocket on port 2048"
serverSocket := ServerSocket port: 2048.

"Accept a connection on this port - this call will block until a client connects. The socket that is returned can then be used to talk to the client"
socketA := serverSocket accept. 

At this stage, if you display the value of socketA, it will be nil since no connection has yet been established.


Establishing a Client Connection

An instance of the Socket class represents one end of a connection. A client must create a Socket, identifying the server to which it wants to connect (by InternetAddress and port number), and then issue a #connect call. If the connection succeeds, each end will have an appropriate Socket object that can be used to talk across the link.

Examples

In the Client workspace:

"Identify the host and port of the server to connect to"
socketB := Socket port: 2048 address: (InternetAddress ipAddress: #[192 169 0 1]).

"Connect to the server - this call will block until the connection is acknowledged"
socketB connect.

So now we have an open connection with socketA at one end and socketB at the other.


Talking Bytes across a Socket Connection

The lowest level of Socket communication is the sending and receiving of bytes. You can use the #sendByte: method to send single bytes (integers between zero and 255 inclusive). For sending larger quantities of data the #sendByteArray: method is more efficient. These calls will block until the data has actually been sent, although not necessarily until it has been received at far end of the connection.

Examples

In the Client workspace send some bytes to socketB.

socketB sendByte: 255.
socketB sendByteArray: #[1 2 3 4].
socketB sendByteArray: 'hello'.

Use #receiveByte to read a single byte from a Socket and to answer its integer value. If no data is available to be read, the call will block until the first byte is received. If you are expecting a number of bytes, then you can use #receiveByteArray: to receive these into a ByteArray in a single operation. Note, though, that you must provide #receiveByteArray: with the size of the array to allocate and return. The call will block until at least this number of bytes has been received by the Socket.

You may want to find out if a Socket has data available to be read before issuing one of these blocking calls. You can test this using the #hasInput method, which will answer true if one or more bytes are queued up waiting to be received.

Examples

In the Server workspace let's receive the bytes from socketA that we sent earlier through socketB. You should display the results of evaluating each of the following.

In the Server workspace:

socketA hasInput.

socketA receiveByte.
socketA receiveByteArray: 4.
(socketA receiveByteArray: 5) asString.

socket hasInput.

The last test for input should return false to indicate that there are no more bytes waiting for collection from the Socket.


Streaming over a Socket Connection

A generally more convenient method for transferring data across a Socket connection is to use streams. The Socket package includes two stream subclasses SocketReadStream and SocketWriteStream for this purpose. These classes also provide a buffering mechanism that, under most circumstances, dramatically increases performance.

Each Socket supports both a read stream and a write stream and these can be accessed using the #readStream and #writeStream methods respectively. Once you have an appropriate stream object you can use the standard Stream protocol for sending and receiving data. The only additional methods are #flush, for flushing out the buffers of a SocketWriteStream, and #hasInput, for checking the presence of data on a SocketReadStream without the need for blocking.

Examples

Let's perform our previous experiments again, but this time using the Socket streams. In the Client workspace:

socketB writeStream nextPut: 255.
socketB writeStream nextPutAll: #[1 2 3 4].
socketB writeStream nextPutAll: 'hello'.
socketB writeStream flush.

Note the need to finally flush the buffer of the SocketWriteStream to ensure all data is actually sent. Now, in the Server workspace, let's receive the data using the SocketReadStream associated with socketA. Display the results of evaluating the following:

socketA readStream hasInput.

socketA readStream next.
socketA readStream next: 4.
(socketA readStream next: 5) asString.

socketA readStream hasInput.

You'll probably find yourself using the stream interface for the majority of your work. As you can see from the above examples, it is useful for sending both binary and textual data, which makes it ideal for use when one end of the connection perhaps isn't even written in Smalltalk. You might find yourself wanting to write a connection between a Dolphin server and a Java client or, maybe, a C++ server and a Dolphin client. The stream interface will adequately support such scenarios.


Talking Objects across a Socket Connection

If you are safe in the knowledge that both ends of a connection are being handled by a Dolphin application then you can make use of an STB Filer to send higher levels of data over the link. As you are probably aware, the STB (Smalltalk Binary) mechanism is able to stream entire object graphs into a serial format and then to rebuild an equivalent set of objects at some destination location.

To send an object across a Socket write stream using the STB mechanism you must first create an STBOutFiler on to the stream. A number of simple or complex objects can then be put to the filer, which will serialize them onto the stream and across the socket connection. To retrieve these objects at a destination Socket you must have an STBInFiler that has been created on the Socket's read stream. It is then simply a matter of pulling the objects from the filer, one by one, using its #next method.

Examples

Let us send some more complex objects from socketB to socketA and vice versa. We'll send a Dictionary from the Client to the Server and add some items to it at the Server end. Then we'll return the updated Dictionary back to the Client.

In the Client workspace:

outFilerB := STBOutFiler on: socketB writeStream.
outFilerB nextPut: ##(Dictionary new
	at: 'Jones' put: '171 209 9000';
	at: 'Smith' put: '181 200 2345';
	yourself).
outFilerB stream flush.

In the Server workspace we receive the Dictionary, add to it, and return it:

inFilerA := STBInFiler on: socketA readStream.
myDictionary := inFilerA next.
myDictionary at: 'Thomas' put: '1785 66928'.

outFilerA := STBOutFiler on: socketA writeStream.
outFilerA nextPut: myDictionary.
outFilerA stream flush.

Now, back in the Client workspace, we'll receive the returned Dictionary and display it to confirm that it has the new entry.

inFilerB := STBInFiler on: socketB readStream.

inFilerB next. "Display it"

Tip: when you create an STBInFiler onto a Socket read stream, this operation will generally block. This is because the filer initially attempts to read some header information from the stream. If this is inconvenient then you should ensure that the STBOutFiler at the other end of the connection is always created first, and the stream flushed, so that the header information is immediately available at the receiving end.

Convenience methods for reading and writing objects

To avoid the need to create explicit STB Filer objects, we have added a couple of convenience methods to the Socket class for sending and receiving objects. You can use the #send: method to send an object using a dynamically created STBOutFiler and then, at the destination Socket, use #receive to receive it. The only disadvantage when using these methods is that they are slightly less efficient for sending large numbers of objects since a new STB Filer must be created for each one.

Examples

Let's illustrate the bare bones of a "Smalltalk Server". We'll get the client to send bits of Smalltalk source to the server for evaluation, and the results will then be sent back to the client for display.

In the Server workspace, start-up a server process:

serverProcess := [[answer := Compiler evaluate: socketA receive logged: false.
	socketA send: answer] repeat] fork.

This will repeatedly take String requests from the server Socket and use the compiler to evaluate them. The resulting answer object is then sent back to the client for display.

In the Client workspace, send some requests to the server and receive the results:

socketB send: '3+4'; receive. "Display it"
socketB send: '200 factorial'; receive. "Display it"

Obviously, you want to be somewhat careful about the requests you make of the server in this demonstration. Remember, it is serializing the actual result object across the Socket link (not just its textual representation) so, if this is very large, you might end up waiting a long time for the operation to complete. Also, be aware that not all objects are serializable using the STB mechanism.

When you've finished, shut down the server process to tidy up:

serverProcess terminate.

Monitoring Blocking Calls

So far we have been using blocking Socket calls executed from a workspace and therefore relying on the fact that another user interface process will be automatically started when the original is blocked. If you were to use these calls from within your application classes you would normally want to start-up a background process specifically for this purpose. Maintaining one or more background processes can be tedious, especially in situations where the requirements are fairly straightforward. For this reason, we have introduced a class, BlockingCallMonitor, to help with the task of maintaining a background process in which a blocking call can be easily made and monitored.

Once instantiated, a BlockingCallMonitor instance can be configured with a Block that, when evaluated, will run the proposed blocking call. It can also be configured with Blocks to be executed when the call completes or when an error occurs. The default completion block merely triggers a #completedWith: event that will be supplied with the result of the call as its argument. The default error block re-signals the error, but you might often replace this with another Block that triggers an appropriate event.

Examples

First, a simple example demonstrating how a BlockingCallMonitor can be used to indicate when an STB streamed object has been received and fully assembled by a Socket. In the Server workspace:

monitor := BlockingCallMonitor callBlock: [socketA receive].
monitor when: #completedWith: send: #notify: to: MessageBox.
monitor monitor.

Tip: note that the parameter passed when #completedWith: is triggered will also be forwarded as the parameter to MessageBox>>notify:. For this reason, this example only works when socketA receives String objects.

Now, in the Client workspace:

socketB send: 'Hello Sockets'.
socketB send: 'Hello Again'.

For each send of a String from socketB you should see the monitor bring up a MessageBox displaying the string's value. Since the monitor is repeatedly evaluating the call block on its background process this can continue ad-infinitum if required.

You should always aim to terminate a BlockingCallMonitor's monitoring process when you finished using it. Otherwise, the monitor object will not be garbage collected. In the Server workspace:

monitor terminate.

As a more complicated example let's see how multiple BlockingCallMonitors can be used to easily implement a complete "Smalltalk Server" capable of servicing multiple client connections. Because of the complexity, you should probably copy the code below into a new workspace via the Clipboard to avoid any obvious typos that might prevent it from working first time.

"Here is the code to set up the Smalltalk Server"
serverSocket := ServerSocket port: 2048.
acceptanceMonitor := BlockingCallMonitor new.
acceptanceMonitor callBlock: [serverSocket accept].
acceptanceMonitor completionBlock: [:socket | 
	serverMonitor := BlockingCallMonitor new.
	serverMonitor callBlock: [Compiler evaluate: socket receive logged: false].
	serverMonitor completionBlock: [:answer | socket send: answer].
	serverMonitor errorBlock: [:error | Sound beep. serverMonitor terminate].
	serverMonitor monitor ].
acceptanceMonitor monitor.

A new ServerSocket is set up on port 2048. A BlockingCallMonitor is created (the acceptanceMonitor) to accept connection requests from clients and, as each arrives, another one is instantiated (the serverMonitor) to receive and handle the Smalltalk evaluations. Note that we are using the error block of the serverMonitor to detect any errors and shut down the monitor process. This will also detect the situation when the client socket is closed and clean up appropriately.

Now, let's connect some clients and make some requests of the server.

"Client A"
socketA := Socket port: 2048 address: (InternetAddress ipAddress: #[192 168 0 1]).
socketA connect.
socketA send: '5 factorial'; receive. "Display it"
"Client B"
socketB := Socket port: 2048 address: (InternetAddress ipAddress: #[192 168 0 1]).
socketB connect.
socketB send: '3+4'; receive. "Display it"
socketB send: '((1 to: 10) collect: [:i | i ]) reverse'; receive. "Display it"

"Close each client"
socketA close.
socketB close.

As each client socket is closed you should hear a beep as the appropriate error block is executed and the corresponding serverMonitor is terminated. When you're finished, don't forget to clean up the acceptanceMonitor before closing the workspace.

acceptanceMonitor terminate.

Using the Non-Blocking Calls

As mentioned previously, there are a number of non-blocking versions of some of the Socket calls available for use in simple situations where the additional complexity of starting a background process or using a BlockingCallMonitor is not warranted. The following non-blocking calls are available:

ServerSocket>>acceptNoWait

Similar to #accept, this method starts listening for a client connection but returns immediately. When when a connection is accepted, a #connectionAccepted: event is triggered off the ServerSocket with the newly connected server-side Socket as its parameter.

Socket>>connectNoWait

Similar to #connect for client Sockets, this method attempts to connect to an appropriate server but returns immediately. When the connection request is accepted by the server, a #connected event is triggered off the Socket.

Socket>>hasInput

The #receive, #receiveByte and #receiveByteArray: methods all block until sufficient data is read so that they can complete. However, it is possible to detect whether a Socket has input present at any time by using the #hasInput method. This returns immediately and answers true if data is available or false otherwise.

A Socket will also trigger a #dataRead event off itself whenever new data is received across the connection. Intercepting this can be used to indicate whether is then appropriate to actually receive the new data using one of the blocking calls above.


Exceptions

Error handling for Sockets is achieved through the standard Exception mechanism. The default outcome of an error is a walkback indicating an unhandled exception. Exceptions can be handled by closing the appropriate code in a block and using #on:do:. All Socket errors are subclassed from SocketError but a few of the more useful errors such as SocketClosed and SocketWaitCancelled are distinguished explicitly.

Tip: the SocketWouldBlock error class is provided for internal use only and is not intended to be explicitly trapped by user code.


Additional Samples - Chat

A sample application illustrating the use of the Socket Connection to implement a simple text based Chat window is provided in the Samples\Chat subdirectory under your Dolphin installation. Use the Package Browser to load Chat.pac and follow the instructions in the Package comment.