Article updated on

Java Sockets Test de Rendimiento Cliente/Servidor (Bidireccional Asíncrono)

Me pidieron desarrollar un servicio que pudiera enviar y recibir peticiones asíncronamente. El servicio era de tipo TCP/IP y tenía que poder soportar unos 300 usuarios simultaneamente enviando o recibiendo de 50 a 100 mensajes por segundo.

A pesar de que Java usa el SO para manejar hilos, tenía dudas sobre el rendimiento, consumo de CPU, memoria y demás.

Uso Open JDK 1.6 con un Pentium 4 Dual Core at 2.8Ghz.

1 - Servidor

Características:

  • Varios clientes conectados a un solo servidor.
  • El servidor envía mensajes a los clientes conectados de forma aleatoria.
  • Cada cliente se maneja con un hilo.
  • Se usa un ConcurrentHashMap para guardar todos los objetos de conexión de los clientes.
  • Cuando se desconectar todos los clientes se muestran las estadísticas del servidor.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class Server {
    private static final int PORT = 9001;
    private static final long PAUSE_BETWEEEN_MSGS = 10; // millisecs
    // a map to store all the ThreadedCliHandlers
    private static ConcurrentHashMap<String, ThreadCliHandler> chm
        = new ConcurrentHashMap<String, ThreadCliHandler>();
    // for statistics
    private static int msg = 0;
    private static Date date = new Date();
    private static int threads_created = 0;
    public static void main(String[] args) throws Exception {
        System.out.println("Server OK port on " + PORT);
        ServerSocket socketServer = new ServerSocket(PORT);
        sendMsgsToRandomClients();
        try {
            while (true) {
                threads_created++;
                new ThreadCliHandler(socketServer.accept()).start();
            }
        } finally {
            socketServer.close();
        }
    }
    /**
     * This method sends messages to a random outPutStream
     */
    private static void sendMsgsToRandomClients() {
        new Thread("Send-to-Clients") {            
            public void run() {
                try {
                    boolean showInfo = true;
                    while (true) {
                        Random generator = new Random();
                        for (; chm.size() > 0; msg++) {
                            //gets a random Key from the list
                            String randomKey = new ArrayList<String>(
                                    chm.keySet()).get(generator.nextInt(chm
                                    .keySet().size()));
                            ThreadCliHandler cc = chm.get(randomKey);
                            //sends the message
                            if (!cc.socket.isClosed()) {
                                cc.out.println("From server to client "
                                        + randomKey + " MSG sent: " + msg + "\r");
                                cc.out.flush();
                            } else {
                                chm.remove(randomKey);
                            }
                            Thread.sleep(PAUSE_BETWEEEN_MSGS);
                            showInfo = true;
                        }
                        Thread.sleep(PAUSE_BETWEEEN_MSGS);
                        if (showInfo) {
                            System.out.println(
                                    "Array size: "     + chm.keySet().size()
                                    + " msgs sent: " + msg
                                    + " threads-created: " + threads_created
                                    + " server up since: "+ date);
                            showInfo = false;
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
    /**
     * All the requests received by the server are handled by a ThreadCliHandler
     */
    private static class ThreadCliHandler extends Thread {
        private Socket socket;
        private BufferedReader in;
        private PrintWriter out;
        private String nameSocket = "";
        public ThreadCliHandler(Socket socket) {
            this.socket = socket;
        }
        public void run() {
            try {
                // Create streams for the socket.
                in = new BufferedReader(new InputStreamReader(
                        socket.getInputStream()));
                out = new PrintWriter(socket.getOutputStream(), true);
                out.print("Insert Client Name:");
                out.flush();
                // The first input line is the client name in this example
                nameSocket = in.readLine();
                // stores the hander in the list
                chm.put(nameSocket, this);
                System.out.println("SRV-REC new client: " + nameSocket);
                while (true) {
                    String inLine = in.readLine();
                    // its only null if something went wrong
                    if (inLine != null && inLine.toLowerCase().indexOf("bye") == -1) {
                        System.out.println("SRV-REC " + nameSocket + ": "
                                + inLine);
                    } else {
                        System.out.println("SRV-REC " + nameSocket + ": "
                                + inLine);
                        break;
                    }
                }
            } catch (SocketException es) {
                // happens if the client disconnects unexpectedly
                System.out.println(es);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                chm.remove(nameSocket);
                try {
                    socket.close();
                } catch (IOException e) {}
            }
        }
    }
}

 

1.1 Arrancar el Servidor

Compila y Arranca el servidor. Si es desde consola javac Server.java para compilar y  Java Server para ejecutar. Si todo va bien deberías ver.

img/0/61/1.png

1.2 Prueba el Servidor

Usa telnet o similar para verificar que el cliente funciona. Telnet the Server ex. telnet localhost 9001 . (Si usas windows mas reciente que el XP tendrás que instalarlo si no puedes pasar a la parte del cliente) #client

img/0/61/2.png

Dale un nombre al cliente si quieres distinguirlo de los demás clientes en el servidor.

img/0/61/telnet-reciviendo.png

Escribe "bye" y pulsa Entrar para cerrar la sesión con el servidor.  Puedes usar tantos clientes telnet como quieras desde diferentes ordenadores o el mismo. Cuando se terminan todas las conexiones con el servidor, este muestra los resultados.

2 - Cliente

Características:

  • Crea un número de conexiones al servidor. Aleatoriamente elige una de esas conexiones y envía un mensaje al servidor.
  • Cada cliente puede recibir mensajes del servidor.
  • Bloquea el hilo principal hasta que los clientes comienzan a enviar mensajes usando los métodos wait() y notifyAll().
  • Usa el join() para hacer que el hilo principal espere hasta que los clientes terminen.
  • Se puede ajustar la duración del test.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
class Client {    
    private static final String HOST = "localhost";
    private static final int PORT = 9001;
    private static final int NUM_CLIENTS = 100;
    private static final long PAUSE_BETWEEEN_MSGS = 10;
    private static final long TEST_DURATION = 1000*60; //(millis)    
    private static int msgsSent = 0;
    private static int msgsRec = 0;
    //Map to store all clients
    private static ConcurrentHashMap<String, ThreadCliHandler> chm
                = new ConcurrentHashMap<String, ThreadCliHandler>();
    public static void main(String args[]) {
        long l = System.currentTimeMillis();
        //creates the socket connections to the server
        for (int i = 0; i < NUM_CLIENTS; i++) {
            new ThreadCliHandler("Client " + i).start();
        }
        Lock.waitThread();        
        //this code sends msgs to the server through a randomly chosen client PrintWriter
        Random generator = new Random();
        while(System.currentTimeMillis()<(l+TEST_DURATION)){                        
            String randomKey = new ArrayList<String>(chm.keySet()).get(generator.nextInt(chm.keySet().size()));                    
            ThreadCliHandler cc = chm.get(randomKey);
            if(!cc.socket.isClosed()){
                cc.out.println("from client "+ randomKey + " to Server MSG "+ msgsSent++);
                cc.out.flush();
            }
            try {
                Thread.sleep(PAUSE_BETWEEEN_MSGS);
            } catch (InterruptedException e) {e.printStackTrace();}
        }    
        //tells the server that it's going to disconnect
        for (ConcurrentHashMap.Entry<String, ThreadCliHandler> entry : chm.entrySet()) {
            ThreadCliHandler cc = entry.getValue();
            if(!cc.socket.isClosed()){
                cc.out.println("BYE");
                cc.out.flush();
                try {
                    //waits for all ThreadCliHandlers to die
                    cc.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }                
            }
        }    
        System.out.println("Threads: "+ chm.keySet().size()
                + " msgs-sent:"+ msgsSent  
                + " msgs-received:"+ msgsRec
                + " average msgs-sent/sec:" + (msgsSent*1000/(System.currentTimeMillis() - l))
                + " average msgs-received/sec:" + (msgsRec*1000/(System.currentTimeMillis() - l))
                + " time: "+ (System.currentTimeMillis() - l));
    }
    //Handles every socket client incoming messages in one single thread
    private static class ThreadCliHandler extends Thread {
        private Socket socket;
        private BufferedReader in; // AutoFlush
        private PrintWriter out;
        private String clientName;
        public ThreadCliHandler(String clientName) {
            this.clientName = clientName;
        }
        public void run() {
            try {
                socket = new Socket(HOST, PORT);
                in = new BufferedReader(new InputStreamReader(
                        socket.getInputStream()));
                out = new PrintWriter(new OutputStreamWriter(
                        socket.getOutputStream()));
                // sends the clientName to the server
                out.println(clientName);
                out.flush();
                chm.put(clientName, this);
                // to notify the main thread of changes in clientsMap
                Lock.unlock();
                String inLine = null;
                while (true) {
                    // its only null if something went wrong
                    inLine = in.readLine();
                    if (inLine == null || inLine.indexOf("BYE") > -1) {
                        break;
                    } else {
                        System.out.println("CLI-REC: " + inLine);
                        msgsRec++;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    chm.remove(Thread.currentThread().getName());
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(clientName + " ends..");
        }
    }
}
class Lock {
    private static boolean isRunning = false;    
    public static synchronized void unlock() {
        if(isRunning)
            return;    
        isRunning = true;
        Lock.class.notifyAll();
    }
    public static synchronized void waitThread(){
        while(!isRunning){
            try {
                Lock.class.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            isRunning = true;
        }
    }
}

 

2.1 Ejecuta el cliente

Compila y Arranca el cliente. Si es desde consola javac Server.java para compilar y  Java Server para ejecutar. Si todo va bien deberías ver.

img/0/61/client-reciviendo.png

Ajusta los parámetros de los clientes según lo necesites. Cuando el cliente termina deberías ver los resultados del test. Por defecto está establecido a 100 clientes con una pausa de 10 milisegundos.

img/0/61/resultado.png

Resultados

He probado este ejemplo con 2 máquinas diferentes y normalmente obtengo unos 95 mensajes/segundo con 500 usuarios simultáneo. La CPU no sube dramáticamente con estos parámetros. Mi límite esta en unos 800 mensajes/segundo con 1000 usuarios concurrentes (borrar el  thread sleep  en el cliente y servidor).

img/0/61/r1.png

img/0/61/r2.png

Si usas un sólo hilo exclusivamente por cada uno de los sockets que se conecte se incrementa el uso de la memoria y de la CPU consumida. Si necesitas una gran cantidad de usuarios concurrentes usa java.nio package. Se supone que eleva la latencia (no demasiado en mi caso) pero reduce la CPU y la memoria consimida. Si quieres un ejemplo de Servidor NIO aquí.

Conclusiones y Notas

Con este ejemplo se que podría desarrollar un servicio socket en java que cumple con los requisitos de 300 usuarios simultáneos a 100 mensajes por segundo sin sobrecargar el SO.

Sé que este ejemplo se podrían mejorar varias cosas para mejorar la eficiencia y rendimiento, pero el código se estaba haciendo demasiado grande. Si tienes alguna idea o sugerencia aquí