diff --git a/TcpServer.js b/TcpServer.js index 1c961f5..8b6cab4 100644 --- a/TcpServer.js +++ b/TcpServer.js @@ -22,12 +22,14 @@ function TcpServer(connectionListener: (socket: Socket) => void) { return new TcpServer(connectionListener); } - // $FlowFixMe: suppressing this error flow doesn't like EventEmitter - EventEmitter.call(this); + if (EventEmitter instanceof Function) { + EventEmitter.call(this); + } var self = this; this._socket = new Socket(); + // $FlowFixMe: suppressing this error flow doesn't like EventEmitter this._socket.on('connect', function() { self.emit('listening'); @@ -35,7 +37,6 @@ function TcpServer(connectionListener: (socket: Socket) => void) { // $FlowFixMe: suppressing this error flow doesn't like EventEmitter this._socket.on('connection', function(socket) { self._connections++; - self.emit('connection', socket); }); // $FlowFixMe: suppressing this error flow doesn't like EventEmitter @@ -45,7 +46,6 @@ function TcpServer(connectionListener: (socket: Socket) => void) { // $FlowFixMe: suppressing this error flow doesn't like EventEmitter this._socket.on('error', function(error) { self.emit('error', error); - self._socket.destroy(); }); if (typeof connectionListener === 'function') { @@ -64,16 +64,21 @@ TcpServer.prototype._debug = function() { } }; -TcpServer.prototype.listen = function(options: { port: number, hostname: ?string }, callback: ?() => void) : TcpServer { +// TODO : determine how to properly overload this with flow +TcpServer.prototype.listen = function() : TcpServer { + var args = this._socket._normalizeConnectArgs(arguments); + var options = args[0]; + var callback = args[1]; + var port = options.port; - var hostname = options.hostname || 'localhost'; + var host = options.host || 'localhost'; if (callback) { this.on('listening', callback); } - Sockets.createSocket(this._socket._id); - Sockets.listen(this._socket._id, hostname, port); + this._socket._registerEvents(); + Sockets.listen(this._socket._id, host, port); return this; }; diff --git a/TcpSocket.js b/TcpSocket.js index 843cb46..bf2ad2b 100644 --- a/TcpSocket.js +++ b/TcpSocket.js @@ -19,7 +19,7 @@ var Sockets = NativeModules.TcpSockets; var base64 = require('base64-js'); var Base64Str = require('./base64-str'); var noop = function () {}; -var usedIds = []; +var instances = 0; var STATE = { DISCONNECTED: 0, CONNECTING: 1, @@ -27,37 +27,34 @@ var STATE = { }; function TcpSocket(options: ?{ id: ?number }) { - // $FlowFixMe: suppressing this error flow doesn't like EventEmitter - EventEmitter.call(this); + if (!(this instanceof TcpSocket)) { + return new TcpSocket(options); + } + + if (EventEmitter instanceof Function) { + EventEmitter.call(this); + } if (options && options.id) { - // native generated sockets range from 5000-6000 // e.g. incoming server connections this._id = Number(options.id); - if (usedIds.indexOf(this._id) !== -1) { + if (this._id <= instances) { throw new Error('Socket id ' + this._id + 'already in use'); } } else { // javascript generated sockets range from 1-1000 - this._id = Math.floor((Math.random() * 1000) + 1); - while (usedIds.indexOf(this._id) !== -1) { - this._id = Math.floor((Math.random() * 1000) + 1); - } + this._id = instances++; } - usedIds.push(this._id); - - // these will be set once there is a connection - this.readable = this.writable = false; - - this._registerEvents(); - // ensure compatibility with node's EventEmitter if (!this.on) { this.on = this.addListener.bind(this); } + // these will be set once there is a connection + this.writable = this.readable = false; + this._state = STATE.DISCONNECTED; } @@ -71,24 +68,21 @@ TcpSocket.prototype._debug = function() { } }; -TcpSocket.prototype.connect = function(options: ?{ port: ?number | ?string, host: ?string, localAddress: ?string, localPort: ?number }, callback: ?() => void) { - if (this._state !== STATE.DISCONNECTED) { - throw new Error('Socket is already bound'); +// TODO : determine how to properly overload this with flow +TcpSocket.prototype.connect = function(options, callback) : TcpSocket { + this._registerEvents(); + + if (options === null || typeof options !== 'object') { + // Old API: + // connect(port, [host], [cb]) + var args = this._normalizeConnectArgs(arguments); + return TcpSocket.prototype.connect.apply(this, args); } if (typeof callback === 'function') { this.once('connect', callback); } - if (!options) { - options = { - host: 'localhost', - port: 0, - localAddress: null, - localPort: null - }; - } - var host = options.host || 'localhost'; var port = options.port || 0; var localAddress = options.localAddress; @@ -107,7 +101,7 @@ TcpSocket.prototype.connect = function(options: ?{ port: ?number | ?string, host throw new TypeError('"port" option should be a number or string: ' + port); } - port = Number(port); + port = +port; if (!isLegalPort(port)) { throw new RangeError('"port" option should be >= 0 and < 65536: ' + port); @@ -117,8 +111,10 @@ TcpSocket.prototype.connect = function(options: ?{ port: ?number | ?string, host this._state = STATE.CONNECTING; this._debug('connecting, host:', host, 'port:', port); - Sockets.createSocket(this._id); + this._destroyed = false; Sockets.connect(this._id, host, Number(port), options); + + return this; }; // Check that the port number is not NaN when coerced to a number, @@ -181,6 +177,10 @@ TcpSocket.prototype.destroy = function() { }; TcpSocket.prototype._registerEvents = function(): void { + if (this._subs && this._subs.length > 0) { + return; + } + this._subs = [ DeviceEventEmitter.addListener( 'tcp-' + this._id + '-connect', this._onConnect.bind(this) @@ -207,27 +207,20 @@ TcpSocket.prototype._unregisterEvents = function(): void { this._subs = []; }; -TcpSocket.prototype._onConnect = function(address: { port: string, address: string, family: string }): void { +TcpSocket.prototype._onConnect = function(address: { port: number, address: string, family: string }): void { this._debug('received', 'connect'); - this.writable = this.readable = true; - this._state = STATE.CONNECTED; - this._address = address; - this._address.port = Number(this._address.port); - + setConnected(this, address); this.emit('connect'); }; -TcpSocket.prototype._onConnection = function(info: { id: number, address: { port: string, address: string, family: string } }): void { +TcpSocket.prototype._onConnection = function(info: { id: number, address: { port: number, address: string, family: string } }): void { this._debug('received', 'connection'); var socket = new TcpSocket({ id: info.id }); - socket.writable = this.readable = true; - socket._state = STATE.CONNECTED; - socket._address = info.address; - socket._address.port = Number(socket._address.port); - + socket._registerEvents(); + setConnected(socket, info.address); this.emit('connection', socket); }; @@ -250,17 +243,14 @@ TcpSocket.prototype._onData = function(data: string): void { TcpSocket.prototype._onClose = function(hadError: boolean): void { this._debug('received', 'close'); - this._unregisterEvents(); - - this._state = STATE.DISCONNECTED; - - this.emit('close', hadError); + setDisconnected(this, hadError); }; TcpSocket.prototype._onError = function(error: string): void { this._debug('received', 'error'); this.emit('error', normalizeError(error)); + this.destroy(); }; TcpSocket.prototype.write = function(buffer: any, callback: ?(err: ?Error) => void) : boolean { @@ -303,6 +293,22 @@ TcpSocket.prototype.write = function(buffer: any, callback: ?(err: ?Error) => vo return true; }; +function setConnected(socket: TcpSocket, address: { port: number, address: string, family: string } ) { + socket.writable = socket.readable = true; + socket._state = STATE.CONNECTED; + socket._address = address; +} + +function setDisconnected(socket: TcpSocket, hadError: boolean): void { + if (socket._state === STATE.DISCONNECTED) { + return; + } + + socket._unregisterEvents(); + socket._state = STATE.DISCONNECTED; + socket.emit('close', hadError); +} + function normalizeError(err) { if (err) { if (typeof err === 'string') { @@ -313,4 +319,24 @@ function normalizeError(err) { } } +// Returns an array [options] or [options, cb] +// It is the same as the argument of Socket.prototype.connect(). +TcpSocket.prototype._normalizeConnectArgs = function(args) { + var options = {}; + + if (args[0] !== null && typeof args[0] === 'object') { + // connect(options, [cb]) + options = args[0]; + } else { + // connect(port, [host], [cb]) + options.port = args[0]; + if (typeof args[1] === 'string') { + options.host = args[1]; + } + } + + var cb = args[args.length - 1]; + return typeof cb === 'function' ? [options, cb] : [options]; +}; + module.exports = TcpSocket; diff --git a/TcpSockets.js b/TcpSockets.js index 8acc7b3..8b97f42 100644 --- a/TcpSockets.js +++ b/TcpSockets.js @@ -17,10 +17,10 @@ exports.createServer = function(connectionListener: (socket: Socket) => void) : return new Server(connectionListener); }; -exports.connect = exports.createConnection = function(options: ?{ port: ?number | ?string, host: ?string, localAddress: ?string, localPort: ?number}, callback: ?() => void) : Socket { +// TODO : determine how to properly overload this with flow +exports.connect = exports.createConnection = function() : Socket { var tcpSocket = new Socket(); - tcpSocket.connect(options, callback); - return tcpSocket; + return Socket.prototype.connect.apply(tcpSocket, tcpSocket._normalizeConnectArgs(arguments)); }; exports.isIP = function(input: string) : number { diff --git a/examples/rctsockets/.flowconfig b/examples/rctsockets/.flowconfig index 05cad20..a91d449 100644 --- a/examples/rctsockets/.flowconfig +++ b/examples/rctsockets/.flowconfig @@ -7,12 +7,24 @@ # Some modules have their own node_modules with overlap .*/node_modules/node-haste/.* -# Ignore react-tools where there are overlaps, but don't ignore anything that -# react-native relies on -.*/node_modules/react-tools/src/React.js -.*/node_modules/react-tools/src/renderers/shared/event/EventPropagators.js -.*/node_modules/react-tools/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js -.*/node_modules/react-tools/src/shared/vendor/core/ExecutionEnvironment.js +# Ugh +.*/node_modules/babel.* +.*/node_modules/babylon.* +.*/node_modules/invariant.* + +# Ignore react and fbjs where there are overlaps, but don't ignore +# anything that react-native relies on +.*/node_modules/fbjs-haste/.*/__tests__/.* +.*/node_modules/fbjs-haste/__forks__/Map.js +.*/node_modules/fbjs-haste/__forks__/Promise.js +.*/node_modules/fbjs-haste/__forks__/fetch.js +.*/node_modules/fbjs-haste/core/ExecutionEnvironment.js +.*/node_modules/fbjs-haste/core/isEmpty.js +.*/node_modules/fbjs-haste/crypto/crc32.js +.*/node_modules/fbjs-haste/stubs/ErrorUtils.js +.*/node_modules/react-haste/React.js +.*/node_modules/react-haste/renderers/dom/ReactDOM.js +.*/node_modules/react-haste/renderers/shared/event/eventPlugins/ResponderEventPlugin.js # Ignore commoner tests .*/node_modules/commoner/test/.* @@ -43,9 +55,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-7]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-7]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-0]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-0]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -0.17.0 +0.20.1 diff --git a/examples/rctsockets/index.js b/examples/rctsockets/index.js index 65c838d..915e497 100644 --- a/examples/rctsockets/index.js +++ b/examples/rctsockets/index.js @@ -24,7 +24,7 @@ function randomPort() { var serverPort = randomPort(); var server = net.createServer(function(socket) { - console.log('server connected'); + console.log('server connected on ' + JSON.stringify(socket.address())); socket.on('data', function (data) { console.log('Server Received: ' + data); @@ -34,21 +34,33 @@ var server = net.createServer(function(socket) { socket.on('error', function(error) { console.log('error ' + error); }); -}).listen({ port: serverPort }, function() { +}).listen(serverPort, function() { console.log('opened server on ' + JSON.stringify(server.address())); }); -var client = net.createConnection({ port: serverPort }, function() { +server.on('error', function(error) { + console.log('error ' + error); +}); + +var client = net.createConnection(serverPort, function() { console.log('opened client on ' + JSON.stringify(client.address())); client.write('Hello, server! Love, Client.'); }); client.on('data', function(data) { - console.log('Client Received: ' + data); + console.log('Client Received: ' + data); client.destroy(); // kill client after server's response server.close(); }); +client.on('error', function(error) { + console.log('client error ' + error); +}); + +client.on('close', function() { + console.log('client close'); +}); + var rctsockets = React.createClass({ render: function() { return ( @@ -80,14 +92,4 @@ var styles = StyleSheet.create({ }, }); -// only works for 8-bit chars -function toByteArray(obj) { - var uint = new Uint8Array(obj.length); - for (var i = 0, l = obj.length; i < l; i++){ - uint[i] = obj.charCodeAt(i); - } - - return new Uint8Array(uint); -} - AppRegistry.registerComponent('rctsockets', () => rctsockets); diff --git a/ios/TcpSocketClient.h b/ios/TcpSocketClient.h index 6102fca..0ba3cba 100644 --- a/ios/TcpSocketClient.h +++ b/ios/TcpSocketClient.h @@ -31,7 +31,7 @@ typedef enum RCTTCPError RCTTCPError; - (void)onData:(NSNumber *)clientID data:(NSData *)data; - (void)onClose:(TcpSocketClient*)client withError:(NSError *)err; - (void)onError:(TcpSocketClient*)client withError:(NSError *)err; -- (NSNumber*)generateRandomId; +- (NSNumber*)getNextId; @end @@ -67,7 +67,7 @@ typedef enum RCTTCPError RCTTCPError; - (BOOL)listen:(NSString *)host port:(int)port error:(NSError **)error; -- (NSDictionary *)getAddress; +- (NSDictionary *)getAddress; /** * write data diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index bde4c0a..fa78b95 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -85,22 +85,22 @@ NSString *const RCTTCPErrorDomain = @"RCTTCPErrorDomain"; return result; } -- (NSDictionary *)getAddress +- (NSDictionary *)getAddress { if (_tcpSocket) { if (_tcpSocket.isConnected) { - return @{ @"port": @(_tcpSocket.connectedPort).stringValue, + return @{ @"port": @(_tcpSocket.connectedPort), @"address": _tcpSocket.connectedHost ?: @"unknown", @"family": _tcpSocket.isIPv6?@"IPv6":@"IPv4" }; } else { - return @{ @"port": @(_tcpSocket.localPort).stringValue, + return @{ @"port": @(_tcpSocket.localPort), @"address": _tcpSocket.localHost ?: @"unknown", @"family": _tcpSocket.isIPv6?@"IPv6":@"IPv4" }; } } - return @{ @"port": @"0", + return @{ @"port": @(0), @"address": @"unknown", @"family": @"unkown" }; } @@ -201,7 +201,7 @@ NSString *const RCTTCPErrorDomain = @"RCTTCPErrorDomain"; - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { - TcpSocketClient *inComing = [[TcpSocketClient alloc] initWithClientId:[_clientDelegate generateRandomId] + TcpSocketClient *inComing = [[TcpSocketClient alloc] initWithClientId:[_clientDelegate getNextId] andConfig:_clientDelegate andSocket:newSocket]; [_clientDelegate onConnection: inComing diff --git a/ios/TcpSockets.m b/ios/TcpSockets.m index de91fbf..3c34d67 100644 --- a/ios/TcpSockets.m +++ b/ios/TcpSockets.m @@ -11,9 +11,13 @@ #import "TcpSockets.h" #import "TcpSocketClient.h" +// offset native ids by 5000 +#define COUNTER_OFFSET 5000 + @implementation TcpSockets { NSMutableDictionary *_clients; + int _counter; } RCT_EXPORT_MODULE() @@ -27,11 +31,11 @@ RCT_EXPORT_MODULE() } } -RCT_EXPORT_METHOD(createSocket:(nonnull NSNumber*)cId) +- (TcpSocketClient *)createSocket:(nonnull NSNumber*)cId { if (!cId) { RCTLogError(@"%@.createSocket called with nil id parameter.", [self class]); - return; + return nil; } if (!_clients) { @@ -40,10 +44,12 @@ RCT_EXPORT_METHOD(createSocket:(nonnull NSNumber*)cId) if (_clients[cId]) { RCTLogError(@"%@.createSocket called twice with the same id.", [self class]); - return; + return nil; } _clients[cId] = [TcpSocketClient socketClientWithId:cId andConfig:self]; + + return _clients[cId]; } RCT_EXPORT_METHOD(connect:(nonnull NSNumber*)cId @@ -51,8 +57,10 @@ RCT_EXPORT_METHOD(connect:(nonnull NSNumber*)cId port:(int)port withOptions:(NSDictionary *)options) { - TcpSocketClient* client = [self findClient:cId callback:nil]; - if (!client) return; + TcpSocketClient *client = _clients[cId]; + if (!client) { + client = [self createSocket:cId]; + } NSError *error = nil; if (![client connect:host port:port withOptions:options error:&error]) @@ -88,8 +96,10 @@ RCT_EXPORT_METHOD(listen:(nonnull NSNumber*)cId host:(NSString *)host port:(int)port) { - TcpSocketClient* client = [self findClient:cId callback:nil]; - if (!client) return; + TcpSocketClient* client = _clients[cId]; + if (!client) { + client = [self createSocket:cId]; + } NSError *error = nil; if (![client listen:host port:port error:&error]) @@ -176,13 +186,8 @@ RCT_EXPORT_METHOD(listen:(nonnull NSNumber*)cId [_clients removeObjectForKey:cId]; } --(NSNumber*)generateRandomId { - int r = 0; - do { - r = (arc4random() % 1000) + 5001; - } while(_clients[@(r)]); - - return @(r); +-(NSNumber*)getNextId { + return @(_counter++ + COUNTER_OFFSET); } @end