Dear Sirs, with the upcoming release of swift 5, it will be possible to have an API for the socket programming?
Thanks a lot in advance!
And have a good day!
Gian Luca
1 Like
dlbuckley
(Dale Buckley)
2
You can already interact with the standard POSIX socket APIs as you would normally, but if you wanted a higher level abstraction you can use SwiftNIO.
Jon_Shier
(Jon Shier)
3
If you're on Apple platforms, there's also Network.framework.
Bokka
(Gian Luca)
4
Thanks a lot for the replies!
Please, I need help. I don't understand how to install the library SwiftNIO, and if I would use the Network framework, someone can provide a quick and easy example of server and client?
Thanks again, and excuse me, I am a noob in swift I know only something of VB6 and Python...
You can start with IBMs BlueSocket as it's relatively simple and straightforward. Though at some point you may notice that there's something wrong with blocking networking API, and then it would be the perfect moment to meet SwiftNIO :) It will take a while to get used to Future/Promise philosophy, but I promise (sic) that it will pay off. Closest analogy here to SwiftNIO would be asyncio module from Python.
2 Likes
eskimo
(Quinn “The Eskimo!”)
6
if I would use the Network framework, someone can provide a quick and
easy example of server and client?
Pasted in below is a bunch of snippets from a simple TCP client and server.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
Client:
var connection: NWConnection?
func start() {
let connection = NWConnection(host: "example.com", port: 80, using: .tcp)
connection.stateUpdateHandler = self.stateDidChange(to:)
self.setupReceive(on: connection)
connection.start(queue: .main)
self.connection = connection
}
func stateDidChange(to state: NWConnection.State) {
switch state {
case .setup:
break
case .waiting(let error):
self.connectionDidFail(error: error)
case .preparing:
break
case .ready:
self.status = "Connected"
case .failed(let error):
self.connectionDidFail(error: error)
case .cancelled:
break
}
}
Server:
var listener: NWListener?
func start() throws {
let listener = try NWListener(using: .tcp, on: 12345)
listener.stateUpdateHandler = self.stateDidChange(to:)
listener.newConnectionHandler = self.didAccept(connection:)
listener.start(queue: .main)
self.listener = listener
}
func stateDidChange(to newState: NWListener.State) {
switch newState {
case .setup:
break
case .waiting:
break
case .ready:
break
case .failed(let error):
self.listenerDidFail(error: error)
case .cancelled:
break
}
}
var nextID: Int = 0
var connectionsHandlers: [Int: ConnectionHandler] = [:]
func didAccept(connection: NWConnection) {
let handler = ConnectionHandler(connection: connection, uniqueID: self.nextID)
self.nextID += 1
self.connectionsHandlers[handler.uniqueID] = handler
handler.didStopCallback = self.connectionDidStop(_:)
handler.start()
}
func stop() {
if let listener = self.listener {
self.listener = nil
listener.cancel()
}
for handler in self.connectionsHandlers.values {
handler.cancel()
}
self.connectionsHandlers.removeAll()
}
Data transfer:
func setupReceive(on connection: NWConnection) {
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, contentContext, isComplete, error) in
if let data = data, !data.isEmpty {
// … process the data …
self.status = "did receive \(data.count) bytes"
}
if isComplete {
// … handle end of stream …
self.stop(status: "EOF")
} else if let error = error {
// … handle error …
self.connectionDidFail(error: error)
} else {
self.setupReceive(on: connection)
}
}
}
func sendStreamOriented(connection: NWConnection, data: Data) {
connection.send(content: data, completion: .contentProcessed({ error in
if let error = error {
self.connectionDidFail(error: error)
}
}))
}
func sendEndOfStream(connection: NWConnection) {
connection.send(content: nil, contentContext: .defaultStream, isComplete: true, completion: .contentProcessed({ error in
if let error = error {
self.connectionDidFail(error: error)
}
}))
}
8 Likes
gmhcode
(Gmhcode)
7
I wonder if you could provide a link for this code or a sample project. these snippets are incomplete, they contain undeclared types and variables.
eskimo
(Quinn “The Eskimo!”)
8
I wonder if you could provide a link for this code or a sample
project.
The project I took that from is not publicly available.
These snippets are incomplete …
Indeed. Both snippets are meant to be embedded inside a class. For the client side the missing declarations are:
class Client {
var status: String = ""
func connectionDidFail(error: Error) {
…
}
func stop(status: String) {
…
}
}
The server side is a little more complex, in that you’ll need a ConnectionHandler class that wraps the connection and its ID. Each instance of that class works much like the client class.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
crith
9
Hi @eskimo, I too am a tad confused. I'm sure I'm missing something very obvious, but I just cannot get the message received to trigger. I setup two basic functions to make sure I wasn't overcomplicating things, but it's still not working.
Test Server
func server() {
listener = try? NWListener(using: .tcp, on: 91)
listener?.newConnectionHandler = rec(_:)
listener?.stateUpdateHandler = { state in
switch(state) {
case .ready:
print("Listening")
default:
break;
}
}
listener?.start(queue: .main)
}
func rec(_ connection: NWConnection) {
print("New Connection")
sharedConnection = connection
sharedConnection?.stateUpdateHandler = { state in
switch(state) {
case .ready:
print("Setting Receive")
sharedConnection?.receiveMessage { (data, context, done, error) in
print("Received Something")
}
default:
break
}
}
sharedConnection?.start(queue: .main)
}
Test Client
func client() {
connector = NWConnection(host: "192.168.0.5", port: 91, using: .tcp)
connector?.stateUpdateHandler = { state in
switch(state) {
case .ready:
print("Sending Message")
connector?.send(content: "TEST".data(using: .utf8), contentContext: .defaultMessage, isComplete: false, completion: .idempotent)
break
default:
break
}
}
connector?.start(queue: .main)
}
I just call server and then client, all the print statements fire except for the receive one.
eskimo
(Quinn “The Eskimo!”)
10
It’s hard to say what’s going wrong here, so I just sat down and reworked my code into something that you can run:
Summary
import Foundation
import Network
class Connection {
init(nwConnection: NWConnection) {
self.nwConnection = nwConnection
self.id = Connection.nextID
Connection.nextID += 1
}
private static var nextID: Int = 0
let nwConnection: NWConnection
let id: Int
var didStopCallback: ((Error?) -> Void)? = nil
func start() {
print("connection \(self.id) will start")
self.nwConnection.stateUpdateHandler = self.stateDidChange(to:)
self.setupReceive()
self.nwConnection.start(queue: .main)
}
func send(data: Data) {
self.nwConnection.send(content: data, completion: .contentProcessed( { error in
if let error = error {
self.connectionDidFail(error: error)
return
}
print("connection \(self.id) did send, data: \(data as NSData)")
}))
}
func stop() {
print("connection \(self.id) will stop")
}
private func stateDidChange(to state: NWConnection.State) {
switch state {
case .setup:
break
case .waiting(let error):
self.connectionDidFail(error: error)
case .preparing:
break
case .ready:
print("connection \(self.id) ready")
case .failed(let error):
self.connectionDidFail(error: error)
case .cancelled:
break
default:
break
}
}
private func connectionDidFail(error: Error) {
print("connection \(self.id) did fail, error: \(error)")
self.stop(error: error)
}
private func connectionDidEnd() {
print("connection \(self.id) did end")
self.stop(error: nil)
}
private func stop(error: Error?) {
self.nwConnection.stateUpdateHandler = nil
self.nwConnection.cancel()
if let didStopCallback = self.didStopCallback {
self.didStopCallback = nil
didStopCallback(error)
}
}
private func setupReceive() {
self.nwConnection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, _, isComplete, error) in
if let data = data, !data.isEmpty {
print("connection \(self.id) did receive, data: \(data as NSData)")
}
if isComplete {
self.connectionDidEnd()
} else if let error = error {
self.connectionDidFail(error: error)
} else {
self.setupReceive()
}
}
}
}
class Client {
init() {
let nwConnection = NWConnection(host: "127.0.0.1", port: 12345, using: .tcp)
self.connection = Connection(nwConnection: nwConnection)
}
let connection: Connection
func start() {
self.connection.didStopCallback = self.didStopCallback(error:)
self.connection.start()
}
func didStopCallback(error: Error?) {
if error == nil {
exit(EXIT_SUCCESS)
} else {
exit(EXIT_FAILURE)
}
}
static func run() {
let client = Client()
client.start()
dispatchMain()
}
}
class Server {
init() {
self.listener = try! NWListener(using: .tcp, on: 12345)
self.timer = DispatchSource.makeTimerSource(queue: .main)
}
let listener: NWListener
let timer: DispatchSourceTimer
func start() throws {
print("server will start")
self.listener.stateUpdateHandler = self.stateDidChange(to:)
self.listener.newConnectionHandler = self.didAccept(nwConnection:)
self.listener.start(queue: .main)
self.timer.setEventHandler(handler: self.heartbeat)
self.timer.schedule(deadline: .now() + 5.0, repeating: 5.0)
self.timer.activate()
}
func stateDidChange(to newState: NWListener.State) {
switch newState {
case .setup:
break
case .waiting:
break
case .ready:
break
case .failed(let error):
print("server did fail, error: \(error)")
self.stop()
case .cancelled:
break
default:
break
}
}
private var connectionsByID: [Int: Connection] = [:]
private func didAccept(nwConnection: NWConnection) {
let connection = Connection(nwConnection: nwConnection)
self.connectionsByID[connection.id] = connection
connection.didStopCallback = { _ in
self.connectionDidStop(connection)
}
connection.start()
print("server did open connection \(connection.id)")
}
private func connectionDidStop(_ connection: Connection) {
self.connectionsByID.removeValue(forKey: connection.id)
print("server did close connection \(connection.id)")
}
private func stop() {
self.listener.stateUpdateHandler = nil
self.listener.newConnectionHandler = nil
self.listener.cancel()
for connection in self.connectionsByID.values {
connection.didStopCallback = nil
connection.stop()
}
self.connectionsByID.removeAll()
self.timer.cancel()
}
private func heartbeat() {
let timestamp = Date()
print("server heartbeat, timestamp: \(timestamp)")
for connection in self.connectionsByID.values {
let data = "heartbeat, connection: \(connection.id), timestamp: \(timestamp)\r\n"
connection.send(data: Data(data.utf8))
}
}
static func run() {
let listener = Server()
try! listener.start()
dispatchMain()
}
}
func main() {
switch CommandLine.arguments.dropFirst() {
case ["client"]: Client.run()
case ["server"]: Server.run()
default:
print("usage: NWTest server | client")
exit(EXIT_FAILURE)
}
}
main()
I tested this on macOS 10.14.4.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
7 Likes
svenyonson
(Steve Johnson)
11
Hi Quinn,
I have code that uses NWConnection, as above, but I am stuck understanding how to wrap that in a unit test such that I can wait for the state to change to .ready, send a message, and receive a response. As it is the XCTest ends before the message is even sent.
Typically one uses XCTestExpectations when testing asynchronous code. The expectation is fulfilled when the response is received. See
https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations
2 Likes
Can someone (hopefully @eskimo Quinn “The Eskimo!” @ DTS @ Apple) help me understand how can "data" mutate in this example? The crash is intermittent and took me a day of running the app to catch. See screenshot:
eskimo
(Quinn “The Eskimo!”)
14
Can someone … help me understand how can data mutate in this
example?
To summarise, it seems your issue is that:
-
On line 2578 you check that data.count is of a specific value.
-
The code then runs down to line 2586.
-
Which calls a delegate method.
-
Which calls a whole bunch of other stuff.
-
Which eventually crashes with a memory access exception.
-
You then look up the backtrace to this code (and that’s a way up the backtrace, 35 frames in all).
-
And the debugger shows you that data.count is now 0.
Right?
If so, it’s hard to say exactly what might be causing that. Even if the debugger is working perfectly — which is a pretty big ‘if’ — you’ve crashed with a memory access exception, which means that by definition your program is in an undefined state.
The crash is intermittent and took me a day of running the app to
catch.
That makes things tricky. I recommend that you start with the standard memory debugging tools to see if that makes the problem easier to reproduce.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
1 Like
Thanks for a speedy reply.
"-Which calls a delegate method.
-Which calls a whole bunch of other stuff.
-Which eventually crashes with a memory access exception."
The delegate method tries to decode "data" into a struct in the first line, like so:
if let packet = try? JSONDecoder().decode(MyStruct.self, from: data){
That line crashes due to "data" being 0 (although "data" may not be 0, if I understand you correctly, due to the program being in an undefined state after crash, which causes debugger to misreport).
I guess what I am trying to ask is--can I count on "data" not to mutate? It's declared as "let Data?", so do I need to clone it before using it? My guess is no, but I have to ask... @eskimo
Thank you.
I may be mis-reading your screenshot, but sizePrefix on line 2578 has the white background that makes me think it was autofilled by the editor, and not written in. I'd expected Xcode to throw an error in that situation, but check to make sure that's not a code-block waiting to be filled in.
I highlighted it to copy and paste in po
1 Like
tera
18
I think data shown as empty is a red herring, and when in doubt I wouldn't trust debugger, as I've seen many instances when it lied to me. Logging is your friend – put it in place, reproduce the issue and analyse the logs, it would probably require several iterations until you catch it, but it's doable.
1 Like
You are reading the sizePrefix as radix 16 (hex) number. How does that play together with receive and all? Sorry if this is a stupid question, haven't used NWConnection myself.
I make two calls to receive one TCP packet. First is for a min/max of fixed size and that packet (sizePrefix) is (once decoded) a hexadecimal string, which represents the byte size of actual data. The second call is to receive actual data and is of min/max = sizePrefix