jueves, 13 de junio de 2013

Websocket Android-PC

Pues bien, parece que como bloggero me muero de hambre, pero que se le va a hacer, el trabajo se ha puesto muy pesado. Lo bueno es que es durante estos periodos de trabajo extenuante cuando mas se aprende.

La aplicación que estamos desarrollando en el trabajo implica comunicación de una página web en la computadora con una aplicación en el dispositivo Android, conectado con el cable USB y utilizando el modo Debug, en un principio intenté utilizando un applet cargado en la página web, que se conectara como cliente por medio de Sockets a un servidor en la aplicación Android, funcionó bien en mi página de pruebas, pero cuando lo pasé al sitio web real tenía problemas al pasar de una página a otra, es decir, al desconectar un cliente y conectar otro, ademas el uso de applets implica que se tenga instalada una máquina virtual de java en la computadora, que se tenga que firmar el applet y que el usuario deba dar permisos, cosa que no le gustó al super-jefe.

Por esta razón seguí investigando y encontré que se podía utilizar Web Sockets con lenguaje javaScript y aquí está el resultado.

Para empezar, en el proyecto android se importa la librería  java_websocket.jar

Después, adapté el código de AutobahnServerTest.java para utilizarlo como una clase en el proyecto android, quedando así el código:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.jpiedra.utilities;

import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Collection;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

public class ServerWebSocket extends WebSocketServer {
 private static int counter = 0;

 public ServerWebSocket(int port) throws UnknownHostException {
  super(new InetSocketAddress(port));
 }

 @Override
 public void onOpen(WebSocket conn, ClientHandshake handshake) {
  counter++;
  System.out.println("///////////Opened connection number" + counter);
 }

 @Override
 public void onClose(WebSocket conn, int code, String reason, boolean remote) {
  System.out.println("closed");
 }

 @Override
 public void onError(WebSocket conn, Exception ex) {
  System.out.println("Error:");
  ex.printStackTrace();
 }

 @Override
 public void onMessage(WebSocket conn, String message) {
  conn.send(message);
 }

 @Override
 public void onMessage(WebSocket conn, ByteBuffer blob) {
  conn.send(blob);
 }

 /**
  * Sends <var>text</var> to all currently connected WebSocket clients.
  * 
  * @param text
  *            The String to send across the network.
  * @throws InterruptedException
  *             When socket related I/O errors occur.
  */
 public void sendToAll(String text) {
  Collection<WebSocket> con = connections();
  synchronized (con) {
   for (WebSocket c : con) {
    c.send(text);
   }
  }
 }
}

Si revisan el código original verán que los cambios son mínimos:

    Queda un único constructor que recibe un puerto.
    Se elimina la función onWebsocketMessageFragment
    Se elimina la función main
    Se cambia el nombre para entender mejor mi código :)
   
A continuación se crea el archivo SocketConnection.java con el siguiente código


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.jpiedra.utilities;

import java.net.UnknownHostException;
import java.util.Observable;

import org.java_websocket.WebSocket;

import com.jpiedra.utilities.ServerWebSocket;

/**
 * The Class SocketConnection.
 * @Autor J Jesús Piedra Chávez
 * @Version 1.0 (05-22-2013)
 */

public class SocketConnection extends Observable {

 private final int PORT = 38300;
 protected ServerWebSocket server;
 public String contentInfo;
 private static SocketConnection socketConnection = null;

 /**
  * Instantiates a new server
  */
 protected SocketConnection() {

  try {
   server = new ServerWebSocket(PORT) {
    public void onMessage(WebSocket conn, String message) {
     contentInfo = message;
     setChanged();
     notifyObservers();
    }
   };
   server.start();
  } catch (UnknownHostException e) {
   
   e.printStackTrace();
  }
 }

 static public SocketConnection getInstance() {
  
  if (socketConnection == null) {
   socketConnection = new SocketConnection();
  }
  return socketConnection;
 }

 /**
  * Send data. Send the message
  * 
  * @param message
  *            the information
  */
 public void sendData(String message) throws Exception {

  server.sendToAll(message);
 }
}

Para este también me base en un ejemplo de chat java usando Sockets, pero ya no me acuerdo de donde lo saqué y la verdad es que ya está muy diferente :)

Los puntos importantes en este código son:

    La clase extiende Observable para poder comunicarse con cualquier vista, agregada previamente a sus observers.
    Se sobreescribe el metodo onMessage del objeto server, para poder hacer algo cuando se recibe un mensaje.
    Se usa una función getInstance() para tener una sola instancia de la clase, es decir un singleton.
   
En el .java de la vista que hará algo con los mensajes recibidos y que enviará mensajes tenemos este código:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import com.jpiedra.utilities.SocketConnection;


/**
 * The Class ScreenSaverActivity.
 * 
 * @Autor J Jesús Piedra Chávez
 * @Version 1.0 (05-22-2013)
 */

public class ScreenSaverActivity extends Activity implements Observer {

 public SocketConnection socketConnection;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_screensaver);

  socketConnection = SocketConnection.getInstance();
  socketConnection.deleteObservers();
  socketConnection.addObserver(this);
 }

@Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }

 @Override
 public void update(Observable observable, Object data) {

        }

    En este se declara a la vista en cuestión como observer del objeto socketConnection.
    Se usa el método update(Observable observable, Object data) para recibir mensajes, es aqui donde entra la funcionalidad del observer. En este caso el mensaje recibido se almacena en la variable socketConnection.contentInfo, es esta información la que debemos manipular.
    Para enviar mensajes se usa el metodo sendData(message) del objeto socketConnection.
   
Por último, en un archivo javaScript, o directamente en un script dentro de la página, se coloca el siguiente código:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var ws;

window.onload = function() {
 if (!window.WebSocket) {
  alert("FATAL: WebSocket not natively supported.");
 } else {
  connect();
 }
};

window.onunload = function() {
 disconnect();
};

function connect() {
 ws = new WebSocket("ws://127.0.0.1:38300");
 ws.onopen = function() {
  androidStatus("READY");
 };
 ws.onmessage = function(e) {
  receiveMessage(e.data);
 };
 ws.onclose = function() {
  androidStatus("STOPPED");
  ws = null;
 };
}

function sendMessage(message) {
 if (ws) {
  ws.send(message);
 }
}

function disconnect() {
 if (ws) {
  ws.close();
  ws = null;
 }
}

function receiveMessage(message) {

}

function androidStatus(message) {

}

    En window.onload se crea la conección.
    La función androidStatus se ejecuta al establecer conección, con el mensaje READY, y al desconectarse, con el mensaje STOPPED.
    Se usa la función sendMessage(message) para enviar mensajes.
    Se usa la función receivedMessage(message) para manipular los mensajes recibidos, desde aquí podemos por ejemplo lanzar una alerta con este mensaje :)

Como mencioné al principio, para que todo esto funcione el dispositivo Android debe estar en modo debug, además debe ejecutarse el siguiente comando ADB:

adb forward tcp:38300 tcp:38300 cada vez que se reconecte el dispositivo.

La aplicación en el dispositivo Android debe estar iniciada antes de abrir la página web.


Espero que lessea de ayuda.

P.D. El código lo puse con ayuda de una página llamada Source code beautifier, está chida :)