Implement WebSocket module for Android. Fixes #2837

Summary: The JavaScript code for Android is same as the iOS counterpart, I just added few new lines and used arrow functions instead of binding `this`.
Closes https://github.com/facebook/react-native/pull/2839

Reviewed By: @​svcscm, @vjeux

Differential Revision: D2498703

Pulled By: @mkonicek

fb-gh-sync-id: 3fe958dd5af0efba00df07515f8e33b5d87eb05b
This commit is contained in:
Satyajit Sahoo 2015-10-07 08:28:34 -07:00 committed by facebook-github-bot-8
parent 57a7387d85
commit f4857a6d42
10 changed files with 335 additions and 177 deletions

View File

@ -9,7 +9,7 @@
/* Begin PBXBuildFile section */
1338BBE01B04ACC80064A9C9 /* RCTSRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1338BBDD1B04ACC80064A9C9 /* RCTSRWebSocket.m */; };
1338BBE11B04ACC80064A9C9 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */; };
3C86DF7C1ADF695F0047B81A /* RCTWebSocketManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */; };
3C86DF7C1ADF695F0047B81A /* RCTWebSocketModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -18,8 +18,8 @@
1338BBDE1B04ACC80064A9C9 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = "<group>"; };
1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = "<group>"; };
3C86DF461ADF2C930047B81A /* libRCTWebSocket.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocket.a; sourceTree = BUILT_PRODUCTS_DIR; };
3C86DF7A1ADF695F0047B81A /* RCTWebSocketManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketManager.h; sourceTree = "<group>"; };
3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketManager.m; sourceTree = "<group>"; tabWidth = 2; };
3C86DF7A1ADF695F0047B81A /* RCTWebSocketModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketModule.h; sourceTree = "<group>"; };
3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketModule.m; sourceTree = "<group>"; tabWidth = 2; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -40,8 +40,8 @@
1338BBDD1B04ACC80064A9C9 /* RCTSRWebSocket.m */,
1338BBDE1B04ACC80064A9C9 /* RCTWebSocketExecutor.h */,
1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */,
3C86DF7A1ADF695F0047B81A /* RCTWebSocketManager.h */,
3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */,
3C86DF7A1ADF695F0047B81A /* RCTWebSocketModule.h */,
3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */,
3C86DF471ADF2C930047B81A /* Products */,
);
indentWidth = 2;
@ -112,7 +112,7 @@
buildActionMask = 2147483647;
files = (
1338BBE01B04ACC80064A9C9 /* RCTSRWebSocket.m in Sources */,
3C86DF7C1ADF695F0047B81A /* RCTWebSocketManager.m in Sources */,
3C86DF7C1ADF695F0047B81A /* RCTWebSocketModule.m in Sources */,
1338BBE11B04ACC80064A9C9 /* RCTWebSocketExecutor.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -9,6 +9,6 @@
#import "RCTBridgeModule.h"
@interface RCTWebSocketManager : NSObject <RCTBridgeModule>
@interface RCTWebSocketModule : NSObject <RCTBridgeModule>
@end

View File

@ -7,7 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTWebSocketManager.h"
#import "RCTWebSocketModule.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@ -29,11 +29,11 @@
@end
@interface RCTWebSocketManager () <RCTSRWebSocketDelegate>
@interface RCTWebSocketModule () <RCTSRWebSocketDelegate>
@end
@implementation RCTWebSocketManager
@implementation RCTWebSocketModule
{
RCTSparseArray *_sockets;
}

View File

@ -1,39 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebSocket
*
*/
'use strict';
var WebSocketBase = require('WebSocketBase');
class WebSocket extends WebSocketBase {
connectToSocketImpl(url: string): void {
console.warn('WebSocket is not yet supported on Android');
}
closeConnectionImpl(): void{
console.warn('WebSocket is not yet supported on Android');
}
cancelConnectionImpl(): void {
console.warn('WebSocket is not yet supported on Android');
}
sendStringImpl(message: string): void {
console.warn('WebSocket is not yet supported on Android');
}
sendArrayBufferImpl(): void {
console.warn('WebSocket is not yet supported on Android');
}
}
module.exports = WebSocket;

View File

@ -1,126 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebSocket
*
*/
'use strict';
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
var RCTWebSocketManager = require('NativeModules').WebSocketManager;
var WebSocketBase = require('WebSocketBase');
class Event {
constructor(type) {
this.type = type.toString();
}
}
class MessageEvent extends Event {
constructor(type, eventInitDict) {
super(type);
Object.assign(this, eventInitDict);
}
}
var WebSocketId = 0;
class WebSocket extends WebSocketBase {
_socketId: number;
_subs: any;
connectToSocketImpl(url: string): void {
this._socketId = WebSocketId++;
RCTWebSocketManager.connect(url, this._socketId);
this._registerEvents(this._socketId);
}
closeConnectionImpl(): void {
RCTWebSocketManager.close(this._socketId);
}
cancelConnectionImpl(): void {
RCTWebSocketManager.close(this._socketId);
}
sendStringImpl(message: string): void {
RCTWebSocketManager.send(message, this._socketId);
}
sendArrayBufferImpl(): void {
// TODO
console.warn('Sending ArrayBuffers is not yet supported');
}
_unregisterEvents(): void {
this._subs.forEach(e => e.remove());
this._subs = [];
}
_registerEvents(id: number): void {
this._subs = [
RCTDeviceEventEmitter.addListener(
'websocketMessage',
function(ev) {
if (ev.id !== id) {
return;
}
var event = new MessageEvent('message', {
data: ev.data
});
this.onmessage && this.onmessage(event);
this.dispatchEvent(event);
}.bind(this)
),
RCTDeviceEventEmitter.addListener(
'websocketOpen',
function(ev) {
if (ev.id !== id) {
return;
}
this.readyState = this.OPEN;
var event = new Event('open');
this.onopen && this.onopen(event);
this.dispatchEvent(event);
}.bind(this)
),
RCTDeviceEventEmitter.addListener(
'websocketClosed',
function(ev) {
if (ev.id !== id) {
return;
}
this.readyState = this.CLOSED;
var event = new Event('close');
this.onclose && this.onclose(event);
this.dispatchEvent(event);
this._unregisterEvents();
RCTWebSocketManager.close(id);
}.bind(this)
),
RCTDeviceEventEmitter.addListener(
'websocketFailed',
function(ev) {
if (ev.id !== id) {
return;
}
var event = new Event('error');
event.message = ev.message;
this.onerror && this.onerror(event);
this.dispatchEvent(event);
this._unregisterEvents();
RCTWebSocketManager.close(id);
}.bind(this)
)
];
}
}
module.exports = WebSocket;

View File

@ -0,0 +1,124 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebSocket
*/
'use strict';
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
var RCTWebSocketModule = require('NativeModules').WebSocketModule;
var Platform = require('Platform');
var WebSocketBase = require('WebSocketBase');
var WebSocketEvent = require('WebSocketEvent');
var WebSocketId = 0;
var CLOSE_NORMAL = 1000;
/**
* Browser-compatible WebSockets implementation.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
*/
class WebSocket extends WebSocketBase {
_socketId: number;
_subs: any;
connectToSocketImpl(url: string): void {
this._socketId = WebSocketId++;
RCTWebSocketModule.connect(url, this._socketId);
this._registerEvents(this._socketId);
}
closeConnectionImpl(code?: number, reason?: string): void {
this._closeWebSocket(this._socketId, code, reason);
}
cancelConnectionImpl(): void {
this._closeWebSocket(this._socketId);
}
sendStringImpl(message: string): void {
RCTWebSocketModule.send(message, this._socketId);
}
sendArrayBufferImpl(): void {
// TODO
console.warn('Sending ArrayBuffers is not yet supported');
}
_closeWebSocket(id: number, code?: number, reason?: string): void {
if (Platform.OS === 'android') {
/*
* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
*/
var statusCode = typeof code === 'number' ? code : CLOSE_NORMAL;
var closeReason = typeof reason === 'string' ? reason : '';
RCTWebSocketModule.close(statusCode, closeReason, id);
} else {
RCTWebSocketModule.close(id);
}
}
_unregisterEvents(): void {
this._subs.forEach(e => e.remove());
this._subs = [];
}
_registerEvents(id: number): void {
this._subs = [
RCTDeviceEventEmitter.addListener('websocketMessage', ev => {
if (ev.id !== id) {
return;
}
var event = new WebSocketEvent('message', {
data: ev.data
});
this.onmessage && this.onmessage(event);
this.dispatchEvent(event);
}),
RCTDeviceEventEmitter.addListener('websocketOpen', ev => {
if (ev.id !== id) {
return;
}
this.readyState = this.OPEN;
var event = new WebSocketEvent('open');
this.onopen && this.onopen(event);
this.dispatchEvent(event);
}),
RCTDeviceEventEmitter.addListener('websocketClosed', ev => {
if (ev.id !== id) {
return;
}
this.readyState = this.CLOSED;
var event = new WebSocketEvent('close');
event.code = ev.code;
event.reason = ev.reason;
this.onclose && this.onclose(event);
this.dispatchEvent(event);
this._unregisterEvents();
this._closeWebSocket(id);
}),
RCTDeviceEventEmitter.addListener('websocketFailed', ev => {
if (ev.id !== id) {
return;
}
var event = new WebSocketEvent('error');
event.message = ev.message;
this.onerror && this.onerror(event);
this.dispatchEvent(event);
this._unregisterEvents();
this._closeWebSocket(id);
})
];
}
}
module.exports = WebSocket;

View File

@ -7,7 +7,6 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebSocketBase
*
*/
'use strict';
@ -94,7 +93,6 @@ class WebSocketBase extends EventTarget {
sendArrayBufferImpl(): void {
throw new Error('Subclass must define sendArrayBufferImpl method');
}
}
module.exports = WebSocketBase;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebSocketEvent
*/
'use strict';
/**
* Event object passed to the `onopen`, `onclose`, `onmessage`, `onerror`
* callbacks of `WebSocket`.
*
* The `type` property is "open", "close", "message", "error" respectively.
*
* In case of "message", the `data` property contains the incoming data.
*/
class WebSocketEvent {
constructor(type, eventInitDict) {
this.type = type.toString();
Object.assign(this, eventInitDict);
}
}
module.exports = WebSocketEvent;

View File

@ -0,0 +1,170 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.websocket;
import java.io.IOException;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketCall;
import com.squareup.okhttp.ws.WebSocketListener;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okio.Buffer;
import okio.BufferedSource;
public class WebSocketModule extends ReactContextBaseJavaModule {
private Map<Integer, WebSocket> mWebSocketConnections = new HashMap<>();
private ReactContext mReactContext;
public WebSocketModule(ReactApplicationContext context) {
super(context);
mReactContext = context;
}
private void sendEvent(String eventName, WritableMap params) {
mReactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
@Override
public String getName() {
return "WebSocketModule";
}
@ReactMethod
public void connect(final String url, final int id) {
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(10, TimeUnit.SECONDS);
client.setWriteTimeout(10, TimeUnit.SECONDS);
// Disable timeouts for read
client.setReadTimeout(0, TimeUnit.MINUTES);
Request request = new Request.Builder()
.tag(id)
.url(url)
.build();
WebSocketCall.create(client, request).enqueue(new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocketConnections.put(id, webSocket);
WritableMap params = Arguments.createMap();
params.putInt("id", id);
sendEvent("websocketOpen", params);
}
@Override
public void onClose(int code, String reason) {
WritableMap params = Arguments.createMap();
params.putInt("id", id);
params.putInt("code", code);
params.putString("reason", reason);
sendEvent("websocketClosed", params);
}
@Override
public void onFailure(IOException e, Response response) {
notifyWebSocketFailed(id, e.getMessage());
}
@Override
public void onPong(Buffer buffer) {
}
@Override
public void onMessage(BufferedSource bufferedSource, WebSocket.PayloadType payloadType) {
String message;
try {
message = bufferedSource.readUtf8();
} catch (IOException e) {
notifyWebSocketFailed(id, e.getMessage());
return;
}
try {
bufferedSource.close();
} catch (IOException e) {
FLog.e(
ReactConstants.TAG,
"Could not close BufferedSource for WebSocket id " + id,
e);
}
WritableMap params = Arguments.createMap();
params.putInt("id", id);
params.putString("data", message);
sendEvent("websocketMessage", params);
}
});
// Trigger shutdown of the dispatcher's executor so this process can exit cleanly
client.getDispatcher().getExecutorService().shutdown();
}
@ReactMethod
public void close(int code, String reason, int id) {
WebSocket client = mWebSocketConnections.get(id);
if (client == null) {
// This is a programmer error
throw new RuntimeException("Cannot close WebSocket. Unknown WebSocket id " + id);
}
try {
client.close(code, reason);
mWebSocketConnections.remove(id);
} catch (Exception e) {
FLog.e(
ReactConstants.TAG,
"Could not close WebSocket connection for id " + id,
e);
}
}
@ReactMethod
public void send(String message, int id) {
WebSocket client = mWebSocketConnections.get(id);
if (client == null) {
// This is a programmer error
throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);
}
try {
client.sendMessage(
WebSocket.PayloadType.TEXT,
new Buffer().writeUtf8(message));
} catch (IOException e) {
notifyWebSocketFailed(id, e.getMessage());
}
}
private void notifyWebSocketFailed(int id, String message) {
WritableMap params = Arguments.createMap();
params.putInt("id", id);
params.putString("message", message);
sendEvent("websocketFailed", params);
}
}

View File

@ -21,6 +21,7 @@ import com.facebook.react.modules.fresco.FrescoModule;
import com.facebook.react.modules.network.NetworkingModule;
import com.facebook.react.modules.storage.AsyncStorageModule;
import com.facebook.react.modules.toast.ToastModule;
import com.facebook.react.modules.websocket.WebSocketModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.views.drawer.ReactDrawerLayoutManager;
import com.facebook.react.views.image.ReactImageManager;
@ -47,6 +48,7 @@ public class MainReactPackage implements ReactPackage {
new AsyncStorageModule(reactContext),
new FrescoModule(reactContext),
new NetworkingModule(reactContext),
new WebSocketModule(reactContext),
new ToastModule(reactContext));
}