| /* | |
| * 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 androidx.annotation.Nullable; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStreamWriter; | |
| import java.io.PrintWriter; | |
| import java.net.InetAddress; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.net.UnknownHostException; | |
| import java.nio.charset.Charset; | |
| import java.util.concurrent.ExecutorService; | |
| import org.webrtc.ThreadUtils; | |
| /** | |
| * 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 ExecutorService executor; | |
| private final ThreadUtils.ThreadChecker executorThreadCheck; | |
| 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( | |
| ExecutorService executor, TCPChannelEvents eventListener, String ip, int port) { | |
| this.executor = executor; | |
| executorThreadCheck = new ThreadUtils.ThreadChecker(); | |
| executorThreadCheck.detachThread(); | |
| 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() { | |
| executorThreadCheck.checkIsOnValidThread(); | |
| socket.disconnect(); | |
| } | |
| /** | |
| * Sends a message on the socket. | |
| * | |
| * @param message Message to be sent. | |
| */ | |
| public void send(String message) { | |
| executorThreadCheck.checkIsOnValidThread(); | |
| 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); | |
| } | |
| }); | |
| } | |
| /** | |
| * 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; | |
| @Nullable | |
| private PrintWriter out; | |
| @Nullable | |
| private Socket rawSocket; | |
| /** | |
| * Connect to the peer, potentially a slow operation. | |
| * | |
| * @return Socket connection, null if connection failed. | |
| */ | |
| @Nullable | |
| 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( | |
| new OutputStreamWriter(rawSocket.getOutputStream(), Charset.forName("UTF-8")), true); | |
| in = new BufferedReader( | |
| new InputStreamReader(rawSocket.getInputStream(), Charset.forName("UTF-8"))); | |
| } 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. | |
| @Nullable | |
| 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. */ | |
| @Nullable | |
| @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. */ | |
| @Nullable | |
| @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; | |
| } | |
| } | |
| } |