Thursday, January 12, 2017

Using the BlueSocket framework to create an echo server

In this post we will create an echo server using the BlueSocket  framework developed by IBM.  An echo server is a server application that “echoes” back to the client any text that it sends to the server.  We will be using the BlueSocket framework because it makes it very easy to develop cross-platform client and server applications, using Swift, that connect using standard sockets.  The BlueSocket framework works with applications developed for iOS, macOS and Linux systems.  We will start this post off by explaining what Berkeley sockets are and give a real brief introduction to network addressing.

The Berkeley Socket API (Application Programming Interface) is a set of standard functions for creating Internet and Unix domain sockets that are used for inter-process communications.  Other socket APIs exists however Berkeley Sockets are generally regarded as the standard.

The Berkeley Socket API was originally introduced in 1983 when 4.2BSD was released.  The API has evolved, with very little modification, into part of the Portable Operating System Interface (POSIX) specification. Today, all modern operating systems have some implementation of the Berkeley Socket Interface for connecting devices to the Internet.  Even Winsock (Windows Sockets) which was developed in 1993,  is based on the Berkeley standards.

BSD Sockets generally rely on client/server architecture.  Client/Server architecture is an approach where a host is assigned either a client or a server role.  We define these roles like this:
  • Server:  A server is a device that selectively shares resources with other devices.
  • Client:  A client is a device that connects to a server to make use of the shared resources.

An example of the client/server architecture is the Internet.  When we open a web page in our favorite browser, like https://www.google.com, the browser (and therefore our device) becomes the client and the web server that we connected to become the server.

It is important to note that any device can be a server, a client or both.  As an example, our e-mail client may be connecting to a mail server, which makes us a client and at the same time we have file sharing enabled which also makes our device a server.

The Socket API generally makes use of two core protocols: 
  • TCP (Transmission Control Protocol) – TCP provides a reliable, ordered and error checked delivery of a stream of data between two devices on the same network.  TCP is generally used when we need to ensure that all packets are correctly received and in the correct order (Example:  Web Pages or E-Mail).
  • UDP (User Datagram Protocol)– UDP does not provide any of the error checking or reliability of TCP but offers much less overhead.  UDP is generally used when sending the information to the client quickly is more important then missing a small percentage of packets (Example:  Streaming video).
Darwin, which is an open source POSIX compliant operating system, forms the core set of components upon which Mac OS X and iOS are based.  This means that both OS X AND iOS contains the BSD Socket Library.  Most version of Linux are also very compliant with POSIX however they are not officially certified.  We will find that all Linux distributions also contain the BSD Socket Library.

BSD sockets can be used to build both client and server applications.  In this post we will be building an echo server.  An echo server is a server that simply “echoes” back the text that it received.  Before we look building this server, lets take a quick look at network addressing work.

Every device on an Internet Protocol (IP) network has a unique identifier know as an IP Address.  The IP Address serves two basic purposes:  host and location identification. There are currently two IP address formats known as IPv4 and IPv6

The IPv4 format is currently the standard for the Internet and most internal intranets.  This format stores the address as a 32 bit number An IPv4 address looks like this 83.166.169.231.

The IPv6 format is the latest revision of the Internet Protocol (IP) and stores the address using 128 bits.  It was developed to eventually replace IPv4 and to address the long anticipated problem of running out of IPv4 addresses.  An IPv6 address looks like this:  2001:0db8:0000:0000:0000:ff00:0042:8329.  An IPv6 can be shortened where consecutive all zero fields can be replaced by two colons.  The previous address could be rewritten to:  2001:0db8::ff00:0042:8329.

An IP address only identifies the device itself however any device may have multiple applications running on it that needs to communicate over the network.  These applications may be server applications like a web server or a client application like a web browser.  Ports are used to identify the application to communicate with.

A port is an application or process specific software construct serving as a communications endpoint on a device connected to an IP network.  Where an IP Address identifies the device to connect too the port number identifies the specific application to connect too. 

The best way to think of network addressing is to think about how you mail a letter.  In order for a letter to reach its destination you must put the complete address on the envelope.  For example, if you were going to send a letter to friend that lived at the following address:

      Jon Hoffman
      123 Main St
      San Francisco CA, 94123

If I were to translate that into network addressing, the IP Address would be equal to the street, city, state and zip code (Main St, San Francisco CA, 94123) and the street address (123) would be equal to port number.  So the IP address will get you to the exact location of the device, and the port number will tell you what door to knock on.

A devices has 65,536 available ports with the first 1024 being reserved for common protocols like HTTP, HTTPS, SSH, SMTP……

While it is pretty straight forward to create a socket server using the standard libraries that come with Darwin and Linux, there are minor differences with these libraries that can make it a challenge to write code that work on iOS, macOS and Linux systems.  Luckily we have a couple really good frameworks to help us create BSD socket clients and servers.  In this post we will be using IBM’s BlueSocket framework. 

Whatever system you will be developing your application for, the BlueSocket github page has instructions on how to include it with your project.  The sample project on the Mastering Swift github page uses Swift’s Package Manager to include the framework.  The project, in the github repository is titled echoServerSingleThread because it is a single threaded server so we can show how the BlueSocket framework works.  We will be creating a multi-threaded server that can handle multiple connections in a future post.

Lets start off by importing the frameworks needed:

#if os(Linux)
    import Glibc
#else
    import Darwin
#endif
import Foundation
import Socket

Now lets create a class called EchoServer and add a couple of properties to it:

class EchoServer {
    let bufferSize = 1024
    let port: Int
    var listenSocket: Socket? = nil
    var connected = [Int32: Socket]()
    var acceptNewConnection = true
}

The bufferSize property defines the maximum number of characters that our server can read at one time.  The port property is the port number that the server will bind too.  The listenSocket property is the listening socket for the server.  The connected property holds the list of client sockets.  Since this server will be single threaded, only one client can be connected at a time but we still made this an array so we can easily turn this code into a multi-client server in the future.  Finally the acceptNewConnection property will be true when our server is accepting new connections.
Next we will create an initializer and deinitializer.

    init(port: Int) {
        self.port = port
    }

    deinit {
        for socket in connected.values {
            socket.close()
        }
        listenSocket?.close()
    }

When we initialize the EchoServer type we will need to provide the port number that our server will bind to.  The deinitializer will close all client connections to the server and also the listening socket.  Now we are going to create the method that will start our sever. 

    func start() throws {
        let socket = try Socket.create()
        listenSocket = socket
        try socket.listen(on: port)
        print("Listening port: \(socket.listeningPort)")
        repeat {
            let connectedSocket = try socket.acceptClientConnection()
            print("Connection from: \(connectedSocket.remoteHostname)")
            newConnection(socket: connectedSocket)
        } while acceptNewConnection     
    }

Rather than trying to catch and respond to errors setting up the server, this method will throw any errors back to the code that called it if there is a problem.  This way the code that called the function will know that there was a problem setting up the server and respond appropriately. 

Next we use the create() class method to create the socket.  This method is defined like this:

    public class func create(family: ProtocolFamily = .inet, type: SocketType = .stream, proto: SocketProtocol = .tcp) throws -> Socket

As we can see this method takes three arguments.  Each of these arguments have default values.  The family argument can contain three possible values:

    Socket.ProtocolFamily.inet - IPv4
    Socket.ProtocolFamily.inet6 – IPv6
    Socket.ProtocolFamily.unix – UNIX

The type property can have two possible types:

    Socket.SocketType.stream  -  Stream (generally for TCP)
    Socket.SocketType.datagram  -  Datagram (generally for UDP)

Finally the proto property can have three possible values:

    Socket.SocketProtocol.tcp  -  TCP
    Socket.SocketProtocol.udp  -  UDP
    Socket.SocketProtocol.unix  -  UNIX

We use the listen(on:) method to bind the server to the port we wish the server to listen too. 
We set up a repeat loop and within the loop we use the acceptClientConnection() method to accept the next available client connection when it is available.  Once the client connection is available we call the newConnection(socket:) method which we will see next.  The repeat loop continues as long as the acceptNewConnection variable is true.

Now lets see what the newConnection(socket:) method looks like.  This method will be called when a new connection is established.  With our single threaded server we really did not need to create a separate method for new connections but to make it easier to create a multi-threaded server that will respond to multiple clients we went ahead and separated the functionality in the beginning.  Here is the code for the newConnection(socket:) method.

    func newConnection(socket: Socket) {
        connected[socket.socketfd] = socket
        var cont = true
        var dataRead = Data(capacity: bufferSize)
        repeat {
            do {
                let bytes = try socket.read(into: &dataRead)
                if bytes > 0 {
                    if let readStr = String(data: dataRead, encoding: .utf8) {
                        print("Received: \(readStr)")
                        try socket.write(from: readStr)
                        if readStr.hasPrefix("quit") {
                            cont = false
                            socket.close()
                        }
                        dataRead.count = 0
                    }
                }
            } catch let error {
                print("error: \(error)")
            }
        } while cont
        connected.removeValue(forKey: socket.socketfd)
        socket.close()
    }

This method starts off by adding the socket to the connected[] array which holds the list of connected clients.  We then configure the dataRead variable with a capacity equal to the bufferSize property which is the maximum number of characters the server will read at once.

Next there is a repeat loop that will continue to repeat until the client sends a message that starts with “quit”.  Within the repeat loop we have a do-catch block that will catch any errors in the communication. 

Within the do-catch block we use the read(into:) method to read the next message from the client.  We put that message in the dataRead variable.  The method itself returns the total number of bytes read.  In the next line we verify that the total number of bytes read is greater than zero.  If it is greater than zero we convert the message to a string and use the write(from:) method to echo it back to the client. 

We check to see if the message has a prefix of “quit” and if so we close the socket and set the cont variable to false to exit out of the repeat loop.  This will close the connection between the client and the server.

Now in order to create the socket we need to create an instance of the EchoServer class and call the start() method.  The following code shows how to do this:

    let server = EchoServer(port: 3333)
    do {
        try server.start()
    } catch let error {
        print("Error: \(error)")
    }

With this code we bind the server to port 3333.  We can now build our project and run it.  If it starts up without any errors we can use telnet to test it.  On the same device that the echo server is running on we can test the server using telnet like this:

    telnet 127.0.0.1 3333

If all is well, telnet will connect to the echo server and we can not type in a message and hit the enter key.  Whatever message was typed in should be echoed back.  In the next couple of posts we will be creating a client application that will connect to our echo server and also a server that can handle multiple clients at once.

If you use Google Plus, you can become a member of the Swift Linux community that I just set up.  Hopefully this community will grow and will become a good resource for the Swift Linux community.
I will begin putting source code for my blog posts in my Mastering Swift github page.





No comments:

Post a Comment