Java tutorial
/* Copyright (C) 2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package net.carlh.toast; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ConnectException; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Client { /** True if all threads should stop */ private AtomicBoolean stop = new AtomicBoolean(false); /** True if we're fairly sure we're connected */ private AtomicBoolean connected = new AtomicBoolean(false); /** Mutex to protect changes to the value of socket */ private final Object mutex = new Object(); /** Socket that we are talking to the server with */ private Socket socket; /** Thread to read data from socket */ private Thread readThread; /** Thread to write data to socket */ private Thread writeThread; /** Lock and condition to tell the write thread that there is something to do */ private final Lock lock = new ReentrantLock(); private final Condition writeCondition = lock.newCondition(); /** Things that need writing */ private ArrayList<String> toWrite = new ArrayList<String>(); /** Thread to ping the server */ private Thread pingThread; private AtomicBoolean ping = new AtomicBoolean(false); /** True if we have received a pong reply to our last ping */ private AtomicBoolean pong = new AtomicBoolean(false); /** Handlers that will be told about incoming commands */ private ArrayList<Handler> handlers = new ArrayList<Handler>(); /** Ping interval in ms (must be less than timeout) */ private int pingInterval = 4000; /** Socket timeout in ms */ private int timeout = 5000; public void start(final String hostName, final int port) throws java.net.UnknownHostException, java.io.IOException { /* Thread to read stuff from the server */ readThread = new Thread(new Runnable() { private byte[] getData(Socket socket, int length) { byte[] d = new byte[length]; int offset = 0; while (offset < length) { try { int t = socket.getInputStream().read(d, offset, length - offset); if (t == -1) { break; } offset += t; } catch (SocketException e) { /* This is probably because the socket has been closed in order to make this thread terminate. */ Log.e("Toast", "SocketException in client.getData", e); break; } catch (IOException e) { Log.e("Toast", "IOException in Client.getData()", e); break; } } return java.util.Arrays.copyOf(d, offset); } public void run() { while (!stop.get()) { try { synchronized (mutex) { /* Connect */ socket = new Socket(hostName, port); socket.setSoTimeout(timeout); } /* Keep going until there is a problem on read */ while (true) { byte[] b = getData(socket, 4); if (b.length != 4) { break; } int length = ((b[0] & 0xff) << 24) | ((b[1] & 0xff) << 16) | ((b[2] & 0xff) << 8) | (b[3] & 0xff); if (length < 0 || length > (256 * 1024)) { /* Don't like the sound of that */ Log.e("Toast", "Strange length " + length); break; } byte[] d = getData(socket, length); if (d.length != length) { break; } try { handler(new JSONObject(new String(d))); } catch (JSONException e) { Log.e("Toast", "Exception " + e.toString()); } } synchronized (mutex) { /* Close the socket and go back round to connect again */ socket.close(); socket = null; } } catch (ConnectException e) { Log.e("Toast", "ConnectException"); } catch (UnknownHostException e) { Log.e("Toast", "UnknownHostException"); } catch (IOException e) { Log.e("Client", "IOException"); } finally { try { Thread.sleep(timeout); } catch (java.lang.InterruptedException e) { } } } } }); readThread.start(); /* Thread to send stuff to the server */ writeThread = new Thread(new Runnable() { public void run() { while (!stop.get()) { lock.lock(); try { while (toWrite.size() == 0 && !stop.get()) { writeCondition.await(); } } catch (InterruptedException e) { } finally { lock.unlock(); } String s = null; lock.lock(); if (toWrite.size() > 0) { s = toWrite.get(0); toWrite.remove(0); } lock.unlock(); synchronized (mutex) { try { if (socket != null && s != null) { socket.getOutputStream().write((s.length() >> 24) & 0xff); socket.getOutputStream().write((s.length() >> 16) & 0xff); socket.getOutputStream().write((s.length() >> 8) & 0xff); socket.getOutputStream().write((s.length() >> 0) & 0xff); socket.getOutputStream().write(s.getBytes()); } } catch (IOException e) { Log.e("Toast", "IOException in write"); } } } } }); writeThread.start(); /* Thread to send pings every so often */ pingThread = new Thread(new Runnable() { public void run() { while (!stop.get()) { if (ping.get() == true && pong.get() == false) { for (Handler h : handlers) { h.sendEmptyMessage(0); } setConnected(false); } pong.set(false); try { JSONObject json = new JSONObject(); json.put("type", "ping"); send(json); ping.set(true); Thread.sleep(pingInterval); } catch (JSONException e) { } catch (InterruptedException e) { } } } }); pingThread.start(); } private void handler(JSONObject json) { try { if (json.has("type") && json.get("type").equals("pong")) { setConnected(true); pong.set(true); } else { for (Handler h : handlers) { Message m = Message.obtain(); Bundle b = new Bundle(); b.putString("json", json.toString()); m.setData(b); h.sendMessage(m); } } } catch (JSONException e) { } } /** Send to server */ public void send(JSONObject json) { lock.lock(); try { toWrite.add(json.toString()); writeCondition.signal(); } finally { lock.unlock(); } } public void stop() { stop.set(true); /* Wake the write thread so it notices that we want to stop */ lock.lock(); try { writeCondition.signal(); } finally { lock.unlock(); } /* This interrupts blocking reads in the read thread */ try { synchronized (mutex) { if (socket != null) { socket.close(); } } } catch (IOException e) { } /* Interrupt the ping thread */ pingThread.interrupt(); /* Interrupt sleeps in the read thread */ readThread.interrupt(); try { readThread.join(); writeThread.join(); pingThread.join(); } catch (InterruptedException e) { } } /** Add a handler which will be called with an empty message * when connection state changes, or a message containing * a JSON block (with key "json"). */ void addHandler(Handler handler) { handlers.add(handler); } private void setConnected(boolean c) { if (c == connected.get()) { return; } connected.set(c); for (Handler h : handlers) { h.sendEmptyMessage(0); } } public boolean getConnected() { return connected.get(); } }