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