BlueSocket
Overview
Socket framework for Swift using the Swift Package Manager. Works on macOS and Linux.
Contents
- Socket: Generic low level socket framework. Pure Swift.
Prerequisites
Swift
- Swift Open Source
swift-DEVELOPMENT-SNAPSHOT-2016-07-25-atoolchain (Minimum REQUIRED for latest release)
macOS
- macOS 10.11.6 (El Capitan) or higher
- Xcode Version 8.0 beta 3 (8S174q) or higher using the above toolchain (Recommended)
Linux
- Ubuntu 15.10 (or 14.04 but only tested on 15.10)
- The Swift Open Source toolchain listed above
Add-ons
- BlueSSLService can be used to add SSL support.
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
Before starting
The first you need to do is import the Socket framework. This is done by the following:
import Socket
Creating a socket.
BlueSocket provides four different factory methods that are used to create an instance. These are:
create()- This creates a fully configured default socket. A default socket is created withfamily: .inet,type: .stream, andproto: .tcp.create(family family: ProtocolFamily, type: SocketType, proto: SocketProtocol)- This API allows you to create a configuredSocketinstance customized for your needs. You can customize the protocol family, socket type and socket protocol.create(connectedUsing signature: Signature)- This API will allow you create aSocketinstance and have it attempt to connect to a server based on the information you pass in theSocket.Signature.create(fromNativeHandle nativeHandle: Int32, address: Address?)- This API lets you wrap a native file descriptor describing an existing socket in a new instance ofSocket.
Closing a socket.
To close the socket of an open instance, the following function is provided:
close()- This function will perform the necessary tasks in order to cleanly close an open socket.
Listen on a socket.
To use BlueSocket to listen for an connection on a socket the following API is provided:
listen(on port: Int, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG)The first parameterport, is the port to be used to listen on. The second parameter,maxBacklogSizeallows you to set the size of the queue holding pending connections. The function will determine the appropriate socket configuration based on theportspecified. For convenience on macOS, the constantSocket.SOCKET_MAX_DARWIN_BACKLOGcan be set to use the maximum allowed backlog size. The default value for all platforms isSocket.SOCKET_DEFAULT_MAX_BACKLOG, currently set to 50. For server use, it may be necessary to increase this value.
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()
guard let socket = socket else {
fatalError("Could not create socket.")
}
try socket.listen(on: 1337)
Accepting a connection from a listening socket.
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:
acceptClientConnection()- This function accepts the connection and returns a newSocketinstance based on the newly connected socket. The instance that was listening in unaffected.acceptConnection()- This function accepts the incoming connection, replacing and closing the existing listening socket. The properties that were formerly associated with the listening socket are replaced by the properties that are relevant to the newly connected socket.
Connecting a socket to a server.
In addition to the create(connectedUsing:) factory method described above, BlueSocket supports two additional functions for connecting a Socket instance to a server. They are:
connect(to host: String, port: Int32)- This API allows you to connect to a server based on thehostnameandportyou provide.connect(using signature: Signature)- This API allows you specify the connection information by providing aSocket.Signatureinstance containing the information. Refer toSocket.Signaturein Socket.swift for more information.
Reading data from a socket.
BlueSocket supports four different ways to read data from a socket. These are (in recommended use order):
read(into data: inout Data)- This function reads all the data available on a socket and returns it in theDataobject that was passed.read(into data: NSMutableData)- This function reads all the data available on a socket and returns it in theNSMutableDataobject that was passed.readString()- This function reads all the data available on a socket and returns it as anString. Anilis returned if no data is available for reading.read(into buffer: UnsafeMutablePointer<CChar>, bufSize: Int)- This function allows you to read data into a buffer of a specified size by providing an unsafe pointer to that buffer and an integer the denotes the size of that buffer. This API (in addition to other types of exceptions) will throw aSocket.SOCKET_ERR_RECV_BUFFER_TOO_SMALLif the buffer provided is too small. You will need to call again with proper buffer size (seeError.bufferSizeNeededin Socket.swift for more information).
Writing data to a Socket.
In addition to reading from a socket, BlueSocket also supplies four methods for writing data to a socket. These are (in recommended use order):
write(from data: Data)- This function writes the data contained within theDataobject to the socket.write(from data: NSData)- This function writes the data contained within theNSDataobject to the socket.write(from string: String)- This function writes the data contained in theStringprovided to the socket.write(from buffer: UnsafePointer<Void>, bufSize: Int)- This function writes the data contained within the buffer of the specified size by providing an unsafe pointer to that buffer and an integer the denotes the size of that buffer.
Miscellaneous Utility Functions
hostnameAndPort(from address: Address)- This class function provides a means to extract the hostname and port from a givenSocket.Address. On successful completion, a tuple containing thehostnameandportare returned.checkStatus(for sockets: [Socket])- This class function allows you to check status of an array ofSocketinstances. Upon completion, a tuple containing twoSocketarrays is returned. The first array contains theSocketinstances are that have data available to be read and the second array containsSocketinstances that can be written to. This API does not block. It will check the status of eachSocketinstance and then return the results.wait(for sockets: [Socket], timeout: UInt, waitForever: Bool = false)- This class function allows for monitoring an array ofSocketinstances, waiting for either a timeout to occur or data to be readable at one of the monitoredSocketinstances. If a timeout of zero (0) is specified, this API will check each socket and return immediately. Otherwise, it will wait until either the timeout expires or data is readable from one or more of the monitoredSocketinstances. If a timeout occurs, this API will returnnil. If data is available on one or more of the monitoredSocketinstances, those instances will be returned in an array. If thewaitForeverflag is set to true, the function will wait indefinitely for data to become available regardless of the timeout value specified.isReadableOrWritable()- This instance function allows to determine whether aSocketinstance is readable and/or writable. A tuple is returned containing twoBoolvalues. The first, if true, indicates theSocketinstance has data to read, the second, if true, indicates that theSocketinstance can be written to.setBlocking(shouldBlock: Bool)- This instance function allows you control whether or not thisSocketinstance should be placed in blocking mode or not. Note: AllSocketinstances are, by default, created in blocking mode.
Complete Example
The following example shows how to create a simple echo server.
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
import Darwin
import Foundation
import Socket
#elseif os(Linux)
import Glibc
import Foundation
import Socket
#endif
class EchoServer {
static let QUIT: String = "QUIT"
let port: Int
var keepRunning: Bool = true
var listenSocket: Socket? = nil
init(port: Int) {
self.port = port
}
deinit {
self.listenSocket?.close()
}
func run() {
do {
try self.listenSocket = Socket.create()
guard let socket = self.listenSocket else {
print("Unable to unwrap socket...")
return
}
try socket.listen(on: self.port, maxBacklogSize: 10)
print("Listening on port: \(self.port)")
// Replace the listening socket with the newly accepted connection...
try socket.acceptConnection()
print("Accepted connection from: \(socket.remoteHostname) on port \(socket.remotePort)")
try socket.write(from: "Hello, type 'QUIT' to end session\n")
var bytesRead = 0
repeat {
let readData = NSMutableData()
bytesRead = try socket.read(into: readData)
if bytesRead > 0 {
guard let response = String(data: readData as Data, encoding: String.Encoding.utf8) else {
print("Error decoding response...")
readData.length = 0
break
}
// If the user typed `QUIT`, we'll shutdown the server...
if response.hasPrefix(EchoServer.QUIT) {
self.keepRunning = false
}
print("Server received from connection at \(socket.remoteHostname):\(socket.remotePort): \(response) ")
// Echo back to the client what we receivied...
let reply = "Server response: \n\(response)\n"
try socket.write(from: reply)
}
// No bytes read, the other side closed the connection...
if bytesRead == 0 {
break
}
} while self.keepRunning
socket.close()
} catch let error {
// See if it's a socket error or something else...
guard let socketError = error as? Socket.Error else {
print("Unexpected error...")
return
}
print("Error reported: \(socketError.description)")
}
}
}
let port = 1337
let server = EchoServer(port: port)
print("Connect using Terminal via 'telnet 127.0.0.1 \(port)'")
server.run()