Kitura / BlueSocket

Socket framework for Swift using the Swift Package Manager. Works on iOS, macOS, and Linux.
Apache License 2.0
1.41k stars 197 forks source link
linux macos networking socket swift

APIDoc Build Status - Master macOS iOS Linux Apache 2 Slack Status

BlueSocket

Socket framework for Swift using the Swift Package Manager. Works on iOS, macOS, and Linux.

Prerequisites

Swift

macOS

iOS

Note:

If creating a UDP server on iOS, you may need to follow a few steps:

Linux

Other Platforms

Add-ins

Build

To build Socket from the command line:

% cd <path-to-clone>
% swift build

Testing

To run the supplied unit tests for Socket from the command line:

% cd <path-to-clone>
% swift build
% swift test

Using BlueSocket

Including in your project

Swift Package Manager

To include BlueSocket into a Swift Package Manager package, add it to the dependencies attribute defined in your Package.swift file. You can select the version using the majorVersion and minor parameters. For example:

    dependencies: [
        .Package(url: "https://github.com/Kitura/BlueSocket.git", majorVersion: <majorVersion>, minor: <minor>)
    ]

Carthage

To include BlueSocket in a project using Carthage, add a line to your Cartfile with the GitHub organization and project names and version. For example:

    github "Kitura/BlueSocket" ~> <majorVersion>.<minor>

CocoaPods

To include BlueSocket in a project using CocoaPods, you just add BlueSocket to your Podfile, for example:

    platform :ios, '10.0'

    target 'MyApp' do
        use_frameworks!
        pod 'BlueSocket'
    end

Before starting

The first thing you need to do is import the Socket framework. This is done by the following:

import Socket

Family, Type and Protocol Support

BlueSocket supports the following families, types and protocols:

Creating a socket.

BlueSocket provides four different factory methods that are used to create an instance. These are:

Setting the read buffer size.

BlueSocket allows you to set the size of the read buffer that it will use. Then, depending on the needs of the application, you can change it to a higher or lower value. The default is set to Socket.SOCKET_DEFAULT_READ_BUFFER_SIZE which has a value of 4096. The minimum read buffer size is Socket.SOCKET_MINIMUM_READ_BUFFER_SIZE which is set to 1024. Below illustrates how to change the read buffer size (exception handling omitted for brevity):

let mySocket = try Socket.create()
mySocket.readBufferSize = 32768

The example above sets the default read buffer size to 32768. This setting should be done prior to using the Socket instance for the first time.

Closing a socket.

To close the socket of an open instance, the following function is provided:

Listen on a socket (TCP/UNIX).

To use BlueSocket to listen for a connection on a socket the following API is provided:

Example:

The following example creates a default Socket instance and then immediately starts listening on port 1337. Note: Exception handling omitted for brevity, see the complete example below for an example of exception handling.

var socket = try Socket.create()
try socket.listen(on: 1337)

Accepting a connection from a listening socket (TCP/UNIX).

When a listening socket detects an incoming connection request, control is returned to your program. You can then either accept the connection or continue listening or both if your application is multi-threaded. BlueSocket supports two distinct ways of accepting an incoming connection. They are:

Connecting a socket to a server (TCP/UNIX).

In addition to the create(connectedUsing:) factory method described above, BlueSocket supports three additional instance functions for connecting a Socket instance to a server. They are:

Reading data from a socket (TCP/UNIX).

BlueSocket supports four different ways to read data from a socket. These are (in recommended use order):

Writing data to a Socket (TCP/UNIX).

In addition to reading from a socket, BlueSocket also supplies four methods for writing data to a socket. These are (in recommended use order):

Listening for a datagram message (UDP).

BlueSocket supports three different ways to listen for incoming datagrams. These are (in recommended use order):

Reading a datagram (UDP).

BlueSocket supports three different ways to read incoming datagrams. These are (in recommended use order):

Writing a datagram (UDP).

BlueSocket also supplies four methods for writing datagrams to a socket. These are (in recommended use order):

IMPORTANT NOTE about NSData and NSMutableData

The read and write APIs above that use either NSData or NSMutableData will probably be deprecated in the not so distant future.

Miscellaneous Utility Functions

Complete Example

The following example shows how to create a relatively simple multi-threaded echo server using the new GCD based Dispatch API. What follows is code for a simple echo server that once running, can be accessed via telnet ::1 1337.


import Foundation
import Socket
import Dispatch

class EchoServer {

    static let quitCommand: String = "QUIT"
    static let shutdownCommand: String = "SHUTDOWN"
    static let bufferSize = 4096

    let port: Int
    var listenSocket: Socket? = nil
    var continueRunningValue = true
    var connectedSockets = [Int32: Socket]()
    let socketLockQueue = DispatchQueue(label: "com.kitura.serverSwift.socketLockQueue")
    var continueRunning: Bool {
        set(newValue) {
            socketLockQueue.sync {
                self.continueRunningValue = newValue
            }
        }
        get {
            return socketLockQueue.sync {
                self.continueRunningValue
            }
        }
    }

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

    deinit {
        // Close all open sockets...
        for socket in connectedSockets.values {
            socket.close()
        }
        self.listenSocket?.close()
    }

    func run() {

        let queue = DispatchQueue.global(qos: .userInteractive)

        queue.async { [unowned self] in

            do {
                // Create an IPV6 socket...
                try self.listenSocket = Socket.create(family: .inet6)

                guard let socket = self.listenSocket else {

                    print("Unable to unwrap socket...")
                    return
                }

                try socket.listen(on: self.port)

                print("Listening on port: \(socket.listeningPort)")

                repeat {
                    let newSocket = try socket.acceptClientConnection()

                    print("Accepted connection from: \(newSocket.remoteHostname) on port \(newSocket.remotePort)")
                    print("Socket Signature: \(String(describing: newSocket.signature?.description))")

                    self.addNewConnection(socket: newSocket)

                } while self.continueRunning

            }
            catch let error {
                guard let socketError = error as? Socket.Error else {
                    print("Unexpected error...")
                    return
                }

                if self.continueRunning {

                    print("Error reported:\n \(socketError.description)")

                }
            }
        }
        dispatchMain()
    }

    func addNewConnection(socket: Socket) {

        // Add the new socket to the list of connected sockets...
        socketLockQueue.sync { [unowned self, socket] in
            self.connectedSockets[socket.socketfd] = socket
        }

        // Get the global concurrent queue...
        let queue = DispatchQueue.global(qos: .default)

        // Create the run loop work item and dispatch to the default priority global queue...
        queue.async { [unowned self, socket] in

            var shouldKeepRunning = true

            var readData = Data(capacity: EchoServer.bufferSize)

            do {
                // Write the welcome string...
                try socket.write(from: "Hello, type 'QUIT' to end session\nor 'SHUTDOWN' to stop server.\n")

                repeat {
                    let bytesRead = try socket.read(into: &readData)

                    if bytesRead > 0 {
                        guard let response = String(data: readData, encoding: .utf8) else {

                            print("Error decoding response...")
                            readData.count = 0
                            break
                        }
                        if response.hasPrefix(EchoServer.shutdownCommand) {

                            print("Shutdown requested by connection at \(socket.remoteHostname):\(socket.remotePort)")

                            // Shut things down...
                            self.shutdownServer()

                            return
                        }
                        print("Server received from connection at \(socket.remoteHostname):\(socket.remotePort): \(response) ")
                        let reply = "Server response: \n\(response)\n"
                        try socket.write(from: reply)

                        if (response.uppercased().hasPrefix(EchoServer.quitCommand) || response.uppercased().hasPrefix(EchoServer.shutdownCommand)) &&
                            (!response.hasPrefix(EchoServer.quitCommand) && !response.hasPrefix(EchoServer.shutdownCommand)) {

                            try socket.write(from: "If you want to QUIT or SHUTDOWN, please type the name in all caps. 😃\n")
                        }

                        if response.hasPrefix(EchoServer.quitCommand) || response.hasSuffix(EchoServer.quitCommand) {

                            shouldKeepRunning = false
                        }
                    }

                    if bytesRead == 0 {

                        shouldKeepRunning = false
                        break
                    }

                    readData.count = 0

                } while shouldKeepRunning

                print("Socket: \(socket.remoteHostname):\(socket.remotePort) closed...")
                socket.close()

                self.socketLockQueue.sync { [unowned self, socket] in
                    self.connectedSockets[socket.socketfd] = nil
                }

            }
            catch let error {
                guard let socketError = error as? Socket.Error else {
                    print("Unexpected error by connection at \(socket.remoteHostname):\(socket.remotePort)...")
                    return
                }
                if self.continueRunning {
                    print("Error reported by connection at \(socket.remoteHostname):\(socket.remotePort):\n \(socketError.description)")
                }
            }
        }
    }

    func shutdownServer() {
        print("\nShutdown in progress...")

        self.continueRunning = false

        // Close all open sockets...
        for socket in connectedSockets.values {

            self.socketLockQueue.sync { [unowned self, socket] in
                self.connectedSockets[socket.socketfd] = nil
                socket.close()
            }
        }

        DispatchQueue.main.sync {
            exit(0)
        }
    }
}

let port = 1337
let server = EchoServer(port: port)
print("Swift Echo Server Sample")
print("Connect with a command line window by entering 'telnet ::1 \(port)'")

server.run()

This server can be built by specifying the following Package.swift file using Swift 4.

import PackageDescription

let package = Package(
    name: "EchoServer",
    dependencies: [
        .package(url: "https://github.com/Kitura/BlueSocket.git", from:"1.0.8"),
    ],
    targets: [
    .target(
        name: "EchoServer",
        dependencies: [
            "Socket"
        ]),
    ]
)

Or if you are still using Swift 3, by specifying the following Package.swift file.

import PackageDescription

let package = Package(
    name: "EchoServer",
    dependencies: [
    .Package(url: "https://github.com/Kitura/BlueSocket.git", majorVersion: 1, minor: 0),
    ],
    exclude: ["EchoServer.xcodeproj"]
)

The following command sequence will build and run the echo server on Linux. If running on macOS or with any toolchain NEWER than the 8/18 toolchain, you can omit the -Xcc -fblocks switch as it's no longer needed.

$ swift build -Xcc -fblocks
$ .build/debug/EchoServer
Swift Echo Server Sample
Connect with a command line window by entering 'telnet ::1 1337'
Listening on port: 1337

Community

We love to talk server-side Swift and Kitura. Join our Slack to meet the team!

License

This library is licensed under Apache 2.0. Full license text is available in LICENSE.