lunes, 9 de julio de 2018

Modificar una cadena en JList y en JComboBox

En clase me han preguntado como puedo modificar una cadena dentro de un componente Swing como ComboBox. Juguemos un poco con estas ideas que son muy interesantes.

Cuando trabajamos en un entorno gráfico con swing y queremos gestionar una lista de elementos tipo String habitualmente usamos un JList o un JComboBox. Esta gestión es muy sencilla en ambos componentes tanto para añadir como para eliminar elementos.

El problema viene cuando tenemos una lista de Strings y queremos modificar uno de ellos. Veamos la situación y la solución.

Trabajo con JList
En el caso del JList es imprescindible usar un modelo asociado, es decir, una colección de elementos que se encarga de gestionar los datos y según cambie dicha colección cambia el aspecto visual del JList. Se podría usar sin modelo pero entonces el JList no es modificable.
Veamos un ejemplo, crea una clase Formulario que herede de JFrame de la siguiente forma:

import java.awt.*;
import javax.swing.*;

public class Formulario extends JFrame {
    JList<String> lst;
    DefaultListModel<String> modeloLista;

    public Formulario()  {
        super();
        setLayout(new FlowLayout());
        
        String[] dias={"Lunes", "Martes", "Miércoles", "Jueves",
                        "Biernes", "Sábado", "Domingo"
        };
        modeloLista= new DefaultListModel<>();
        lst= new JList<>(modeloLista);
        add(lst);
        
        for (String s:dias){
            modeloLista.addElement(s);
        }
    }

}

Si conoces los conceptos básicos del JList el código anterior no debería de tener mayor problema salvo el de las ganas de arrancarse los ojos al ver escrito uno de los días de la semana con B en lugar de con V.

Por supuesto, en una clase aparte escribe el main:

import javax.swing.*;

public class Principal {

public static void main(String[] args) {
            Formulario frame = new Formulario();
        
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(200, 200);   
            frame.setVisible(true);
}

}

Bien ¿Cómo podemos arreglar el problema del Biernes/Viernes? Como sabemos el tipo String es inmutable, es decir, no podemos acceder a su primera posición y cambiar la B por la V. Pero podemos cambiar la palabra entera a través del modelo. Es decir, puedo escribir:

        modeloLista.setElementAt("Viernes", 4);


Y ya estaría corregido. De esta forma el método setElement soluciona nuestro problema de forma muy sencilla.

Trabajo con JComboBox
Tratemos de repetir lo anterior con JComboBox. Lo primero que se nos ocurrirá es usar el JComboBox de forma directa y veremos que se permite añadir y eliminar items pero no modificarlos. Por tanto una solución sería quitar el incorrecto e insertar el correcto. Veámoslo:

public class Formulario2 extends JFrame {
    JComboBox<String> cb;

    public Formulario2()  {
        super();
        setLayout(new FlowLayout());
        
        String[] dias={"Lunes", "Martes", "Miércoles", "Jueves",
                        "Biernes", "Sábado", "Domingo"
        };        
        cb= new JComboBox<>(dias);
        add(cb);
        
        cb.removeItemAt(4);
        cb.insertItemAt("Viernes",4);
    }

}

También muy sencillo. Incluso se podría hacer también a través del modelo.

¡Pero yo quiero cambiar la cadena por dentro!
Pues no puedes. Bueeeno, sí pero entonces tenemos que acudir a otra clase. Es decir, no podemos usar String porque String es inmutable. Esto como ya comenté implica que una vez creado no puede modificarse. Se le podrá asignar un String distinto, pero el previo no puede ser modificado.
Por tanto si necesitamos hacer cambios a bajo nivel en una cadena lo que tenemos que hacer es usar un StringBuilder que es un tipo String pero mutable. Es más incómodo para ciertas tareas pero más versátil y potente.
El siguiente ejemplo muestra como usarlo:

public class Formulario3 extends JFrame {
JComboBox<StringBuilder> cb;

    public Formulario3()  {
        super();
        setLayout(new FlowLayout());
       
        StringBuilder[] dias={
        new StringBuilder("Lunes"),
        new StringBuilder("Martes"), 
        new StringBuilder("Miércoles"), 
        new StringBuilder("Jueves"),
        new StringBuilder("Biernes"),
        new StringBuilder("Sábado"), 
        new StringBuilder("Domingo")
        };        
        cb= new JComboBox<>(dias);
        add(cb);

        cb.getItemAt(4).setCharAt(0,'V');

    }

}

Se puede observar que la creación es más compleja ya que hay que realizar un new por cada cadena . En el caso del String no es necesario porque a un String siempre le asignamos u otro String que ya existe en memoria o un literal String que reserva memoria en tiempo de compilación.
Sin embargo StringBuilder no, es una clase "normal" y hay que reservar memoria para ella, de ahí que se complique la creación del array dias.
Sin embaargo una vez creado el combo con sus cadenas, se puede modificar mediante diversas funciones como setCharAt, insert, delete, etc...
Si en lugar de un carácter quisiéramos cambiar varios podríamos hacer lo siguiente:

        cb.getItemAt(4).deleteCharAt(0);
        cb.getItemAt(4).insert(0,"Viii");

Estas dos líneas sustituirían a la del método setCharAt.

Puedes ver el proyecto para Eclipse en el siguiente enlace.

¡Hasta el próximo post!

miércoles, 4 de julio de 2018

Otra introducción más a Git

¿Y por qué? No porque las entradas que ya existen sean malas, al revés, hay algunas fantásticas como el resumen de DZone o el propio libro editado por Git, pero normalmente son o muy escuetas o excesivas si se desea aprender los cuatro conceptos básicos.

Lo que planteo es una especie de práctica guiada para aprender a usar Git y un repositorio remoto como Bitbucket o GitHub. Es posible que haya alguna ya hecha del estilo o mejor, pero preferí preparar una para mis alumnos y ya de paso dejarla por aquí por si a alguien le viene bien. Siguiendo esta práctica, deberíais entender las bases de Git.

Empezamos:

Git es un sistema de control de versiones diseñado por Linus Torvalds con el objetivo entre otros de coordinar el desarrollo del kernel de Linux en el que participan varios centenares de programadores distribuidos por todo el mundo. Dicho kernel está compuesto actualmente por más de 9 millones de líneas de código y más de 25000 archivos. Todo esto es una prueba clara de que Git es un buen sistema para controlar versiones de software de forma distribuida. 
Si tras terminar este artículo quieres profundizar, existe un libro gratuito que recomiendo seguir para dominar más este sistema.
Una característica de git que lo hace rápido es que por cada cambio guarda una “fotografía” de todos los archivos de un proyecto lo que lo hace rápido a la hora de recuperar archivos de versiones previas al revés que otros que guardan diferencias y deben reconstruir las versiones previas cuando se desea volver a ellas.
La instalación en un sistema Linux con apt u otro gestor de paquetes es simple:
$ sudo apt install git-all

Esto instala tanto git como otras herramientas que nos pueden hacer falta. Debes tener en cuenta que para usar todo el potencial de git, este debe ser usado desde línea de comando. A esto se le añade la ventaja que no dependes de un interfaz de usuario de un IDE.
Para instalarlo en macOS, Windows o si prefieres otra versión en Linux distinta a la de los repositorios ve a la web oficial donde puedes descargarlo.

Identidad
Tras la instalación lo primero que haremos es especificarnos como usuarios del sistema git. Esto es necesario porque por cada actualización que se envía de un archivo (commit) se guarda la identidad. Lo más sencillo es establecer una identidad global para el equipo en el que te encuentres y que la cojan todos los proyectos. 
Esto se hace cambiando las “variables de entorno” de git de la siguiente forma:
$ git config --global user.name "Cirilla de Cintra"
$ git config --global user.email "ciri@kaermorhen.com"

Si en un momento dado para un proyecto particular se quieren cambiar las credenciales simplemente se ejecuta el comando anterior sin el modificador –global.

Repositorio de proyecto
A continuación se le indica a git que proyecto me puede interesar tener gestionado. Para ello simplemente me voy al directorio de proyecto y ejecuto el comando:
$ git init

Esto crea un directorio .git donde se va a almacenar toda la información necesaria para el control de versiones de dicho proyecto.
Si se prefiere hacer desde modo gráfico, ejecuta el comando:
$ git citool

Esto abre el entorno gráfico de configuración de repositorios de git:


Le das a create new repository y ahí seleccionas el directorio de proyecto que te interesa gestionar. Además este mantiene una lista de repositorios creados. Veremos la clonación más adelante.
Si has creado alguno mediante línea de comando se puede añadir a esta lista mediante Open Existing Directory.
No veremos este entorno gráfico porque al final lo que nos interesará es poder manejar git desde cualquier sitio, y eso se hace mejor desde la consola.

Uso básico
Para ver como funciona git, vamos a ir haciendo la prueba con elementos muy sencillos. En principio está pensado para terminal de Linux/macOS pero si quieres trabajar en Windows va a funcionar igual salvo los comandos propios del sistema operativo, por ejemplo en lugar de mkdir ejecutarás md o en lugar de ls usarás dir. Sigue los siguientes pasos:
Crea un directorio denominado prueba_git y accede a él.
$ mkdir prueba_git
$ cd prueba_git

Crea mediante un editor de texto dos archivos dentro de dicho directorio. Uno denominado texto.txt y otro script.sh. En el primero introduce simplemente las palabras uno, dos, tres cada una en una línea. El segundo será un script con estas dos líneas:
echo Listado completo
ls -l

Pásalo a ejecutable y pruébalo mediante los comandos:
$ chmod a+x script.sh
$ ./script.sh

Añádelo como nuevo repositorio local
$ git init

En este momento tenemos un repositorio git con dos archivos sin rastreo (untracked). Puedes verlo con el comando:
$ git status

Esto significa que aunque estos archivos estén en el directorio, no están gestionados por git. 
Prueba a ejecutar el siguiente comando para añadirlo a archivos rastreado:
$ git add script.sh
$ git status

Ahora verás que texto.txt sigue sin estar rastreado pero script.sh pasa a ser un archivo rastreado por git (tracked). Esto significa que git comprobará si hay cambios desde la última versión. Si embargo aún no está confirmado. Esto es, no se ha realizado una “foto” del estado del archivo en ningún momento, no se ha actualizado la versión. Esto implica que no hay ninguna versión previa. Si un archivo sin confirmar se intenta sincronizar desde otro equipo no lo haría a la última versión.
Por tanto confirmamos o hacemos un commit para establecer una versión del archivo.

$ git commit -m "Confirmación inicial"
$ git status

Aparece solo el aviso de texto.txt que no está en seguimiento. El resto esta guardado como versión.
Al confirmar se añade un mensaje que queda guardado con cada commit. Ahí se suelen indicar los cambios desde la última versión. Si hubiera muchos cambios o el mensaje fuera muy largo se puede hacer 
$ git commit -F mensaje.txt

En el archivo mensaje.txt, lógicamente, iría todo el texto asociado al commit.
Añadimos también texto.txt:
$ git add texto.txt
$ git commit -m “Añadido archivo texto.txt”
$ git status

Tras esto nos dice que está todo en el directorio confirmado. 
Ahora abre el archivo texto.txt y añádele otra línea con la palabra cuatro. Tras ello ejecuta:
$ git status

Verás que git lo toma como “modificado”, pero indica que no hay cambios agregados al commit. Para prepararlo para el siguiente commit hay que ejecutar de nuevo add. Add es un comando que además de añadir a rastreo nuevos archivos, los prepara para el siguiente commit (puede no interesar actualizar en versión algún archivo que está en prueba).
$ git add texto.txt
$ git status

No hagas todavía commit. Si ahora modificas de nuevo texto.txt y observas el estado de git verás que aparece en stage (preparado para commit) y en modificado sin preparar. Si se hace ahora el commit, confirmaría la versión anterior, no la última modificación. Debes de hacer add si quieres incluirlo todo: 
$ git add texto.txt
$ git commit -m “Ampliada la explicación del texto”
$ git status

Vuelve a estar todo limpio. 
Puedes probar a ejecutar de nuevo la herramienta gráfica para ver que lo anterior se puede hacer también desde aquí.
$ git citool

Es posible deshacer cambios pero es la parte más “peligrosa” de git ya que se podrían llegar a perder datos, es por ello que hasta que se coja un poco de práctica es recomendable no acceder a esta parte.
Ciclo de vida de Git
Podemos resumir el ciclo de vida de archivos en el repositorio local con el siguiente esquema que aparece en el libro mencionado anteriormente:

Un archivo puede estar:
  • Untracked: no se gestiona con git.
  • Staged: gestionado y preparado para confirmar en el siguiente commit.
  • Unmodified: Estado del archivo tras el último commit, podemos consiferarlo una “versión” cerrada del mismo.
  • Modified: Un archivo modificado desde el último commit. Es necesario mandarlo mediante add a Stage para indicar que puede confirmarse como válido en el siguiente commit. 
Por tanto se mantienen tres árboles de trabajo. Como si fueran tres directorios:
Directorio de trabajo local: tu propio directorio donde tienes tus archivos.
Index o Stage: Es el almacén donde se colocan los archivos preparados.
HEAD: Que apunta a donde se ha hecho el último commit (ultima versión).

Si el flujo de trabajo habitual me está obligando continuamente a pasar de modificados a la de preparación mediante add y luego confirmar mediante commit, puedo saltarme un paso (o mejor dicho, resumirlo) ejecutando el comando 
$ git commit -a -m "Nueva versión"

Ojo, porque si hay un nuevo archivo estoy obligado a hacer add. Esto es válido solo para modificaciones. Así en un único comando realizas add y commit.
También es cómodo hacerse un script con las acciones repetitivas que hacemos.
Si quieres verinformación sobre la última versión confirmada:
$ git show

Por otro lado, si queremos ver los cambios que se han ido realizando en forma de históricopuedes ejecutar el comando:
$ git log

Para salir usa la q.
Diferencias
Coge ahora el archivo texto.txt, añádele una línea (será la cinco) y elimina otra (la tres, por ejemplo).
Para conocer las diferencias entre un archivo modificado y el que se ha confirmado en la última versión se puede usar el comando 
$ git diff

Esto devuelve una respuesta en “Formato Unificado” (Unified format) el cual es un formato específico para ver diferencias entre archivos. Sin pasar a explicarlo todo, si que podemos entender la parte final donde se relatan las diferencias:
@@ -1,4 +1,4 @@
uno
dos
-tres
cuatro
+cinco

En la primera línea (@@) el – se lee como “archivo previo” y el + como “archivo final”. Es decir, aquí nos indica que se están mostrando las filas de la 1 a la 4 en el archivo origen y en el final. 
En las otras líneas el – es línea eliminada y el más linea insertada. Todo esto se complica con archivos grandes y muchos cambios. Con el tiempo lo irás entendiendo mejor.
Más información leyendo este enlace:

Ignorar archivos
Cuando empiezo a tener una cantidad de archivos importantes suele suceder que hay algunos que no me interesa gestionar a nivel de versiones. Por ejemplo, si estoy programando en java, los archivos .class no tiene sentido gestionarlos. O archivos de texto o directorios temporales (muchas veces se marcan con extensión tmp o acabados en ~).
Para evitar estos archivos se debe crear un archivo con el nombre
.gitignore

Y para el ejemplo anterior podría contener las siguientes lineas:
# ignora los archivos terminados en .class
*.class

# ignora los archivos terminados en ~
*~

# pero no el archivo importante~, aun cuando había ignorado los archivos terminados en ~ en la linea anterior
!importante~


Veamos esto en funcionamiento: 
Añade a tu directorio (o en un subdirectorio) el archivo Hola.java. Por si no lo tienes cópialo de aquí y compílalo. 
class Hola {
    public static void main(String[] args){
        System.out.println("Welcome to the Java World");
    }
}

También crea alguno aunque no tenga nada que acabe en ~ y también uno denominado importante~. Ejecuta:
$ git status

Añade la plantilla anterior al archivo .gitignore. A continuación ejecuta de nuevo:
$ git status

Verás que hay archivos que se ignoran. Haz lo siguiente 
$ git add .
$ git commit -m “Añadidos fuentes en java y archivos importantes”

Con el . le indicamos a add que añada todo lo que está pendiente. También subdirectorios si hubiera.
Dispones de algunas plantillas ya realizadas en :

Eliminar y quitar de seguimiento
Si deseas eliminar archivos lo mejor es hacerlo a través del comando:
$ git rm importante~

Y luego un commit. 
Si lo has eliminado previamente mediante un rm normal (o del en Windows), tendrás que ejecutar también git rm o git rm -f si ya está confirmado el archivo.
Si había algún archivo ya en seguimiento que deseas quitar (por ejemplo los .class), para sacarlos de la zona de seguimiento (HEAD) ejecuta:
$ git reset HEAD *.class

Realmente HEAD es un puntero a la versión de trabajo del repositorio (la última versión confirmada). 
Con esto podemos ver de nuevo el ciclo de vida asociado a los distintos comandos vistos basándonos en el dibujo anterior:

Numeración de versiones
Cuando ejecutas un git log, ves como ya se comentó el historial de versiones. El número identificativo de la versión es un código único que aparece al lado de la palabra commit de cada versión. Por ejemplo:
commit 4b156d03695c859181e5499d074a6a98daeb761b

Este es un número bastante complejo sobre todo si se quiere referenciar en alguna forma, por ejemplo con un show o para recuperar cierta versión en unfuturo. Veamos el primer ejemplo, tendríamos que hacer:
$ git show 4b156d03695c859181e5499d074a6a98daeb761b

pero evidentemente coge uno de los número que te haya salido a ti en el log. 
Para añadir un tag es muy simple. Si se quiere añadir a la versión actual simplemente se escribe:
$ git tag v0.7

Si quieres añadir un tag a una versión anterior, por ejemplo la que se comentó al principio del apartado, simplemente se añade el código hash al final del comando:
$ git tag v0.3 4b156d03695c859181e5499d074a6a98daeb761b 

Si ahora haces:
$ git log --decorate

Verás que en el log salen las versiones. También puedes hacer:
$ git show v0.3

La numeración de versiones puede depender de la empresa, pero es muy habitual una numeración estándar como la que aparece en el siguiente enlace:

Ramas
Una gran utilidad de los sistemas de control de versiones es la realización de ramas, es decir, archivos paralelos a los principales con el objetivo de hacer pruebas o poner en práctica ideas. Luego la rama se puede abandonar, eliminar o mezclar con la rama principal. 
Este es un punto complejo, sobre todo por la solución de conflictos que pueden aparecer, y no vamos a profundizar en él.
Otra ventaja de las ramas en git es que pueden mantenerse solo en el sistema local sin necesidad de llevarlas al remoto. Esto es muy práctico para que programadores en equipo hagan sus pruebas en sus equipos locales sin tener que ramificar el proyecto general de todo el equipo.
Ejecuta el siguiente comando para crear una rama con el nombre Prueba: 
$ git branch Prueba
$ git log --oneline --decorate

Verás que ahora en la versión más reciente aparecen dos ramas, la principal, master, y la que has creado (Prueba). El argumento --oneline simplemente hace el log más breve.
Ahora si sigues cambiando cosas lo haces en master, porque es donde apunta HEAD que es el puntero de trabajo. Para cambiar a la rama de prueba:
$ git checkout Prueba
$ git commit -a -m "Rama para pruebas de código"
$ git log --oneline --decorate

Puedes ver en el log que ahora HEAD apunta a Prueba por lo que todos los cambios será en paralelo con la master noafectando a esta. Es decir los cambios que hagas en cualquier archivo no afectan a la rama principal.
Prueba a cambiar el Hola.java de añadiéndole la línea:
System.out.println("I have no more branches to commit, said the Ent");

A continuación haz el commit que le corresponda:
$ git commit -a -m “Llegan los Ents a Java”
$ git tag v0.5-Ent-Release

Si el experimento convence se puede juntar nuevamente con la rama principal o con otra cualquiera con 
$ git checkout master
$ git merge Prueba

Evidentemente este ejemplo es muy simple pero da una idea de la potencia del uso de ramas. Si quieres profundizar en este apartado se recomienda leer este enlace.

Repositorios remotos: GitHub

Uno de los puntos interesantes de Git y cualquier control de versiones, es la posibilidad de trabajo en equipo teniendo un repositorio al que pueden acceder varios programadores desde su equipo. De esta forma un programador tiene su repositorio local, tal cual lo hemos usado hasta ahora, y luego tiene un repositorio remoto al que vuelca sus modificaciones y desde el que vuelca al suyo modificaciones que hayan hecho terceros si están incluidos en el proyecto.
GitHub es precisamente un proveedor de repositorios Git para que distintos usuarios puedan compartir sus proyectos. El uso es gratuito siempre que sea público. Si se requiere un servidor privado se puede obtener con cierto coste. La web es: 
También existe para estudiantes un pack especial que permite repositorios privados y otras herramientas: https://education.github.com/pack
Conviene añadir además que aunque sea el más conocido, github no es el único servidor de repositorios git. Si necesitas, por ejemplo, uno gratuito pero que sea privado puedes usar:
La ventaja de este último es que para una pequeña empresa donde se desarrolle un proyecto privado con hasta 5 colaboradores la licencia es totalmente gratuita.
Si lo único que necesitas es clonar un proyectode GitHub (o de cualquier otro repositorio remoto), no es necesario tener cuenta. Simplemente ve a tu directorio de usuario y ejecuta:

Esto hace una réplica exacta del repositorio remoto en el directorio actual.
Cuando quieres gestionar tus propios proyecto o colaborar con otro (hacer un fork) es cuando sí debes crearte una cuenta. Conéctate y crea una.
Puedes probar el enlace: 
Que da un curso rápido de manejo de git/github.
Para disponer de un nuevo repositorio para un proyecto lo primero que debes hacer es crear el proyecto en tu cuenta de github para luego sincronizar los archivos locales con el remoto.
Suponiendo que tienes un repositorio denominado MiPrueba donde quieres enlazar el directorio actual ya gestionado por git (si no ejecuta un init primero) haz:

Esto establece un nombre (origin) para el repositorio remoto. Normalmente se suele dejar origin como nombre pero puede cambiarse.
Lo primero es sincronizar el remoto con el local mediante pull:
git pull origin master
Y luego cuando hayas trabajado para “volcar” el contenido
git push -u origin master

Ojo, esto debes probarlo con un repositorio remoto tuyo. Si lo haces contra el anterior te dará error pues no tienes permisos para modificarlo.
Este comando lo que hace es volcar tu rama principal (denominada master) local en el repositorio remoto (origin). El parámetro -u sirve para recordar estos nombres de forma que a partir del primer push simplemente haciendo 
git push

Ya reconoce origin y master. 
Con 
git remote -v

Puedes ver el remoto al que está enlazado (pueden ser varios).
Ve ahora a GitHub y modifica un archivo. Para que esto aparezca actualizado en el directorio local se debe hacer
git pull

Si no se usó previamente el parámetro -u se debe agregar además origin master.
Si no hay conflicto, aunque las modificaciones sean en ambos lados, se realiza la mezcla correctamente. Haz la prueba añadiendo en ambos lados una línea a un archivo.
Si intentas hacer un push te lo va a rechazar diciendo que primero has de incorporar los elementos remotos a tu repositorio local. Por tanto primero:
git pull

Verás que añade la línea remota a tu archivo.
Ahora al hacer el push se ajusta el remoto.
Si lo que cambia es una misma línea de forma distinta, al hacer pull informa del conflicto e indica que se deben reparar los problemas indicados (similar a los conflictos de las ramas). Si abres el archivo de conflicto verás la zona de conflicto. Algo así:
<<<<<<< HEAD
System.out.println("Bienvenido to de C#");
=======
System.out.println("Welcome to de Csharp");
>>>>>>> 77b72c16373b5d7a0bd291b50c4b217030f26f6a

Habría que corregir el conflicto. Como ya se indicó la parte de conflictos es compleja y por ahora no profundizaremos hasta que tengamos más soltura con git.

Epílogo

Y hasta aquí está el manejo básico de git y github. Si quieres aprender más te dejo algunas referencias que usé para este tutorial y que te pueden servir de ayuda.

¡Hasta el próximo post!

miércoles, 27 de junio de 2018

Servidor multihilo con sockets en C#


Cuando se trata de realizar un servidor que pueda atender a múltiples clientes la estructura básica salvando elementos de seguridad siempre es muy estándar.
El servidor se tiene que poner a la escucha y luego hay que hacer un bucle que reciba a los clientes y que luego se encargue de lanzar hilos para atender a cada cliente por separado. 
En pseudocódigo podríais verlo de la siguiente forma:

InicioServidor(){
    Crear socket de escucha.
    Iniciar escucha
    mientras servidor funcionando{
        Esperar cliente.
        crear socket de conexión con cliente
        Lanzar hilo de atención a cliente.
    }
}

Bien, aquí para algunos habrá cosas muy raras... sockets... hilos... No voy a entrar en profundidad (quizá en otra entrada) pero si sabéis lo que son no tendréis mayor problema, y los que no, os cuento:

Un socket es una entidad mediante la cual un programa puede conectarse a una red mediante distintos protocolos. Es necesario usar este concepto porque un programa realmente lo único que puede es trabajar con la memoria y cuando necesita acceder a un elemento externo a la memoria (red, archivos,  bases de datos, etc...) necesita de alguna forma para pedirle al sistema operativo que le de esos datos. En lenguajes como C# o Java esos elementos son objetos definidos a partir de una clase y que ya vienen programados para lograr tal fin. 
Por tanto nos quedamos con la idea de que un socket permite a mi programa comunicarse por red. 
Además los sockets pueden definirse como de escucha, usados por servidores para esperar a que un cliente le haga una petición, y luego de comunicación, que son los que se usan para que cliente y servidor "dialoguen" según un protocolo.

Un hilo se puede ver como una función que es capaz de ejecutarse "al mismo tiempo" que otras funciones. Esto tiene sus ventajas pero la denominada programación multihilo debe ser cuidadosa. No voy a entrar tampoco en este ámbito ya que daría para varios artículos. Simplemente quedaros con la idea de que si lanzo varios hilos estos comparten tiempo de CPU y pueden ejecutarse "a la vez". Esto tiene una ventaja en servidores y es que pueden estar atendiendo a varios clientes conectados al mismo tiempo.

Entonces ¿qué es eso de lanzar el hilo de "atención al cliente"? Pues simplemente es ejecutar una función donde se establece toda la comunicación con el cliente según el protocolo de comunicación que se haya definido con la ventaja de que dicha ejecución se está ejecutando y al mismo tiempo la función InicioServidor se sigue ejecutando para atender a más clientes.

Para entenderlo de forma práctica piensa en un servidor de archivos por FTP. El hilo principal, lo que sería InicioServidor, está permanentemente pendiente de que se conecten clientes. En cuanto uno lo hace, se ejecuta una función que atiende las peticiones del cliente (descargar, archivo, subirlo, ver listados, etc.). Al ser en un hilo puede escuchar a otro cliente, conectarlo y atender también sus peticiones. 

Bueno, y dejando la teoría a un lado ¿cómo hago todo esto en c#? Pues veámoslo paso a paso pero antes os comento que como vamos a usar librerías muy básicas esto debería funcionar en cualquier sistema operativo que soporte .net.

Lo primero es crear un proyecto nuevo en consola. Vamos a crear una clase donde se gestione todo el servidor y la instanciaremos y ejecutaremos desde el Main.

Dicha clase será así. Más abajo la explico por partes.

    public class Servidor
    {


        private const int PUERTO = 31416;
        private Socket socketEscucha;

        public void InicioServidor()
        {
            Console.WriteLine("Servidor");
            IPEndPoint ie = new IPEndPoint(IPAddress.Any, PUERTO);
            socketEscucha = new Socket(AddressFamily.InterNetwork,
                                              SocketType.Stream,
                                              ProtocolType.Tcp);
            socketEscucha.Bind(ie); //OJO, si el puerto de escucha 
                                    //está ocupado lanza excepción.
            socketEscucha.Listen(5);
            
Console.WriteLine("A la espera en puerto {0}", PUERTO);
            try
            {
                while (true)
                {
                    Socket socketCliente = socketEscucha.Accept();
                    Thread hiloCliente = new Thread(AtencionACliente);
                    hiloCliente.IsBackground = true;
                    hiloCliente.Start(socketCliente);
                }
            }
            catch (SocketException)
            {
                Console.WriteLine("Finalizado");
            }

        }

        public void AtencionACliente(object o)
        {

        }
    }

Si no lo has hecho necesitarás añadir los siguientes using:
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;

Bien, si observas detenidamente la función IniciaServidor es similar a la vista en seudocódigo. Pero veámosla por partes:

Primero se establece un puerto de escucha mediante la clase IPEndPoint. En el caso de un servidor hay que indicarle en que interfaz de red se quiere hacer la escucha (podemos tener varias tarjetas de red, con Any indicamos que se queda a la escucha en todos) y en que puerto se queda escuchando. En este caso el 31416.

Luego creamos el socket en el que el servidor se quedará escuchando  usando el siguiente constructor:


Socket(AddresFamily familia, SocketType tipo, ProtocolType protocolo)


familia: Enumerado con el que especificamos el tipo de direccionamiento usado. No se limita a IPv4 o IPv6 si no que admite otros estándares como AppleTalk, Netbios, OSI, etc...).  Usaremos InternetWork para IPv4. Digamos que describe la red con la que vamos a trabajar.

tipo: El tipo de socket que vamos a usar. En principio usaremos sólo Stream que sirve para conexiones bidireccionales basadas en conexión (como TCP, pero no es la única). Pero admite otros tipos como sin conexión para UDP (Dgram) o incluso bajar a la capa de transporte (RAW) para usar otros protocolos como ICMP.

protocolo: El protocolo de comunicación usado . Usaremos para este ejemplo Tcp.

Una vez instanciado el socket hay que enlazarlo al puerto de escucha a través del IPEndPoint. Esto se realiza mediante el comando Bind. Si el puerto estuviera ocupado, saltaría una excepción que puede usar para controlar este hecho. 

Tras ello se ejecuta el comando Listen el cual establece el socket como de escucha y se indica la cantidad de clientes que podrían estar a la espera mientras no se haya tramitado el cliente actual.

Y entramos en el bucle de atención a clientes. Lo mantendremos en ejecución dependiendo de una propiedad booleana que podríamos cambiar desde otro hilo o incluso como parte del protocolo de comunicación con los clientes haciendo que haya algún comando para finalizar dicho servidor.

Accept es un comando que por defecto bloquea la ejecución de código. Es decir, el bucle queda parado hasta que haya un cliente en la cola. En ese momento Accept recoge dicho cliente, crea y devuelve  un socket para comunicarse con dicho cliente y permite que la ejecución de código continue.

Una vez que tenemos el socket de comunicación con el cliente (socketCliente) el programa continúa creando un hilo para atender al cliente y donde estableceremos el protocolo. Posteriormente y mientras se ejecuta dicho hilo el bucle vuelve al Accept para esperar otro cliente.

Es en el lanzamiento de dicho hilo (Start) donde se le pasa como parámetro el socket de comunicación.

Supongo que te habrás dado cuenta que todo el bucle está metido dentro de un try/catch. Esto es una forma cómoda de finalizar el servidor. El bucle en principio es infinito salvo que salte una excepción. Y esta la vamos a provocar cuando desde un cliente se desee apagar el servidor ya que vamos a realizar el Close del socket de escucha y esto provocará una excepción en el Accept lo que hace que se libere.

La propiedad IsBackground sirve para provocar el cierre de los hilos que la tengan a true en el caso de que se finalice el hilo principal.Y ese comportamiento es el que queremos. Una vez terminado el Main que todo termine.

¿Y que pasa con el cliente? Pues ahí vamos: en la función que se lanza como hilo, AtencionACLiente, es donde establecemos el protocolo de comunicación, es decir, la forma en que se establece la comunicación entre cliente y servidor. Como el objetivo de este artículo es la realización de la parte multihilo, haremos un protocolo muy sencillo como se ve a continuación:

Servidor envía mensaje de bienvenida al cliente.
Servidor espera a que el cliente envíe una palabra y devuelve al cliente la misma palabra.
Habrá dos excepciones: 
    #salir: el cliente finaliza la conexión.
    #apagar: el servidor termina.

Una cosa: no vamos a programar el cliente, si no que usaremos como tal cualquier cliente telnet.

Teniendo esto en cuenta, la función AtencioNACliente quedaría así:

        public void AtencionACliente(object o)
        {
            // Faltaría el control de excepciones
            string mensaje;
            Boolean clienteActivo = true;
            Socket cliente = (Socket)o;
            IPEndPoint ieCliente = (IPEndPoint)cliente.RemoteEndPoint;

            Console.WriteLine("Conectado con el cliente {0} en el puerto {1}",
                            ieCliente.Address, ieCliente.Port);

            NetworkStream ns = new NetworkStream(cliente);
            StreamReader sr = new StreamReader(ns);
            StreamWriter sw = new StreamWriter(ns);

            sw.WriteLine("Bienvenido al servidor");
            sw.Flush();

            while (clienteActivo)
            {
                    // Recibe mensaje del cliente
                    mensaje = sr.ReadLine();

                    //Se recibe null si se cierra el cliente de golpe
                    if (mensaje == null)
                        break;

                    // Gestion del protocolo
                    switch (mensaje)
                    {
                        case "#salir":
                            clienteActivo = false;
                            break;
                        case "#apagar":
                            clienteActivo = false;
                            socketEscucha.Close();
                            break;
                        default:
                            sw.WriteLine(mensaje);
                            sw.Flush();
                            break;
                    }

                    Console.WriteLine("{0} dice: {1}",
                        ieCliente.Address, mensaje);
            }

            Console.WriteLine("Conexión finalizada con {0}:{1}",
                    ieCliente.Address, ieCliente.Port);

            sw.Close();
            sr.Close();
            ns.Close();
            cliente.Close();
        }

Lo primero que hacemos es recoger el socket de comunicación. El casting es necesario porque cuando se pasan parámetros a hilos solo se puede hacer con un parámetro de tipo object, y como sabes podemos hacer el casting al tipo que tenemos seguro que es el real del parámetro.

Los Console.WriteLine simplemente muestran información en la consola del servidor para confirmar que todo está funcionando correctamente. Es importante recalcar, pues es un error típico cuando se empieza con este tipo de aplicaciones, que aunque esta función es la de tratamiento del cliente se está ejecutando en el servidor. La relación con el cliente es exclusivamente el protocolo de comunicación.

A continuación podríamos empezar a enviar y recibir datos mediante los métodos Send y Receive del objeto Socket, pero se suele simplificar cuando se desea enviar texto utilizando Streams. Conlleva al go más de inicialización pero facilita mucho luego el proceso ya que nos ahorra tener que gestionar los buffers de memoria de envío y recepción además de las conversiones de datos (pues a través del socket se envían bytes puros).

Se establece el stream de red (NetworkStream) asociado al socket y a continuación los streams de escritura y lectura (StreamWriter y StreamReader) para envío y recepción de datos. Lo mejor es que se manejan igual que los streams de archivos, lo que incluye la consola, por lo que es muy sencillo trabajar a golpe de Write/WriteLine y ReadLine.



Tras establecer los streams, mediante el de escritura se pueden enviar datos mediante los métodos Write o WriteLine del StreamWriter y se pueden recibir mediante ReadLine del StreamReader.

El Flush es necesario porque al igual que sucede con archivos las escrituras son en memoria y no se hacen efectivas hasta ejecutar el Flush o cerrar el Stream. Podría ponerse también la propiedad AutoFlush a True para ahorrarnos los Flush.

El resto de la función es sencilla. Tras enviar el mensaje de bienvenida se queda a la espera de recibir un comando. Mediante el switch se gestiona el protocolo. Pero antes se comprueba que el mensaje no sea null. Es null cuando el cliente se ha cerrado "sin avisar" (por ejemplo que se cierre la aplicación cliente de forma inesperada). 

En el switch se finaliza el bucle de cliente si se recibe el comando #salir. Se apaga el servidor cerrando el socket de escucha si se recibe el comando #apagar. Y finalmente se devuelve el mensaje si se recibe cualquier otra cosa.

Una vez finalizado el bucle es necesario cerrar los streams y el socket de cliente.

Por supuesto se pueden hacer muchas comprobaciones y control de excepciones que me he saltado por sencillez. Pero la estructura básica está ahí. Cualquier comentario, duda o errata que encontréis avisad en los comentarios y estaré encantado de leerlo/solucionarlo.

Por supuesto nos faltaría el main que podría ser algo así:

    public class Principal{
        public static void Main(String[] args){
            Servidor servidor = new Servidor();
            servidor.InicioServidor();
        }
    }


No creo que necesite explicación.

Una vez que ejecutes el programa, abre una consola y conéctate como se ve en el siguiente ejemplo (si estás en otro ordenador tendrás que usar la IP correspondiente):
Curro:~$ telnet 127.0.0.1 31416
Bienvenido al servidor
Hola
Hola
#salir
Curro:~$ 

En la consola del servidor verás algo así:
Servidor
A la espera en puerto 31416
Conectado con el cliente 127.0.0.1 en el puerto 50608
127.0.0.1 dice: Hola
127.0.0.1 dice: #salir
Conexión finalizada con 127.0.0.1:50608

Por supuesto prueba a abrir varias consolas y verás que es capaz de trabajar con ellas en paralelo.

Podéis ver el proyecto completo en este enlace a github.

¡Hasta el próximo post!