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.
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
Dale un nombre al cliente si quieres distinguirlo de los demás clientes en el servidor.
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.
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.
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).
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í