OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright 2016 The WebRTC Project Authors. All rights reserved. |
| 3 * |
| 4 * Use of this source code is governed by a BSD-style license |
| 5 * that can be found in the LICENSE file in the root of the source |
| 6 * tree. An additional intellectual property rights grant can be found |
| 7 * in the file PATENTS. All contributing project authors may |
| 8 * be found in the AUTHORS file in the root of the source tree. |
| 9 */ |
| 10 |
| 11 package org.appspot.apprtc; |
| 12 |
| 13 import android.util.Log; |
| 14 |
| 15 import org.appspot.apprtc.util.LooperExecutor; |
| 16 |
| 17 import java.io.BufferedReader; |
| 18 import java.io.IOException; |
| 19 import java.io.InputStreamReader; |
| 20 import java.io.PrintWriter; |
| 21 import java.net.InetAddress; |
| 22 import java.net.ServerSocket; |
| 23 import java.net.Socket; |
| 24 import java.net.UnknownHostException; |
| 25 |
| 26 /** |
| 27 * Replacement for WebSocketChannelClient for direct communication between two I
P addresses. Handles |
| 28 * the signaling between the two clients using a TCP connection. |
| 29 * |
| 30 * <p>All public methods should be called from a looper executor thread |
| 31 * passed in a constructor, otherwise exception will be thrown. |
| 32 * All events are dispatched on the same thread. |
| 33 */ |
| 34 public class TCPChannelClient { |
| 35 private static final String TAG = "TCPChannelClient"; |
| 36 |
| 37 private final LooperExecutor executor; |
| 38 private final TCPChannelEvents eventListener; |
| 39 private TCPSocket socket; |
| 40 |
| 41 /** |
| 42 * Callback interface for messages delivered on TCP Connection. All callbacks
are invoked from the |
| 43 * looper executor thread. |
| 44 */ |
| 45 public interface TCPChannelEvents { |
| 46 void onTCPConnected(boolean server); |
| 47 void onTCPMessage(String message); |
| 48 void onTCPError(String description); |
| 49 void onTCPClose(); |
| 50 } |
| 51 |
| 52 /** |
| 53 * Initializes the TCPChannelClient. If IP is a local IP address, starts a lis
tening server on |
| 54 * that IP. If not, instead connects to the IP. |
| 55 * |
| 56 * @param eventListener Listener that will receive events from the client. |
| 57 * @param ip IP address to listen on or connect to. |
| 58 * @param port Port to listen on or connect to. |
| 59 */ |
| 60 public TCPChannelClient( |
| 61 LooperExecutor executor, TCPChannelEvents eventListener, String ip, int po
rt) { |
| 62 this.executor = executor; |
| 63 this.eventListener = eventListener; |
| 64 |
| 65 InetAddress address; |
| 66 try { |
| 67 address = InetAddress.getByName(ip); |
| 68 } catch (UnknownHostException e) { |
| 69 reportError("Invalid IP address."); |
| 70 return; |
| 71 } |
| 72 |
| 73 if (address.isAnyLocalAddress()) { |
| 74 socket = new TCPSocketServer(address, port); |
| 75 } else { |
| 76 socket = new TCPSocketClient(address, port); |
| 77 } |
| 78 |
| 79 socket.start(); |
| 80 } |
| 81 |
| 82 /** |
| 83 * Disconnects the client if not already disconnected. This will fire the onTC
PClose event. |
| 84 */ |
| 85 public void disconnect() { |
| 86 checkIfCalledOnValidThread(); |
| 87 |
| 88 socket.disconnect(); |
| 89 } |
| 90 |
| 91 /** |
| 92 * Sends a message on the socket. |
| 93 * |
| 94 * @param message Message to be sent. |
| 95 */ |
| 96 public void send(String message) { |
| 97 checkIfCalledOnValidThread(); |
| 98 |
| 99 socket.send(message); |
| 100 } |
| 101 |
| 102 /** |
| 103 * Helper method for firing onTCPError events. Calls onTCPError on the executo
r thread. |
| 104 */ |
| 105 private void reportError(final String message) { |
| 106 executor.execute(new Runnable() { |
| 107 @Override |
| 108 public void run() { |
| 109 eventListener.onTCPError(message); |
| 110 } |
| 111 }); |
| 112 } |
| 113 |
| 114 /** |
| 115 * Helper method for debugging purposes. |
| 116 * Ensures that TCPChannelClient method is called on a looper thread. |
| 117 */ |
| 118 private void checkIfCalledOnValidThread() { |
| 119 if (!executor.checkOnLooperThread()) { |
| 120 throw new IllegalStateException( |
| 121 "TCPChannelClient method is not called on valid thread"); |
| 122 } |
| 123 } |
| 124 |
| 125 |
| 126 /** |
| 127 * Base class for server and client sockets. Contains a listening thread that
will call |
| 128 * eventListener.onTCPMessage on new messages. |
| 129 */ |
| 130 private abstract class TCPSocket extends Thread { |
| 131 // Lock for editing out and rawSocket |
| 132 protected final Object rawSocketLock; |
| 133 private PrintWriter out; |
| 134 private Socket rawSocket; |
| 135 |
| 136 /** |
| 137 * Connect to the peer, potentially a slow operation. |
| 138 * |
| 139 * @return Socket connection, null if connection failed. |
| 140 */ |
| 141 public abstract Socket connect(); |
| 142 /** Returns true if sockets is a server rawSocket. */ |
| 143 public abstract boolean isServer(); |
| 144 |
| 145 TCPSocket() { |
| 146 rawSocketLock = new Object(); |
| 147 } |
| 148 |
| 149 /** |
| 150 * The listening thread. |
| 151 */ |
| 152 @Override |
| 153 public void run() { |
| 154 // Receive connection to temporary variable first, so we don't block |
| 155 Socket tempSocket = connect(); |
| 156 BufferedReader in; |
| 157 |
| 158 synchronized (rawSocketLock) { |
| 159 if (rawSocket != null) { |
| 160 Log.e(TAG, "Socket already existed and will be replaced."); |
| 161 } |
| 162 |
| 163 rawSocket = tempSocket; |
| 164 |
| 165 // Connecting failed, error has already been reported, just exit |
| 166 if (rawSocket == null) { |
| 167 return; |
| 168 } |
| 169 |
| 170 try { |
| 171 out = new PrintWriter(rawSocket.getOutputStream(), true); |
| 172 in = new BufferedReader(new InputStreamReader(rawSocket.getInputStream
())); |
| 173 } catch (IOException e) { |
| 174 reportError("Failed to open IO on rawSocket: " + e.getMessage()); |
| 175 return; |
| 176 } |
| 177 } |
| 178 |
| 179 executor.execute(new Runnable() { |
| 180 @Override |
| 181 public void run() { |
| 182 eventListener.onTCPConnected(isServer()); |
| 183 } |
| 184 }); |
| 185 |
| 186 while (true) { |
| 187 final String message; |
| 188 try { |
| 189 message = in.readLine(); |
| 190 } catch (IOException e) { |
| 191 reportError("Failed to read from rawSocket: " + e.getMessage()); |
| 192 break; |
| 193 } |
| 194 |
| 195 // No data received, rawSocket probably closed |
| 196 if (message == null) { |
| 197 break; |
| 198 } |
| 199 |
| 200 executor.execute(new Runnable() { |
| 201 @Override |
| 202 public void run() { |
| 203 Log.v(TAG, "Receive: " + message); |
| 204 eventListener.onTCPMessage(message); |
| 205 } |
| 206 }); |
| 207 } |
| 208 |
| 209 Log.d(TAG, "Receiving thread exiting..."); |
| 210 |
| 211 // Close the rawSocket if it is still open |
| 212 disconnect(); |
| 213 } |
| 214 |
| 215 /** |
| 216 * Closes the rawSocket if it is still open. Also fires the onTCPClose event
. |
| 217 */ |
| 218 public void disconnect() { |
| 219 try { |
| 220 synchronized (rawSocketLock) { |
| 221 if (rawSocket != null) { |
| 222 rawSocket.close(); |
| 223 rawSocket = null; |
| 224 out = null; |
| 225 |
| 226 eventListener.onTCPClose(); |
| 227 } |
| 228 } |
| 229 } catch (IOException e) { |
| 230 reportError("Failed to close rawSocket: " + e.getMessage()); |
| 231 } |
| 232 } |
| 233 |
| 234 /** |
| 235 * Sends a message on the socket. Should only be called on the executor thre
ad. |
| 236 */ |
| 237 public void send(String message) { |
| 238 Log.v(TAG, "Send: " + message); |
| 239 |
| 240 synchronized (rawSocketLock) { |
| 241 if (out == null) { |
| 242 reportError("Sending data on closed socket."); |
| 243 return; |
| 244 } |
| 245 |
| 246 out.write(message + "\n"); |
| 247 out.flush(); |
| 248 } |
| 249 } |
| 250 } |
| 251 |
| 252 private class TCPSocketServer extends TCPSocket { |
| 253 // Server socket is also guarded by rawSocketLock. |
| 254 private ServerSocket serverSocket; |
| 255 |
| 256 final private InetAddress address; |
| 257 final private int port; |
| 258 |
| 259 public TCPSocketServer(InetAddress address, int port) { |
| 260 this.address = address; |
| 261 this.port = port; |
| 262 } |
| 263 |
| 264 /** Opens a listening socket and waits for a connection. */ |
| 265 @Override |
| 266 public Socket connect() { |
| 267 Log.d(TAG, "Listening on [" + address.getHostAddress() + "]:" + Integer.to
String(port)); |
| 268 |
| 269 synchronized (rawSocketLock) { |
| 270 if (serverSocket != null) { |
| 271 Log.e(TAG, "Server rawSocket was already listening and new will be ope
ned."); |
| 272 } |
| 273 |
| 274 try { |
| 275 serverSocket = new ServerSocket(port, 0, address); |
| 276 return serverSocket.accept(); |
| 277 } catch (IOException e) { |
| 278 reportError("Failed to receive connection: " + e.getMessage()); |
| 279 return null; |
| 280 } |
| 281 } |
| 282 } |
| 283 |
| 284 /** Closes the listening socket and calls super. */ |
| 285 @Override |
| 286 public void disconnect() { |
| 287 try { |
| 288 synchronized (rawSocketLock) { |
| 289 if (serverSocket != null) { |
| 290 serverSocket.close(); |
| 291 serverSocket = null; |
| 292 } |
| 293 } |
| 294 } catch (IOException e) { |
| 295 reportError("Failed to close server socket: " + e.getMessage()); |
| 296 } |
| 297 |
| 298 super.disconnect(); |
| 299 } |
| 300 |
| 301 @Override |
| 302 public boolean isServer() { |
| 303 return true; |
| 304 } |
| 305 } |
| 306 |
| 307 private class TCPSocketClient extends TCPSocket { |
| 308 final private InetAddress address; |
| 309 final private int port; |
| 310 |
| 311 public TCPSocketClient(InetAddress address, int port) { |
| 312 this.address = address; |
| 313 this.port = port; |
| 314 } |
| 315 |
| 316 /** Connects to the peer. */ |
| 317 @Override |
| 318 public Socket connect() { |
| 319 Log.d(TAG, "Connecting to [" + address.getHostAddress() + "]:" + Integer.t
oString(port)); |
| 320 |
| 321 try { |
| 322 return new Socket(address, port); |
| 323 } catch (IOException e) { |
| 324 reportError("Failed to connect: " + e.getMessage()); |
| 325 return null; |
| 326 } |
| 327 } |
| 328 |
| 329 @Override |
| 330 public boolean isServer() { |
| 331 return false; |
| 332 } |
| 333 } |
| 334 } |
OLD | NEW |