How do I upgrade nw_connection to a websocket connection?

(in objective-c, not swift), I have tried prepending the websocket options with nw_protocol_stack_prepend_application_protocol to my existing tls/tcp parameters but it is failing to upgrade the connection status and throwing an error. I cannot find any good examples or documentation on the Apple developer forums for this. Why do none of the functions have examples?

Replies

it is failing to upgrade the connection status and throwing an error.

It’s hard to offer insight into that without knowing what the error is. Regardless, here’s a simple WebSocket client in C:

@import Foundation;
@import Network;

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)

    NSLog(@"will start");

    nw_endpoint_t endpoint = nw_endpoint_create_url("ws://127.0.0.1:12345/test");
    nw_parameters_t params = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
    nw_protocol_stack_t stack = nw_parameters_copy_default_protocol_stack(params);
    nw_protocol_options_t ws = nw_ws_create_options(nw_ws_version_13);
    nw_protocol_stack_prepend_application_protocol(stack, ws);
    nw_connection_t connection = nw_connection_create(endpoint, params);
    nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
        NSLog(@"state did change, new: %d, error: %@", (int) state, error);
    });
    nw_connection_set_queue(connection, dispatch_get_main_queue());
    nw_connection_start(connection);
    
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 1000 * NSEC_PER_MSEC), 1000 * NSEC_PER_MSEC, 500 * NSEC_PER_MSEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"will send");
        NSString * message = [NSString stringWithFormat:@"Hello Cruel World %@", [NSDate date]];
        const char * messageC = message.UTF8String;
        dispatch_data_t content = dispatch_data_create(messageC, strlen(messageC), dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_DEFAULT);
        
        nw_protocol_metadata_t metadata = nw_ws_create_metadata(nw_ws_opcode_text);
        nw_content_context_t context = nw_content_context_create("send");
        nw_content_context_set_metadata_for_protocol(context, metadata);
        
        nw_connection_send(connection, content, context, true, ^(nw_error_t error) {
            NSLog(@"did send, error: %@", error);
        });
    });
    dispatch_activate(timer);
    
    dispatch_main();

    return EXIT_SUCCESS;
}

Annoyingly, my go-to WebSocket test site, websocket.org, is no longer available so I ended up creating a simple server to match. That code is pasted in below.

If anyone has an alternative WebSocket test site, please post the details here. My code here only tests the Mac-to-Mac case, and I’ve been bitten by stuff like that before (for example, back in the day, certain Secure Transport features would work Mac-to-Mac but fail with third-party TLS stacks; or was it the other way around? :-).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


@import Foundation;
@import Network;

static void startReceive(nw_connection_t connection) {
    nw_connection_receive(connection, 1, 65536, ^(dispatch_data_t content, nw_content_context_t context, bool isComplete, nw_error_t error) {
        #pragma unused(context)
        #pragma unused(isComplete)
        if (error != NULL) {
            NSLog(@"connection receive did fail, error: %@", error);
            return;
        }
        if (content != NULL) {
            size_t size = dispatch_data_get_size(content);
            NSLog(@"connection did receive content, size: %zu", size);
        }
        startReceive(connection);
    });
}

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)

    NSLog(@"will start");
    
    NSMutableArray * connections = [[NSMutableArray alloc] init];

    nw_parameters_t params = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
    nw_protocol_stack_t stack = nw_parameters_copy_default_protocol_stack(params);
    nw_protocol_options_t ws = nw_ws_create_options(nw_ws_version_13);
    nw_protocol_stack_prepend_application_protocol(stack, ws);
    nw_endpoint_t localEndpoint = nw_endpoint_create_host("::", "12345");
    nw_parameters_set_local_endpoint(params, localEndpoint);
    nw_listener_t listener = nw_listener_create(params);
    nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t error) {
        NSLog(@"listener state did change, new: %d, error: %@", (int) state, error);
    });
    nw_listener_set_new_connection_handler(listener, ^(nw_connection_t  connection) {
        NSLog(@"listener did accept connection");
        [connections addObject:connection];
        nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
            NSLog(@"connection state did change, new: %d, error: %@", (int) state, error);
        });
        nw_connection_set_queue(connection, dispatch_get_main_queue());
        nw_connection_start(connection);
        startReceive(connection);
    });
    nw_listener_set_queue(listener, dispatch_get_main_queue());
    nw_listener_start(listener);

    dispatch_main();
}
  • I found the website: https://echo.websocket.events/.ws It does allow you to test out WebSocket connections, but using the code above I was not able to establish a connection. The connection was stuck at the nw_connection_state_waiting state. The authors of the website also provide a docker image to run a server like this locally, but I got the same result. Any ideas why the connection is not establishing?

  • Looking at the error code , it corresponds to POSIX error ECONNREFUSED (Connection Refused).

Add a Comment

I’m working with WebSockets today and I wanted a Swift version of the above. I ended up porting it line-for-line, so I thought I’d share it here for the benefit of others (and, most importantly, Future Quinn™ :-).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import Network

func main() {
    print("will start")

    let endpoint = NWEndpoint.url(URL(string: "ws://127.0.0.1:12345/test")!)
    let params = NWParameters.tcp
    let stack = params.defaultProtocolStack
    let ws = NWProtocolWebSocket.Options(.version13)
    stack.applicationProtocols.insert(ws, at: 0)
    let connection = NWConnection(to: endpoint, using: params)
    connection.stateUpdateHandler = { newState in
        print("state did change, new: \(newState)")
    }
    connection.start(queue: .main)
    
    let timer = DispatchSource.makeTimerSource(queue: .main)
    timer.schedule(deadline: .now() + 1.0, repeating: 1.0)
    timer.setEventHandler {
        print("will send")
        
        let message = Data("Hello Cruel World \(Date())".utf8)
        let metadata = NWProtocolWebSocket.Metadata(opcode: .text)
        let context = NWConnection.ContentContext(identifier: "send", metadata: [metadata])
        connection.send(content: message, contentContext: context, isComplete: true, completion: NWConnection.SendCompletion.contentProcessed({ error in
            print("did send, error: \(error.flatMap { "\($0)" } ?? "-")")
        }))
    }
    timer.activate()
    
    withExtendedLifetime(connection) {
        dispatchMain()
    }
}

main()
exit(EXIT_SUCCESS)

import Foundation
import Network

private func startReceive(connection: NWConnection) {
    connection.receiveMessage { content, _, _, error in
        if let error = error {
            print("connection receive did fail, error: \(error)")
            return
        }
        if let content = content {
            print("connection did receive content, count: \(content.count)")
        }
        startReceive(connection: connection)
    }
}

func main() throws {
    print("will start")

    var connections: [NWConnection] = []

    let params = NWParameters.tcp
    let stack = params.defaultProtocolStack
    let ws = NWProtocolWebSocket.Options(.version13)
    stack.applicationProtocols.insert(ws, at: 0)
    let listener = try NWListener(using: params, on: 12345)
    listener.stateUpdateHandler = { newState in
        print("listener state did change, new: \(newState)")
    }
    listener.newConnectionHandler = { connection in
        print("listener did accept connection")
        connections.append(connection)
        connection.stateUpdateHandler = { newState in
            print("connection state did change, new: \(newState)")
        }
        connection.start(queue: .main)
        startReceive(connection: connection)
    }
    listener.start(queue: .main)

    withExtendedLifetime(listener) {
        dispatchMain()
    }
}

try! main()

it corresponds to POSIX error ECONNREFUSED

That suggests a low-level connectivity problem. Notably, I’m not seeing that here in my office. Here’s what I did:

  1. Using Xcode 13.2.1 on macOS 12.1, I created a new command-line tool project with the C client code from my first post (I assume you’re using C rather than Swift because you commented on my first post, not my second).

  2. I changed nw_endpoint_create_url("ws://127.0.0.1:12345/test") to nw_endpoint_create_url("wss://echo.websocket.events/.ws").

  3. I changed NW_PARAMETERS_DISABLE_PROTOCOL to NW_PARAMETERS_DEFAULT_CONFIGURATION, to enable TLS (my code used an ws URL, whereas your example is clearly intended to use wss and hence you need TLS).

  4. I ran it. It printed:

    2022-01-14 09:39:58.488498+0000 xxot[839:6135282] will start
    2022-01-14 09:39:58.513357+0000 xxot[839:6136041] state did change, new: 2, error: (null)
    2022-01-14 09:39:58.999719+0000 xxot[839:6136043] state did change, new: 3, error: (null)
    2022-01-14 09:39:59.512637+0000 xxot[839:6136041] will send
    

As you can see, the connection went through just fine (state 3 is nw_connection_state_ready).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Thanks Quinn, it works for me nicely as well.

Add a Comment