diff --git a/src/android/Connection.java b/src/android/Connection.java index e7457e9..d8015da 100644 --- a/src/android/Connection.java +++ b/src/android/Connection.java @@ -12,148 +12,147 @@ /** * @author viniciusl * - * This class represents a socket connection, behaving like a thread to listen + * This class represents a socket connection, behaving like a thread to listen * a TCP port and receive data */ public class Connection extends Thread { - private SocketPlugin hook; - - private Socket callbackSocket; - private PrintWriter writer; - private OutputStream outputStream; - private BufferedReader reader; - - private Boolean mustClose; - private String host; - private int port; - - - /** - * Creates a TCP socket connection object. - * - * @param pool Object containing "sendMessage" method to be called as a callback for data receive. - * @param host Target host for socket connection. - * @param port Target port for socket connection - */ - public Connection(SocketPlugin pool, String host, int port) { - super(); - setDaemon(true); - - this.mustClose = false; - this.host = host; - this.port = port; - this.hook = pool; - } - - - /** - * Returns socket connection state. - * - * @return true if socket connection is established or false case else. - */ - public boolean isConnected() { - - boolean result = ( - this.callbackSocket == null ? false : - this.callbackSocket.isConnected() && - this.callbackSocket.isBound() && - !this.callbackSocket.isClosed() && - !this.callbackSocket.isInputShutdown() && - !this.callbackSocket.isOutputShutdown()); - - // if everything apparently is fine, time to test the streams - if (result) { - try { - this.callbackSocket.getInputStream().available(); - } catch (IOException e) { - // connection lost - result = false; - } - } - - return result; - } - - /** - * Closes socket connection. - */ - public void close() { - // closing connection - try { - //this.writer.close(); - //this.reader.close(); - callbackSocket.shutdownInput(); - callbackSocket.shutdownOutput(); - callbackSocket.close(); - this.mustClose = true; - } catch (IOException e) { - e.printStackTrace(); - } - } - - - /** - * Writes on socket output stream to send data to target host. - * - * @param data information to be sent - */ - public void write(String data) { - this.writer.println(data); - } - - - - /** - * Outputs to socket output stream to send binary data to target host. - * - * @param data information to be sent - */ - public void writeBinary(byte[] data) throws IOException { - this.outputStream.write(data); - } - - - - /* (non-Javadoc) - * @see java.lang.Thread#run() - */ - public void run() { - String chunk = null; - - // creating connection - try { - this.callbackSocket = new Socket(this.host, this.port); - this.writer = new PrintWriter(this.callbackSocket.getOutputStream(), true); - this.outputStream = this.callbackSocket.getOutputStream(); - this.reader = new BufferedReader(new InputStreamReader(callbackSocket.getInputStream())); - - // receiving data chunk - while(!this.mustClose){ - - try { - - if (this.isConnected()) { - chunk = reader.readLine(); - - if (chunk != null) { - chunk = chunk.replaceAll("\"\"", "null"); - System.out.print("## RECEIVED DATA: " + chunk); - hook.sendMessage(this.host, this.port, chunk); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - } catch (UnknownHostException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } catch (IOException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - - } + private SocketPlugin hook; + + private Socket callbackSocket; + private PrintWriter writer; + private OutputStream outputStream; + private BufferedReader reader; + + private Boolean mustClose; + private String host; + private int port; + + + /** + * Creates a TCP socket connection object. + * + * @param pool Object containing "sendMessage" method to be called as a callback for data receive. + * @param host Target host for socket connection. + * @param port Target port for socket connection + */ + public Connection(SocketPlugin pool, String host, int port) { + super(); + setDaemon(true); + + this.mustClose = false; + this.host = host; + this.port = port; + this.hook = pool; + } + + + /** + * Returns socket connection state. + * + * @return true if socket connection is established or false case else. + */ + public boolean isConnected() { + + boolean result = ( + this.callbackSocket == null ? false : + this.callbackSocket.isConnected() && + this.callbackSocket.isBound() && + !this.callbackSocket.isClosed() && + !this.callbackSocket.isInputShutdown() && + !this.callbackSocket.isOutputShutdown()); + + // if everything apparently is fine, time to test the streams + if (result) { + try { + this.callbackSocket.getInputStream().available(); + } catch (IOException e) { + // connection lost + result = false; + } + } + + return result; + } + + /** + * Closes socket connection. + */ + public void close() { + // closing connection + try { + //this.writer.close(); + //this.reader.close(); + callbackSocket.shutdownInput(); + callbackSocket.shutdownOutput(); + callbackSocket.close(); + this.mustClose = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + + + /** + * Writes on socket output stream to send data to target host. + * + * @param data information to be sent + */ + public void write(String data) { + this.writer.println(data); + } + + + + /** + * Outputs to socket output stream to send binary data to target host. + * + * @param data information to be sent + */ + public void writeBinary(byte[] data) throws IOException { + this.outputStream.write(data); + } + + + /* (non-Javadoc) + * @see java.lang.Thread#run() + */ + public void run() { + byte[] chunk = new byte[512]; + + // creating connection + try { + this.callbackSocket = new Socket(this.host, this.port); + this.writer = new PrintWriter(this.callbackSocket.getOutputStream(), true); + this.outputStream = this.callbackSocket.getOutputStream(); + this.reader = new BufferedReader(new InputStreamReader(callbackSocket.getInputStream())); + + // receiving data chunk + while(!this.mustClose){ + + try { + + if (this.isConnected()) { + int bytesRead = callbackSocket.getInputStream().read(chunk); + byte[] line = new byte[bytesRead]; + System.arraycopy(chunk, 0, line, 0, bytesRead); + + if (bytesRead > 0) { + hook.sendMessage(this.host, this.port, line); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + } catch (UnknownHostException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } catch (IOException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + + } } diff --git a/src/android/SocketPlugin.java b/src/android/SocketPlugin.java index e9b9caa..e50e52e 100644 --- a/src/android/SocketPlugin.java +++ b/src/android/SocketPlugin.java @@ -2,6 +2,8 @@ import android.annotation.SuppressLint; +import android.util.Base64; + import java.io.IOException; import java.util.HashMap; @@ -17,7 +19,7 @@ /** * @author viniciusl * - * Plugin to handle TCP socket connections. + * Plugin to handle TCP socket connections. */ /** * @author viniciusl @@ -25,323 +27,323 @@ */ public class SocketPlugin extends CordovaPlugin { - private Map pool = new HashMap(); // pool of "active" connections - - /* (non-Javadoc) - * @see org.apache.cordova.CordovaPlugin#execute(java.lang.String, org.json.JSONArray, org.apache.cordova.CallbackContext) - */ - @Override - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - - if (action.equals("connect")) { - this.connect(args, callbackContext); - return true; - - }else if(action.equals("isConnected")) { - this.isConnected(args, callbackContext); - return true; - - }else if(action.equals("send")) { - this.send(args, callbackContext); - return true; - - }else if(action.equals("sendBinary")) { - this.sendBinary(args, callbackContext); - return true; - - } else if (action.equals("disconnect")) { - this.disconnect(args, callbackContext); - return true; - - } else if (action.equals("disconnectAll")) { - this.disconnectAll(callbackContext); - return true; - - } else { - return false; - } - } - - /** - * Build a key to identify a socket connection based on host and port information. - * - * @param host Target host - * @param port Target port - * @return connection key - */ - @SuppressLint("DefaultLocale") - private String buildKey(String host, int port) { - return (host.toLowerCase() + ":" + port); - } - - /** - * Opens a socket connection. - * - * @param args - * @param callbackContext - */ - private void connect (JSONArray args, CallbackContext callbackContext) { - String key; - String host; - int port; - Connection socket; - - // validating parameters - if (args.length() < 2) { - callbackContext.error("Missing arguments when calling 'connect' action."); - } else { - - // opening connection and adding into pool - try { - - // preparing parameters - host = args.getString(0); - port = args.getInt(1); - key = this.buildKey(host, port); - - // creating connection - if (this.pool.get(key) == null) { - socket = new Connection(this, host, port); - socket.start(); - this.pool.put(key, socket); - } - - // adding to pool - callbackContext.success(key); - - } catch (JSONException e) { - callbackContext.error("Invalid parameters for 'connect' action: " + e.getMessage()); - } - } - } - - /** - * Returns connection information - * - * @param args - * @param callbackContext - */ - private void isConnected(JSONArray args, CallbackContext callbackContext) { - Connection socket; - - // validating parameters - if (args.length() < 1) { - callbackContext.error("Missing arguments when calling 'isConnected' action."); - } else { - try { - // retrieving parameters - String key = args.getString(0); - - // getting socket - socket = this.pool.get(key); - - // checking if socket was not found and his connectivity - if (socket == null) { - callbackContext.error("No connection found with host " + key); - - } else { - - // ending send process - callbackContext.success( (socket.isConnected() ? 1 : 0) ); - } - - } catch (JSONException e) { - callbackContext.error("Unexpected error sending information: " + e.getMessage()); - } - } - } - - - /** - * Send information to target host - * - * @param args - * @param callbackContext - */ - private void send(JSONArray args, CallbackContext callbackContext) { - Connection socket; - - // validating parameters - if (args.length() < 2) { - callbackContext.error("Missing arguments when calling 'send' action."); - } else { - try { - // retrieving parameters - String key = args.getString(0); - String data = args.getString(1); - - // getting socket - socket = this.pool.get(key); - - // checking if socket was not found and his connectivity - if (socket == null) { - callbackContext.error("No connection found with host " + key); - - } else if (!socket.isConnected()) { - callbackContext.error("Invalid connection with host " + key); - - } else if (data.length() == 0) { - callbackContext.error("Cannot send empty data to " + key); - - } else { - - // write on output stream - socket.write(data); - - // ending send process - callbackContext.success(); - } - - } catch (JSONException e) { - callbackContext.error("Unexpected error sending information: " + e.getMessage()); - } - } - } - - - /** - * Send binary information to target host - * - * @param args - * @param callbackContext - */ - private void sendBinary(JSONArray args, CallbackContext callbackContext) { - Connection socket; - - // validating parameters - if (args.length() < 2) { - callbackContext.error("Missing arguments when calling 'sendBinary' action."); - } else { - try { - // retrieving parameters - String key = args.getString(0); - JSONArray jsData = args.getJSONArray(1); - byte[] data = new byte[jsData.length()]; - for (int i=0; i> it = this.pool.entrySet().iterator(); - - while( it.hasNext() ) { - - // retrieving object - Map.Entry pairs = (Entry) it.next(); - Connection socket = pairs.getValue(); - - // checking connection - if (socket.isConnected()) { - socket.close(); - } - - // removing from pool - this.pool.remove(pairs.getKey()); - } - - callbackContext.success("All connections were closed."); - } - - - /** - * Callback for Connection object data receive. Relay information to javascript object method: window.tlantic.plugins.socket.receive(); - * - * @param host - * @param port - * @param chunk - */ - public synchronized void sendMessage(String host, int port, String chunk) { - final String receiveHook = "window.tlantic.plugins.socket.receive(\"" + host + "\"," + port + ",\"" + this.buildKey(host, port) + "\",\"" + chunk.replace("\"", "\\\"") + "\");"; - - cordova.getActivity().runOnUiThread(new Runnable() { - - @Override - public void run() { - webView.loadUrl("javascript:" + receiveHook); - } - - }); - } - + private Map pool = new HashMap(); // pool of "active" connections + + /* (non-Javadoc) + * @see org.apache.cordova.CordovaPlugin#execute(java.lang.String, org.json.JSONArray, org.apache.cordova.CallbackContext) + */ + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + + if (action.equals("connect")) { + this.connect(args, callbackContext); + return true; + + }else if(action.equals("isConnected")) { + this.isConnected(args, callbackContext); + return true; + + }else if(action.equals("send")) { + this.send(args, callbackContext); + return true; + + }else if(action.equals("sendBinary")) { + this.sendBinary(args, callbackContext); + return true; + + } else if (action.equals("disconnect")) { + this.disconnect(args, callbackContext); + return true; + + } else if (action.equals("disconnectAll")) { + this.disconnectAll(callbackContext); + return true; + + } else { + return false; + } + } + + /** + * Build a key to identify a socket connection based on host and port information. + * + * @param host Target host + * @param port Target port + * @return connection key + */ + @SuppressLint("DefaultLocale") + private String buildKey(String host, int port) { + return (host.toLowerCase() + ":" + port); + } + + /** + * Opens a socket connection. + * + * @param args + * @param callbackContext + */ + private void connect (JSONArray args, CallbackContext callbackContext) { + String key; + String host; + int port; + Connection socket; + + // validating parameters + if (args.length() < 2) { + callbackContext.error("Missing arguments when calling 'connect' action."); + } else { + + // opening connection and adding into pool + try { + + // preparing parameters + host = args.getString(0); + port = args.getInt(1); + key = this.buildKey(host, port); + + // creating connection + if (this.pool.get(key) == null) { + socket = new Connection(this, host, port); + socket.start(); + this.pool.put(key, socket); + } + + // adding to pool + callbackContext.success(key); + + } catch (JSONException e) { + callbackContext.error("Invalid parameters for 'connect' action: " + e.getMessage()); + } + } + } + + /** + * Returns connection information + * + * @param args + * @param callbackContext + */ + private void isConnected(JSONArray args, CallbackContext callbackContext) { + Connection socket; + + // validating parameters + if (args.length() < 1) { + callbackContext.error("Missing arguments when calling 'isConnected' action."); + } else { + try { + // retrieving parameters + String key = args.getString(0); + + // getting socket + socket = this.pool.get(key); + + // checking if socket was not found and his connectivity + if (socket == null) { + callbackContext.error("No connection found with host " + key); + + } else { + + // ending send process + callbackContext.success( (socket.isConnected() ? 1 : 0) ); + } + + } catch (JSONException e) { + callbackContext.error("Unexpected error sending information: " + e.getMessage()); + } + } + } + + + /** + * Send information to target host + * + * @param args + * @param callbackContext + */ + private void send(JSONArray args, CallbackContext callbackContext) { + Connection socket; + + // validating parameters + if (args.length() < 2) { + callbackContext.error("Missing arguments when calling 'send' action."); + } else { + try { + // retrieving parameters + String key = args.getString(0); + String data = args.getString(1); + + // getting socket + socket = this.pool.get(key); + + // checking if socket was not found and his connectivity + if (socket == null) { + callbackContext.error("No connection found with host " + key); + + } else if (!socket.isConnected()) { + callbackContext.error("Invalid connection with host " + key); + + } else if (data.length() == 0) { + callbackContext.error("Cannot send empty data to " + key); + + } else { + + // write on output stream + socket.write(data); + + // ending send process + callbackContext.success(); + } + + } catch (JSONException e) { + callbackContext.error("Unexpected error sending information: " + e.getMessage()); + } + } + } + + + /** + * Send binary information to target host + * + * @param args + * @param callbackContext + */ + private void sendBinary(JSONArray args, CallbackContext callbackContext) { + Connection socket; + + // validating parameters + if (args.length() < 2) { + callbackContext.error("Missing arguments when calling 'sendBinary' action."); + } else { + try { + // retrieving parameters + String key = args.getString(0); + JSONArray jsData = args.getJSONArray(1); + byte[] data = new byte[jsData.length()]; + for (int i=0; i> it = this.pool.entrySet().iterator(); + + while( it.hasNext() ) { + + // retrieving object + Map.Entry pairs = (Entry) it.next(); + Connection socket = pairs.getValue(); + + // checking connection + if (socket.isConnected()) { + socket.close(); + } + + // removing from pool + this.pool.remove(pairs.getKey()); + } + + callbackContext.success("All connections were closed."); + } + + + /** + * Callback for Connection object data receive. Relay information to javascript object method: window.tlantic.plugins.socket.receive(); + * + * @param host + * @param port + * @param chunk + */ + public synchronized void sendMessage(String host, int port, byte[] chunk) { + final String receiveHook = "window.tlantic.plugins.socket.receive(\"" + host + "\"," + port + ",\"" + this.buildKey(host, port) + "\",window.atob(\"" + Base64.encodeToString(chunk, Base64.DEFAULT) + "\"));"; + + cordova.getActivity().runOnUiThread(new Runnable() { + + @Override + public void run() { + webView.loadUrl("javascript:" + receiveHook); + } + + }); + } + } diff --git a/src/ios/CDVSocketPlugin.h b/src/ios/CDVSocketPlugin.h index a9d445b..ff9003a 100644 --- a/src/ios/CDVSocketPlugin.h +++ b/src/ios/CDVSocketPlugin.h @@ -10,7 +10,8 @@ -(void) disconnectAll: (CDVInvokedUrlCommand *) command; -(void) isConnected: (CDVInvokedUrlCommand *) command; -(void) send: (CDVInvokedUrlCommand *) command; +-(void) sendBinary: (CDVInvokedUrlCommand *) command; -(BOOL) disposeConnection :(NSString *)key; -@end \ No newline at end of file +@end diff --git a/src/ios/CDVSocketPlugin.m b/src/ios/CDVSocketPlugin.m index 1dd6e72..f79601b 100644 --- a/src/ios/CDVSocketPlugin.m +++ b/src/ios/CDVSocketPlugin.m @@ -7,19 +7,19 @@ @implementation CDVSocketPlugin : CDVPlugin - (NSString*) buildKey : (NSString*) host : (int) port { NSString* tempHost = [host lowercaseString]; NSString* tempPort = [NSString stringWithFormat : @"%d", port]; - + return [[tempHost stringByAppendingString : @":"] stringByAppendingString:tempPort]; } - (void) connect : (CDVInvokedUrlCommand*) command { // Validating parameters if ([command.arguments count] < 2) { - + // Triggering parameter error CDVPluginResult* result = [CDVPluginResult resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Missing arguments when calling 'connect' action."]; - + [self.commandDelegate sendPluginResult : result callbackId : command.callbackId @@ -29,23 +29,23 @@ - (void) connect : (CDVInvokedUrlCommand*) command { if (!pool) { self->pool = [[NSMutableDictionary alloc] init]; } - + // Running in background to avoid thread locks [self.commandDelegate runInBackground:^{ - + CDVPluginResult* result = nil; Connection* socket = nil; NSString* key = nil; NSString* host = nil; int port = 0; - + // Opening connection and adding into pool @try { // Preparing parameters host = [command.arguments objectAtIndex : 0]; port = [[command.arguments objectAtIndex : 1] integerValue]; key = [self buildKey : host : port]; - + // Checking existing connections if ([pool objectForKey : key]) { NSLog(@"Recovered connection with %@", key); @@ -59,10 +59,10 @@ - (void) connect : (CDVInvokedUrlCommand*) command { socket = [[Connection alloc] initWithNetworkAddress:host :port]; [socket setDelegate:self]; [socket open]; - + // Adding to pool [self->pool setObject:socket forKey:key]; - + // Formatting success response result = [CDVPluginResult resultWithStatus : @@ -75,7 +75,7 @@ - (void) connect : (CDVInvokedUrlCommand*) command { resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Unexpected exception when executing 'connect' action."]; } - + // Returns the Callback Resolution [self.commandDelegate sendPluginResult : result callbackId : command.callbackId]; @@ -86,33 +86,33 @@ - (void) connect : (CDVInvokedUrlCommand*) command { - (void) isConnected : (CDVInvokedUrlCommand *) command { // Validating parameters if ([command.arguments count] < 1) { - + // Triggering parameter error CDVPluginResult* result = [CDVPluginResult resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Missing arguments when calling 'isConnected' action."]; - + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId ]; - + } else { - + // running in background to avoid thread locks [self.commandDelegate runInBackground : ^{ - + CDVPluginResult* result= nil; Connection* socket = nil; NSString* key = nil; - + @try { // Preparing parameters key = [command.arguments objectAtIndex:0]; - + // Getting connection from pool socket = [pool objectForKey:key]; - + // Checking if socket was not found and his conenctivity if (socket == nil) { NSLog(@"Connection not found"); @@ -121,7 +121,7 @@ - (void) isConnected : (CDVInvokedUrlCommand *) command { messageAsString : @"No connection found with host."]; } else { NSLog(@"Checking data connection..."); - + // Formatting success response result = [CDVPluginResult resultWithStatus : CDVCommandStatus_OK @@ -134,7 +134,7 @@ - (void) isConnected : (CDVInvokedUrlCommand *) command { resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Unexpected exception when executon 'isConnected' action."]; } - + // Returning callback resolution [self.commandDelegate sendPluginResult : result @@ -147,24 +147,26 @@ - (void) isConnected : (CDVInvokedUrlCommand *) command { - (BOOL) disposeConnection : (NSString *) key { Connection* socket = nil; BOOL result = NO; - + @try { // Getting connection from pool socket = [pool objectForKey : key]; - + // Closing connection if (socket) { [pool removeObjectForKey : key]; - - if ([socket isConnected]) - [socket close]; - + + // Call close on the socket whether it's connected or not, to clean + // up the read/write streams and event handlers so we don't leak + // memory + [socket close]; + socket = nil; - + NSLog(@"Closed connection with %@", key); } else NSLog(@"Connection %@ already closed!", key); - + // Setting success result = YES; } @@ -184,15 +186,15 @@ - (void) disconnect : (CDVInvokedUrlCommand*) command { CDVPluginResult* result = [CDVPluginResult resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Missing arguments when calling 'disconnect' action."]; - + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; } else { // Running in background to avoid thread locks [self.commandDelegate runInBackground : ^{ - + CDVPluginResult* result= nil; NSString *key = nil; - + @try { // Preparing parameters key = [command.arguments objectAtIndex : 0]; @@ -211,7 +213,7 @@ - (void) disconnect : (CDVInvokedUrlCommand*) command { resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Unexpected exception when executing 'disconnect' action."]; } - + // Returns the Callback Resolution [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; }]; @@ -221,24 +223,24 @@ - (void) disconnect : (CDVInvokedUrlCommand*) command { - (void) disconnectAll: (CDVInvokedUrlCommand *) command { // Running in background to avoid thread locks [self.commandDelegate runInBackground:^{ - + CDVPluginResult* result = nil; Connection * socket = nil; BOOL partial = NO; - + @try { - + // Iterating connection pool for (id key in pool) { socket = [pool objectForKey : key]; - + // Try to close it if (![self disposeConnection : key]) { // If no success, need to set as partial disconnection partial = YES; } } - + // Formatting result if (partial) result = [CDVPluginResult @@ -263,43 +265,43 @@ - (void) disconnectAll: (CDVInvokedUrlCommand *) command { } - (void) send: (CDVInvokedUrlCommand *) command { - + // Validating parameters if ([command.arguments count] < 2) { // Triggering parameter error CDVPluginResult* result = [CDVPluginResult resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Missing arguments when calling 'send' action."]; - + [self.commandDelegate sendPluginResult : result callbackId:command.callbackId ]; - + } else { - + // Running in background to avoid thread locks [self.commandDelegate runInBackground : ^{ - + CDVPluginResult* result= nil; Connection* socket = nil; NSString* data = nil; NSString* key = nil; - + @try { // Preparing parameters key = [command.arguments objectAtIndex : 0]; - + // Getting connection from pool socket = [pool objectForKey : key]; - + // Checking if socket was not found and his conenctivity if (socket == nil) { NSLog(@"Connection not found"); result = [CDVPluginResult resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"No connection found with host."]; - + } else if (![socket isConnected]) { NSLog(@"Socket is not connected."); result = [CDVPluginResult @@ -308,11 +310,11 @@ - (void) send: (CDVInvokedUrlCommand *) command { } else { // Writting on output stream data = [command.arguments objectAtIndex : 1]; - + NSLog(@"Sending data to %@ - %@", key, data); - + [socket write:data]; - + // Formatting success response result = [CDVPluginResult resultWithStatus : CDVCommandStatus_OK @@ -325,27 +327,97 @@ - (void) send: (CDVInvokedUrlCommand *) command { resultWithStatus : CDVCommandStatus_ERROR messageAsString : @"Unexpected exception when executon 'send' action."]; } - + // Returning callback resolution [self.commandDelegate sendPluginResult : result callbackId : command.callbackId]; }]; } } -- (void) sendMessage :(NSString *)host :(int)port :(NSString *)chunk { - - // Handling escape chars - NSMutableString *data = [NSMutableString stringWithString : chunk]; - [data replaceOccurrencesOfString : @"\n" - withString : @"\\n" - options : NSCaseInsensitiveSearch - range : NSMakeRange(0, [data length])]; - +- (void) sendBinary: (CDVInvokedUrlCommand *) command { + + // Validating parameters + if ([command.arguments count] < 2) { + // Triggering parameter error + CDVPluginResult* result = [CDVPluginResult + resultWithStatus : CDVCommandStatus_ERROR + messageAsString : @"Missing arguments when calling 'sendBinary' action."]; + + [self.commandDelegate + sendPluginResult : result + callbackId:command.callbackId + ]; + + } else { + + // Running in background to avoid thread locks + [self.commandDelegate runInBackground : ^{ + + CDVPluginResult* result= nil; + Connection* socket = nil; + NSArray* data = nil; + NSString* key = nil; + + @try { + // Preparing parameters + key = [command.arguments objectAtIndex : 0]; + + // Getting connection from pool + socket = [pool objectForKey : key]; + + // Checking if socket was not found and his connectivity + if (socket == nil) { + NSLog(@"Connection not found"); + result = [CDVPluginResult + resultWithStatus : CDVCommandStatus_ERROR + messageAsString : @"No connection found with host."]; + + } else if (![socket isConnected]) { + NSLog(@"Socket is not connected."); + result = [CDVPluginResult + resultWithStatus : CDVCommandStatus_ERROR + messageAsString : @"Invalid connection with host."]; + } else { + // Writing on output stream + data = [command.arguments objectAtIndex : 1]; + + NSMutableData *buf = [[NSMutableData alloc] init]; + + for (int i = 0; i < [data count]; i++) + { + int byte = [data[i] intValue]; + [buf appendBytes : &byte length:1]; + } + + [socket writeBinary:buf]; + + // Formatting success response + result = [CDVPluginResult + resultWithStatus : CDVCommandStatus_OK + messageAsString : key]; + } + } + @catch (NSException *exception) { + NSLog(@"Exception: %@", exception); + result = [CDVPluginResult + resultWithStatus : CDVCommandStatus_ERROR + messageAsString : @"Unexpected exception when executon 'sendBinary' action."]; + } + + // Returning callback resolution + [self.commandDelegate sendPluginResult : result callbackId : command.callbackId]; + }]; + } +} + +- (void) sendMessage :(NSString *)host :(int)port :(NSData *)chunk { + NSString *base64Encoded = [chunk base64EncodedStringWithOptions:0]; + // Relay to webview - NSString *receiveHook = [NSString stringWithFormat : @"window.tlantic.plugins.socket.receive('%@', %d, '%@', '%@' );", - host, port, [self buildKey : host : port], [NSString stringWithString : data]]; - - [self writeJavascript:receiveHook]; + NSString *receiveHook = [NSString stringWithFormat : @"window.tlantic.plugins.socket.receive('%@', %d, '%@', window.atob('%@') );", + host, port, [self buildKey : host : port], base64Encoded]; + + [self.commandDelegate evalJs : receiveHook]; } -@end \ No newline at end of file +@end diff --git a/src/ios/Connection.h b/src/ios/Connection.h index 651a316..9b5ed6f 100644 --- a/src/ios/Connection.h +++ b/src/ios/Connection.h @@ -1,6 +1,6 @@ @protocol ConnectionDelegate -- (void) sendMessage : (NSString *) host : (int)port : (NSString *) chunk; +- (void) sendMessage : (NSString *) host : (int)port : (NSData *) line; @end @@ -21,7 +21,8 @@ - (void) open; - (void) close; - (void) write : (NSString*) data; +- (void) writeBinary : (NSData*) chunk; - (void) stream : (NSStream *) theStream handleEvent : (NSStreamEvent) streamEvent; -@end \ No newline at end of file +@end diff --git a/src/ios/Connection.m b/src/ios/Connection.m index 958847c..ff655fb 100644 --- a/src/ios/Connection.m +++ b/src/ios/Connection.m @@ -24,16 +24,16 @@ - (void) open { // Init network communication settings CFReadStreamRef readStream; CFWriteStreamRef writeStream; - + // Opening connection CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)_host, _port, &readStream, &writeStream); - + // Configuring input stream reader = objc_retainedObject(readStream); [reader setDelegate:self]; [reader scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [reader open]; - + // Configuring output stream writer = objc_retainedObject(writeStream); [writer setDelegate:self]; @@ -47,7 +47,7 @@ - (void) close { [writer removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [writer setDelegate:nil]; writer = nil; - + // Closing input stream [reader close]; [reader removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; @@ -60,50 +60,60 @@ - (void) write : (NSString *) data { [writer write : [chunk bytes] maxLength : [chunk length]]; } +- (void) writeBinary : (NSData *) chunk { + [writer write : [chunk bytes] maxLength : [chunk length]]; +} + - (void) stream : (NSStream *) theStream handleEvent : (NSStreamEvent) streamEvent { - switch (streamEvent) { - case NSStreamEventOpenCompleted: - NSLog(@"Stream opened!"); + switch (streamEvent) { + case NSStreamEventOpenCompleted: + NSLog(@"Stream opened!"); connected = YES; - break; - + break; + // Data receiving - case NSStreamEventHasBytesAvailable: + case NSStreamEventHasBytesAvailable: if (theStream == reader) { - uint8_t buffer[10240]; - NSInteger len; - + void* buffer = malloc(512); + NSInteger len = 0; + NSMutableData *packet = [[NSMutableData alloc] init]; + NSData *line; + NSInteger totalLength = 0; + while ([reader hasBytesAvailable]) { + // NSInputStream is notorious for not fully reading a whole TCP packet, + // requiring subsequent combination of values len = [reader read : buffer maxLength : sizeof(buffer)]; - - if (len > 0) { - - NSString *chunk = [[NSString alloc] initWithBytes : buffer - length : len - encoding : NSASCIIStringEncoding]; - - if (nil != chunk) { - NSLog(@"Received data: %@", chunk); - [_hook sendMessage : _host : _port : chunk]; - } + + // copy the bytes to the mutable buffer and update the total length + [packet appendBytes : buffer length:len]; + totalLength = totalLength + len; + + line = [packet subdataWithRange:NSMakeRange(0, totalLength)]; + } + + // now that no more bytes are available, send the packet + if (len >= 0) { + if (nil != line) { + [_hook sendMessage : _host : _port : line]; } } } break; - + case NSStreamEventErrorOccurred: NSLog(@"Cannot connect to the host!"); connected = NO; break; - + case NSStreamEventEndEncountered: NSLog(@"Stream closed!"); connected = NO; break; - + default: NSLog(@"Unknown event!"); } } -@end \ No newline at end of file +@end diff --git a/www/socket.js b/www/socket.js index f2340de..ceeadc5 100644 --- a/www/socket.js +++ b/www/socket.js @@ -26,7 +26,7 @@ Socket.prototype.disconnectAll = function (successCallback, errorCallback) { 'use strict'; exec(successCallback, errorCallback, this.pluginRef, 'disconnectAll', []); }; - + // Socket.prototype.isConnected = function (connectionId, successCallback, errorCallback) { 'use strict'; @@ -50,7 +50,7 @@ Socket.prototype.receive = function (host, port, connectionId, chunk) { 'use strict'; var evReceive = document.createEvent('Events'); - + evReceive.initEvent(this.receiveHookName, true, true); evReceive.metadata = { connection: {