From 398935bb67713c04dfb0ab36b30a350a578792c3 Mon Sep 17 00:00:00 2001 From: Andy Prock Date: Wed, 18 Nov 2015 15:11:03 -0800 Subject: [PATCH] implement addMembership/dropMembership for android this enables support for protocols such as ssdp, which require multicasting bump to version 1.1.1 --- UdpSocket.js | 16 ++- android/src/main/AndroidManifest.xml | 5 +- .../com/tradle/react/UdpReceiverTask.java | 74 ++++--------- .../java/com/tradle/react/UdpSenderTask.java | 16 +-- .../com/tradle/react/UdpSocketClient.java | 89 +++++++++++---- .../java/com/tradle/react/UdpSockets.java | 103 +++++++++++++++--- ios/ReactUdp.podspec | 2 +- ios/UdpSockets.m | 10 ++ package.json | 2 +- 9 files changed, 213 insertions(+), 104 deletions(-) diff --git a/UdpSocket.js b/UdpSocket.js index f735d33..f0815a9 100644 --- a/UdpSocket.js +++ b/UdpSocket.js @@ -247,12 +247,20 @@ UdpSocket.prototype.setMulticastLoopback = function(flag, callback) { // nothing yet } -UdpSocket.prototype.addMembership = function(multicastAddress, multicastInterface, callback) { - // nothing yet +UdpSocket.prototype.addMembership = function(multicastAddress) { + if (this._state !== STATE.BOUND) { + throw new Error('you must bind before addMembership()') + } + + Sockets.addMembership(this._id, multicastAddress); } -UdpSocket.prototype.dropMembership = function(multicastAddress, multicastInterface, callback) { - // nothing yet +UdpSocket.prototype.dropMembership = function(multicastAddress) { + if (this._state !== STATE.BOUND) { + throw new Error('you must bind before addMembership()') + } + + Sockets.dropMembership(this._id, multicastAddress); } UdpSocket.prototype.ref = function() { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a864a45..f66d0f5 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ - - + + + diff --git a/android/src/main/java/com/tradle/react/UdpReceiverTask.java b/android/src/main/java/com/tradle/react/UdpReceiverTask.java index 2439834..c2e897b 100644 --- a/android/src/main/java/com/tradle/react/UdpReceiverTask.java +++ b/android/src/main/java/com/tradle/react/UdpReceiverTask.java @@ -5,22 +5,16 @@ * Created by Andy Prock on 9/24/15. */ -package com.tradle.react; + package com.tradle.react; -import android.os.AsyncTask; -import android.util.Base64; + import android.os.AsyncTask; + import android.util.Base64; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; + import java.io.IOException; + import java.lang.ref.WeakReference; + import java.net.DatagramPacket; + import java.net.DatagramSocket; + import java.net.InetAddress; /** * This is a specialized AsyncTask that receives data from a socket in the background, and @@ -31,24 +25,24 @@ public class UdpReceiverTask extends AsyncTask { private static final String TAG = "UdpReceiverTask"; private static final int MAX_UDP_DATAGRAM_LEN = 1024; - private DatagramChannel mChannel; + private DatagramSocket mSocket; private WeakReference mReceiverListener; /** * An {@link AsyncTask} that blocks to receive data from a socket. * Received data is sent via the {@link OnDataReceivedListener} */ - public UdpReceiverTask(DatagramChannel channel, UdpReceiverTask.OnDataReceivedListener + public UdpReceiverTask(DatagramSocket socket, UdpReceiverTask.OnDataReceivedListener receiverListener) { - this.mChannel = channel; + this.mSocket = socket; this.mReceiverListener = new WeakReference<>(receiverListener); } /** * Returns the UdpReceiverTask's DatagramChannel. */ - public DatagramChannel getChannel() { - return mChannel; + public DatagramSocket getSocket() { + return mSocket; } /** @@ -58,29 +52,17 @@ public class UdpReceiverTask extends AsyncTask { protected Void doInBackground(Void... a) { OnDataReceivedListener receiverListener = mReceiverListener.get(); - Selector selector = null; - try { - selector = Selector.open(); - mChannel.register(selector, SelectionKey.OP_READ); - } catch (ClosedChannelException cce) { - if (receiverListener != null) { - receiverListener.didReceiveError(cce.getMessage()); - } - } catch (IOException ioe) { - if (receiverListener != null) { - receiverListener.didReceiveError(ioe.getMessage()); - } - } + final byte[] buffer = new byte[MAX_UDP_DATAGRAM_LEN]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - final ByteBuffer packet = ByteBuffer.allocate(MAX_UDP_DATAGRAM_LEN); - while(!isCancelled()){ + while (!isCancelled()) { try { - if(selector.selectNow() >= 1){ - final InetSocketAddress address = (InetSocketAddress) mChannel.receive(packet); - String base64Data = Base64.encodeToString(packet.array(), Base64.NO_WRAP); - receiverListener.didReceiveData(base64Data, address.getHostName(), address.getPort()); - packet.clear(); - } + mSocket.receive(packet); + + final InetAddress address = packet.getAddress(); + final String base64Data = Base64.encodeToString(packet.getData(), packet.getOffset(), + packet.getLength(), Base64.NO_WRAP); + receiverListener.didReceiveData(base64Data, address.getHostName(), packet.getPort()); } catch (IOException ioe) { if (receiverListener != null) { receiverListener.didReceiveError(ioe.getMessage()); @@ -102,17 +84,7 @@ public class UdpReceiverTask extends AsyncTask { */ @Override protected void onCancelled() { - OnDataReceivedListener receiverListener = mReceiverListener.get(); - - if (mChannel != null && mChannel.isOpen()){ - try { - mChannel.close(); - } catch (IOException ioe) { - if (receiverListener != null) { - receiverListener.didReceiveError(ioe.getMessage()); - } - } - } +// mSocket.close(); } /** diff --git a/android/src/main/java/com/tradle/react/UdpSenderTask.java b/android/src/main/java/com/tradle/react/UdpSenderTask.java index e96f1bd..e8605b7 100644 --- a/android/src/main/java/com/tradle/react/UdpSenderTask.java +++ b/android/src/main/java/com/tradle/react/UdpSenderTask.java @@ -11,9 +11,9 @@ import android.os.AsyncTask; import java.io.IOException; import java.lang.ref.WeakReference; +import java.net.DatagramPacket; +import java.net.DatagramSocket; import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; /** * Specialized AsyncTask that transmits data in the background, and notifies listeners of the result. @@ -21,11 +21,11 @@ import java.nio.channels.DatagramChannel; public class UdpSenderTask extends AsyncTask { private static final String TAG = "UdpSenderTask"; - private DatagramChannel mChannel; + private DatagramSocket mSocket; private WeakReference mListener; - public UdpSenderTask(DatagramChannel channel, OnDataSentListener listener) { - this.mChannel = channel; + public UdpSenderTask(DatagramSocket socket, OnDataSentListener listener) { + this.mSocket = socket; this.mListener = new WeakReference<>(listener); } @@ -34,7 +34,9 @@ public class UdpSenderTask extends AsyncTask mPendingSends; - private DatagramChannel mSenderChannel; + private DatagramSocket mSocket; private UdpSocketClient(Builder builder) { this.mReceiverListener = builder.receiverListener; @@ -47,6 +46,14 @@ public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedList this.mPendingSends = new ConcurrentHashMap<>(); } + /** + * Checks to see if client is receiving multicast packets. + * @return boolean true if receiving multicast packets, else false. + */ + public boolean isMulticast() { + return (mReceiverTask != null && mReceiverTask.getSocket() instanceof MulticastSocket); + } + /** * Binds to a specific port or address. A random port is used if the address is {@code null}. * @@ -60,9 +67,9 @@ public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedList * binding. */ public void bind(Integer port, @Nullable String address) throws IOException { - DatagramChannel receiverChannel = DatagramChannel.open(); - receiverChannel.configureBlocking(false); - mReceiverTask = new UdpReceiverTask(receiverChannel, this); + mSocket = new DatagramSocket(null); + + mReceiverTask = new UdpReceiverTask(mSocket, this); SocketAddress socketAddress; if (address != null) { @@ -71,14 +78,52 @@ public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedList socketAddress = new InetSocketAddress(port); } - DatagramSocket socket = receiverChannel.socket(); - socket.setReuseAddress(mReuseAddress); - socket.bind(socketAddress); + mSocket.setReuseAddress(mReuseAddress); + mSocket.bind(socketAddress); // begin listening for data in the background mReceiverTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + /** + * Adds this socket to the specified multicast group. Rebuilds the receiver task with a + * MulticastSocket. + * + * @param address the multicast group to join + * @throws UnknownHostException + * @throws IOException + */ + public void addMembership(String address) throws UnknownHostException, IOException { + final int port = mReceiverTask.getSocket().getLocalPort(); + mReceiverTask.cancel(true); + + final MulticastSocket socket = new MulticastSocket(port); + socket.joinGroup(InetAddress.getByName(address)); + + mReceiverTask = new UdpReceiverTask(socket, this); + + // begin listening for data in the background + mReceiverTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + /** + * Removes this socket from the specified multicast group. + * + * @param address the multicast group to leave + * @throws UnknownHostException + * @throws IOException + */ + public void dropMembership(String address) throws UnknownHostException, IOException { + if (mReceiverTask != null && mReceiverTask.getSocket() instanceof MulticastSocket) { + if (!mReceiverTask.isCancelled()) { + mReceiverTask.cancel(true); + } + + final MulticastSocket socket = (MulticastSocket) mReceiverTask.getSocket(); + socket.leaveGroup(InetAddress.getByName(address)); + } + } + /** * Creates a UdpSenderTask, and transmits udp data in the background. * @@ -87,18 +132,18 @@ public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedList * @param address destination address * @param callback callback for results * @throws UnknownHostException + * @throws IOException */ public void send(String base64String, Integer port, String address, @Nullable Callback callback) throws UnknownHostException, IOException { - if (null == mSenderChannel || !mSenderChannel.isOpen()) { - mSenderChannel = DatagramChannel.open(); - mSenderChannel.configureBlocking(true); + if (null == mSocket || !mSocket.isBound()) { + return; } byte[] data = Base64.decode(base64String, Base64.NO_WRAP); - UdpSenderTask task = new UdpSenderTask(mSenderChannel, this); + UdpSenderTask task = new UdpSenderTask(mSocket, this); UdpSenderTask.SenderPacket packet = new UdpSenderTask.SenderPacket(); - packet.data = ByteBuffer.wrap(data); + packet.data = data; packet.socketAddress = new InetSocketAddress(address, port); if (callback != null) { @@ -115,9 +160,9 @@ public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedList */ public void setBroadcast(boolean flag) throws SocketException { if (mReceiverTask != null) { - DatagramChannel channel = mReceiverTask.getChannel(); - if (channel != null) { - channel.socket().setBroadcast(flag); + DatagramSocket socket = mReceiverTask.getSocket(); + if (socket != null) { + socket.setBroadcast(flag); } } } @@ -129,10 +174,14 @@ public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedList if (mReceiverTask != null && !mReceiverTask.isCancelled()) { // stop the receiving task, and close the channel mReceiverTask.cancel(true); + if (!mReceiverTask.getSocket().isClosed()) { + mReceiverTask.getSocket().close(); + } } - if (mSenderChannel != null && mSenderChannel.isOpen()) { - mSenderChannel.close(); + if (mSocket != null && !mSocket.isClosed()) { + mSocket.close(); + mSocket = null; } } diff --git a/android/src/main/java/com/tradle/react/UdpSockets.java b/android/src/main/java/com/tradle/react/UdpSockets.java index da60599..ec323bf 100644 --- a/android/src/main/java/com/tradle/react/UdpSockets.java +++ b/android/src/main/java/com/tradle/react/UdpSockets.java @@ -5,27 +5,29 @@ * Created by Andy Prock on 9/24/15. */ -package com.tradle.react; + package com.tradle.react; -import android.support.annotation.Nullable; -import android.util.SparseArray; + import android.content.Context; + import android.net.wifi.WifiManager; + import android.support.annotation.Nullable; + import android.util.SparseArray; -import com.facebook.common.logging.FLog; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.GuardedAsyncTask; -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.ReadableMap; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; + import com.facebook.common.logging.FLog; + import com.facebook.react.bridge.Arguments; + import com.facebook.react.bridge.Callback; + import com.facebook.react.bridge.GuardedAsyncTask; + 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.ReadableMap; + import com.facebook.react.bridge.WritableMap; + import com.facebook.react.modules.core.DeviceEventManagerModule; -import java.io.IOException; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.util.concurrent.ExecutionException; + import java.io.IOException; + import java.net.SocketException; + import java.net.UnknownHostException; + import java.util.concurrent.ExecutionException; /** * The NativeModule in charge of storing active {@link UdpSocketClient}s, and acting as an api layer. @@ -33,6 +35,7 @@ import java.util.concurrent.ExecutionException; public final class UdpSockets extends ReactContextBaseJavaModule implements UdpSocketClient.OnDataReceivedListener, UdpSocketClient.OnRuntimeExceptionListener { private static final String TAG = "UdpSockets"; + private WifiManager.MulticastLock mMulticastLock; private SparseArray mClients = new SparseArray<>(); private boolean mShuttingDown = false; @@ -154,6 +157,65 @@ public final class UdpSockets extends ReactContextBaseJavaModule }.execute(); } + /** + * Joins a multi-cast group + */ + @ReactMethod + public void addMembership(final Integer cId, final String multicastAddress) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + UdpSocketClient client = findClient(cId, null); + if (client == null) { + return; + } + + if (mMulticastLock == null) { + WifiManager wifiMgr = (WifiManager) getReactApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + mMulticastLock = wifiMgr.createMulticastLock("react-native-udp"); + mMulticastLock.setReferenceCounted(true); + } + + if (!client.isMulticast()) { + // acquire the multi-cast lock, IF this is the + // first addMembership call for this client. + mMulticastLock.acquire(); + } + + try { + client.addMembership(multicastAddress); + } catch (IOException ioe) { + // an exception occurred + FLog.e(TAG, "addMembership", ioe); + } + } + }.execute(); + } + + /** + * Leaves a multi-cast group + */ + @ReactMethod + public void dropMembership(final Integer cId, final String multicastAddress) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + UdpSocketClient client = findClient(cId, null); + if (client == null) { + return; + } + + try { + client.dropMembership(multicastAddress); + } catch (IOException ioe) { + // an exception occurred + FLog.e(TAG, "dropMembership", ioe); + } + } + }.execute(); + } + /** * Sends udp data via the {@link UdpSocketClient} */ @@ -193,6 +255,11 @@ public final class UdpSockets extends ReactContextBaseJavaModule return; } + if (client.isMulticast() && mMulticastLock.isHeld()) { + // drop the multi-cast lock if this is a multi-cast client + mMulticastLock.release(); + } + try { client.close(); callback.invoke(); diff --git a/ios/ReactUdp.podspec b/ios/ReactUdp.podspec index 9f98941..9c3a0de 100644 --- a/ios/ReactUdp.podspec +++ b/ios/ReactUdp.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'ReactUdp' - s.version = '0.1.0' + s.version = '1.1.1' s.summary = 'node\'s dgram API in React Native.' s.description = <<-DESC Enables accessing udp sockets in React Native. diff --git a/ios/UdpSockets.m b/ios/UdpSockets.m index 7d7c2af..feb6677 100644 --- a/ios/UdpSockets.m +++ b/ios/UdpSockets.m @@ -114,6 +114,16 @@ RCT_EXPORT_METHOD(setBroadcast:(nonnull NSNumber*)cId callback(@[[NSNull null]]); } +RCT_EXPORT_METHOD(addMembership:(nonnull NSNumber*)cId + multicastAddress:(NSString *)address) { + /* nop */ +} + +RCT_EXPORT_METHOD(dropMembership:(nonnull NSNumber*)cId + multicastAddress:(NSString *)address) { + /* nop */ +} + - (void) onData:(UdpSocketClient*) client data:(NSData *)data host:(NSString *)host port:(uint16_t)port { NSMutableDictionary* _clients = [UdpSockets clients]; diff --git a/package.json b/package.json index 1cffc3f..dbbd1df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-udp", - "version": "1.1.0", + "version": "1.1.1", "description": "node's dgram API for react-native", "main": "UdpSockets.js", "scripts": {