Using Sockets

Overview

The Curl Language provides APIs to use TCP sockets and UDP sockets. Both kinds of sockets use an address/port pair to uniquely identify a socket location. For a client to talk to a server, it must know the address and port that the server is listening on. The Curl socket API can look up an address, given a hostname.
TCP sockets are based on making a connection from a client to a server, and then having reliable, bi-directional byte streams between the client and server. The class that is used for a connected socket is DataTCPSocket. To connect a client to a server, create a DataTCPSocket, and then call DataTCPSocket.connect, or call DataTCPSocket.async-connect. You must supply a hostname or IP address as well as a port that identifies what server you are connecting to. You can supply these with remote-name or remote-address parameter as well as a remote-port parameter to the DataTCPSocket.default constructor, or to one of the connect methods. If one of those calls completes successfully, then you can call DataTCPSocket.to-InputStream and DataTCPSocket.to-OutputStream to get a ByteInputStream or ByteOutputStream that you can use to communicate with the server. When the socket is no longer needed, call DataTCPSocket.close, and close any streams that you got from the socket.
The Curl API imposes security restrictions on client TCP socket connections, see Network Access Restrictions.
Although there are synchronous socket calls for each operation, you should use them only for testing, because while a socket call is waiting for the server to respond, the UI of your applet is unresponsive. The step by step explanations in this section discuss only the asynchronous calls. These calls send an event that calls an EventHandler from the event loop when an operation has completed, failed, timed out, or been canceled. Asynchronous operations can be canceled by calling AsyncWorker.cancel on the AsyncWorker that is returned from each asynchronous method.
The IDE documentation provides extended examples that illustrate the Curl API for TCP and UDP sockets. See Extended Examples for information on using extended examples.
The extended examples are in the file:
d:\automated-build-temp\build\win32-atom\docs\default\examples\dguide\sockets.zip
This archive file contains the following applet files:
You must run these example applets with privilege. The following discussion refers to code in the applet files.

TCP Socket Client

The sample applet socket-http-client.curl shows how to use either the synchronous or asynchronous TCP socket API to make an HTTP request to a web server and get back the response.
The sample first gets from the Url the hostname and port that identify the HTTP web server that it is supposed to connect to, and creates a DataTCPSocket with that information. This action is performed in the procedure create-socket-and-command-bytes in the class SocketHttpRequest. The following line returns the DataTCPSocket.
{DataTCPSocket remote-name = host, remote-port = port}
In the following code fragment, the applet tries to connect to the HTTP web server. It supplies a event handler for AsyncConnectSocketEvent events. AsyncConnectSocketEvent contains information about the attempted connection. If the AsyncConnectSocketEvent.exception field has an exception in it the connection failed and the applet abandons this request. If AsyncConnectSocketEvent.canceled? is true, the connection request was canceled, and the applet abandons the request.
If the request succeeds, the applet gets an output stream from the socket and writes the HTTP request using OutputStream-of.async-write.
{socket.async-connect
    {on e:AsyncConnectSocketEvent do
        {if-non-null ex = e.exception then
            {cleanup-proc}
            {callback ex, null, null, false}
         elseif e.canceled? then
            {cleanup-proc}
            {callback null, null, null, true}
         else
            set output-stream = {socket.to-OutputStream}
            set worker.worker =
                {output-stream.async-write
                    bytes,
                    {on e:AsyncStreamWriteEvent do
                        ...
                    }
                }
        }
    }
}
The next code fragment shows the EventHandler passed to OutputStream-of.async-write, which is called when the write has completed, failed, or been canceled. If the write failed or was canceled, we abandon the request. If it succeeds, the applet gets an input stream from the socket and reads the HTTP response using InputStream-of.async-read. If you pass partial? = true to OutputStream-of.async-write, the EventHandler passed to OutputStream-of.async-write receives an event as each part of the ByteArray is written, and can look at AsyncStreamWriteEvent.data-written and AsyncStreamWriteEvent.total-data-written to see how much data has been written.
{output-stream.async-write
    bytes,
    {on e:AsyncStreamWriteEvent do
        {if-non-null ex = e.exception then
            {cleanup-proc}
            {callback ex, null, null, false}
         elseif e.canceled? then
            {cleanup-proc}
            {callback null, null, null, true}
         else
            set input-stream = {socket.to-InputStream}
            set worker.worker =
                {input-stream.async-read
                    {on e:AsyncStreamReadEvent do
                        ...
                    }
                }
        }
    }
}
The last code fragment shows the event handler passed to InputStream-of.async-read, which is called when the read succeeds, fails or is canceled. The applet cleans up the socket and both of the streams made from the socket, and handles either the read data or the exception.
This applet only does a single HTTP request on this connection, and the headers included in the HTTP request told the HTTP web server to close the connection when it was done, so the applet reads until is sees the EOF from the server closing the socket. If this was a client that was going to continue using the connection, then you would need to either know how much data to read, and pass in the n parameter with a byte count, or pass in partial? = true and parse the data in each AsyncStreamReadEvent as it was received.
{on e:AsyncStreamReadEvent do
    {if-non-null ex = e.exception
     then
        {cleanup-proc}
        {callback ex, null, null, false}
     elseif e.canceled? then
        {cleanup-proc}
        {callback null, null, null, true}
     else
        {callback
            null,
            e.data asa ByteArray,
            socket.local-address.address-as-String & ":" & socket.local-port,
            false
        }
        {cleanup-proc}
    }
}

TCP Socket Server

To make a TCP server, create an AcceptorTCPSocket, then call AcceptorTCPSocket.bind to tell it to start listening for connections from clients. You can provide the local-port parameter to either the AcceptorTCPSocket.default constructor or to the AcceptorTCPSocket.bind method. If you do not specify a port,the system will choose a port randomly to listen on when you call AcceptorTCPSocket.bind. Note that the client socket that connects to this server needs to provide the port number to be able to connect to the server. Then you call AcceptorTCPSocket.accept or AcceptorTCPSocket.async-accept to accept incoming connections. When those methods succeed, they result in a DataTCPSocket that you can then use in the same manner as a connected client socket. When the accepted socket is no longer needed, call DataTCPSocket.close and close any streams that you got from the socket. When the server socket is no longer need, call AcceptorTCPSocket.close.
The sample applet socket-http-server.curl shows how to use either the synchronous or asynchronous TCP socket API to implement a simple HTTP 1.0 web server.
The sample first creates the AcceptorTCPSocket. It creates it with no local-port, and allows the system to assign one, but it could request a port if needed. It then calls AcceptorTCPSocket.bind to bind the socket to a particular local port which it can then return so that a client can be told where to connect to. Also AcceptorTCPSocket.bind makes the socket start listening for incoming connections,
The following code fragment is from the procedure make-server-socket in the class HttpServer.
def server-socket =
    {AcceptorTCPSocket
||--            local-port = 80
    }
{server-socket.bind}
{return server-socket,
    server-socket.local-port,
    "http://localhost:" & server-socket.local-port & "/"
}
In the next code fragment, the applet calls AcceptorTCPSocket.async-accept to let incoming connections finish connecting and get a DataTCPSocket for the incoming connections. This socket is used to read and write data to the clients. With the default parameters AcceptorTCPSocket.async-accept continues calling the EventHandler with an AsyncAcceptSocketEvent for each incoming connection until it is canceled or a failure occurs. Once the AsyncAcceptSocketEvent is received, the applet checks to see if there was a failure, or if it was canceled, or if a connection was accepted. The AsyncAcceptSocketEvent has the DataTCPSocket and the address and port of the incoming connection. The applet then makes an input stream and starts reading the HTTP request.
{server-socket.async-accept
    {on e:AsyncAcceptSocketEvent do
        {if e.exception != null or e.canceled? then
            {if e.exception != null then
                {status-callback "Failed accepting " & e.exception.message}
            }
            {server-socket.close}
            {return}
        }
        def socket = e.socket
        def remote-id =
            socket.remote-address.address-as-String & ":" &
        socket.remote-port
        {status-callback "Connection from " & remote-id}
        def input-stream =
            {socket.to-InputStream}
        def read-worker =
            {input-stream.async-read
                partial? = true,
                append? = true,
                {on e:AsyncStreamReadEvent do
                    ...
                }
            }
    }
}
The server must keep reading the HTTP request until it has gotten the whole request, but there is no way to know how long the request is except to check the data as it arrives. So the call to InputStream-of.async-read passes partial? = true to indicate that it wants to get AsyncStreamReadEvents as the data arrives, and it passes append? = true to indicate that all of the data should just be appended to the same ByteArray. The event handler checks for failures or canceling. If it did get data, it tries to parse it to see if it has received the whole HTTP request. If it has not and AsyncStreamReadEvent.done? is false, then it just returns and waits for another AsyncStreamReadEvent. But if it did not receive the whole HTTP request and AsyncStreamReadEvent.done? is true, then it got an EOF without getting the whole HTTP request. If it has gotten the whole HTTP request then it prepares an answer and cancels the InputStream-of.async-read because it has all of the data that it needs.
The following code fragment illustrates this process.
def read-worker =
    {input-stream.async-read
        partial? = true,
        append? = true,
        {on e:AsyncStreamReadEvent do
            {if e.exception != null or e.canceled? then
                {if e.exception != null then
                    {status-callback
                        remote-id & e.exception.message
                    }
                }
                {socket.close}
                {input-stream.close}
                {return}
            }
            {status-callback
                remote-id & " read " &
                (e.data asa ByteArray).size
            }
            def (http-command, request-path, http-version,
                 request-headers) =
                     {HttpServer.parse-request (e.data asa ByteArray)}
            {if request-path != null then
                {read-worker.cancel}
                {status-callback
                    remote-id & " got command " &
                    http-command & " " & request-path & " " & http-version
                }
                def response-data =
                    {HttpServer.make-response
                        http-command,
                        request-path,
                        http-version,
                        request-headers
                    }
                {status-callback
                    remote-id & " writing " &
                    response-data.size
                }
                def output-stream =
                    {socket.to-OutputStream}
                {output-stream.async-write
                    response-data,
                    {on e:AsyncStreamWriteEvent do
                        ...
                    }
                }
             elseif e.done? then
                {status-callback
                    remote-id & " got incomplete command."
                }
            }
        }
    }
The next code fragment shows how the applet finally writes a response to a ByteOutputStream from the socket, and when that completes and the EventHandler is called, closes all of the streams and the socket. If this server needs to read more commands or send more responses from the same client socket, it does not close the streams and socket, and starts another InputStream-of.async-read, or another OutputStream-of.async-write. Also note that while InputStream-of.async-read or OutputStream-of.async-write are in progress the AcceptorTCPSocket.async-accept is still active, and accepts further incoming connections and calls the EventHandler. So this server can handle numerous clients at the same time without making them wait, limited only by the computer running the server.
def output-stream =
    {socket.to-OutputStream}
{output-stream.async-write
    response-data,
    {on e:AsyncStreamWriteEvent do
        {if e.exception != null then
            {status-callback
                remote-id &
                e.exception.message
            }
         elseif e.canceled? then
        }
        {socket.close}
        {input-stream.close}
        {output-stream.close}
        {status-callback remote-id & " done."}
    }
}

UDP Sockets

UDP sockets allow for sending packets from one UDP socket to any other one, with no guarantees of reliability, and with limits on the size of the packets. First create a UDPSocket. You can supply a local-port to either the UDPSocket.default constructor or provide it to UDPSocket.bind. If you do not supply a local port, then the system chooses a port randomly to listen on when you call UDPSocket.bind. Next you call UDPSocket.read-packet or UDPSocket.async-read-packet to read incoming packets, or call UDPSocket.write-packet or UDPSocket.async-write-packet to start writing packets. UDP sockets are not connected to any particular server or client, although you can provide a remote-port and remote-name to UDPSocket.default that are used as the default destination to write packets to, and to filter packets that are read so that only ones from the specified remote-name and remote-port are seen. In any case the address and port are always returned when a packet is read. When the socket is no longer needed, call UDPSocket.close.
There are security restrictions on where a UDP socket can write a packet to or read one from see Network Access Restrictions.
The sample applet udp-server.curl shows how to use both synchronous and asynchronous APIs, to implement a server that reads a packet, changes everything in it to upper-case letters, and sends it back to whomever sent it. There is also a client sample called udp-client.curl that is similar to the server sample except that it sends a packet to the server and waits for a response.
The following code fragments are from udp-server.curl. As the first fragment illustrates, the sample first creates the UDPSocket. It creates it with no local-port, and allows the system to assign one, but it could request a port if needed. It then calls UDPSocket.bind to bind the socket to a particular local port which it can then return so that a client can be told where to send packets. Also UDPSocket.bind makes the socket start listening for incoming packets. A UDP socket that does not need to initially wait for packets from somewhere else could skip calling UDPSocket.bind and start by writing a packet.
def socket = {UDPSocket}
{socket.bind}
The next fragment shows how the sample waits for packets to arrive from wherever else, by calling UDPSocket.async-read-packet. With the default parameters UDPSocket.async-read-packet continues calling the EventHandler with an AsyncReadPacketSocketEvent for each incoming packet until it is canceled or a failure occurs. Once the AsyncReadPacketSocketEvent is received, the code checks to see if there was a failure or if it was canceled, and if not, it does whatever it needs to do to the packet data, and sends a response to the address and port that sent the packet. It does this by adding the packet, address and port to an array of those that are ready to be sent, and then calls the code that will make sure that a packet from that array is in the process of being sent.
def read-worker =
    {socket.async-read-packet
        {on e:AsyncReadPacketSocketEvent do
            {if e.exception != null or e.canceled? then
                {socket.close}
             else
                def packet = {non-null e.packet-data}
                {UDPServer.upcase-packet packet}

                {packet-array.append packet}
                {address-array.append {non-null e.remote-address}}
                {port-array.append e.remote-port}
                {status
                    e.remote-address.address-as-String & ":" &
                    e.remote-port & " read " & packet.size & " bytes"
                }
                {write-proc}
            }
        }
    }
Finally, we have the code that sees if it needs to start writing one of the packets that is ready to be sent. If there is not one being sent already, it calls UDPSocket.async-write-packet with the packet and the address and port that it needs to be sent to. When that write completes or fails or is canceled it recurses to write the next one, or shuts down if there was an error of it was canceled. While this writing is in progress, additional incoming packets will also be read with the continuing UDPSocket.async-read-packet call, processed, and adding to the list of reply packets to be sent out. Note that with UDP sockets, packets can get dropped by the various network layers, and may not reach their destination.
def write-proc =
    {proc {}:void
        {if packet-array.size > 0 and socket.open? and
            write-worker == null
         then
            def packet-size = packet-array[0].size
            def remote-address = address-array[0]
            def remote-port = port-array[0]
            set write-worker =
                {socket.async-write-packet
                    packet-array[0],
                    remote-address = remote-address,
                    remote-port = remote-port,
                    {on e:AsyncWritePacketSocketEvent do
                        {status
                            remote-address.address-as-String & ":" &
                            remote-port & " wrote " & packet-size & " bytes"
                        }
                        set write-worker = null
                        {if e.exception != null or e.canceled? then
                            {socket.close}
                         else
                            {write-proc}
                        }
                    }
                }
            {packet-array.remove 0}
            {address-array.remove 0}
            {port-array.remove 0}
        }
    }

IPv6 support

API version 8.0 adds support for IPv6 addresses to the socket, HTTP and security systems. The older APIs continue to work as they always have, except that most calls will also accept IPv6 addresses now. IPv6 addresses can be returned by the operating system or passed in a String or Url if the operating system supports IPv6. An application that wants to do things with IPv6 addresses may need to use the new APIs, but most applications that simply pass around host names or Urls will not need to do anything specil.
API version 8.0 extends the SocketInetAddress class with new methods and getters than can use both IPv4 and IPv6 addresses depending on which address families the host operating system supports. Also when possible DataTCPSocket.start-connect and UDPSocket.write-packet try to convert addresses to which ever address family will work. But other calls will fail when passed an address that the operating system does not support.
The socket APIs accept IPv6 addresses in the following formats: And the Url APIs accept the formats that have '[' ']' around the address.
Hostnames are more portable than numeric addresses, but if your application only uses hostnames or Strings with addresses in them, it should work without modification for IPv4 and IPv6 servers and clients, as long as they are properly configured, and they both support the appropriate address family.