martes, 23 de marzo de 2021

Java Socket (Comunicación entre servidores). Comunicación asíncrona (en tiempo real)

 0. Introducción

Para establecer comunicación mediante java sockets, se requiere un servidor que esté a la escucha y un cliente que lo llame.

El servidor puede responder y se puede establecer un diálogo entre cliente y servidor.

Hay que definir un número de puerto de conexión

Se puede implementar cliente y servidor en la misma máquina o en máquinas distintas.

Lo importante a remarcar és como se ejecuta un proceso en modo ASÍNCRONO y como se utilizan "Piped Streams", para recoger la información generada por el proceso a medida que se ejecuta.


1. pom.xml

Veamos el fichero

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>ximodante</groupId>
  <artifactId>SocketServer</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>SocketServer</name>
  <description>SocketServer</description>
  
  <properties>
    <maven.compiler.source>15</maven.compiler.source> <!-- 13,12 -->
    <maven.compiler.target>15</maven.compiler.target> <!-- 13, 12 -->

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  
    <lombok.version>1.18.18</lombok.version>
    <commons-io.version>2.8.0</commons-io.version><!-- abans 2.6 -->
    <commons-exec.version>1.3</commons-exec.version>
    <lang3.version>3.11</lang3.version> <!-- OK Antes 3.10, 3.9 -->
    
  </properties>
  
  <dependencies>
    <dependency>
	  <groupId>org.projectlombok</groupId>
	  <artifactId>lombok</artifactId>
	  <version>${lombok.version}</version>
	  <scope>provided</scope>
	</dependency>
	
    <dependency>
	  <groupId>commons-io</groupId>
	  <artifactId>commons-io</artifactId>
   	  <version>${commons-io.version}</version>
	</dependency>
	
    <dependency>
	  <groupId>org.apache.commons</groupId>
	  <artifactId>commons-lang3</artifactId>
	  <version>${lang3.version}</version>
	</dependency>
	
	 <dependency>
	  <groupId>org.apache.commons</groupId>
	  <artifactId>commons-exec</artifactId>
	  <version>${commons-exec.version}</version>
	</dependency>
    
  </dependencies>

  <!-- COMPILER PLUGIN -->
	<!-- This is OK http://crunchify.com/how-to-create-build-java-project-including-all-dependencies-using-maven-maven-resources-maven-dependency-maven-jar-plugin-tutorial/ -->
	<build>
		<pluginManagement>
			<plugins>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.8.1</version>
					<configuration>
						<!-- <release>13</release> -->

						<!-- compilerArgs>..enable-preview</compilerArgs --> <!-- CAMBIAR . POR - -->


						<showWarnings>true</showWarnings>
						<showDeprecation>true</showDeprecation>
					</configuration>

				</plugin>
			</plugins>
		</pluginManagement>

		<plugins>
			<plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.2.0</version>
				<executions>
					<execution>
						<id>copy-resources</id>
						<phase>validate</phase>
						<goals>
							<goal>copy-resources</goal>
						</goals>
						<configuration>
							<outputDirectory>${basedir}/target/MiCarpeta</outputDirectory>
							<resources>
								<resource>
									<directory>resources</directory>
									<filtering>true</filtering>
								</resource>
							</resources>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<version>3.1.2</version>
				<executions>
					<execution>
						<id>copy-dependencies</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/MiCarpeta/lib</outputDirectory>
							<overWriteReleases>false</overWriteReleases>
							<overWriteSnapshots>false</overWriteSnapshots>
							<overWriteIfNewer>true</overWriteIfNewer>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>3.2.0</version>
				<configuration>
					<archive>
						<manifest>
							<addClasspath>true</addClasspath>
							<classpathPrefix>lib/</classpathPrefix>
							<mainClass>u.requests.sockets.SimpleSocketServer</mainClass>
						</manifest>
						<manifestEntries>
							<Class-Path>.</Class-Path>
						</manifestEntries>
					</archive>

					<finalName>MiCarpeta/socketserver</finalName>
				</configuration>
			</plugin>
		</plugins>

	</build>

</project>

Solamente remarcar:

  • versión de java
  • librerias
  • compilación a jar

2. El cliente (SimpleSocketClienteExample.java)

Esta clase es la del cliente que simplemente necesita:

1. Saber la IP del servidor y el puerto
2. Tener un canal de entrada (o recepción) de los mensajes del servidor 
3. Un canal de salida para enviar mensajes al servidor
4. Un código identificador en la respuesta para saber que tiene que hay quye terminar la conexión.


 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package u.requests.sockets;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import lombok.NoArgsConstructor;


/**
 * A client caller to server
 * @author eduard
 *
 */
@NoArgsConstructor
public class SimpleSocketClientExample {
	private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public SimpleSocketClientExample(String ip, int port) {
    	
    }
    /**
     * starts a connection to the server to a port
     * @param ip
     * @param port
     * @throws UnknownHostException
     * @throws IOException
     */
    public void startConnection(String ip, int port) throws UnknownHostException, IOException {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) throws IOException {
    	System.out.println("Sending to server:"+ msg);
    	out.println(msg);
        
        //out.println("repeat "+msg);
        String resp="";
        String line = in.readLine();
        if (line!=null) resp=resp + "\n" + line;
        while (line != null && line.length() > 0 ) { 
        	resp=resp + "\n" + line;
        	System.out.println("linea client:"+line);
        	line = in.readLine();
        }	
        //out.println(""); // Para terminar rl dendfmessage
        return resp;
    }

    public void stopConnection() throws IOException {
        in.close();
        out.close();
        clientSocket.close();
    }
    
    public static void main (String[] args) throws IOException {
    	SimpleSocketClientExample client = new SimpleSocketClientExample();
    	String ip="192.168.10.5";
    	int port=6666;
        //client.startConnection("127.0.0.1", 6666);
    	if (args!=null && args.length==2) {
    		ip=args[0];
    		port=Integer.parseInt(args[1]);
    	}
    	client.startConnection(ip, port);
    	String response ="";
    	for (int i=0;i<3;i++) {
    		response = client.sendMessage("hello serverKK-->"+i);
    		 
    		//assertEquals("hello client", response);
    		//System.out.println("hello server ->" + response);
    		//response = client.sendMessage("");
    		//System.out.println("hello server ->" + response);
    	}	
    	response = client.sendMessage("");
    	//System.out.println("hello server ->" + response);
        client.stopConnection();
    }
}

3. El servidor


3.1. SimpleSocketServer.java

Es la clase que se dedica a escuchar en el puerto predefinido en "app.properties" , para ello es la clase que se debe ejecutar


package u.requests.sockets;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Properties;

import utils.PropertiesUtils;

/**
 * Only open port for serving messages The important part is the access to
 * ServerRequestHandler03 that manages all the logic
 * 
 * @author ximo
 *
 */
public class SimpleSocketServer extends Thread {
	private boolean isExecutedFromJar = false; // ????? Cambiar al compilar
	private ServerSocket serverSocket;
	private int port;
	private boolean running = false;
	// private String propsFileName="properties"+File.separator+"app.properties";
	private Properties props = null;

	public SimpleSocketServer() {
		port = 6666;
	}

	public void getMyPort() {
		try {
			props = PropertiesUtils.getProperties(isExecutedFromJar, "app");
			port = Integer.parseInt(props.getProperty("socket.port", "" + port));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void startServer() {
		try {
			serverSocket = new ServerSocket(port);
			this.start();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void stopServer() {
		running = false;
		this.interrupt();
	}

	@Override
	public void run() {
		running = true;
		// This is for accepting multiple connections
		while (running) {
			try {
				System.out.println("System.out.println-->Listening for a connection");

				// Call accept() to receive the next connection
				Socket socket = serverSocket.accept();
				// socket.getPort();

				// Pass the socket to the RequestHandler thread for processing
				ServerRequestHandler requestHandler = new ServerRequestHandler(socket, props);
				requestHandler.start();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		SimpleSocketServer server = null;

		try {
			server = new SimpleSocketServer();
			if (args != null) {
				if (args.length > 0) {
					if (args[0].trim().length() > 0)
						server.isExecutedFromJar = true;
				}
			}
			server.getMyPort();
			System.out.println("SimpleSocketServer03.Start server on port: " + server.port);
			server.startServer();
			/*
			 * // Automatically shutdown in 1 minute try { Thread.sleep( 60000 ); } catch(
			 * Exception e ) { e.printStackTrace(); }
			 */
		} catch (Exception e) {
			if (server != null) {
				server.stopServer();
				System.out.println("server stopped");
				e.printStackTrace();
			}
		}
	}
}

Solo remarcar la clase ServerRequestHandler que se encarga de recibir mensajes del ciente y actuar en consecuencia.


3.2. ServerRequestHandler.java

Es la clase que se dedica a interpretar los mensajes de entrada y actuar en consecuencia.

Primeramente se lee el mensaje, que se interpreta como un comando a ejecutar.
Este comando puede ser o un shell script o un programa ejecutable.
El programa ejecutable, si le damos la ruta de java y un jar, nos ejecuta un jar de java

Hay una utilidad llamada CmdUtils que se basa en Apache commons exec para ejecutar programas externos desde java, que permite ejecutar los comandos de forma asíncrona y ademas redirigir la salida de pantalla a un OutputStream

Para poder leer asíncronamente el "OutputSteam", se ha utilizado un PipedOutputStream que se conecta a un PipedInputStream, así cada vez que el programa ejecutado da una salida por pantalla, automáticamente es recibida por el PipedInputStream y por tanto se puede enviar al cliente (mediante el socket ) dicha respuesta.

Pero no se va a enviar toda la información, sino que se hace un filtro de aquellas líneas que tengan determinadas palabras clave como (SHOWMESSAGE o ERROR). así el cliente pude saber en todo momento como anda la ejecución

package u.requests.sockets;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Properties;

import utils.CmdUtils;

class ServerRequestHandler extends Thread {

	private Socket socket;
	private String command = "";
	// private String myPath="";
	private String shellsFolder = "";
	private String jarsFolder = "";
	private String javaPath = "";
	private Properties props = null;
	private Boolean isError = false;
	private BufferedReader inSocket = null;
	private PrintWriter outSocket = null;
	private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

	public ServerRequestHandler(Socket socket, Properties props) {
		this.socket = socket;
		this.props = props;
	}

	@Override
	public void run() {
		PipedOutputStream out = new PipedOutputStream();
		PipedInputStream in = new PipedInputStream();
		try {
			System.out.println("RH-->Received a connection");

			// Get input and output streams
			inSocket = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			outSocket = new PrintWriter(socket.getOutputStream(), true);

			command = inSocket.readLine();
			outSocket.println("4.1 Des del server: Obtenint comandament a executar...(" + command + ")");

			if (command.trim().length() < 2)
				showError("ERROR: Comandament curt:(" + command + ")");
			else {
				try {

					shellsFolder = props.getProperty("shells.folder").trim();
					jarsFolder = props.getProperty("jars.folder").trim();
					javaPath = props.getProperty("java.path").trim();
					// command=myPath.trim() + File.separator+ command;
					command = command.replace("JAVA_PATH", javaPath).replace("SHELLS_FOLDER", shellsFolder)
							.replace("JARS_FOLDER", jarsFolder);
					System.out.println("COMMAND=" + command);
				} catch (Exception e) {
					showError("ERROR : Obrint paràmetres del fitxer de propietats... app.properties" + e.getMessage());
				}
			}
			
			if (!isError) {
				outSocket.println("4.2 Des del server: Executant...(" + command + ")");

				try {
					out.connect(in);
					// Execute and wait for end of execution
					// if(CmdUtils.execProgram(command, null, null, false, outputStream) !=0)
					// showError("ERROR 4: Executant " + command + "... ");
					// Asynchronous
					if (CmdUtils.execProgram(command, null, null, true, out) != 0)
						showError("ERROR 4: Executant " + command + "... ");
				} catch (Exception e) {
					showError("ERROR 4.2: Executant " + command + "... " + e.getMessage());
				}
			}

			
			if (!isError) {
				outSocket.println("4.3 Des del server: Informant ...");
				try {
					byte[] bt = in.readNBytes(1);
					int i = 0;
					String line = "";
					while (bt.length > 0) {

						line = line + (char) bt[0];
						if (line.contains("\n")) {
							String lineUP = line.toUpperCase();
							if (lineUP.contains("SHOWMESS") || lineUP.contains("ERROR")) {
								outSocket.println("4.3." + ++i + "-->" + line);
								System.out.println("4.3." + i + "-->" + line);
							}
							line = "";
						} // else System.out.println("KKKKKKK="+line);

						bt = in.readNBytes(1);
					}
				} catch (Exception e) {
					showError("ERROR 4.3: Executant " + command + "... " + e.getMessage());
				}
			}

			if (!isError)
				outSocket.println("FIN:"); // END OF PROCESS SIGNAL!!

			// Close our connection
			inSocket.close();
			outSocket.close();
			socket.close();

			System.out.println("Rh:->Connection closed");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void showError(String message) {
		isError = true;
		outSocket.println(message);
	}
}

 

3.3. CmdUtils.java


Aunque esta clase ya se vió en una entrada anterior, se va a mostrar igualmente

package utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;



/**
 * Utility class for executing programs locally and remotely
 * 
 * @author eduard
 *
 */
public class CmdUtils {
	/**
	 * 
	 * @param commandLine            Line to execute
	 * @param optSuccessValue        (Optional) If the success return is not equal
	 *                               to "0". For instance Acrobat returns 1
	 * @param optMaxDurationMiliSecs (Optional) Maximum wait until kill the program
	 *                               if blocked,
	 * @param isAsync                True if we want to execute the command
	 *                               asynchronously
	 * @param outputStream           OutputStream to collect the output
	 *                               messages of the program
	 * 
	 * @return
	 * @throws ExecuteException
	 * @throws IOException
	 */
	public static int execProgram(String commandLine, Integer optSuccessValue, Long optMaxDurationMiliSecs,
			Boolean isAsync, OutputStream outputStream) throws ExecuteException, IOException {

		int exitValue = 0;
		ExecuteWatchdog watchdog = null;

		CommandLine cmdLine = CommandLine.parse(commandLine);
		DefaultExecutor executor = new DefaultExecutor();

		if (outputStream != null) {
			PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
			executor.setStreamHandler(streamHandler);
		}

		if (optSuccessValue != null)
			executor.setExitValue(optSuccessValue);
		if (optMaxDurationMiliSecs != null) {
			watchdog = new ExecuteWatchdog(optMaxDurationMiliSecs);
			executor.setWatchdog(watchdog);
		}
		if (isAsync != null && isAsync)
			executor.execute(cmdLine, new DefaultExecuteResultHandler());

		else
			exitValue = executor.execute(cmdLine);

		return exitValue;
	}

	

	
	public static void main(String[] args) {
	   String[] commandLines = { 
              "/usr/bin/java -version", "java -version",
	      "ls /home/eduard/",
	      "/usr/bin/AutoFirma  sign -i /home/ximo/kk/xmlprueba.xml -o /home/ximo/kk/xml_firma103.xsig -format xades -store mozilla -alias EPN1"
	   };
           int i = 0;
	   try {
		for (String s : commandLines) {
		System.out.println(i++ + "-->" + s);
		execProgram(s, null, null, null, outputStream);
	  	int j = 0;
		for (String line : outputStream.toString().split("\\n"))
	 	  System.out.println("Action:" + i + "." + ++j + "-->" + line);
	
	   } catch (Exception e) {
		e.printStackTrace();
	   }
        }		
}