We are having issues establishing a TCP/IP connection using GCDAsyncSocket [xcode 12.5 and iOS 14.5/14.6] on one of our devices. The error we receive is - Error Domain=NSPOSIXErrorDomain Code=60 "Operation timed out" UserInfo={NSLocalizedDescription= Operation timed out, NSLocalizedFailureReason=Error in connect() function}
GCDAsyncSocket Connect Error
Well, if it’s returning an error and not crashing then you’re doing better than most people!
Seriously though, there’s an important takeaway from that thread: You should plan some time to move away from GCDAsyncSocket. It’s based on legacy APIs and Network framework continues to add support for cool new stuff that you’ll eventually want. For example, CFSocketStream
, one of the legacy APIs underlying GCDAsyncSocket, does not support TLS 1.3 )-:
Still, you still need a solution to your immediate problem. Apropos that:
-
What sort of connection are you using? TCP? UDP?
-
Are you using TLS?
-
Is it to a local network address? Or to a server out there on the wider Internet?
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
- We are using TCP.
- We are not using TLS.
- We are attempting connections to a local network address.
At the same time, we would like to go ahead with your suggestion and use the Network Framework. Our App needs to establish multiple asynchronous sockets for read/write to our local hardware devices. Is there a guided procedure that you can point us to in order to expedite the migration process so that we get this resolved for our clients ASAP?
Thanks
We are attempting connections to a local network address.
In that case the most likely cause of the problem you’re seeing is local network privacy, which is more rigorously enforced starting with iOS 14.5. See the Local Network Privacy FAQ for details.
Having said that, I still recommend a move to Network framework. You wrote:
We are using Objective-C.
OK.
Network framework doesn’t have an Objective-C API per se (it’s Swift and C) but it’s quite easy to use the C API from Objective-C. The only official sample code we have for the C API is nwcat but it’s a little complex because it demonstrates a bunch of different facilities, like UDP and TLS. So, to get you up and running quickly I’ve pasted a snippet in below. This uses Network framework to fetch https://example.com
. It’s not a full sample, but it should be enough to get you started.
IMPORTANT Because Network framework is a C API it can be a little clunky to call from Objective-C. Probably the biggest impedance mismatch is between dispatch_data_t
and NSData
. If you find that particularly annoying, you should just create some wrappers that go from one to the other.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
@interface Connection : NSObject
@property (nonatomic, strong, readonly) nw_endpoint_t endpoint;
@property (nonatomic, strong, readwrite) nw_connection_t connection;
@end
@implementation Connection
- (instancetype)init {
self = [super init];
if (self != nil) {
self->_endpoint = nw_endpoint_create_host("example.com", "80");
}
return self;
}
- (void)start {
assert(self.connection == NULL);
nw_parameters_t paramters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
self.connection = nw_connection_create(self.endpoint, paramters);
nw_connection_set_queue(self.connection, dispatch_get_main_queue());
nw_connection_set_state_changed_handler(self.connection, ^(nw_connection_state_t state, nw_error_t error) {
NSLog(@"did change state, state: %d, error: %@", (int) state, error);
});
[self sendRequest];
[self startReceive];
nw_connection_start(self.connection);
}
- (void)sendRequest {
NSString * getRequest = @
"GET / HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n"
"\r\n"
;
NSData * getRequestData = [getRequest dataUsingEncoding:NSUTF8StringEncoding];
dispatch_data_t getRequestDispatchData = dispatch_data_create(getRequestData.bytes, getRequestData.length, NULL, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
nw_connection_send(self.connection, getRequestDispatchData, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, true, ^(nw_error_t error) {
NSLog(@"send complete, error: %@", error);
});
}
- (void)startReceive {
NSLog(@"will start receive");
nw_connection_receive(self.connection, 1, 65536, ^(dispatch_data_t data, nw_content_context_t context, bool isComplete, nw_error_t error) {
#pragma unused(context)
if (data != nil) {
dispatch_data_apply(data, ^bool(dispatch_data_t region, size_t offset, const void * _Nonnull buffer, size_t size) {
#pragma unused(region)
#pragma unused(offset)
NSData * chunk = [NSData dataWithBytes:buffer length:size];
NSLog(@"data: %@", chunk.debugDescription);
return true;
});
}
if (isComplete) {
NSLog(@"EOF");
return;
}
if (error != nil) {
NSLog(@"error: %@", error);
return;
}
[self startReceive];
});
}
@end
Are you able to shed any light as to why you believe this could be the case?
Not really. There’s a world of potential reasons for this and it’s hard to debug in the context of DevForums. If you’d like to dig into this, my advice is that you open a DTS tech support incident, which will allow Matt or me to allocate a significant amount of time to helping you out.
However, it might just be easier to erase this device and re-test. My experience is that development devices tend to accumulate a lot of cruft and oftentimes a reset can clear that out.
Also, create a simple Network framework test project and try that on the problematic device. It’ll be interesting to see whether this is a Sockets-only issue.
Oh, one last thing: DTS doesn’t support third-party libraries, like GCDAsyncSocket. From our perspective that is your code! So, if you do end up opening a TSI then be prepared to ask questions in terms of the Apple APIs we support (in this case that’s BSD Sockets).
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Thanks for the sample code. We have been able to send/receive data using the above code. However, we have now run into some new problems.
- nw_connection_t client [in this case the iPhone] does not get an event handler if the TCP server becomes unavailable and therefore has no means of handling the disconnect.
- What would be the right process to handle multiple client TCP socket connections to multiple servers? Would be able to provide us with some sample code?
nw_connection_t
client [in this case the iPhone] does not get an event handler if the TCP server becomes unavailable and therefore has no means of handling the disconnect.
Please clarify “becomes unavailable”? There are three common scenarios here:
-
The server goes offline when there’s no connection in place. In that case the problem shows up when establishing an outgoing TCP connection.
-
There’s a TCP connection in place and the server closes it.
-
There’s a TCP connection in place and the server simply disappears off the network (for example, the user unplugs the Ethernet cable leading to the server, or turns off its power).
Which are you concerned about?
What would be the right process to handle multiple client TCP socket connections to multiple servers?
You need one nw_connection_t
object for each of those TCP connections.
Would be able to provide us with some sample code?
I kinda already did. The Connection
class I posted handles a single nw_connection_t
, so you’d just instantiate that multiple times, once for each TCP connection you want to run. My Connection
class hard wires the endpoint but it’d be simply to change -init
to -initWithEndpoint:
.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
- "becomes unavailable" has the following scenarios for us -
- There is a TCP connection in place and the iPhone[TCP client] goes out of range and therefore the connection is lost to the server.
In this instance, we need to reestablish the connection when the iPhone is back in range. The iPhone does not need to send any request to the server but needs to continuosly receive data (eg: every sec) from the server for live telemetry.
- There is a TCP connection in place and the server disappears off the network - eg: out of range or turns its power off.
-
Does 'nw_connection_t' have a 'state' property that can be checked on a timely basis to identify the state of the connection?
-
We have also been using - CFStreamCreatePairWithSocketToHost, CFReadStreamRef, CFWriteStreamRef in our existing application.
- Will this continue to be supported in future or are there chances of this becoming redundant as well?
- Point being, that if 'nw_connection_t' does not serve our purpose, then do we have a potential long term solution in using CFStreamCreatePairWithSocketToHost instead of our current GCDAsyncSocket?
Will [
CFSocketStream
] continue to be supported in future or are there chances of this becoming redundant as well?
CFSocketStream
is deprecated as of the iOS 15 beta SDK. There’s no announced plans to remove it, that would break a lot of apps, but such deprecations do have consequences:
-
When Apple introduces a new platform, or even a new architecture on an existing platform, we often leave deprecated APIs behind.
-
Deprecated APIs generally don’t get new features. For example, Apple recently introduced TLS 1.3 support but it’s not available via our deprecated APIs.
-
In many cases we eventually get around to removing the API from the SDK, meaning that it’s still available to existing built binaries but no longer available at compile time.
Now is the time to make the leap to the Network framework.
Does
nw_connection_t
have a 'state' property that can be checked on a timely basis to identify the state of the connection?
Yes, and no. There is a state property but we generally recommend against polling it. Rather, you’d implement a state update handler that the connection calls when the state changes. See the call to nw_connection_set_state_changed_handler
in my code snippet above.
Having said that, if you turn off the power to the accessory that doesn’t generate any traffic on the network and thus the client can’t tell that it’s happened. The usual approach here is to enable TCP keepalive. This is somewhat misnamed, in that it doesn’t keep your connection alive but rather allows the connection to determine that it’s dead.
In Network framework you enable TCP keepalive via nw_tcp_options_set_enable_keepalive
and friends.
In this instance, we need to reestablish the connection when the iPhone is back in range.
Our recommended approach here is that, once you detect that the connection has failed, you immediately start a new connection. This will go into the waiting state (nw_connection_state_waiting
), allowing it to connect again once the Wi-Fi network comes back up.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Would you be able to give us some assistance as to where and how to enable - 'nw_tcp_options_set_enable_keepalive'. Which object do we set it for?
We have tried going through the available documentation but could not find any examples to implement.
nw_tcp_options_set_enable_keepalive
takes a nw_protocol_options_t
, so you should work backwards from there. The nwcat sample code has this code to set IP-level options:
nw_protocol_options_t ip_options = nw_protocol_stack_copy_internet_protocol(protocol_stack);
…
nw_ip_options_set_version(ip_options, nw_ip_version_4);
If you replace nw_protocol_stack_copy_internet_protocol
with nw_protocol_stack_copy_transport_protocol
you get the transport options (TCP in your case), and you can then call nw_tcp_options_set_enable_keepalive
on that.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"