diff --git a/README.md b/README.md index 39d1a30..de7a354 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ The plugin creates a "Socket" object exposed on window.tlantic.plugins.socket. T * connect: opens a socket connection; * disconnect: closes a socket connection; * disconnectAll: closes ALL opened connections; -* send: send data using a given connection; +* send: send text data using a given connection; +* sendBinary: send binary data using a given connection; * isConnected: returns a boolean falg representing socket connectivity status; * receive: callback used by plugin's native code. Can be override by a custom implementation. @@ -90,7 +91,7 @@ window.tlantic.plugins.socket.connect( ### send (successCallback, errorCallback, connectionId, data) -Sends information and calls success callback if information was send and does not wait for any response. To check how to receive data, please see the item below. +Sends text information and calls success callback if information was sent and does not wait for any response. To check how to receive data, please see the item below. Example: @@ -108,7 +109,29 @@ window.tlantic.plugins.socket.send( ); ``` -### isConnected (connectionId, successCallback, errorCallback) +### sendBinary (successCallback, errorCallback, connectionId, data) + +Sends binary data and calls success callback if information was sent and does not wait for any response. To check how to receive data, please see the item below. + +Binary data to be sent is passed in `data` which is a JSONArray, one integer element per byte. + +Example: + +``` +window.tlantic.plugins.socket.sendBinary( + function () { + console.log('worked!'); + }, + + function () { + console.log('failed!'); + }, + '192.168.2.5:18002', + [ 0x00, 0x01, 0x00, 0xFD ] +); +``` + +#### isConnected (connectionId, successCallback, errorCallback) Returns a boolean value representing the connection status. True, if socket streams are opened and false case else. Both values are supposed to be returned through successCallback. The error callback is called only when facing errors due the check process. diff --git a/src/android/Connection.java b/src/android/Connection.java index f834dd7..d8015da 100644 --- a/src/android/Connection.java +++ b/src/android/Connection.java @@ -3,6 +3,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; @@ -11,135 +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 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); - } - - - - /* (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.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 91f5e37..e50e52e 100644 --- a/src/android/SocketPlugin.java +++ b/src/android/SocketPlugin.java @@ -2,6 +2,10 @@ import android.annotation.SuppressLint; +import android.util.Base64; + +import java.io.IOException; + import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -15,7 +19,7 @@ /** * @author viniciusl * - * Plugin to handle TCP socket connections. + * Plugin to handle TCP socket connections. */ /** * @author viniciusl @@ -23,264 +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("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()); - } - } - } - - /** - * Closes an existing connection - * - * @param args - * @param callbackContext - */ - private void disconnect (JSONArray args, CallbackContext callbackContext) { - String key; - Connection socket; - - // validating parameters - if (args.length() < 1) { - callbackContext.error("Missing arguments when calling 'disconnect' action."); - } else { - - try { - // preparing parameters - key = args.getString(0); - - // getting connection from pool - socket = pool.get(key); - - // closing socket - if (socket != null) { - - // checking connection - if (socket.isConnected()) { - socket.close(); - } - - // removing from pool - pool.remove(key); - } - - // ending with success - callbackContext.success("Disconnected from " + key); - - } catch (JSONException e) { - callbackContext.error("Invalid parameters for 'connect' action:" + e.getMessage()); - } - } - } - - /** - * Closes all existing connections - * - * @param callbackContext - */ - private void disconnectAll (CallbackContext callbackContext) { - // building iterator - Iterator> 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); - } - - }); - } - -} \ No newline at end of file + 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 8add8da..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'; @@ -39,12 +39,18 @@ Socket.prototype.send = function (successCallback, errorCallback, connectionId, exec(successCallback, errorCallback, this.pluginRef, 'send', [connectionId, typeof data == 'string' ? data : JSON.stringify(data)]); }; +/// +Socket.prototype.sendBinary = function (successCallback, errorCallback, connectionId, data) { + 'use strict'; + exec(successCallback, errorCallback, this.pluginRef, 'sendBinary', [connectionId, data]); +}; + // Socket.prototype.receive = function (host, port, connectionId, chunk) { 'use strict'; var evReceive = document.createEvent('Events'); - + evReceive.initEvent(this.receiveHookName, true, true); evReceive.metadata = { connection: {