Direct IP connect functionality for AppRTC Android demo.
This allows connecting between clients without using external servers, which is useful to OEMs if they are working in a network without internet connection. Implementation uses custom AppRTCClient that replaces WebSocketRTCClient if roomId looks like an IP. Instead of a web socket, this class uses direct TCP connection between peers as a signaling channel. Review-Url: https://codereview.webrtc.org/1963053002 Cr-Commit-Position: refs/heads/master@{#12789}
This commit is contained in:
parent
5a216d0489
commit
299ccdee0c
@ -139,7 +139,6 @@ public class CallActivity extends Activity
|
|||||||
private HudFragment hudFragment;
|
private HudFragment hudFragment;
|
||||||
private CpuMonitor cpuMonitor;
|
private CpuMonitor cpuMonitor;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@ -240,8 +239,15 @@ public class CallActivity extends Activity
|
|||||||
commandLineRun = intent.getBooleanExtra(EXTRA_CMDLINE, false);
|
commandLineRun = intent.getBooleanExtra(EXTRA_CMDLINE, false);
|
||||||
runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
|
runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
|
||||||
|
|
||||||
// Create connection client and connection parameters.
|
// Create connection client. Use DirectRTCClient if room name is an IP otherwise use the
|
||||||
|
// standard WebSocketRTCClient.
|
||||||
|
if (loopback || !DirectRTCClient.IP_PATTERN.matcher(roomId).matches()) {
|
||||||
appRtcClient = new WebSocketRTCClient(this, new LooperExecutor());
|
appRtcClient = new WebSocketRTCClient(this, new LooperExecutor());
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Using DirectRTCClient because room name looks like an IP.");
|
||||||
|
appRtcClient = new DirectRTCClient(this);
|
||||||
|
}
|
||||||
|
// Create connection parameters.
|
||||||
roomConnectionParameters = new RoomConnectionParameters(
|
roomConnectionParameters = new RoomConnectionParameters(
|
||||||
roomUri.toString(), roomId, loopback);
|
roomUri.toString(), roomId, loopback);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.appspot.apprtc;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.appspot.apprtc.util.LooperExecutor;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.webrtc.IceCandidate;
|
||||||
|
import org.webrtc.PeerConnection;
|
||||||
|
import org.webrtc.SessionDescription;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of AppRTCClient that uses direct TCP connection as the signaling channel.
|
||||||
|
* This eliminates the need for an external server. This class does not support loopback
|
||||||
|
* connections.
|
||||||
|
*/
|
||||||
|
public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents {
|
||||||
|
private static final String TAG = "DirectRTCClient";
|
||||||
|
private static final int DEFAULT_PORT = 8888;
|
||||||
|
|
||||||
|
// Regex pattern used for checking if room id looks like an IP.
|
||||||
|
static final Pattern IP_PATTERN = Pattern.compile(
|
||||||
|
"("
|
||||||
|
// IPv4
|
||||||
|
+ "((\\d+\\.){3}\\d+)|"
|
||||||
|
// IPv6
|
||||||
|
+ "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::"
|
||||||
|
+ "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|"
|
||||||
|
+ "\\[(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})\\]|"
|
||||||
|
// IPv6 without []
|
||||||
|
+ "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|"
|
||||||
|
+ "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})|"
|
||||||
|
// Literals
|
||||||
|
+ "localhost"
|
||||||
|
+ ")"
|
||||||
|
// Optional port number
|
||||||
|
+ "(:(\\d+))?"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final LooperExecutor executor;
|
||||||
|
private final SignalingEvents events;
|
||||||
|
private TCPChannelClient tcpClient;
|
||||||
|
private RoomConnectionParameters connectionParameters;
|
||||||
|
|
||||||
|
private enum ConnectionState {
|
||||||
|
NEW, CONNECTED, CLOSED, ERROR
|
||||||
|
};
|
||||||
|
|
||||||
|
// All alterations of the room state should be done from inside the looper thread.
|
||||||
|
private ConnectionState roomState;
|
||||||
|
|
||||||
|
public DirectRTCClient(SignalingEvents events) {
|
||||||
|
this.events = events;
|
||||||
|
executor = new LooperExecutor();
|
||||||
|
|
||||||
|
executor.requestStart();
|
||||||
|
roomState = ConnectionState.NEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the room, roomId in connectionsParameters is required. roomId must be a valid
|
||||||
|
* IP address matching IP_PATTERN.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void connectToRoom(RoomConnectionParameters connectionParameters) {
|
||||||
|
this.connectionParameters = connectionParameters;
|
||||||
|
|
||||||
|
if (connectionParameters.loopback) {
|
||||||
|
reportError("Loopback connections aren't supported by DirectRTCClient.");
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
connectToRoomInternal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnectFromRoom() {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
disconnectFromRoomInternal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
executor.requestStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the room.
|
||||||
|
*
|
||||||
|
* Runs on the looper thread.
|
||||||
|
*/
|
||||||
|
private void connectToRoomInternal() {
|
||||||
|
this.roomState = ConnectionState.NEW;
|
||||||
|
|
||||||
|
String endpoint = connectionParameters.roomId;
|
||||||
|
|
||||||
|
Matcher matcher = IP_PATTERN.matcher(endpoint);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
reportError("roomId must match IP_PATTERN for DirectRTCClient.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ip = matcher.group(1);
|
||||||
|
String portStr = matcher.group(matcher.groupCount());
|
||||||
|
int port;
|
||||||
|
|
||||||
|
if (portStr != null) {
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(portStr);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
reportError("Invalid port number: " + portStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
port = DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpClient = new TCPChannelClient(executor, this, ip, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects from the room.
|
||||||
|
*
|
||||||
|
* Runs on the looper thread.
|
||||||
|
*/
|
||||||
|
private void disconnectFromRoomInternal() {
|
||||||
|
roomState = ConnectionState.CLOSED;
|
||||||
|
|
||||||
|
if (tcpClient != null) {
|
||||||
|
tcpClient.disconnect();
|
||||||
|
tcpClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendOfferSdp(final SessionDescription sdp) {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (roomState != ConnectionState.CONNECTED) {
|
||||||
|
reportError("Sending offer SDP in non connected state.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
jsonPut(json, "sdp", sdp.description);
|
||||||
|
jsonPut(json, "type", "offer");
|
||||||
|
sendMessage(json.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendAnswerSdp(final SessionDescription sdp) {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
jsonPut(json, "sdp", sdp.description);
|
||||||
|
jsonPut(json, "type", "answer");
|
||||||
|
sendMessage(json.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendLocalIceCandidate(final IceCandidate candidate) {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
jsonPut(json, "type", "candidate");
|
||||||
|
jsonPut(json, "label", candidate.sdpMLineIndex);
|
||||||
|
jsonPut(json, "id", candidate.sdpMid);
|
||||||
|
jsonPut(json, "candidate", candidate.sdp);
|
||||||
|
|
||||||
|
if (roomState != ConnectionState.CONNECTED) {
|
||||||
|
reportError("Sending ICE candidate in non connected state.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendMessage(json.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send removed Ice candidates to the other participant. */
|
||||||
|
@Override
|
||||||
|
public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
jsonPut(json, "type", "remove-candidates");
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
for (final IceCandidate candidate : candidates) {
|
||||||
|
jsonArray.put(toJsonCandidate(candidate));
|
||||||
|
}
|
||||||
|
jsonPut(json, "candidates", jsonArray);
|
||||||
|
|
||||||
|
if (roomState != ConnectionState.CONNECTED) {
|
||||||
|
reportError("Sending ICE candidate removals in non connected state.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendMessage(json.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// TCPChannelClient event handlers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the client is the server side, this will trigger onConnectedToRoom.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onTCPConnected(boolean isServer) {
|
||||||
|
if (isServer) {
|
||||||
|
roomState = ConnectionState.CONNECTED;
|
||||||
|
|
||||||
|
SignalingParameters parameters = new SignalingParameters(
|
||||||
|
// Ice servers are not needed for direct connections.
|
||||||
|
new LinkedList<PeerConnection.IceServer>(),
|
||||||
|
isServer, // Server side acts as the initiator on direct connections.
|
||||||
|
null, // clientId
|
||||||
|
null, // wssUrl
|
||||||
|
null, // wwsPostUrl
|
||||||
|
null, // offerSdp
|
||||||
|
null // iceCandidates
|
||||||
|
);
|
||||||
|
events.onConnectedToRoom(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTCPMessage(String msg) {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(msg);
|
||||||
|
String type = json.optString("type");
|
||||||
|
if (type.equals("candidate")) {
|
||||||
|
events.onRemoteIceCandidate(toJavaCandidate(json));
|
||||||
|
} else if (type.equals("remove-candidates")) {
|
||||||
|
JSONArray candidateArray = json.getJSONArray("candidates");
|
||||||
|
IceCandidate[] candidates = new IceCandidate[candidateArray.length()];
|
||||||
|
for (int i = 0; i < candidateArray.length(); ++i) {
|
||||||
|
candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i));
|
||||||
|
}
|
||||||
|
events.onRemoteIceCandidatesRemoved(candidates);
|
||||||
|
} else if (type.equals("answer")) {
|
||||||
|
SessionDescription sdp = new SessionDescription(
|
||||||
|
SessionDescription.Type.fromCanonicalForm(type),
|
||||||
|
json.getString("sdp"));
|
||||||
|
events.onRemoteDescription(sdp);
|
||||||
|
} else if (type.equals("offer")) {
|
||||||
|
SessionDescription sdp = new SessionDescription(
|
||||||
|
SessionDescription.Type.fromCanonicalForm(type),
|
||||||
|
json.getString("sdp"));
|
||||||
|
|
||||||
|
SignalingParameters parameters = new SignalingParameters(
|
||||||
|
// Ice servers are not needed for direct connections.
|
||||||
|
new LinkedList<PeerConnection.IceServer>(),
|
||||||
|
false, // This code will only be run on the client side. So, we are not the initiator.
|
||||||
|
null, // clientId
|
||||||
|
null, // wssUrl
|
||||||
|
null, // wssPostUrl
|
||||||
|
sdp, // offerSdp
|
||||||
|
null // iceCandidates
|
||||||
|
);
|
||||||
|
roomState = ConnectionState.CONNECTED;
|
||||||
|
events.onConnectedToRoom(parameters);
|
||||||
|
} else {
|
||||||
|
reportError("Unexpected TCP message: " + msg);
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
reportError("TCP message JSON parsing error: " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTCPError(String description) {
|
||||||
|
reportError("TCP connection error: " + description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTCPClose() {
|
||||||
|
events.onChannelClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// Helper functions.
|
||||||
|
private void reportError(final String errorMessage) {
|
||||||
|
Log.e(TAG, errorMessage);
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (roomState != ConnectionState.ERROR) {
|
||||||
|
roomState = ConnectionState.ERROR;
|
||||||
|
events.onChannelError(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(final String message) {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
tcpClient.send(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put a |key|->|value| mapping in |json|.
|
||||||
|
private static void jsonPut(JSONObject json, String key, Object value) {
|
||||||
|
try {
|
||||||
|
json.put(key, value);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a Java candidate to a JSONObject.
|
||||||
|
private static JSONObject toJsonCandidate(final IceCandidate candidate) {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
jsonPut(json, "label", candidate.sdpMLineIndex);
|
||||||
|
jsonPut(json, "id", candidate.sdpMid);
|
||||||
|
jsonPut(json, "candidate", candidate.sdp);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a JSON candidate to a Java object.
|
||||||
|
private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException {
|
||||||
|
return new IceCandidate(json.getString("id"),
|
||||||
|
json.getInt("label"),
|
||||||
|
json.getString("candidate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,362 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.appspot.apprtc;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.appspot.apprtc.util.LooperExecutor;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacement for WebSocketChannelClient for direct communication between two IP addresses. Handles
|
||||||
|
* the signaling between the two clients using a TCP connection.
|
||||||
|
*
|
||||||
|
* <p>All public methods should be called from a looper executor thread
|
||||||
|
* passed in a constructor, otherwise exception will be thrown.
|
||||||
|
* All events are dispatched on the same thread.
|
||||||
|
*/
|
||||||
|
public class TCPChannelClient {
|
||||||
|
private static final String TAG = "TCPChannelClient";
|
||||||
|
|
||||||
|
private final LooperExecutor executor;
|
||||||
|
private final TCPChannelEvents eventListener;
|
||||||
|
private TCPSocket socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface for messages delivered on TCP Connection. All callbacks are invoked from the
|
||||||
|
* looper executor thread.
|
||||||
|
*/
|
||||||
|
public interface TCPChannelEvents {
|
||||||
|
void onTCPConnected(boolean server);
|
||||||
|
void onTCPMessage(String message);
|
||||||
|
void onTCPError(String description);
|
||||||
|
void onTCPClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the TCPChannelClient. If IP is a local IP address, starts a listening server on
|
||||||
|
* that IP. If not, instead connects to the IP.
|
||||||
|
*
|
||||||
|
* @param eventListener Listener that will receive events from the client.
|
||||||
|
* @param ip IP address to listen on or connect to.
|
||||||
|
* @param port Port to listen on or connect to.
|
||||||
|
*/
|
||||||
|
public TCPChannelClient(
|
||||||
|
LooperExecutor executor, TCPChannelEvents eventListener, String ip, int port) {
|
||||||
|
this.executor = executor;
|
||||||
|
this.eventListener = eventListener;
|
||||||
|
|
||||||
|
InetAddress address;
|
||||||
|
try {
|
||||||
|
address = InetAddress.getByName(ip);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
reportError("Invalid IP address.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isAnyLocalAddress()) {
|
||||||
|
socket = new TCPSocketServer(address, port);
|
||||||
|
} else {
|
||||||
|
socket = new TCPSocketClient(address, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects the client if not already disconnected. This will fire the onTCPClose event.
|
||||||
|
*/
|
||||||
|
public void disconnect() {
|
||||||
|
checkIfCalledOnValidThread();
|
||||||
|
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message on the socket.
|
||||||
|
*
|
||||||
|
* @param message Message to be sent.
|
||||||
|
*/
|
||||||
|
public void send(String message) {
|
||||||
|
checkIfCalledOnValidThread();
|
||||||
|
|
||||||
|
socket.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for firing onTCPError events. Calls onTCPError on the executor thread.
|
||||||
|
*/
|
||||||
|
private void reportError(final String message) {
|
||||||
|
Log.e(TAG, "TCP Error: " + message);
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
eventListener.onTCPError(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for debugging purposes.
|
||||||
|
* Ensures that TCPChannelClient method is called on a looper thread.
|
||||||
|
*/
|
||||||
|
private void checkIfCalledOnValidThread() {
|
||||||
|
if (!executor.checkOnLooperThread()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"TCPChannelClient method is not called on valid thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for server and client sockets. Contains a listening thread that will call
|
||||||
|
* eventListener.onTCPMessage on new messages.
|
||||||
|
*/
|
||||||
|
private abstract class TCPSocket extends Thread {
|
||||||
|
// Lock for editing out and rawSocket
|
||||||
|
protected final Object rawSocketLock;
|
||||||
|
private PrintWriter out;
|
||||||
|
private Socket rawSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the peer, potentially a slow operation.
|
||||||
|
*
|
||||||
|
* @return Socket connection, null if connection failed.
|
||||||
|
*/
|
||||||
|
public abstract Socket connect();
|
||||||
|
/** Returns true if sockets is a server rawSocket. */
|
||||||
|
public abstract boolean isServer();
|
||||||
|
|
||||||
|
TCPSocket() {
|
||||||
|
rawSocketLock = new Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The listening thread.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.d(TAG, "Listening thread started...");
|
||||||
|
|
||||||
|
// Receive connection to temporary variable first, so we don't block.
|
||||||
|
Socket tempSocket = connect();
|
||||||
|
BufferedReader in;
|
||||||
|
|
||||||
|
Log.d(TAG, "TCP connection established.");
|
||||||
|
|
||||||
|
synchronized (rawSocketLock) {
|
||||||
|
if (rawSocket != null) {
|
||||||
|
Log.e(TAG, "Socket already existed and will be replaced.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSocket = tempSocket;
|
||||||
|
|
||||||
|
// Connecting failed, error has already been reported, just exit.
|
||||||
|
if (rawSocket == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
out = new PrintWriter(rawSocket.getOutputStream(), true);
|
||||||
|
in = new BufferedReader(new InputStreamReader(rawSocket.getInputStream()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportError("Failed to open IO on rawSocket: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v(TAG, "Execute onTCPConnected");
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.v(TAG, "Run onTCPConnected");
|
||||||
|
eventListener.onTCPConnected(isServer());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
final String message;
|
||||||
|
try {
|
||||||
|
message = in.readLine();
|
||||||
|
} catch (IOException e) {
|
||||||
|
synchronized (rawSocketLock) {
|
||||||
|
// If socket was closed, this is expected.
|
||||||
|
if (rawSocket == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError("Failed to read from rawSocket: " + e.getMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No data received, rawSocket probably closed.
|
||||||
|
if (message == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.v(TAG, "Receive: " + message);
|
||||||
|
eventListener.onTCPMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Receiving thread exiting...");
|
||||||
|
|
||||||
|
// Close the rawSocket if it is still open.
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the rawSocket if it is still open. Also fires the onTCPClose event.
|
||||||
|
*/
|
||||||
|
public void disconnect() {
|
||||||
|
try {
|
||||||
|
synchronized (rawSocketLock) {
|
||||||
|
if (rawSocket != null) {
|
||||||
|
rawSocket.close();
|
||||||
|
rawSocket = null;
|
||||||
|
out = null;
|
||||||
|
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
eventListener.onTCPClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportError("Failed to close rawSocket: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message on the socket. Should only be called on the executor thread.
|
||||||
|
*/
|
||||||
|
public void send(String message) {
|
||||||
|
Log.v(TAG, "Send: " + message);
|
||||||
|
|
||||||
|
synchronized (rawSocketLock) {
|
||||||
|
if (out == null) {
|
||||||
|
reportError("Sending data on closed socket.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(message + "\n");
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TCPSocketServer extends TCPSocket {
|
||||||
|
// Server socket is also guarded by rawSocketLock.
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
|
||||||
|
final private InetAddress address;
|
||||||
|
final private int port;
|
||||||
|
|
||||||
|
public TCPSocketServer(InetAddress address, int port) {
|
||||||
|
this.address = address;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens a listening socket and waits for a connection. */
|
||||||
|
@Override
|
||||||
|
public Socket connect() {
|
||||||
|
Log.d(TAG, "Listening on [" + address.getHostAddress() + "]:" + Integer.toString(port));
|
||||||
|
|
||||||
|
final ServerSocket tempSocket;
|
||||||
|
try {
|
||||||
|
tempSocket = new ServerSocket(port, 0, address);
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportError("Failed to create server socket: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (rawSocketLock) {
|
||||||
|
if (serverSocket != null) {
|
||||||
|
Log.e(TAG, "Server rawSocket was already listening and new will be opened.");
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSocket = tempSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return tempSocket.accept();
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportError("Failed to receive connection: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closes the listening socket and calls super. */
|
||||||
|
@Override
|
||||||
|
public void disconnect() {
|
||||||
|
try {
|
||||||
|
synchronized (rawSocketLock) {
|
||||||
|
if (serverSocket != null) {
|
||||||
|
serverSocket.close();
|
||||||
|
serverSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportError("Failed to close server socket: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
super.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isServer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TCPSocketClient extends TCPSocket {
|
||||||
|
final private InetAddress address;
|
||||||
|
final private int port;
|
||||||
|
|
||||||
|
public TCPSocketClient(InetAddress address, int port) {
|
||||||
|
this.address = address;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connects to the peer. */
|
||||||
|
@Override
|
||||||
|
public Socket connect() {
|
||||||
|
Log.d(TAG, "Connecting to [" + address.getHostAddress() + "]:" + Integer.toString(port));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Socket(address, port);
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportError("Failed to connect: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isServer() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.appspot.apprtc;
|
||||||
|
|
||||||
|
import org.appspot.apprtc.util.LooperExecutor;
|
||||||
|
import org.appspot.apprtc.util.RobolectricLooperExecutor;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
import org.robolectric.shadows.ShadowLog;
|
||||||
|
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.timeout;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@Config(manifest = Config.NONE)
|
||||||
|
public class TCPChannelClientTest {
|
||||||
|
private static final int PORT = 8888;
|
||||||
|
/**
|
||||||
|
* How long we wait before trying to connect to the server. Chosen quite arbitrarily and
|
||||||
|
* could be made smaller if need be.
|
||||||
|
*/
|
||||||
|
private static final int SERVER_WAIT = 10;
|
||||||
|
private static final int CONNECT_TIMEOUT = 100;
|
||||||
|
private static final int SEND_TIMEOUT = 100;
|
||||||
|
private static final int DISCONNECT_TIMEOUT = 100;
|
||||||
|
private static final String TEST_MESSAGE_SERVER = "Hello, Server!";
|
||||||
|
private static final String TEST_MESSAGE_CLIENT = "Hello, Client!";
|
||||||
|
|
||||||
|
@Mock TCPChannelClient.TCPChannelEvents serverEvents;
|
||||||
|
@Mock TCPChannelClient.TCPChannelEvents clientEvents;
|
||||||
|
|
||||||
|
private RobolectricLooperExecutor executor;
|
||||||
|
private TCPChannelClient server;
|
||||||
|
private TCPChannelClient client;
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
ShadowLog.stream = System.out;
|
||||||
|
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
|
||||||
|
executor = new RobolectricLooperExecutor();
|
||||||
|
executor.requestStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
verifyNoMoreEvents();
|
||||||
|
|
||||||
|
executor.executeAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
client.disconnect();
|
||||||
|
server.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop the executor thread
|
||||||
|
executor.requestStop();
|
||||||
|
try {
|
||||||
|
executor.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConnectIPv4() {
|
||||||
|
setUpIPv4Server();
|
||||||
|
try {
|
||||||
|
Thread.sleep(SERVER_WAIT);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
setUpIPv4Client();
|
||||||
|
|
||||||
|
verify(serverEvents, timeout(CONNECT_TIMEOUT)).onTCPConnected(true);
|
||||||
|
verify(clientEvents, timeout(CONNECT_TIMEOUT)).onTCPConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConnectIPv6() {
|
||||||
|
setUpIPv6Server();
|
||||||
|
try {
|
||||||
|
Thread.sleep(SERVER_WAIT);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
setUpIPv6Client();
|
||||||
|
|
||||||
|
verify(serverEvents, timeout(CONNECT_TIMEOUT)).onTCPConnected(true);
|
||||||
|
verify(clientEvents, timeout(CONNECT_TIMEOUT)).onTCPConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendData() {
|
||||||
|
testConnectIPv4();
|
||||||
|
|
||||||
|
executor.executeAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
client.send(TEST_MESSAGE_SERVER);
|
||||||
|
server.send(TEST_MESSAGE_CLIENT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(serverEvents, timeout(SEND_TIMEOUT)).onTCPMessage(TEST_MESSAGE_SERVER);
|
||||||
|
verify(clientEvents, timeout(SEND_TIMEOUT)).onTCPMessage(TEST_MESSAGE_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisconnectServer() {
|
||||||
|
testConnectIPv4();
|
||||||
|
executor.executeAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
server.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(serverEvents, timeout(DISCONNECT_TIMEOUT)).onTCPClose();
|
||||||
|
verify(clientEvents, timeout(DISCONNECT_TIMEOUT)).onTCPClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisconnectClient() {
|
||||||
|
testConnectIPv4();
|
||||||
|
executor.executeAndWait(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(serverEvents, timeout(DISCONNECT_TIMEOUT)).onTCPClose();
|
||||||
|
verify(clientEvents, timeout(DISCONNECT_TIMEOUT)).onTCPClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpIPv4Server() {
|
||||||
|
setUpServer("0.0.0.0", PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpIPv4Client() {
|
||||||
|
setUpClient("127.0.0.1", PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpIPv6Server() {
|
||||||
|
setUpServer("::", PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpIPv6Client() {
|
||||||
|
setUpClient("::1", PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpServer(String ip, int port) {
|
||||||
|
server = new TCPChannelClient(executor, serverEvents, ip, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpClient(String ip, int port) {
|
||||||
|
client = new TCPChannelClient(executor, clientEvents, ip, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies no more server or client events have been issued
|
||||||
|
*/
|
||||||
|
private void verifyNoMoreEvents() {
|
||||||
|
verifyNoMoreInteractions(serverEvents);
|
||||||
|
verifyNoMoreInteractions(clientEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.appspot.apprtc.util;
|
||||||
|
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LooperExecutor that doesn't use Looper because its implementation in Robolectric is not suited
|
||||||
|
* for our needs. Also implements executeAndWait that can be used to wait until the runnable has
|
||||||
|
* been executed.
|
||||||
|
*/
|
||||||
|
public class RobolectricLooperExecutor extends LooperExecutor {
|
||||||
|
private volatile boolean running = false;
|
||||||
|
private static final int RUNNABLE_QUEUE_CAPACITY = 256;
|
||||||
|
private final BlockingQueue<Runnable> runnableQueue
|
||||||
|
= new ArrayBlockingQueue<>(RUNNABLE_QUEUE_CAPACITY);
|
||||||
|
private long threadId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the runnable passed to the constructor and sets isDone flag afterwards.
|
||||||
|
*/
|
||||||
|
private static class ExecuteAndWaitRunnable implements Runnable {
|
||||||
|
public boolean isDone = false;
|
||||||
|
private final Runnable runnable;
|
||||||
|
|
||||||
|
ExecuteAndWaitRunnable(Runnable runnable) {
|
||||||
|
this.runnable = runnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
runnable.run();
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
isDone = true;
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
threadId = Thread.currentThread().getId();
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
final Runnable runnable;
|
||||||
|
|
||||||
|
try {
|
||||||
|
runnable = runnableQueue.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
if (running) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void requestStart() {
|
||||||
|
if (running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
running = true;
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void requestStop() {
|
||||||
|
running = false;
|
||||||
|
interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void execute(Runnable runnable) {
|
||||||
|
try {
|
||||||
|
runnableQueue.put(runnable);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues runnable to be run and waits for it to be executed by the executor thread
|
||||||
|
*/
|
||||||
|
public void executeAndWait(Runnable runnable) {
|
||||||
|
ExecuteAndWaitRunnable executeAndWaitRunnable = new ExecuteAndWaitRunnable(runnable);
|
||||||
|
execute(executeAndWaitRunnable);
|
||||||
|
|
||||||
|
synchronized (executeAndWaitRunnable) {
|
||||||
|
while (!executeAndWaitRunnable.isDone) {
|
||||||
|
try {
|
||||||
|
executeAndWaitRunnable.wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkOnLooperThread() {
|
||||||
|
return (Thread.currentThread().getId() == threadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user