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


148

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

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

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

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

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

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

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

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


1
연결이 오래 지속되어야한다고 확신하십니까? 제한된 정보만으로는 알기가 어렵지만, 꼭 필요한 경우에만 그렇게 할 것입니다.
markt

예, 오래 실행해야합니다. 데이터는 실시간으로 업데이트되어야하므로 주기적 폴링을 수행 할 수 없으며 데이터가 발생할 때 클라이언트에 데이터를 푸시해야합니다. 이는 지속적인 연결을 의미합니다.
Erik Funkenbusch '16 : 37에

1
그것은 정당한 이유가 아닙니다. HTTP는 오래 실행되는 연결을 잘 지원합니다. 연결을 열고 응답을 기다립니다 (스톨 된 폴링). 이것은 많은 AJAX 스타일 앱 등에서 잘 작동합니다. 어떻게 Gmail이 작동한다고 생각하십니까 :-)
TFD

2
Gmail은 이메일을 정기적으로 폴링하여 작동하며 오래 연결되지 않습니다. 실시간 응답이 필요하지 않은 이메일에는 적합합니다.
Erik Funkenbusch

2
폴링 또는 풀링은 잘 확장되지만 대기 시간이 빠르게 발생합니다. 푸시도 확장되지 않지만 대기 시간을 줄이거 나 제거하는 데 도움이됩니다.
andrewbadera

답변:


92

나는 과거에 이와 비슷한 것을 썼다. 몇 년 전 필자의 연구 결과에 따르면 비동기 소켓을 사용하여 자체 소켓 구현을 작성하는 것이 가장 좋습니다. 즉, 클라이언트가 실제로 아무것도하지 않으면 실제로 적은 리소스가 필요했습니다. 발생하는 모든 것은 .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한 번 에 한 번만 가능하므로 스레드 동기화를 사용할 필요가 없습니다. 그러나 데이터를 가져온 직후에 다음 수신을 호출하도록이 순서를 변경하면 약간 더 빠를 수 있으므로 스레드를 올바르게 동기화해야합니다.

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


1
좋은 답변입니다. Kevin. 현상금을 받기 위해 궤도에있는 것 같습니다. :)
Erik Funkenbusch

6
왜 이것이 가장 높은 투표 응답인지 모르겠습니다. Begin * End *는 C #에서 네트워킹을 수행하는 가장 빠른 방법이 아니며 확장 성이 가장 높지 않습니다. 동기식보다 빠르지 만 Windows에서 실제로이 네트워크 경로를 느리게하는 많은 작업이 있습니다.
esac 2016 년

6
esac이 이전 의견에서 작성한 내용을 명심하십시오. begin-end 패턴은 아마도 어느 시점까지 당신을 위해 작동 할 것입니다. 내 코드가 현재 begin-end를 사용하고 있지만 .net 3.5의 제한 사항이 개선되었습니다. 현상금은 신경 쓰지 않지만이 방법을 구현하더라도 내 답변의 링크를 읽는 것이 좋습니다. "버전 3.5 소켓 성능 향상"
jvanderh

1
나는 충분히 명확하지 않았기 때문에 그냥 던지기를 원했습니다. 이것은 .net 2.0 시대 코드이며 이것이 매우 실용적인 패턴이라고 생각합니다. 그러나 esac의 대답은 .net 3.5를 대상으로하는 경우 다소 현대적인 것으로 보입니다. 유일하게 중요한 것은 이벤트 던지기입니다.)하지만 쉽게 변경할 수 있습니다. 또한이 코드로 처리량 테스트를 수행했으며 이중 코어 opteron 2Ghz에서 100Mbps 이더넷을 최대한 활용할 수 있었고이 코드 위에 암호화 계층이 추가되었습니다.
케빈 닛 베트

1
@KevinNisbet 나는 이것이 매우 늦다는 것을 알고 있지만,이 답변을 사용하여 자신의 서버를 디자인하는 사람에게는 전송이 비동기 적이어야합니다. 그렇지 않으면 교착 상태가 발생할 가능성이 있습니다. 양쪽이 각각의 버퍼를 채우는 데이터를 쓰는 경우 Send, 입력 데이터를 읽는 사람이 없기 때문에 메소드는 양쪽에서 무기한으로 차단됩니다.
Luaan

83

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
    }
}

이것은 매우 간단하고 간단한 예입니다. 감사. 각 방법의 장단점을 평가해야합니다.
에릭 Funkenbusch

나는 그것을 테스트 할 기회가 없었지만 어떤 이유로 여기에서 경쟁 조건의 모호한 느낌을 얻고 있습니다. 첫째, 많은 메시지를 받으면 이벤트가 순서대로 처리되는지 (사용자 앱에는 중요하지 않지만주의해야 함) 알지 못하거나 잘못되어 이벤트가 순서대로 처리됩니다. 둘째, 내가 놓쳤을 수도 있지만 오랜 시간이 걸리면 DataReceived가 계속 실행되는 동안 버퍼를 덮어 쓸 위험이 있습니까? 이러한 정당화되지 않은 우려 사항이 해결되면 이것이 매우 현대적인 해결책이라고 생각합니다.
케빈 닛 베트

1
필자의 경우, 내 텔넷 서버의 경우 100 %, 그렇습니다. 열쇠는 AcceptAsync, ReceiveAsync 등을 호출하기 전에 적절한 콜백 메소드를 설정하는 것입니다. 제 경우에는 별도의 스레드에서 SendAsync를 수행하므로 수락 / 보내기 / 받기 / 보내기 / 받기 / 연결 끊기 패턴을 수행하도록 수정 된 경우 수정해야합니다.
esac

1
포인트 # 2도 고려해야 할 사항입니다. 'Connection'개체를 SocketAsyncEventArgs 컨텍스트에 저장하고 있습니다. 이것이 의미하는 것은 연결 당 하나의 수신 버퍼 만 있다는 것입니다. DataReceived가 완료 될 때 까지이 SocketAsyncEventArgs로 다른 수신을 게시하지 않으므로 완료 될 때까지 더 이상 데이터를 읽을 수 없습니다. 이 데이터에 대해 더 이상 작업을 수행하지 않는 것이 좋습니다. 실제로 수신 된 모든 데이터의 전체 버퍼를 잠금이없는 큐로 옮긴 다음 별도의 스레드에서 처리합니다. 이것은 네트워크 부분에서 낮은 대기 시간을 보장합니다.
esac

1
참고로,이 코드에 대한 단위 테스트 및로드 테스트를 작성했으며 사용자로드를 1 명의 사용자에서 250 명의 사용자 (단일 듀얼 코어 시스템, 4GB의 RAM)로 증가 시키면 100 바이트 (1)의 응답 시간 패킷)과 10000 바이트 (3 패킷)는 전체 사용자로드 곡선에서 동일하게 유지되었습니다.
esac

46

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의 결과를 잠재적으로 수십만 명의 동시 클라이언트로 확장 할 수 있습니다 (올바른 하드웨어와 자신의 처리 코드를 효율적으로 구현했습니다).


1
더 작은 메모리 블록을 고정하기 위해 GCHandle object Alloc 메소드를 사용하여 버퍼를 고정 할 수 있습니다. 이 작업이 완료되면 Marshal 개체의 UnsafeAddrOfPinnedArrayElement를 사용하여 버퍼에 대한 포인터를 얻을 수 있습니다. 예를 들면 다음과 같습니다. GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan

@BobBryan 미묘한 요점을 놓치지 않는 한,이 방법은 큰 블록을 할당하여 솔루션에서 해결하려는 문제, 즉 작은 고정 블록의 반복 할당에 내재 된 극적인 메모리 조각화 가능성을 실제로 해결하지 못합니다. 기억의.
jerryjvl

요점은 메모리에 고정 된 상태로 유지하기 위해 큰 블록을 할당 할 필요가 없다는 것입니다. 더 작은 블록을 할당하고 위의 기술을 사용하여 gc가 움직이지 않도록 메모리에 고정시킬 수 있습니다. 하나의 큰 블록에 대한 참조를 유지하고 필요에 따라 재사용하는 것처럼 작은 블록 각각에 대한 참조를 유지할 수 있습니다. 두 가지 방법 중 하나가 유효합니다. 방금 매우 큰 버퍼를 사용할 필요가 없다는 것을 지적했습니다. 그러나 때로는 매우 큰 버퍼를 사용하는 것이 gc가 더 효율적으로 처리하기 때문에 가장 좋은 방법이라고 말했다.
Bob Bryan

@BobBryan 버퍼를 고정하는 것은 BeginReceive를 호출 할 때 자동으로 발생하므로 고정은 실제로 중요한 지점이 아닙니다. 효율성은;) ...였으며 이는 확장 가능한 서버를 작성할 때 특히 중요하므로 버퍼 공간에 사용할 큰 블록을 할당해야합니다.
jerryjvl 2016 년

@jerryjvl 정말 오래된 질문을하게되어 죄송합니다. 최근에 BeginXXX / EndXXX asynch 메소드에서이 정확한 문제를 발견했습니다. 이것은 훌륭한 게시물이지만 찾기 위해 많은 파기가 필요했습니다. 제안한 솔루션이 마음에 들지만 그 일부를 이해하지 못합니다. "그러면 1 바이트의 BeginReceive 호출을 읽고 콜백의 결과로 나머지 판독을 수행 할 수 있습니다." 콜백의 결과로 나머지 준비 작업을 수행한다는 것은 무슨 의미입니까?
Mausimo

22

위의 코드 샘플을 통해 이미 답변의 대부분을 얻었습니다. 비동기 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'를 추가하십시오.


여기에 좋은 정보가 있습니다. 여러 사람에게 현상금을 수여하기를 바랍니다. 그러나 나는 당신을 upvoted했습니다. 감사합니다.
에릭 Funkenbusch

11

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

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

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

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

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



9

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

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


8

한 가지 궁금합니다.

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

왜 그런 겁니까? 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();
    }
  }
}

Windows는 많은 스레드를 처리 할 수 ​​있지만 .NET은 실제로 처리하도록 설계되지 않았습니다. 각 .NET 앱 도메인에는 스레드 풀이 있으며 해당 스레드 풀을 소진하지 않습니다. 스레드 풀에서 가져온 스레드인지 아닌지 수동으로 스레드를 시작하는지 확실하지 않습니다. 그러나 대부분의 시간 동안 아무것도하지 않는 수백 개의 스레드는 막대한 자원 낭비입니다.
Erik Funkenbusch 님이

1
스레드에 대한 잘못된보기가 있다고 생각합니다. 스레드는 실제로 원할 경우 스레드 풀에서만 나옵니다. 일반 스레드는 그렇지 않습니다. 수백 개의 스레드가 아무것도 낭비하지 않습니다 :) (음, 약간의 메모리이지만 메모리가 너무 저렴하여 더 이상 문제가되지 않습니다.) 이에 대한 몇 가지 샘플 앱을 작성하려고합니다. 일단 완료되면. 그동안 위에 쓴 내용을 다시 살펴보고 내 질문에 답변 해 보시기 바랍니다.
Marcel Popescu

1
작성된 스레드가 스레드 풀에서 나오지 않는다는 스레드의 관점에 대한 Marcel의 의견에 동의하지만 나머지 문장은 정확하지 않습니다. 메모리는 컴퓨터에 얼마나 많이 설치되어 있는지에 관한 것이 아니며, Windows의 모든 응용 프로그램은 가상 주소 공간과 32 비트 시스템에서 실행되어 응용 프로그램의 데이터에 2GB를 제공합니다 (상자에 설치된 램의 양에 관계없이). 여전히 런타임에서 관리해야합니다. 비동기 IO를 수행하면 스레드를 사용하여 대기하지 않고 (IOCP를 사용하여 겹치는 IO를 허용 함) 더 나은 솔루션이며 확장 성이 향상됩니다.
Brian ONeil

7
많은 스레드를 실행할 때 문제가되는 것은 메모리가 아니라 CPU입니다. 스레드 간의 컨텍스트 전환은 상대적으로 비용이 많이 드는 작업이며 활성 스레드가 많을수록 더 많은 컨텍스트 전환이 발생합니다. 몇 년 전 C # 콘솔 앱을 사용하여 PC에서 테스트를 실행했습니다. 내 CPU가 100 % 인 스레드 500 개, 스레드가 중요한 작업을 수행하지 않았습니다. 네트워크 통신의 경우 스레드 수를 유지하는 것이 좋습니다.
sipwiz

1
작업 솔루션을 사용하거나 async / await를 사용합니다. 작업 솔루션은 더 단순 해 보이지만 async / await는 더 확장 가능할 것입니다 (특히 IO 바운드 상황을위한 것임).
Marcel Popescu

5

BeginRead모든 세부 정보를 올바르게 얻을 수 있다면 .NET의 통합 비동기 IO ( 등)를 사용하는 것이 좋습니다. 소켓 / 파일 핸들을 올바르게 설정하면 OS의 기본 IOCP 구현을 사용하여 스레드를 사용하지 않고 (또는 최악의 경우 커널의 IO 스레드 풀에서 나온 스레드를 사용하여) 작업을 완료 할 수 있습니다 스레드 풀 혼잡을 완화하는 데 도움이되는 .NET의 스레드 풀

주요 문제점은 비 차단 모드에서 소켓 / 파일을 여는 것입니다. (와 같은 File.OpenRead) 대부분의 기본 편의 기능 은이 작업을 수행하지 않으므로 직접 작성해야합니다.

다른 주요 관심사 중 하나는 오류 처리입니다. 비동기 I / O 코드를 작성할 때 오류를 올바르게 처리하는 것이 동기 코드에서 수행하는 것보다 훨씬 어렵습니다. 스레드를 직접 사용하지 않더라도 경쟁 조건 및 교착 상태가 발생하기 매우 쉽습니다. 따라서이를 알고 있어야합니다.

가능하면 편리한 라이브러리를 사용하여 확장 가능한 비동기 IO를 수행하는 프로세스를 용이하게해야합니다.

Microsoft의 Concurrency Coordination Runtime 은 이러한 종류의 프로그래밍을 쉽게 수행 할 수 있도록 설계된 .NET 라이브러리의 한 예입니다. 멋지게 보이지만 사용하지 않았으므로 얼마나 잘 확장되는지에 대해서는 언급 할 수 없습니다.

비동기 네트워크 또는 디스크 I / O를 수행해야하는 개인 프로젝트의 경우 지난 1 년 동안 Squared.Task 라는 .NET 동시성 / IO 도구 세트를 사용합니다 . imvu.tasktwisted 와 같은 라이브러리에서 영감을 얻었으며 네트워크 I / O를 수행하는 저장소에 실제 예제 를 포함 시켰습니다 . 또한 내가 작성한 몇 가지 응용 프로그램에서 사용했습니다. 공개적으로 출시 된 가장 큰 NDexer ( 스레드리스 디스크 I / O에 사용)입니다. 이 라이브러리는 imvu.task에 대한 나의 경험을 바탕으로 작성되었으며 상당히 포괄적 인 단위 테스트 세트가 있으므로 시험해 보는 것이 좋습니다. 문제가있는 경우 도움을 드리겠습니다.

제 생각에는 스레드 대신 비동기 / 스레드리스 IO를 사용한 경험을 바탕으로 학습 곡선을 처리 할 준비가되어있는 한 .NET 플랫폼에서 가치있는 노력을 기울입니다. Thread 객체의 비용으로 인한 확장 성 번거 로움을 피할 수 있으며 많은 경우 Futures / Promises와 같은 동시성 프리미티브를 신중하게 사용하여 잠금 및 뮤텍스의 사용을 완전히 피할 수 있습니다.


좋은 정보, 나는 당신의 참고 문헌을 확인하고 무엇이 의미가 있는지 볼 것입니다.
Erik Funkenbusch

3

Kevin의 솔루션을 사용했지만 솔루션에 메시지 재 조립을위한 코드가 부족하다고 말합니다. 개발자는이 코드를 사용하여 메시지를 재 조립할 수 있습니다.

private static void ReceiveCallback(IAsyncResult asyncResult )
{
    ClientInfo cInfo = (ClientInfo)asyncResult.AsyncState;

    cInfo.BytesReceived += cInfo.Soket.EndReceive(asyncResult);
    if (cInfo.RcvBuffer == null)
    {
        // First 2 byte is lenght
        if (cInfo.BytesReceived >= 2)
        {
            //this calculation depends on format which your client use for lenght info
            byte[] len = new byte[ 2 ] ;
            len[0] = cInfo.LengthBuffer[1];
            len[1] = cInfo.LengthBuffer[0];
            UInt16 length = BitConverter.ToUInt16( len , 0);

            // buffering and nulling is very important
            cInfo.RcvBuffer = new byte[length];
            cInfo.BytesReceived = 0;

        }
    }
    else
    {
        if (cInfo.BytesReceived == cInfo.RcvBuffer.Length)
        {
             //Put your code here, use bytes comes from  "cInfo.RcvBuffer"

             //Send Response but don't use async send , otherwise your code will not work ( RcvBuffer will be null prematurely and it will ruin your code)

            int sendLenghts = cInfo.Soket.Send( sendBack, sendBack.Length, SocketFlags.None);

            // buffering and nulling is very important
            //Important , set RcvBuffer to null because code will decide to get data or 2 bte lenght according to RcvBuffer's value(null or initialized)
            cInfo.RcvBuffer = null;
            cInfo.BytesReceived = 0;
        }
    }

    ContinueReading(cInfo);
 }

private static void ContinueReading(ClientInfo cInfo)
{
    try 
    {
        if (cInfo.RcvBuffer != null)
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, cInfo.BytesReceived, cInfo.RcvBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, cInfo.BytesReceived, cInfo.LengthBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
    }
    catch (SocketException se)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
    catch (Exception ex)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
}

class ClientInfo
{
    private const int BUFSIZE = 1024 ; // Max size of buffer , depends on solution  
    private const int BUFLENSIZE = 2; // lenght of lenght , depends on solution
    public int BytesReceived = 0 ;
    public byte[] RcvBuffer { get; set; }
    public byte[] LengthBuffer { get; set; }

    public Socket Soket { get; set; }

    public ClientInfo(Socket clntSock)
    {
        Soket = clntSock;
        RcvBuffer = null;
        LengthBuffer = new byte[ BUFLENSIZE ];
    }   

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

    Socket servSock = (Socket)asyncResult.AsyncState;
    Socket clntSock = null;

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

    }
    catch (SocketException se)
    {
        clntSock.Close();
    }
}
private static void Receive(ClientInfo cInfo )
{
    try
    {
        if (cInfo.RcvBuffer == null)
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, 0, 2, SocketFlags.None, ReceiveCallback, cInfo);

        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, 0, cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);

        }

    }
    catch (SocketException se)
    {
        return;
    }
    catch (Exception ex)
    {
        return;
    }

}


1

네트워크 서버를위한 일반적인 C ++ 프레임 워크 인 ACE (Adaptive Communications Environment)라는 프레임 워크를 사용해 볼 수 있습니다. 이 제품은 매우 견고하고 성숙한 제품으로, 통신 품질까지 높은 신뢰성, 대용량 응용 프로그램을 지원하도록 설계되었습니다.

이 프레임 워크는 매우 광범위한 동시성 모델을 다루며 응용 프로그램에 적합한 모델을 가지고있을 것입니다. 이는 대부분의 불쾌한 동시성 문제가 이미 정리되었으므로 시스템을 더 쉽게 디버깅 할 수있게합니다. 여기서의 단점은 프레임 워크가 C ++로 작성되었으며 가장 따뜻하고 모호한 코드 기반이 아니라는 것입니다. 반면에, 테스트를 거친 산업 등급의 네트워크 인프라와 확장 성이 뛰어난 아키텍처를 즉시 사용할 수 있습니다.


2
좋은 제안이지만 질문 태그에서 OP가 C #을 사용할 것이라고 생각합니다.
JPCosta

난 그것을 알아 챘다; 제안은 이것이 C ++에서 사용할 수 있으며 C #과 동등한 것을 알지 못한다는 것입니다. 이러한 종류의 시스템을 디버깅하는 것은 가장 쉬운 일이 아니며 C ++로 전환하는 것을 의미 하더라도이 프레임 워크로 돌아갈 수 있습니다.
ConcernedOfTunbridgeWells

예, 이것은 C #입니다. 좋은 .net 기반 솔루션을 찾고 있습니다. 좀 더 명확 했어야했지만 사람들이 태그를 읽을 것이라고 생각했습니다.
Erik Funkenbusch


1

글쎄, .NET 소켓은 select () 를 제공하는 것 같습니다 . 입력을 처리하는 데 가장 좋습니다. 출력을 위해 작업 대기열에서 수신 대기하는 소켓 작성기 스레드 풀을 가지고 작업 항목의 일부로 소켓 설명자 / 개체를 허용하므로 소켓 당 스레드가 필요하지 않습니다.


1

.Net 3.5에 추가 된 AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync 메서드를 사용합니다. 나는 벤치 마크를 수행했으며 100 명의 사용자가 지속적으로 데이터를주고받으며 약 35 % 더 빠릅니다 (응답 시간 및 비트 전송률).


1

수락 된 답변을 붙여 넣은 사람들에게 복사하면 _serverSocket.BeginAccept (new AsyncCallback (acceptCallback), _serverSocket)의 모든 호출을 제거하고 acceptCallback 메소드를 다시 작성할 수 있습니다. 다음과 같이 finally {} 절에 넣습니다.

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);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       finally
       {
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);       
       }
     }

내용이 동일하기 때문에 첫 번째 catch를 제거 할 수도 있지만 템플릿 방법이므로 예외를 더 잘 처리하고 오류의 원인을 이해하기 위해 유형이 지정된 예외를 사용해야하므로 유용한 코드로 해당 catch를 구현하십시오.



-1

분명히, 나는 .net 기반 솔루션을 찾고 있습니다 (가능한 경우 C #이지만 모든 .net 언어는 작동합니다)

순전히 .NET을 사용한다면 최고 수준의 확장 성을 얻지 못할 것입니다. GC 일시 중지는 대기 시간을 방해 할 수 있습니다.

서비스를 위해 하나 이상의 스레드를 시작해야합니다. 주어진 시간에 몇 명의 클라이언트가 연결 될지 모르기 때문에 (아마도 수백 개) Asynch API (BeginRecieve 등) 사용을 고려하고 있습니다. 각 연결마다 스레드를 시작하고 싶지 않습니다.

겹친 IO 는 일반적으로 네트워크 통신을위한 Windows의 가장 빠른 API로 간주됩니다. 이것이 Asynch API와 동일한 지 모르겠습니다. 각 호출이 활성 소켓에서 콜백을하는 대신 열려있는 모든 소켓을 확인해야하므로 select를 사용하지 마십시오.


1
GC 일시 중지 설명을 이해하지 못합니다. GC와 직접 관련된 확장 성 문제가있는 시스템을 본 적이 없습니다.
markt

4
GC가 존재하기 때문에 열악한 아키텍처로 인해 확장 할 수없는 앱을 구축 할 가능성이 훨씬 높습니다. .NET과 Java를 모두 사용하여 대규모 확장 가능 + 고성능 시스템이 구축되었습니다. 제공 한 두 링크에서 원인은 직접 가비지 콜렉션이 아니라 힙 스와핑과 관련이 있습니다. 나는 그것이 피할 수 있었던 아키텍처에 실제로 문제가 있다고 의심 할 것이다. 만약 확장 할 수없는 시스템을 구축 할 수없는 언어를 보여줄 수 있다면 기꺼이 사용할 것이다.)
markt

1
이 의견에 동의하지 않습니다. 알 수없는 질문은 Java이며, 더 큰 메모리 할당을 다루고 수동으로 gc를 강제하려고합니다. 나는 실제로 엄청난 양의 메모리 할당을하지 않을 것입니다. 이것은 단지 문제가 아닙니다. 하지만 고마워 예. 비동기 프로그래밍 모델은 일반적으로 오버랩 된 IO 위에 구현됩니다.
Erik Funkenbusch

1
실제로 모범 사례는 지속적으로 수동으로 GC를 강제로 수집하지 않아야합니다. 이로 인해 앱 성능이 저하 될 수 있습니다. .NET GC는 앱 사용에 맞게 조정되는 차세대 GC입니다. GC.Collect를 수동으로 호출해야한다고 생각한다면 코드를 다른 방법으로 작성해야 할 가능성이 높습니다 ..
markt

1
@markt, 가비지 수집에 대해 전혀 모르는 사람들을위한 주석입니다. 유휴 시간이 있으면 수동 수집을 수행하는 데 아무런 문제가 없습니다. 완료 될 때 응용 프로그램이 더 나빠지지는 않습니다. 학술 논문에 따르면 세대 별 GC는 객체 수명의 근사치 때문에 작동합니다. 분명히 이것은 완벽한 표현이 아닙니다. 실제로 "가장 오래된"세대는 가비지 수집되지 않기 때문에 가비지 비율이 가장 높은 역설이 있습니다.
알 수 없음

-1

고성능 서버 개발을 위해 Push Framework 오픈 소스 프레임 워크를 사용할 수 있습니다. IOCP를 기반으로하며 푸시 시나리오 및 메시지 브로드 캐스트에 적합합니다.

http://www.pushframework.com


1
이 포스트는 C #과 .net으로 태그되었습니다. C ++ 프레임 워크를 제안한 이유는 무엇입니까?
Erik Funkenbusch

아마 그가 썼기 때문일 것입니다. potatosoftware.com/…
quillbreaker

푸시 프레임 워크가 여러 서버 인스턴스를 지원합니까? 그렇지 않다면 어떻게 확장됩니까?
esskar
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.