[C#] 확장 가능한 Tcp / Ip 기반 서버를 작성하는 방법

오래 실행되는 연결에 TCP / IP 연결을 허용하는 새로운 Windows 서비스 응용 프로그램을 작성하는 디자인 단계에 있습니다. 즉, 연결이 짧은 많은 HTTP가 아니라 클라이언트가 몇 시간 또는 며칠 동안 연결되어 연결 상태를 유지합니다. 심지어 몇 주).

네트워크 아키텍처를 디자인하는 가장 좋은 방법에 대한 아이디어를 찾고 있습니다. 서비스를 위해 하나 이상의 스레드를 시작해야합니다. 주어진 시간에 몇 명의 클라이언트가 연결 될지 모르기 때문에 (아마도 수백 개) Asynch API (BeginRecieve 등) 사용을 고려하고 있습니다. 각 연결마다 스레드를 시작하고 싶지 않습니다.

데이터는 주로 내 서버에서 클라이언트로 전달되지만 때때로 클라이언트에서 일부 명령이 전송됩니다. 이것은 주로 서버가 클라이언트에게 주기적으로 상태 데이터를 보내는 모니터링 응용 프로그램입니다.

가능한 확장 성을 높이는 가장 좋은 방법에 대한 제안이 있으십니까? 기본 워크 플로우? 감사.

편집 : 분명히하기 위해 .net 기반 솔루션을 찾고 있습니다 (가능한 경우 C #이지만 .net 언어는 작동합니다)

바운티 노트 : 바운티를 받으려면 간단한 답변 이상을 기대합니다. 내가 다운로드 할 수있는 것에 대한 포인터 또는 짧은 예제 인라인으로 솔루션의 실제 예제가 필요합니다. 그리고 .net 및 Windows 기반이어야합니다 (.net 언어는 허용됩니다)

편집 : 좋은 답변을 준 모든 사람에게 감사드립니다. 불행히도, 나는 하나만 받아 들일 수 있었고 더 잘 알려진 Begin / End 방법을 채택하기로 결정했습니다. Esac의 솔루션이 더 나을 수도 있지만 여전히 어떻게 작동하는지 잘 모르겠습니다.

나는 내가 좋다고 생각한 모든 대답을 찬성했다. 나는 너희들을 위해 더 많은 것을 할 수 있기를 바란다. 다시 감사합니다.



답변

나는 과거에 이와 비슷한 것을 썼다. 몇 년 전 필자의 연구 결과에 따르면 비동기 소켓을 사용하여 자체 소켓 구현을 작성하는 것이 가장 좋습니다. 즉, 클라이언트가 실제로 아무것도하지 않으면 실제로 적은 리소스가 필요했습니다. 발생하는 모든 것은 .net 스레드 풀에 의해 처리됩니다.

서버의 모든 연결을 관리하는 클래스로 작성했습니다.

나는 단순히 모든 클라이언트 연결을 유지하기 위해 목록을 사용했지만 더 큰 목록을 위해 더 빠른 조회가 필요한 경우 원하는대로 작성할 수 있습니다.

private List<xConnection> _sockets;

또한 실제로 들어오는 연결을 수신 대기하는 소켓이 필요합니다.

private System.Net.Sockets.Socket _serverSocket;

start 메소드는 실제로 서버 소켓을 시작하고 들어오는 연결을 청취하기 시작합니다.

public bool Start()
{
  System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
  System.Net.IPEndPoint serverEndPoint;
  try
  {
     serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
  }
  catch (System.ArgumentOutOfRangeException e)
  {
    throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
  }
  try
  {
    _serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
   }
   catch (System.Net.Sockets.SocketException e)
   {
      throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
    }
    try
    {
      _serverSocket.Bind(serverEndPoint);
      _serverSocket.Listen(_backlog);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured while binding socket, check inner exception", e);
    }
    try
    {
       //warning, only call this once, this is a bug in .net 2.0 that breaks if 
       // you're running multiple asynch accepts, this bug may be fixed, but
       // it was a major pain in the ass previously, so make sure there is only one
       //BeginAccept running
       _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured starting listeners, check inner exception", e);
    }
    return true;
 }

예외 처리 코드가 나빠 보이는 것에 주목하고 싶지만 그 이유는 예외 억제 코드가 있었기 때문에 false구성 옵션이 설정된 경우 예외가 억제되고 반환 될 것이지만 그것을 제거하고 싶었습니다. 간결한 술.

위의 _serverSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket)은 본질적으로 사용자가 연결할 때마다 acceptCallback 메소드를 호출하도록 서버 소켓을 설정합니다. 이 방법은 많은 차단 작업이있는 경우 추가 작업자 스레드 만들기를 자동으로 처리하는 .Net 스레드 풀에서 실행됩니다. 이는 서버의 모든로드를 최적으로 처리해야합니다.

    private void acceptCallback(IAsyncResult result)
    {
       xConnection conn = new xConnection();
       try
       {
         //Finish accepting the connection
         System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
         conn = new xConnection();
         conn.socket = s.EndAccept(result);
         conn.buffer = new byte[_bufferSize];
         lock (_sockets)
         {
           _sockets.Add(conn);
         }
         //Queue recieving of data from the connection
         conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
         //Queue the accept of the next incomming connection
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
     }

위의 코드는 본질적으로 들어오는 연결 BeginReceive, 클라이언트가 데이터를 보낼 때 실행되는 콜백 인 대기열 acceptCallback을 수락 한 다음 다음에 오는 클라이언트 연결을 수락하는 대기열 을 수락했습니다.

BeginReceive메소드 호출은 클라이언트에서 데이터를 수신 할 때 무엇을해야 하는지를 소켓을 알 것입니다. 의 경우 BeginReceive클라이언트에게 데이터를 보낼 때 데이터를 복사 할 바이트 배열을 제공해야합니다. 이 ReceiveCallback메소드는 호출 될 것이며, 이는 데이터 수신을 처리하는 방법입니다.

private void ReceiveCallback(IAsyncResult result)
{
  //get our connection from the callback
  xConnection conn = (xConnection)result.AsyncState;
  //catch any errors, we'd better not have any
  try
  {
    //Grab our buffer and count the number of bytes receives
    int bytesRead = conn.socket.EndReceive(result);
    //make sure we've read something, if we haven't it supposadly means that the client disconnected
    if (bytesRead > 0)
    {
      //put whatever you want to do when you receive data here

      //Queue the next receive
      conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
     }
     else
     {
       //Callback run but no data, close the connection
       //supposadly means a disconnect
       //and we still have to close the socket, even though we throw the event later
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
   catch (SocketException e)
   {
     //Something went terribly wrong
     //which shouldn't have happened
     if (conn.socket != null)
     {
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
 }

편집 :이 패턴 에서이 코드 영역에서 언급하는 것을 잊었습니다.

//put whatever you want to do when you receive data here

//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);

내가 일반적으로하는 일은 원하는 코드에서 패킷을 메시지로 재구성 한 다음 스레드 풀에서 작업으로 만드는 것입니다. 이렇게하면 클라이언트에서 다음 블록의 BeginReceive가 메시지 처리 코드가 실행되는 동안 지연되지 않습니다.

accept 콜백은 end receive를 호출하여 데이터 소켓 읽기를 완료합니다. 수신 시작 기능에 제공된 버퍼를 채 웁니다. 주석을 남긴 곳에서 원하는 것을 수행 BeginReceive하면 클라이언트가 더 이상 데이터를 보내면 콜백을 다시 실행하는 다음 메소드를 호출합니다 . 이제 클라이언트가 데이터를 보낼 때 수신 콜백이 메시지의 일부로 만 호출 될 수있는 정말 까다로운 부분이 있습니다. 재 조립이 매우 복잡해질 수 있습니다. 나는 내 자신의 방법을 사용하고 이것을하기 위해 독점적 인 프로토콜을 만들었습니다. 나는 그것을 생략했지만 요청하면 추가 할 수 있습니다.이 핸들러는 실제로 내가 작성한 가장 복잡한 코드 조각이었습니다.

public bool Send(byte[] message, xConnection conn)
{
  if (conn != null && conn.socket.Connected)
  {
    lock (conn.socket)
    {
    //we use a blocking mode send, no async on the outgoing
    //since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
       conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
     }
   }
   else
     return false;
   return true;
 }

위의 send 메소드는 실제로 동기 Send호출을 사용합니다 . 메시지 크기와 응용 프로그램의 다중 스레드 특성으로 인해 좋았습니다. 모든 클라이언트에게 보내려면 _sockets 목록을 반복하면됩니다.

위에서 참조한 xConnection 클래스는 기본적으로 소켓이 바이트 버퍼를 포함하는 간단한 래퍼이며 구현시 추가 사항입니다.

public class xConnection : xBase
{
  public byte[] buffer;
  public System.Net.Sockets.Socket socket;
}

또한 여기에 using포함되어 있지 않을 때 항상 화가 나기 때문에 여기에 참조하십시오 .

using System.Net.Sockets;

그것이 도움이되기를 바랍니다. 가장 깨끗한 코드는 아니지만 작동합니다. 또한 코드 변경에 대해 염려해야 할 몇 가지 뉘앙스가 있습니다. 하나의 경우, 한 번에 하나의 BeginAccept통화 만합니다. 몇 년 전이 문제에 대해 매우 성가신 .net 버그가 있었기 때문에 세부 사항을 기억하지 못합니다.

또한 ReceiveCallback코드에서 다음 수신을 큐에 대기하기 전에 소켓에서 수신 한 모든 것을 처리합니다. 즉, 단일 소켓의 경우 실제로는 ReceiveCallback한 번 에 한 번만 가능하므로 스레드 동기화를 사용할 필요가 없습니다. 그러나 데이터를 가져온 직후에 다음 수신을 호출하도록이 순서를 변경하면 약간 더 빠를 수 있으므로 스레드를 올바르게 동기화해야합니다.

또한 많은 코드를 해킹했지만 발생하는 내용의 본질을 남겼습니다. 디자인을 시작하기에 좋은 출발점이 될 것입니다. 이에 대해 더 궁금한 점이 있으면 의견을 남겨주십시오.


답변

C #에서 네트워크 작업을 수행하는 방법에는 여러 가지가 있습니다. 그들 모두는 후드 아래에서 다른 메커니즘을 사용하므로 높은 동시성으로 주요 성능 문제를 겪습니다. Begin * 작업은 많은 사람들이 네트워킹을 수행하는 가장 빠르고 빠른 방법으로 자주 생각하는 작업 중 하나입니다.

이러한 문제를 해결하기 위해 * Async 메서드 집합을 도입했습니다. MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

SocketAsyncEventArgs 클래스는 특수한 고성능 소켓 응용 프로그램에서 사용할 수있는 대체 비동기 패턴을 제공하는 System.Net.Sockets .. ::. Socket 클래스의 향상된 기능 중 일부입니다. 이 클래스는 고성능이 필요한 네트워크 서버 응용 프로그램을 위해 특별히 설계되었습니다. 응용 프로그램은 고급 비동기 패턴을 독점적으로 또는 대상이되는 핫 영역 (예 : 대량의 데이터를 수신 할 때)에서만 사용할 수 있습니다.

이러한 향상된 기능의 주요 특징은 대용량 비동기 소켓 I / O 중에 반복적 인 할당 및 객체 동기화를 피하는 것입니다. System.Net.Sockets .. ::. Socket 클래스에서 현재 구현 한 Begin / End 디자인 패턴에는 각 비동기 소켓 작업에 System .. ::. IAsyncResult 개체가 할당되어 있어야합니다.

커버 아래에서 * Async API는 네트워킹 작업을 수행하는 가장 빠른 방법 인 IO 완료 포트를 사용합니다 ( http://msdn.microsoft.com/en-us/magazine/cc302334.aspx 참조) .

그리고 당신을 돕기 위해 * Async API를 사용하여 작성한 텔넷 서버의 소스 코드를 포함시키고 있습니다. 관련 부분 만 포함하고 있습니다. 또한 데이터를 인라인으로 처리하는 대신 별도의 스레드에서 처리되는 잠금 해제 (대기 대기) 큐로 푸시하도록 선택합니다. 비어있는 경우 새 객체를 생성하는 간단한 풀인 해당 풀 클래스와 비 확정적 인 경우를 제외하고는 실제로 필요하지 않은 자체 확장 버퍼 인 Buffer 클래스를 포함하지 않습니다. 데이터 양. 더 이상 정보를 원하시면 언제든지 PM을 보내주십시오.

 public class Telnet
{
    private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
    private Socket m_ListenSocket;

    /// <summary>
    /// This event fires when a connection has been established.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Connected;

    /// <summary>
    /// This event fires when a connection has been shutdown.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Disconnected;

    /// <summary>
    /// This event fires when data is received on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataReceived;

    /// <summary>
    /// This event fires when data is finished sending on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataSent;

    /// <summary>
    /// This event fires when a line has been received.
    /// </summary>
    public event EventHandler<LineReceivedEventArgs> LineReceived;

    /// <summary>
    /// Specifies the port to listen on.
    /// </summary>
    [DefaultValue(23)]
    public int ListenPort { get; set; }

    /// <summary>
    /// Constructor for Telnet class.
    /// </summary>
    public Telnet()
    {
        m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
        ListenPort = 23;
    }

    /// <summary>
    /// Starts the telnet server listening and accepting data.
    /// </summary>
    public void Start()
    {
        IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
        m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        m_ListenSocket.Bind(endpoint);
        m_ListenSocket.Listen(100);

        //
        // Post Accept
        //
        StartAccept(null);
    }

    /// <summary>
    /// Not Yet Implemented. Should shutdown all connections gracefully.
    /// </summary>
    public void Stop()
    {
        //throw (new NotImplementedException());
    }

    //
    // ACCEPT
    //

    /// <summary>
    /// Posts a requests for Accepting a connection. If it is being called from the completion of
    /// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
    /// the new user.
    /// </summary>
    /// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
    private void StartAccept(SocketAsyncEventArgs e)
    {
        if (e == null)
        {
            e = m_EventArgsPool.Pop();
            e.Completed += Accept_Completed;
        }
        else
        {
            e.AcceptSocket = null;
        }

        if (m_ListenSocket.AcceptAsync(e) == false)
        {
            Accept_Completed(this, e);
        }
    }

    /// <summary>
    /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
    /// and then setup a Receive chain to begin receiving data.
    /// </summary>
    /// <param name="sender">object which posted the AcceptAsync</param>
    /// <param name="e">Information about the Accept call.</param>
    private void Accept_Completed(object sender, SocketAsyncEventArgs e)
    {
        //
        // Socket Options
        //
        e.AcceptSocket.NoDelay = true;

        //
        // Create and setup a new connection object for this user
        //
        Connection connection = new Connection(this, e.AcceptSocket);

        //
        // Tell the client that we will be echo'ing data sent
        //
        DisableEcho(connection);

        //
        // Post the first receive
        //
        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;

        //
        // Connect Event
        //
        if (Connected != null)
        {
            Connected(this, args);
        }

        args.Completed += Receive_Completed;
        PostReceive(args);

        //
        // Post another accept
        //
        StartAccept(e);
    }

    //
    // RECEIVE
    //    

    /// <summary>
    /// Post an asynchronous receive on the socket.
    /// </summary>
    /// <param name="e">Used to store information about the Receive call.</param>
    private void PostReceive(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection != null)
        {
            connection.ReceiveBuffer.EnsureCapacity(64);
            e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);

            if (connection.Socket.ReceiveAsync(e) == false)
            {
                Receive_Completed(this, e);
            }
        }
    }

    /// <summary>
    /// Receive completion callback. Should verify the connection, and then notify any event listeners
    /// that data has been received. For now it is always expected that the data will be handled by the
    /// listeners and thus the buffer is cleared after every call.
    /// </summary>
    /// <param name="sender">object which posted the ReceiveAsync</param>
    /// <param name="e">Information about the Receive call.</param>
    private void Receive_Completed(object sender, SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
        {
            Disconnect(e);
            return;
        }

        connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);

        OnDataReceived(e);

        HandleCommand(e);
        Echo(e);

        OnLineReceived(connection);

        PostReceive(e);
    }

    /// <summary>
    /// Handles Event of Data being Received.
    /// </summary>
    /// <param name="e">Information about the received data.</param>
    protected void OnDataReceived(SocketAsyncEventArgs e)
    {
        if (DataReceived != null)
        {
            DataReceived(this, e);
        }
    }

    /// <summary>
    /// Handles Event of a Line being Received.
    /// </summary>
    /// <param name="connection">User connection.</param>
    protected void OnLineReceived(Connection connection)
    {
        if (LineReceived != null)
        {
            int index = 0;
            int start = 0;

            while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
            {
                string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
                s = s.Backspace();

                LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
                Delegate[] delegates = LineReceived.GetInvocationList();

                foreach (Delegate d in delegates)
                {
                    d.DynamicInvoke(new object[] { this, args });

                    if (args.Handled == true)
                    {
                        break;
                    }
                }

                if (args.Handled == false)
                {
                    connection.CommandBuffer.Enqueue(s);
                }

                start = index;
                index++;
            }

            if (start > 0)
            {
                connection.ReceiveBuffer.Reset(0, start + 1);
            }
        }
    }

    //
    // SEND
    //

    /// <summary>
    /// Overloaded. Sends a string over the telnet socket.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="s">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, string s)
    {
        if (String.IsNullOrEmpty(s) == false)
        {
            return Send(connection, Encoding.Default.GetBytes(s));
        }

        return false;
    }

    /// <summary>
    /// Overloaded. Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, byte[] data)
    {
        return Send(connection, data, 0, data.Length);
    }

    public bool Send(Connection connection, char c)
    {
        return Send(connection, new byte[] { (byte)c }, 0, 1);
    }

    /// <summary>
    /// Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <param name="offset">Starting offset of date in the buffer.</param>
    /// <param name="length">Amount of data in bytes to send.</param>
    /// <returns></returns>
    public bool Send(Connection connection, byte[] data, int offset, int length)
    {
        bool status = true;

        if (connection.Socket == null || connection.Socket.Connected == false)
        {
            return false;
        }

        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;
        args.Completed += Send_Completed;
        args.SetBuffer(data, offset, length);

        try
        {
            if (connection.Socket.SendAsync(args) == false)
            {
                Send_Completed(this, args);
            }
        }
        catch (ObjectDisposedException)
        {
            //
            // return the SocketAsyncEventArgs back to the pool and return as the
            // socket has been shutdown and disposed of
            //
            m_EventArgsPool.Push(args);
            status = false;
        }

        return status;
    }

    /// <summary>
    /// Sends a command telling the client that the server WILL echo data.
    /// </summary>
    /// <param name="connection">Connection to disable echo on.</param>
    public void DisableEcho(Connection connection)
    {
        byte[] b = new byte[] { 255, 251, 1 };
        Send(connection, b);
    }

    /// <summary>
    /// Completion callback for SendAsync.
    /// </summary>
    /// <param name="sender">object which initiated the SendAsync</param>
    /// <param name="e">Information about the SendAsync call.</param>
    private void Send_Completed(object sender, SocketAsyncEventArgs e)
    {
        e.Completed -= Send_Completed;
        m_EventArgsPool.Push(e);
    }

    /// <summary>
    /// Handles a Telnet command.
    /// </summary>
    /// <param name="e">Information about the data received.</param>
    private void HandleCommand(SocketAsyncEventArgs e)
    {
        Connection c = e.UserToken as Connection;

        if (c == null || e.BytesTransferred < 3)
        {
            return;
        }

        for (int i = 0; i < e.BytesTransferred; i += 3)
        {
            if (e.BytesTransferred - i < 3)
            {
                break;
            }

            if (e.Buffer[i] == (int)TelnetCommand.IAC)
            {
                TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
                TelnetOption option = (TelnetOption)e.Buffer[i + 2];

                switch (command)
                {
                    case TelnetCommand.DO:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                    case TelnetCommand.WILL:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                }

                c.ReceiveBuffer.Remove(i, 3);
            }
        }
    }

    /// <summary>
    /// Echoes data back to the client.
    /// </summary>
    /// <param name="e">Information about the received data to be echoed.</param>
    private void Echo(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            return;
        }

        //
        // backspacing would cause the cursor to proceed beyond the beginning of the input line
        // so prevent this
        //
        string bs = connection.ReceiveBuffer.ToString();

        if (bs.CountAfterBackspace() < 0)
        {
            return;
        }

        //
        // find the starting offset (first non-backspace character)
        //
        int i = 0;

        for (i = 0; i < connection.ReceiveBuffer.Count; i++)
        {
            if (connection.ReceiveBuffer[i] != '\b')
            {
                break;
            }
        }

        string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);

        if (connection.Secure)
        {
            s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
        }

        s = s.Replace("\b", "\b \b");

        Send(connection, s);
    }

    //
    // DISCONNECT
    //

    /// <summary>
    /// Disconnects a socket.
    /// </summary>
    /// <remarks>
    /// It is expected that this disconnect is always posted by a failed receive call. Calling the public
    /// version of this method will cause the next posted receive to fail and this will cleanup properly.
    /// It is not advised to call this method directly.
    /// </remarks>
    /// <param name="e">Information about the socket to be disconnected.</param>
    private void Disconnect(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            throw (new ArgumentNullException("e.UserToken"));
        }

        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch
        {
        }

        connection.Socket.Close();

        if (Disconnected != null)
        {
            Disconnected(this, e);
        }

        e.Completed -= Receive_Completed;
        m_EventArgsPool.Push(e);
    }

    /// <summary>
    /// Marks a specific connection for graceful shutdown. The next receive or send to be posted
    /// will fail and close the connection.
    /// </summary>
    /// <param name="connection"></param>
    public void Disconnect(Connection connection)
    {
        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception)
        {
        }
    }

    /// <summary>
    /// Telnet command codes.
    /// </summary>
    internal enum TelnetCommand
    {
        SE = 240,
        NOP = 241,
        DM = 242,
        BRK = 243,
        IP = 244,
        AO = 245,
        AYT = 246,
        EC = 247,
        EL = 248,
        GA = 249,
        SB = 250,
        WILL = 251,
        WONT = 252,
        DO = 253,
        DONT = 254,
        IAC = 255
    }

    /// <summary>
    /// Telnet command options.
    /// </summary>
    internal enum TelnetOption
    {
        Echo = 1,
        SuppressGoAhead = 3,
        Status = 5,
        TimingMark = 6,
        TerminalType = 24,
        WindowSize = 31,
        TerminalSpeed = 32,
        RemoteFlowControl = 33,
        LineMode = 34,
        EnvironmentVariables = 36
    }
}


답변

Coversant의 Chris Mullins가 작성한 .NET을 사용하여 확장 가능한 TCP / IP에 대해 정말 좋은 토론이 있었지만 불행히도 블로그가 이전 위치에서 사라진 것처럼 보이므로 기억에 대한 조언을 함께 작성하려고합니다 (일부 유용한 의견) 그의의는이 스레드에 나타납니다 C ++ 대 C # : 확장 성이 뛰어난 IOCP 서버 개발 )

무엇보다도 클래스 에서 사용 Begin/End하는 Async메소드 와 메소드 는 모두 SocketIOCP (IO Completion Port)를 사용하여 확장 성을 제공합니다. 이는 솔루션을 구현하기 위해 실제로 선택한 두 가지 방법 중 어느 것보다 확장 성 측면에서 훨씬 더 큰 차이를 만듭니다 (올바르게 사용하는 경우 아래 참조).

Chris Mullins의 게시물은 Begin/End내가 개인적으로 경험 한 것입니다. Chris는이를 기반으로 솔루션을 2GB의 메모리가있는 32 비트 시스템에서 최대 10,000,000 개의 동시 클라이언트 연결로 확장하고 충분한 메모리가있는 64 비트 플랫폼에서 10만으로 확장 할 수 있습니다. 이 기술에 대한 내 자신의 경험 (이러한 종류의 부하에 가까운 곳은 아니지만)에서 나는이 지표를 의심 할 이유가 없습니다.

IOCP 및 연결 당 스레드 또는 ‘선택’기본 요소

후드 아래에서 IOCP를 사용하는 메커니즘을 사용하려는 이유는 읽고 자하는 IO 채널에 실제 데이터가있을 때까지 스레드를 깨우지 않는 매우 낮은 수준의 Windows 스레드 풀을 사용하기 때문입니다 ( IOCP는 파일 IO에도 사용할 수 있습니다). 이것의 장점은 Windows가 아직 데이터가 없음을 찾기 위해 스레드로 전환 할 필요가 없기 때문에 서버가 필요한 최소한의 컨텍스트 전환 횟수를 줄입니다.

컨텍스트 스위치는 ‘연결 당 스레드’메커니즘을 확실히 죽일 것입니다. 단, 수십 개의 연결 만 처리하는 경우 가능한 솔루션입니다. 그러나이 메커니즘은 상상력을 확장 할 수 없습니다.

IOCP 사용시 중요한 고려 사항

기억

우선 구현이 너무 순진한 경우 IOCP가 .NET에서 메모리 문제를 쉽게 초래할 수 있음을 이해하는 것이 중요합니다. 모든 IOCP BeginReceive호출은 읽고있는 버퍼를 “고정”시킵니다. 이것이 왜 문제인지에 대한 자세한 설명은 Yun Jin의 웹 로그 : OutOfMemoryException 및 Pinning을 참조하십시오 .

다행히이 문제는 피할 수 있지만 약간의 절충이 필요합니다. 제안 된 솔루션은 byte[]응용 프로그램 시작시 90KB 이상의 큰 버퍼 를 할당하는 것입니다 (.NET 2에서 필요한 크기는 이후 버전에서 더 클 수 있음). 이렇게하는 이유는 대용량 메모리 할당이 효과적으로 자동 고정되는 비 압축 메모리 세그먼트 (대형 개체 힙)에서 자동으로 종료되기 때문입니다. 시작할 때 하나의 큰 버퍼를 할당함으로써이 움직일 수없는 메모리 블록이 방해가되지 않고 조각화를 일으킬 수있는 상대적으로 낮은 주소에 있는지 확인하십시오.

그런 다음 오프셋을 사용하여이 하나의 큰 버퍼를 일부 데이터를 읽어야하는 각 연결에 대해 별도의 영역으로 분할 할 수 있습니다. 여기서 트레이드 오프가 발생합니다. 이 버퍼는 사전 할당되어야하므로 연결 당 필요한 버퍼 공간과 확장하려는 연결 수에 대해 설정하려는 상한을 결정해야합니다 (또는 추상화를 구현할 수 있음). 필요한 경우 추가 고정 버퍼를 할당 할 수 있습니다).

가장 간단한 해결책은이 버퍼 내에서 고유 한 오프셋으로 모든 연결에 단일 바이트를 할당하는 것입니다. 그런 다음 BeginReceive단일 바이트를 읽도록 호출하고 콜백의 결과로 나머지 판독을 수행 할 수 있습니다.

가공

콜백에서 콜백을 Begin받으면 콜백 의 코드가 저수준 IOCP 스레드에서 실행된다는 것을 인식해야합니다. 절대적으로 필수적 이 콜백에서 긴 작업을하지 않는 것이. 복잡한 처리에이 스레드를 사용하면 ‘연결 당 스레드’를 사용하는 것만 큼 효과적으로 확장 성이 없어집니다.

제안 된 솔루션은 콜백을 사용하여 수신 데이터를 처리하기 위해 작업 항목을 대기열에 넣고 다른 스레드에서 실행될 것입니다. IOCP 스레드가 가능한 빨리 풀로 돌아올 수 있도록 콜백 내에서 잠재적으로 작업을 차단하지 마십시오. .NET 4.0에서 가장 쉬운 해결책은을 생성 Task하여 클라이언트 소켓에 대한 참조와 BeginReceive호출 에서 이미 읽은 첫 번째 바이트의 사본을 제공하는 것 입니다. 이 작업은 처리중인 요청을 나타내는 소켓에서 모든 데이터를 읽고 실행 한 다음 BeginReceiveIOCP에 대한 소켓을 한 번 더 큐에 대기시키기 위해 새 호출을 수행합니다 . .NET 4.0 이전 버전에서는 ThreadPool을 사용하거나 고유 한 스레드 작업 큐 구현을 만들 수 있습니다.

요약

기본적 으로이 솔루션에 Kevin의 샘플 코드를 사용하고 다음과 같은 경고를 추가하는 것이 좋습니다.

  • 전달한 버퍼 BeginReceive가 이미 ‘고정’ 되어 있는지 확인하십시오.
  • 전달하는 콜백 BeginReceive은 들어오는 데이터의 실제 처리를 처리하기 위해 작업을 대기열에 넣는 것 이상 을 수행하지 않아야합니다.

그렇게하면 Chris의 결과를 잠재적으로 수십만 명의 동시 클라이언트로 확장 할 수 있습니다 (올바른 하드웨어와 자신의 처리 코드를 효율적으로 구현했습니다).


답변

위의 코드 샘플을 통해 이미 답변의 대부분을 얻었습니다. 비동기 IO 작업을 사용하는 것이 절대적으로 여기에 있습니다. 비동기 IO는 Win32가 내부적으로 확장되도록 설계된 방식입니다. 얻을 수있는 최상의 성능은 완료 포트를 사용하여 소켓을 완료 포트에 바인딩하고 완료 포트 완료를 기다리는 스레드 풀을 갖도록하는 것입니다. 일반적으로 CPU (코어) 당 2-4 개의 스레드가 완료를 기다리는 것이 좋습니다. Windows 성능 팀의 Rick Vicik이 작성한 다음 세 가지 기사를 살펴 보는 것이 좋습니다.

  1. 성능을위한 응용 프로그램 설계-1 부
  2. 성능을위한 응용 프로그램 설계-2 부
  3. 성능을위한 응용 프로그램 설계-3 부

이 기사는 대부분 기본 Windows API를 다루지 만 확장 성과 성능을 파악하려는 사람은 반드시 읽어야합니다. 그들은 관리 측면에 대한 간략한 설명도 가지고 있습니다.

두 번째로해야 할 일은 온라인으로 제공되는 .NET 응용 프로그램 성능 및 확장 성 향상 책을 확인하는 것입니다. 스레드, 비동기 호출 및 잠금 사용에 대해서는 5 장에서 적절하고 유효한 조언을 찾을 수 있습니다. 그러나 실제 보석은 17 장에서 스레드 풀 조정에 대한 실제 지침과 같은 유용한 정보를 찾을 수 있습니다. 이 장의 권장 사항에 따라 maxIothreads / maxWorkerThreads를 조정할 때까지 내 앱에 심각한 문제가있었습니다.

순수한 TCP 서버를 만들고 싶다고 말하면 다음 요점은 의심입니다. 그러나 자신이 모서리에 있고 WebRequest 클래스와 그 파생물을 사용하는 경우 그 문을 지키는 용이 ServicePointManager 라는 경고가 표시 됩니다. 이것은 인생에서 한 가지 목적, 즉 성능을 망치는 구성 클래스입니다. 인위적인 부과 된 ServicePoint.ConnectionLimit에서 서버를 비우지 않으면 응용 프로그램이 확장되지 않습니다 (기본값은 무엇인지 스스로 알아볼 수 있습니다 …). http 요청에서 Expect100Continue 헤더를 전송하는 기본 정책을 다시 고려할 수도 있습니다.

코어 소켓 관리 API에 대해서는 이제 송신 측에서는 상당히 쉽지만 수신 측에서는 훨씬 더 복잡합니다. 높은 처리량과 스케일을 달성하려면 수 신용으로 게시 된 버퍼가 없기 때문에 소켓이 흐름 제어되지 않아야합니다. 이상적으로 고성능을 위해서는 3-4 개의 버퍼를 미리 게시하고 버퍼를 가져 오기 직전에 새 버퍼를 게시 해야 소켓이 항상 네트워크에서 오는 데이터를 입금 할 수있는 위치에 있어야합니다. 당신은 왜 당신이 아마 이것을 빨리 달성 할 수 없을지 알게 될 것입니다.

BeginRead / BeginWrite API를 사용한 후 심각한 작업을 시작하면 트래픽에 대한 보안이 필요하다는 것을 알게됩니다. NTLM / Kerberos 인증 및 트래픽 암호화 또는 최소한 트래픽 변조 방지. 이를 수행하는 방법은 기본 제공 System.Net.Security.NegotiateStream (또는 다른 도메인으로 이동해야하는 경우 SslStream)을 사용하는 것입니다. 이는 스트레이트 소켓 비동기 작업에 의존하는 대신 AuthenticatedStream 비동기 작업에 의존한다는 것을 의미합니다. 소켓을 가져 오자마자 (클라이언트의 연결 또는 서버의 승인에서) 소켓에서 스트림을 작성하고 BeginAuthenticateAsClient 또는 BeginAuthenticateAsServer를 호출하여 인증을 위해 제출하십시오. 인증이 완료된 후 (적어도 기본 InitiateSecurityContext / AcceptSecurityContext의 광기로부터 안전합니다 …) 인증 된 스트림의 RemoteIdentity 속성을 확인하고 제품이 지원해야하는 ACL 확인을 수행하여 인증을 수행합니다. 그런 다음 BeginWrite를 사용하여 메시지를 보내고 BeginRead와 함께 메시지를받습니다. 이것은 AuthenticateStream 클래스가 이것을 지원하지 않기 때문에 여러 수신 버퍼를 게시 할 수 없다는 이전에 이야기 한 문제입니다. BeginRead 작업은 전체 프레임을 수신 할 때까지 내부적으로 모든 IO를 관리합니다. 그렇지 않으면 메시지 인증을 처리 할 수 ​​없습니다 (프레임 해독 및 프레임의 서명 유효성 검사). 내 경험상 AuthenticatedStream 클래스가 수행하는 작업은 상당히 좋으며 아무런 문제가 없습니다. 즉. CPU를 4-5 % 만 사용하여 GB 네트워크를 포화시킬 수 있어야합니다. AuthenticatedStream 클래스는 프로토콜 특정 프레임 크기 제한 (SSL의 경우 16k, Kerberos의 경우 12k)을 부과합니다.

이렇게하면 올바른 길을 시작할 수 있습니다. 여기에 코드를 게시하지 않을 것 입니다 .MSDN 에는 완벽하게 좋은 예가 있습니다 . 나는 이와 같은 많은 프로젝트를 수행했으며 문제없이 연결된 약 1000 명의 사용자로 확장 할 수있었습니다. 위에서 커널이 더 많은 소켓 핸들을 허용하도록 레지스트리 키를 수정해야합니다. XP 나 Vista가 아닌 W2K3 인 서버 OS (예 : 클라이언트 OS) 에 배포해야합니다 .

BTW는 서버 또는 파일 IO에 데이터베이스 작업이 있는지 확인하고 비동기 플레이버도 사용하거나 스레드 풀을 즉시 소모합니다. SQL Server 연결의 경우 연결 문자열에 ‘Asyncronous Processing = true’를 추가하십시오.


답변

내 솔루션 중 일부에서 그러한 서버를 실행하고 있습니다. 다음은 .net에서 여러 가지 방법으로 수행하는 방법에 대한 자세한 설명입니다. .NET의 고성능 소켓으로 더 가까이

최근에 코드를 개선 할 수있는 방법을 찾고 있었으며 “비동기 네트워크 I / O를 사용하여 최고 성능을 달성하는 응용 프로그램에서 사용하기 위해 특별히 포함 된” 버전 3.5의 소켓 성능 향상 “을 살펴 보겠습니다 .

“이러한 향상된 기능의 주요 기능은 대량 비동기 소켓 I / O 동안 반복되는 객체 할당 및 동기화를 피하는 것입니다. 비동기 소켓 I / O를 위해 Socket 클래스에서 현재 구현 한 시작 / 종료 디자인 패턴에는 시스템이 필요합니다. IAsyncResult 개체는 각 비동기 소켓 작업에 할당됩니다. “

링크를 따라 가면 계속 읽을 수 있습니다. 나는 개인적으로 내 코드 샘플을 테스트하여 내가 가진 것과 벤치 마크 할 것입니다.

편집 : 여기 당신은 몇 분 이내에 테스트 코드를 통해 갈 수 있도록 클라이언트와 서버 모두 새로운 3.5 SocketAsyncEventArgs를 사용하는 코드를 작업을 찾을 수 있습니다. 간단한 접근 방법이지만 훨씬 더 큰 구현을 시작하기위한 기초입니다. 또한 거의 2 년 전 MSDN Magazine 의이 기사는 흥미로운 기사였습니다.


답변

WCF net TCP 바인딩 및 발행 / 구독 패턴 사용을 고려 했습니까? WCF를 사용하면 배관 대신 도메인에 [주로] 집중할 수 있습니다.

IDesign의 다운로드 섹션에는 유용한 WCF 샘플과 게시 / 구독 프레임 워크가 많이 있습니다. http://www.idesign.net


답변

한 가지 궁금합니다.

각 연결마다 스레드를 시작하고 싶지 않습니다.

왜 그런 겁니까? Windows는 Windows 2000 이상부터 응용 프로그램에서 수백 개의 스레드를 처리 할 수 ​​있습니다. 스레드를 동기화 할 필요가 없으면 작업하기가 정말 쉽습니다. 특히 많은 I / O를 수행하고 있기 때문에 (CPU 바운드가 아니며 디스크 또는 네트워크 통신에서 많은 스레드가 차단됨)이 제한을 이해하지 못합니다.

멀티 스레드 방식을 테스트 한 결과 무언가 부족한 것을 발견 했습니까? 각 스레드에 대한 데이터베이스 연결도 계획하고 있습니까 (데이터베이스 서버가 종료 될 수 있으므로 나쁜 생각이지만 3 계층 설계로 쉽게 해결할 수 있습니다). 수백 대가 아닌 수천 명의 고객이 있고 실제로 문제가 생길까 걱정하고 있습니까? (32GB 이상의 RAM이 있다면 수천 개의 스레드 또는 심지어 10,000 개를 시도하지만 CPU 바인딩이 아니라면 스레드 전환 시간은 절대적으로 관련이 없어야합니다.)

코드는 다음과 같습니다. 실행 방법을 보려면 http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html 로 이동 하여 그림을 클릭하십시오.

서버 클래스 :

  public class Server
  {
    private static readonly TcpListener listener = new TcpListener(IPAddress.Any, 9999);

    public Server()
    {
      listener.Start();
      Console.WriteLine("Started.");

      while (true)
      {
        Console.WriteLine("Waiting for connection...");

        var client = listener.AcceptTcpClient();
        Console.WriteLine("Connected!");

        // each connection has its own thread
        new Thread(ServeData).Start(client);
      }
    }

    private static void ServeData(object clientSocket)
    {
      Console.WriteLine("Started thread " + Thread.CurrentThread.ManagedThreadId);

      var rnd = new Random();
      try
      {
        var client = (TcpClient) clientSocket;
        var stream = client.GetStream();
        while (true)
        {
          if (rnd.NextDouble() < 0.1)
          {
            var msg = Encoding.ASCII.GetBytes("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
            stream.Write(msg, 0, msg.Length);

            Console.WriteLine("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
          }

          // wait until the next update - I made the wait time so small 'cause I was bored :)
          Thread.Sleep(new TimeSpan(0, 0, rnd.Next(1, 5)));
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

서버 메인 프로그램 :

namespace ManyThreadsServer
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      new Server();
    }
  }
}

고객 클래스 :

  public class Client
  {
    public Client()
    {
      var client = new TcpClient();
      client.Connect(IPAddress.Loopback, 9999);

      var msg = new byte[1024];

      var stream = client.GetStream();
      try
      {
        while (true)
        {
          int i;
          while ((i = stream.Read(msg, 0, msg.Length)) != 0)
          {
            var data = Encoding.ASCII.GetString(msg, 0, i);
            Console.WriteLine("Received: {0}", data);
          }
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

클라이언트 메인 프로그램 :

using System;
using System.Threading;

namespace ManyThreadsClient
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      // first argument is the number of threads
      for (var i = 0; i < Int32.Parse(args[0]); i++)
        new Thread(RunClient).Start();
    }

    private static void RunClient()
    {
      new Client();
    }
  }
}