Good day Everyone! I've been trying for quite a while to send packets p2p using UDP and GCDAsyncUDPSocket library. So i found an open source library to connect to STUN server and keep external port open. It works quite well and i've already had success with sending data in local network. (After having trouble with own router which refused to connect p2p via udp in local network - still no trouble with Android created network)
The problem occurs when i try to send data using UDP hole punching.
STUNClient: https://github.com/soulfly/STUN-iOS
My modified code for UDPServer:
ViewController.h:
#import <UIKit/UIKit.h>
#import "STUNClient.h"
@interface ViewController : UIViewController <STUNClientDelegate>
{
IBOutlet UITextField *portField;
IBOutlet UIButton *startStopButton;
IBOutlet UIWebView *webView;
}
- (IBAction)startStop:(id)sender;
- (IBAction)startStun:(id)sender;
@end
ViewController.m
#import "ViewController.h"
#import "GCDAsyncUdpSocket.h"
#import "DDLog.h"
// Log levels: off, error, warn, info, verbose
static const int ddLogLevel = LOG_LEVEL_VERBOSE;
#define FORMAT(format, ...) [NSString stringWithFormat:(format), ##__VA_ARGS__]
@interface ViewController ()
{
BOOL isRunning;
GCDAsyncUdpSocket *udpSocket;
NSMutableString *log;
STUNClient* stunClient;
}
@end
@implementation ViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
{
log = [[NSMutableString alloc] init];
// Setup our socket.
// The socket will invoke our delegate methods using the usual delegate paradigm.
// However, it will invoke the delegate methods on a specified GCD delegate dispatch queue.
//
// Now we can configure the delegate dispatch queues however we want.
// We could simply use the main dispatch queue, so the delegate methods are invoked on the main thread.
// Or we could use a dedicated dispatch queue, which could be helpful if we were doing a lot of processing.
//
// The best approach for your application will depend upon convenience, requirements and performance.
//
// For this simple example, we're just going to use the main thread.
stunClient= [[STUNClient alloc] init];
udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
webView.dataDetectorTypes = UIDataDetectorTypeNone;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)viewDidUnload
{
[super viewDidUnload];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)getKeyboardHeight:(float *)keyboardHeightPtr
animationDuration:(double *)animationDurationPtr
from:(NSNotification *)notification
{
float keyboardHeight;
double animationDuration;
// UIKeyboardCenterBeginUserInfoKey:
// The key for an NSValue object containing a CGRect
// that identifies the start frame of the keyboard in screen coordinates.
CGRect beginRect = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endRect = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation))
{
keyboardHeight = ABS(beginRect.origin.x - endRect.origin.x);
}
else
{
keyboardHeight = ABS(beginRect.origin.y - endRect.origin.y);
}
// UIKeyboardAnimationDurationUserInfoKey
// The key for an NSValue object containing a double that identifies the duration of the animation in seconds.
animationDuration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
if (keyboardHeightPtr) *keyboardHeightPtr = keyboardHeight;
if (animationDurationPtr) *animationDurationPtr = animationDuration;
}
- (void)keyboardWillShow:(NSNotification *)notification
{
float keyboardHeight = 0.0F;
double animationDuration = 0.0;
[self getKeyboardHeight:&keyboardHeight animationDuration:&animationDuration from:notification];
CGRect webViewFrame = webView.frame;
webViewFrame.size.height -= keyboardHeight;
void (^animationBlock)(void) = ^{
webView.frame = webViewFrame;
};
UIViewAnimationOptions options = 0;
[UIView animateWithDuration:animationDuration
delay:0.0
options:options
animations:animationBlock
completion:NULL];
}
- (void)keyboardWillHide:(NSNotification *)notification
{
float keyboardHeight = 0.0F;
double animationDuration = 0.0;
[self getKeyboardHeight:&keyboardHeight animationDuration:&animationDuration from:notification];
CGRect webViewFrame = webView.frame;
webViewFrame.size.height += keyboardHeight;
void (^animationBlock)(void) = ^{
webView.frame = webViewFrame;
};
UIViewAnimationOptions options = 0;
[UIView animateWithDuration:animationDuration
delay:0.0
options:options
animations:animationBlock
completion:NULL];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
DDLogError(@"webView:didFailLoadWithError: %@", error);
}
- (void)webViewDidFinishLoad:(UIWebView *)sender
{
NSString *scrollToBottom = @"window.scrollTo(document.body.scrollWidth, document.body.scrollHeight);";
[sender stringByEvaluatingJavaScriptFromString:scrollToBottom];
}
- (void)logError:(NSString *)msg
{
NSString *prefix = @"<font color=\"#B40404\">";
NSString *suffix = @"</font><br/>";
[log appendFormat:@"%@%@%@\n", prefix, msg, suffix];
NSString *html = [NSString stringWithFormat:@"<html><body>\n%@\n</body></html>", log];
[webView loadHTMLString:html baseURL:nil];
}
- (void)logInfo:(NSString *)msg
{
NSString *prefix = @"<font color=\"#6A0888\">";
NSString *suffix = @"</font><br/>";
[log appendFormat:@"%@%@%@\n", prefix, msg, suffix];
NSString *html = [NSString stringWithFormat:@"<html><body>\n%@\n</body></html>", log];
[webView loadHTMLString:html baseURL:nil];
}
- (void)logMessage:(NSString *)msg
{
NSString *prefix = @"<font color=\"#000000\">";
NSString *suffix = @"</font><br/>";
[log appendFormat:@"%@%@%@\n", prefix, msg, suffix];
NSString *html = [NSString stringWithFormat:@"<html><body>%@</body></html>", log];
[webView loadHTMLString:html baseURL:nil];
}
- (IBAction)startStop:(id)sender
{
if (isRunning)
{
// STOP udp echo server
[udpSocket close];
[self logInfo:@"Stopped Udp Echo server"];
isRunning = false;
[portField setEnabled:YES];
[startStopButton setTitle:@"Start" forState:UIControlStateNormal];
}
else
{
//*START udp echo server
int port = [portField.text intValue];
if (port < 0 || port > 65535)
{
portField.text = @"";
port = 0;
}
NSError *error = nil;
if (![udpSocket bindToPort:port error:&error])
{
[self logError:FORMAT(@"Error starting server (bind): %@", error)];
return;
}
if (![udpSocket beginReceiving:&error])
{
[udpSocket close];
[self logError:FORMAT(@"Error starting server (recv): %@", error)];
return;
}
[self logInfo:FORMAT(@"Udp Echo server started on port %hu", [udpSocket localPort])];
isRunning = YES;
[portField setEnabled:NO];
[startStopButton setTitle:@"Stop" forState:UIControlStateNormal];
}
}
-(void)didReceivePublicIPandPort:(NSDictionary *) data{
NSLog(@"Public IP=%@, public Port=%@, NAT is Symmetric: %@", [data objectForKey:publicIPKey],
[data objectForKey:publicPortKey], [data objectForKey:isPortRandomization]);
int portT= [((NSNumber*)[data objectForKey:publicPortKey]) intValue];
NSString* ipT=[data objectForKey:publicIPKey];
[self logInfo:FORMAT(@"STUN says external IP: %@ PORT %d", ipT,portT)];
[self logInfo:FORMAT(@"LOCAL IP: %@ LOCAL PORT %d", [udpSocket localHost],[udpSocket localPort])];
isRunning =YES;
[stunClient startSendIndicationMessage];
}
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address
withFilterContext:(id)filterContext
{
if (!isRunning) return;
NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (msg)
{
/* If you want to get a display friendly version of the IPv4 or IPv6 address, you could do this:
NSString *host = nil;
uint16_t port = 0;
[GCDAsyncUdpSocket getHost:&host port:&port fromAddress:address];
*/
[self logMessage:msg];
}
else
{
[self logError:@"Error converting received data into UTF-8 String"];
}
[udpSocket sendData:data toAddress:address withTimeout:-1 tag:0];
}
- (IBAction)startStun:(id)sender
{
[stunClient requestPublicIPandPortWithUDPSocket:udpSocket delegate:self];
}
@end
What I do:
Turn Client and Server app ON.
Push STUN Button - which launches `- (IBAction)startStun:(id)sender;
- Manually input external ip and port from STUN server in ClientApp.
- Send.
Maybe it is router problems? Or i do not completely understand hole punching methodology?