Correct Way to Use Nwconnection for Long-Running Tcp Socket

NWConnection timeout

You can set connection timed out by NWProtocolTCP.Options


lazy var tcpOptions: NWProtocolTCP.Options = {
let options = NWProtocolTCP.Options()
options.connectionTimeout = 5 // connection timed out
return options
}()

lazy var parames: NWParameters = {
let parames = NWParameters(tls: nil, tcp: self.tcpOptions)
if let isOption = parames.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
isOption.version = .v4
}
parames.preferNoProxies = true
return parames
}()

lazy var connection: NWConnection = {
let connection = NWConnection(host: "x.x.x.x", port: xxxx, using: self.parames)
return connection
}()

When trigger the timed out event, the .waiting state will call back.

2021-08-30 23:05:17.xxxxxx+xxxx Demo[xxxx:xxxxxx] [connection] nw_socket_handle_socket_event [C1:1] Socket SO_ERROR [60: Operation timed out]
The connection is waiting for a network path change with: POSIXErrorCode: Operation timed out

reference from Developer Fourms

Detecting a client disconnecting with UDP using Network.framework

A UDP server gets no information via the network about whether a UDP client has disconnected or gone away, unless perhaps the client explicitly sends a some sort of additional message (via UDP, TCP, or other side channel) regarding its disconnect status. So there's nothing to change the NWConnection state (except perhaps some sort of problem with the server itself).

Perhaps the server can assume a disconnect after some agreed upon or negotiated timeout time has passed without some sort of activity. Or number of packets, bytes of data. Etc. And close the connection itself.

Swift UDP Connection Issue

In your function receive you are using the NWConnection.receiveMessage if you check the documentation here:

https://developer.apple.com/documentation/network/nwconnection/3020638-receivemessage

You'll see that it schedules a single receive completion handler. That means that you'll have to do something to trigger it again. What I normally do is have a function like:

private func setupReceive() {
connection.receive(minimumIncompleteLength: 1, maximumLength: MTU) { (data, _, isComplete, error) in
if let data = data, !data.isEmpty {
let message = String(data: data, encoding: .utf8)
print("connection \(self.id) did receive, data: \(data as NSData) string: \(message ?? "-")")
self.send(data: data)
}
if isComplete {
self.connectionDidEnd()
} else if let error = error {
self.connectionDidFail(error: error)
} else {
self.setupReceive() // HERE I SET THE RECEIVE AGAIN
}
}
}

That way, after processing the read, you end up setting up another single receive completion handler.

If you want to see a full example, you can check my article on using Network.framework here:

https://rderik.com/blog/building-a-server-client-aplication-using-apple-s-network-framework/

The example uses TCP instead of UDP, but it should give a general idea.

iOS simple TCP connection example

SocketConnectionVC.h

#import <UIKit/UIKit.h>

@interface SocketConnectionVC : UIViewController<NSStreamDelegate>
{
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;

NSInputStream *inputStream;
NSOutputStream *outputStream;

NSMutableArray *messages;
}

@property (weak, nonatomic) IBOutlet UITextField *ipAddressText;
@property (weak, nonatomic) IBOutlet UITextField *portText;
@property (weak, nonatomic) IBOutlet UITextField *dataToSendText;
@property (weak, nonatomic) IBOutlet UITextView *dataRecievedTextView;
@property (weak, nonatomic) IBOutlet UILabel *connectedLabel;

@end

SocketConnectionVC.m

#import "SocketConnectionVC.h"

@interface SocketConnectionVC ()

@end

@implementation SocketConnectionVC

- (void)viewDidLoad {
[super viewDidLoad];

_connectedLabel.text = @"Disconnected";
}

- (IBAction) sendMessage {

NSString *response = [NSString stringWithFormat:@"msg:%@", _dataToSendText.text];
NSData *data = [[NSData alloc] initWithData:[response dataUsingEncoding:NSASCIIStringEncoding]];
[outputStream write:[data bytes] maxLength:[data length]];

}

- (void) messageReceived:(NSString *)message {

[messages addObject:message];

_dataRecievedTextView.text = message;
NSLog(@"%@", message);
}

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {

NSLog(@"stream event %lu", streamEvent);

switch (streamEvent) {

case NSStreamEventOpenCompleted:
NSLog(@"Stream opened");
_connectedLabel.text = @"Connected";
break;
case NSStreamEventHasBytesAvailable:

if (theStream == inputStream)
{
uint8_t buffer[1024];
NSInteger len;

while ([inputStream hasBytesAvailable])
{
len = [inputStream read:buffer maxLength:sizeof(buffer)];
if (len > 0)
{
NSString *output = [[NSString alloc] initWithBytes:buffer length:len encoding:NSASCIIStringEncoding];

if (nil != output)
{
NSLog(@"server said: %@", output);
[self messageReceived:output];
}
}
}
}
break;

case NSStreamEventHasSpaceAvailable:
NSLog(@"Stream has space available now");
break;

case NSStreamEventErrorOccurred:
NSLog(@"%@",[theStream streamError].localizedDescription);
break;

case NSStreamEventEndEncountered:

[theStream close];
[theStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
_connectedLabel.text = @"Disconnected";
NSLog(@"close stream");
break;
default:
NSLog(@"Unknown event");
}

}

- (IBAction)connectToServer:(id)sender {

NSLog(@"Setting up connection to %@ : %i", _ipAddressText.text, [_portText.text intValue]);
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge CFStringRef) _ipAddressText.text, [_portText.text intValue], &readStream, &writeStream);

messages = [[NSMutableArray alloc] init];

[self open];
}

- (IBAction)disconnect:(id)sender {

[self close];
}

- (void)open {

NSLog(@"Opening streams.");

outputStream = (__bridge NSOutputStream *)writeStream;
inputStream = (__bridge NSInputStream *)readStream;

[outputStream setDelegate:self];
[inputStream setDelegate:self];

[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

[outputStream open];
[inputStream open];

_connectedLabel.text = @"Connected";
}

- (void)close {
NSLog(@"Closing streams.");
[inputStream close];
[outputStream close];
[inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream setDelegate:nil];
[outputStream setDelegate:nil];
inputStream = nil;
outputStream = nil;

_connectedLabel.text = @"Disconnected";
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end

snapshot for ui of this SocketConnectionVC
Sample Image

and Follow these steps

1- input the ip on ipAdress textfield
2- input the port on port textfield
3- press connect button
4- (make sure the ip address and port is correct and the open of stream is fine. you can show the status of stream on console of Xcode)
5- input data to send to server
6- press send button
7- you can show the received message from server on the text view above connect button

TCP Socket no connection timeout

Pulling the network cable will not break a TCP connection(1) though it will disrupt communications. You can plug the cable back in and once IP connectivity is established, all back-data will move. This is what makes TCP reliable, even on cellular networks.

When TCP sends data, it expects an ACK in reply. If none comes within some amount of time, it re-transmits the data and waits again. The time it waits between transmissions generally increases exponentially.

After some number of retransmissions or some amount of total time with no ACK, TCP will consider the connection "broken". How many times or how long depends on your OS and its configuration but it typically times-out on the order of many minutes.

From Linux's tcp.7 man page:

   tcp_retries2 (integer; default: 15; since Linux 2.2)
The maximum number of times a TCP packet is retransmitted in
established state before giving up. The default value is 15, which
corresponds to a duration of approximately between 13 to 30 minutes,
depending on the retransmission timeout. The RFC 1122 specified
minimum limit of 100 seconds is typically deemed too short.

This is likely the value you'll want to adjust to change how long it takes to detect if your connection has vanished.

(1) There are exceptions to this. The operating system, upon noticing a cable being removed, could notify upper layers that all connections should be considered "broken".



Related Topics



Leave a reply



Submit