Sockets asíncronos de C #: ¿Es esto seguro para subprocesos y se realiza correctamente?

Necesito implementar una aplicación de cliente TCP. El cliente y el servidor se envían mensajes entre sí. Quiero que este programa sea lo suficientemente escalable para manejar conexiones a varios servidores al mismo tiempo. Parece que los sockets asincrónicos son el camino a seguir para esto. Soy nuevo en C #, así que estoy bastante seguro de que no sé lo que estoy haciendo aquí. Escribí algunas clases y un programa de consola simple para comenzar. Eventualmente, quiero crear una aplicación de Windows Forms, pero primero quiero comenzar de forma pequeña y simple. La clase Client se ejecuta en su propio hilo. ¿Todo esto es seguro para subprocesos y se hace correctamente? Es mucho código y traté de eliminar algo de grasa.

Program.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;

    namespace FastEyeClient
    {
        class Program
        {
            static void Main(string[] args)
            {
                Client client = new Client();
                client.ConnectEvent += new ConnectEventHandler(OnConnect);
                client.SetLiveStatusEvent += new SetLiveStatusEventHandler(OnSetLiveStatus);

                client.Connect("hostname", 1987);

                Thread.Sleep(1000);

                client.SetLiveStatus("hostname", true);
            }

            private static void OnConnect(object sender, ConnectEventArgs e)
            {
                Console.WriteLine(e.Message);
            }

            private static void OnSetLiveStatus(object sender, SetLiveStatusEventArgs e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }

Client.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;

    namespace FastEyeClient
    {
        public delegate void ConnectEventHandler(object sender, ConnectEventArgs e);
        public delegate void SetLiveStatusEventHandler(object sender, SetLiveStatusEventArgs e);

        public class Client : IDisposable
        {
            public event ConnectEventHandler ConnectEvent;
            public event SetLiveStatusEventHandler SetLiveStatusEvent;

            ServerManager m_Manager;

            EventWaitHandle m_WaitHandle;
            readonly object m_Locker;
            Queue<Event> m_Tasks;
            Thread m_Thread;

            public Client()
            {
                m_Manager = new ServerManager(this);

                m_WaitHandle = new AutoResetEvent(false);
                m_Locker = new object();
                m_Tasks = new Queue<Event>();

                m_Thread = new Thread(Run);
                m_Thread.Start();
            }

            public void EnqueueTask(Event task)
            {
                lock (m_Locker)
                {
                    m_Tasks.Enqueue(task);
                }

                m_WaitHandle.Set();
            }

            public void Dispose()
            {
                EnqueueTask(null);
                m_Thread.Join();
                m_WaitHandle.Close();
            }

            private void Run()
            {
                while (true)
                {
                    Event task = null;

                    lock (m_Locker)
                    {
                        if (m_Tasks.Count > 0)
                        {
                            task = m_Tasks.Dequeue();

                            if (task == null)
                            {
                                return;
                            }
                        }
                    }

                    if (task != null)
                    {
                        task.DoTask(m_Manager);
                    }
                    else
                    {
                        m_WaitHandle.WaitOne();
                    }
                }
            }

            public void Connect(string hostname, int port)
            {
                EnqueueTask(new ConnectEvent(hostname, port));
            }

            public void SetLiveStatus(string hostname, bool status)
            {
                EnqueueTask(new SetLiveEvent(hostname, status));
            }

            public void OnConnect(bool isConnected, string message)
            {
                if (ConnectEvent != null)
                {
                    ConnectEvent(this, new ConnectEventArgs(isConnected, message));
                }
            }

            public void OnSetLiveStatus(string hostname, string message)
            {
                if (SetLiveStatusEvent != null)
                {
                    SetLiveStatusEvent(this, new SetLiveStatusEventArgs(hostname, message));
                }
            }
        }
    }

Server.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Net;
    using System.Net.Sockets;

    namespace FastEyeClient
    {
        public class Server
        {
            private ServerManager m_Manager;
            private string m_Hostname;
            private bool m_IsLive;

            private class StateObject
            {
                public Socket AsyncSocket = null;
                public const int BufferSize = 1024;
                public byte[] Buffer = new byte[BufferSize];
                public StringBuilder Builder = new StringBuilder();
            }

            public Server(ServerManager manager, Socket socket)
            {
                try
                {
                    m_Manager = manager;

                    IPEndPoint endPoint = (IPEndPoint)socket.RemoteEndPoint;
                    IPAddress ipAddress = endPoint.Address;
                    IPHostEntry hostEntry = Dns.GetHostEntry(ipAddress);
                    Hostname = hostEntry.HostName;

                    IsLive = false;

                    StateObject state = new StateObject();
                    state.AsyncSocket = socket;

                    socket.BeginReceive(state.Buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
                }
                catch (Exception)
                {
                }
            }

            public string Hostname
            {
                get
                {
                    return m_Hostname;
                }
                set
                {
                    m_Hostname = value;
                }
            }

            public bool IsLive
            {
                get
                {
                    return m_IsLive;
                }
                set
                {
                    m_IsLive = value;
                }
            }

            private void ReceiveCallback(IAsyncResult result)
            {
                try
                {
                    StateObject state = (StateObject)result.AsyncState;
                    Socket socket = state.AsyncSocket;

                    int read = socket.EndReceive(result);

                    if (read > 0)
                    {
                        state.Builder.Append(Encoding.ASCII.GetString(state.Buffer, 0, read));

                        if (state.Builder.Length > 1)
                        {
                            string messages = state.Builder.ToString();

                            ParseMessages(messages);
                        }
                    }

                    StateObject newState = new StateObject();
                    newState.AsyncSocket = socket;

                    socket.BeginReceive(newState.Buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), newState);
                }
                catch (Exception)
                {
                }
            }

            private void ParseMessages(string messages)
            {
                string[] messagesArray = messages.Split('\n');

                foreach (string message in messagesArray)
                {
                    string[] tokens = message.Split(',');

                    if (tokens[0].Contains("@"))
                    {
                        ParseServerMessage(tokens);
                    }
                }
            }

            private void ParseServerMessage(string[] tokens)
            {
                tokens[0].Remove(0, 1);

                if (tokens[0] == "4")
                {
                    bool status;

                    if (tokens[1] == "0")
                    {
                        status = false;
                        m_Manager.SetLiveStatus(m_Hostname, status);
                    }
                    else if (tokens[1] == "1")
                    {
                        status = true;
                        m_Manager.SetLiveStatus(m_Hostname, status);
                    }
                }
            }
        }
    }

ServerManager.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Net;
    using System.Net.Sockets;

    namespace FastEyeClient
    {
        public class ServerManager
        {
            private Client m_Client;

            private Dictionary<string, Server> m_Servers;
            private object m_Locker;

            public ServerManager(Client client)
            {
                m_Client = client;

                m_Servers = new Dictionary<string, Server>();
                m_Locker = new object();
            }

            public void AddServer(string hostname, int port)
            {
                try
                {
                    IPAddress[] IPs = Dns.GetHostAddresses(hostname);

                    Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                    socket.BeginConnect(IPs, port, new AsyncCallback(ConnectCallback), socket);
                }
                catch (Exception)
                {
                    bool isConnected = false;
                    string message = "Could not connect to server.";

                    m_Client.OnConnect(isConnected, message);
                }
            }

            private void ConnectCallback(IAsyncResult ar)
            {
                bool isConnected;
                string message;

                try
                {
                    Socket socket = (Socket)ar.AsyncState;

                    socket.EndConnect(ar);

                    IPEndPoint endPoint = (IPEndPoint)socket.RemoteEndPoint;
                    IPAddress ipAddress = endPoint.Address;
                    IPHostEntry hostEntry = Dns.GetHostEntry(ipAddress);
                    string hostname = hostEntry.HostName;

                    lock (m_Servers)
                    {
                        if (m_Servers.ContainsKey(hostname))
                        {
                            isConnected = false;
                            message = "Client is already connected to server";
                        }
                        else
                        {
                            m_Servers.Add(hostname, new Server(this, socket));

                            isConnected = true;
                            message = "Successfully connected.";
                        }
                    }

                    m_Client.OnConnect(isConnected, message);
                }
                catch (Exception)
                {
                    isConnected = false;
                    message = "Could not connect to server.";

                    m_Client.OnConnect(isConnected, message);
                }
            }

            public void SetLiveStatus(string hostname, bool newStatus)
            {
                string message;

                lock (m_Locker)
                {
                    if (m_Servers.ContainsKey(hostname))
                    {
                        if (m_Servers[hostname].IsLive == newStatus)
                        {
                            message = "Server is already set to this status.";
                        }
                        else
                        {
                            m_Servers[hostname].IsLive = newStatus;

                            message = "Successfully set new status.";
                        }
                    }
                    else
                    {
                        message = "Server not found.";
                    }
                }

                m_Client.OnSetLiveStatus(hostname, message);
            }
        }
    }

preguntado el 16 de mayo de 11 a las 18:05

Esto podría ser más adecuado para publicar en codereview.stackexchange.com -

Gracias Albin. Lo haré. -

3 Respuestas

No, no es seguro para subprocesos.

Un suscriptor puede darse de baja antes entre el cheque y la invocación:

if (ConnectEvent != null)
{
    ConnectEvent(this, new ConnectEventArgs(isConnected, message));
}

Defina el evento como:

public event ConnectEventHandler ConnectEvent = delegate{};

y elimine el evento para obtener seguridad para subprocesos.


Reduciría el ciclo de ejecución a:

private void Run()
{
    while (true)
    {
        m_WaitHandle.WaitOne();
        Event task = null;

        lock (m_Locker)
        {
            if (m_Tasks.Count == 0)
                            {
                m_WaitHandle.Reset();
                continue;
            }

            task = m_Tasks.Dequeue();
        }

        task.DoTask(m_Manager);
    }
}
  1. El bucle continuará ejecutándose hasta que se reinicie el evento.
  2. Asegúrese de que no se inserten elementos nulos en la cola en lugar de buscar nulos.

contestado el 16 de mayo de 11 a las 22:05

jgauffin, cuando dice al final de la primera parte: "y elimine el evento para obtener seguridad para subprocesos". ¿Que quieres decir con eso? No puedo eliminar el evento. ¿El archivo Program.cs nunca recibiría el evento? - Andrés

  1. ¿Funciona?
  2. ¿Lanza una (s) excepción (es)?

    Error al intentar ejecutar el código del servidor en varios subprocesos:

EVITE intentar manipular, leer o escribir un socket en diferentes subprocesos. Haga que un subproceso acepte conexiones desde el socket del servidor y genere un subproceso para manejar las transacciones. Si tiene demasiados subprocesos a la vez, tendrá 1 subproceso que maneja varios enchufes.

contestado el 16 de mayo de 11 a las 22:05

1. Sí, funciona (no publiqué el 100% del código, pero son cosas simples como las clases Event y EventArgs). 2. Todavía no. Y gracias por el dato. - Andrés

Podría simplificar el patrón productor-consumidor en el Client clase usando BlockingCollection en lugar de una combinación de AutoResetEvent y simplemente viejo Queue.

La EnqueueTask el método se vería así:

public void EnqueueTask(Event task)
{
  m_Queue.Add(task);
}

La Run el método se vería así:

public void Run()
{
  while (true)
  {
    Event task = m_Queue.Take();
    if (task == null)
    {
      return;
    }
    task.DoTask();
  }
}

contestado el 17 de mayo de 11 a las 00:05

BlockingCollection es nuevo, ¿verdad? Desafortunadamente, estoy desarrollando en una versión anterior de .NET. - Andrés

Sí, es nuevo. Aunque hay un backport en la descarga de Reactive Extension para .NET 3.5. Hay una implementación de una cola de bloqueo aquí blogs.msdn.com/b/toub/archive/2006/04/12/blocking-queues.aspx que funciona en todas las versiones. - Brian Gideon

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.