Cách viết máy chủ dựa trên Tcp / Ip có thể mở rộng


148

Tôi đang trong giai đoạn thiết kế để viết một ứng dụng Windows Service mới chấp nhận các kết nối TCP / IP cho các kết nối chạy dài (nghĩa là không giống như HTTP khi có nhiều kết nối ngắn, mà là một máy khách kết nối và duy trì kết nối trong nhiều giờ hoặc nhiều ngày hoặc thậm chí cả tuần).

Tôi đang tìm ý tưởng cho cách tốt nhất để thiết kế kiến ​​trúc mạng. Tôi sẽ cần bắt đầu ít nhất một luồng cho dịch vụ. Tôi đang xem xét sử dụng API Asynch (BeginRecieve, v.v.) vì tôi không biết mình sẽ kết nối được bao nhiêu khách hàng tại bất kỳ thời điểm nào (có thể hàng trăm). Tôi chắc chắn không muốn bắt đầu một chủ đề cho mỗi kết nối.

Dữ liệu chủ yếu sẽ chảy ra máy khách từ máy chủ của tôi, nhưng thỉnh thoảng sẽ có một số lệnh được gửi từ máy khách. Đây chủ yếu là một ứng dụng giám sát trong đó máy chủ của tôi gửi dữ liệu trạng thái định kỳ cho khách hàng.

Bất kỳ đề xuất về cách tốt nhất để làm cho điều này có thể mở rộng càng tốt? Quy trình làm việc cơ bản? Cảm ơn.

EDIT: Để rõ ràng, tôi đang tìm giải pháp dựa trên .net (C # nếu có thể, nhưng bất kỳ ngôn ngữ .net nào cũng sẽ hoạt động)

BOUNTY LƯU Ý: Để được thưởng tiền thưởng, tôi mong đợi nhiều hơn một câu trả lời đơn giản. Tôi sẽ cần một ví dụ hoạt động của một giải pháp, như là một con trỏ tới thứ gì đó tôi có thể tải xuống hoặc một ví dụ ngắn gọn trong dòng. Và nó phải dựa trên .net và Windows (mọi ngôn ngữ .net đều được chấp nhận)

EDIT: Tôi muốn cảm ơn tất cả mọi người đã đưa ra câu trả lời tốt. Thật không may, tôi chỉ có thể chấp nhận một và tôi đã chọn chấp nhận phương thức Bắt đầu / Kết thúc nổi tiếng hơn. Giải pháp của Esac có thể tốt hơn, nhưng nó vẫn đủ mới mà tôi không biết chắc chắn nó sẽ hoạt động như thế nào.

Tôi đã nêu lên tất cả các câu trả lời tôi nghĩ là tốt, tôi ước tôi có thể làm nhiều hơn cho các bạn. Cảm ơn một lần nữa.


1
Bạn có chắc chắn rằng nó cần phải là một kết nối dài hạn? Thật khó có thể nói từ những thông tin hạn chế cung cấp, nhưng tôi sẽ chỉ làm điều đó nếu thực sự cần thiết ..
Markt

Vâng, nó phải được chạy dài. Dữ liệu phải được cập nhật theo thời gian thực, vì vậy tôi không thể thực hiện bỏ phiếu định kỳ, dữ liệu phải được đẩy đến máy khách khi nó xảy ra, có nghĩa là kết nối liên tục.
Erik Funkenbusch

1
Đó không phải là một lý do hợp lệ. Hỗ trợ kết nối chạy lâu chỉ tốt. Bạn chỉ cần mở một kết nối và chờ cho một repsonse (cuộc thăm dò bị đình trệ). Điều này hoạt động tốt đối với nhiều ứng dụng kiểu AJAX, v.v ... Bạn nghĩ gmail hoạt động như thế nào :-)
TFD

2
Gmail hoạt động bằng cách bỏ phiếu định kỳ cho email, nó không giữ kết nối lâu dài. Điều này tốt cho email, nơi không yêu cầu trả lời theo thời gian thực.
Erik Funkenbusch

2
Bỏ phiếu, hoặc kéo, quy mô tốt nhưng phát triển độ trễ nhanh chóng. Đẩy không quy mô là tốt, nhưng giúp giảm hoặc loại bỏ độ trễ.
andrewbadera

Câu trả lời:


92

Tôi đã viết một cái gì đó tương tự như thế này trong quá khứ. Từ nghiên cứu của tôi nhiều năm trước cho thấy rằng việc thực hiện ổ cắm của riêng bạn là đặt cược tốt nhất, sử dụng các ổ cắm không đồng bộ. Điều này có nghĩa là khách hàng không thực sự làm bất cứ điều gì thực sự đòi hỏi tương đối ít tài nguyên. Bất cứ điều gì xảy ra đều được xử lý bởi nhóm luồng .net.

Tôi đã viết nó như là một lớp quản lý tất cả các kết nối cho các máy chủ.

Tôi chỉ đơn giản là sử dụng một danh sách để giữ tất cả các kết nối máy khách, nhưng nếu bạn cần tra cứu nhanh hơn cho danh sách lớn hơn, bạn có thể viết nó theo cách bạn muốn.

private List<xConnection> _sockets;

Ngoài ra, bạn cần các ổ cắm thực sự lắng nghe cho các kết nối không liên quan.

private System.Net.Sockets.Socket _serverSocket;

Phương thức start thực sự khởi động socket máy chủ và bắt đầu lắng nghe mọi kết nối không liên quan.

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

Tôi chỉ muốn lưu ý mã xử lý ngoại lệ có vẻ tệ, nhưng lý do là tôi có mã loại trừ ngoại lệ trong đó để mọi ngoại lệ sẽ bị loại bỏ và trả về falsenếu tùy chọn cấu hình được đặt, nhưng tôi muốn xóa nó cho lợi ích ngắn gọn.

_ServerSocket.BeginAccept (AsyncCallback mới (acceptCallback)), _serverSocket) ở trên về cơ bản đặt ổ cắm máy chủ của chúng tôi để gọi phương thức acceptCallback mỗi khi người dùng kết nối. Phương thức này chạy từ luồng xử lý .Net, tự động xử lý việc tạo các luồng công nhân bổ sung nếu bạn có nhiều thao tác chặn. Điều này sẽ tối ưu xử lý bất kỳ tải trên máy chủ.

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

Đoạn mã trên về cơ bản vừa hoàn thành việc chấp nhận kết nối đi vào, hàng đợi BeginReceivelà cuộc gọi lại sẽ chạy khi máy khách gửi dữ liệu, và sau đó xếp hàng tiếp theo acceptCallbacksẽ chấp nhận kết nối máy khách tiếp theo.

Các BeginReceive gọi phương thức là những gì cho ổ cắm biết phải làm gì khi nhận được dữ liệu từ máy khách. Đối với BeginReceive, bạn cần cung cấp cho nó một mảng byte, đó là nơi nó sẽ sao chép dữ liệu khi máy khách gửi dữ liệu. Các ReceiveCallbackphương pháp sẽ được gọi, đó là cách chúng tôi xử lý dữ liệu nhận được.

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

EDIT: Trong mẫu này tôi quên đề cập rằng trong lĩnh vực mã này:

//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);

Những gì tôi thường làm là trong bất cứ điều gì bạn muốn mã, là thực hiện việc sắp xếp lại các gói thành tin nhắn, và sau đó tạo chúng như các công việc trên nhóm luồng. Bằng cách này, BeginReceive của khối tiếp theo từ máy khách không bị trì hoãn trong khi bất kỳ mã xử lý tin nhắn nào đang chạy.

Cuộc gọi lại chấp nhận kết thúc việc đọc ổ cắm dữ liệu bằng cách gọi kết thúc nhận. Điều này sẽ điền vào bộ đệm được cung cấp trong hàm bắt đầu nhận. Khi bạn làm bất cứ điều gì bạn muốn nơi tôi để lại nhận xét, chúng tôi gọi BeginReceivephương thức tiếp theo sẽ chạy lại cuộc gọi lại nếu khách hàng gửi thêm dữ liệu. Bây giờ đây là phần thực sự khó khăn, khi khách hàng gửi dữ liệu, cuộc gọi lại nhận được của bạn chỉ có thể được gọi với một phần của tin nhắn. Việc lắp lại có thể trở nên rất rất phức tạp. Tôi đã sử dụng phương pháp của riêng mình và tạo ra một loại giao thức độc quyền để làm điều này. Tôi đã để nó ra, nhưng nếu bạn yêu cầu, tôi có thể thêm nó vào. Trình xử lý này thực sự là đoạn mã phức tạp nhất tôi từng viết.

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

Phương thức gửi ở trên thực sự sử dụng đồng bộ Send cuộc gọi , đối với tôi nó vẫn ổn do kích thước tin nhắn và tính chất đa luồng của ứng dụng của tôi. Nếu bạn muốn gửi cho mọi khách hàng, bạn chỉ cần lặp qua Danh sách _sockets.

Lớp xConnection mà bạn thấy được tham chiếu ở trên về cơ bản là một trình bao bọc đơn giản cho một socket để bao gồm bộ đệm byte và trong triển khai của tôi một số tính năng bổ sung.

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

Ngoài ra để tham khảo ở đây là những cái usingtôi bao gồm vì tôi luôn cảm thấy khó chịu khi chúng không được bao gồm.

using System.Net.Sockets;

Tôi hy vọng điều đó hữu ích, nó có thể không phải là mã sạch nhất, nhưng nó hoạt động. Cũng có một số sắc thái của mã mà bạn nên mệt mỏi khi thay đổi. Đối với một, chỉ có mộtBeginAccept gọi bất cứ lúc nào. Đã từng có một lỗi .net rất khó chịu xung quanh vấn đề này, cách đây nhiều năm nên tôi không nhớ lại chi tiết.

Ngoài ra, trong ReceiveCallbackmã, chúng tôi xử lý mọi thứ nhận được từ ổ cắm trước khi chúng tôi xếp hàng nhận tiếp theo. Điều này có nghĩa là đối với một ổ cắm duy nhất, chúng ta chỉ thực sự từng có ReceiveCallbackmột lần tại bất kỳ thời điểm nào và chúng ta không cần phải sử dụng đồng bộ hóa luồng. Tuy nhiên, nếu bạn sắp xếp lại thứ này để gọi nhận tiếp theo ngay sau khi lấy dữ liệu, có thể nhanh hơn một chút, bạn sẽ cần đảm bảo rằng bạn đã đồng bộ hóa đúng các luồng.

Ngoài ra, tôi đã hack rất nhiều mã của mình, nhưng để lại bản chất của những gì đang diễn ra. Đây sẽ là một khởi đầu tốt cho thiết kế của bạn. Để lại một bình luận nếu bạn có thêm bất kỳ câu hỏi xung quanh này.


1
Đây là một câu trả lời hay Kevin .. có vẻ như bạn đang đi đúng hướng để nhận tiền thưởng. :)
Erik Funkenbusch

6
Tôi không biết tại sao đây là câu trả lời được bình chọn cao nhất. Bắt đầu * Kết thúc * không phải là cách nhanh nhất để thực hiện kết nối mạng trong C #, cũng không phải là khả năng mở rộng cao nhất. Nó nhanh hơn đồng bộ, nhưng có rất nhiều hoạt động diễn ra dưới vỏ bọc trong Windows thực sự làm chậm đường dẫn mạng này.
esac

6
Hãy ghi nhớ những gì esac đã viết trong bình luận trước. Mẫu đầu bắt đầu có thể sẽ hoạt động cho bạn đến một điểm, cho biết mã của tôi hiện đang sử dụng đầu, nhưng có những cải tiến đối với các hạn chế của nó trong .net 3.5. Tôi không quan tâm đến tiền thưởng nhưng sẽ khuyên bạn nên đọc liên kết trong câu trả lời của tôi ngay cả khi bạn thực hiện phương pháp này. "Cải tiến hiệu suất ổ cắm trong phiên bản 3.5"
jvanderh

1
Tôi chỉ muốn ném vào chúng vì tôi có thể không đủ rõ ràng, đây là mã thời đại .net 2.0 nơi tôi tin rằng đây là một mô hình rất khả thi. Tuy nhiên, câu trả lời của esac có vẻ hiện đại hơn một chút nếu nhắm mục tiêu .net 3.5, chỉ số duy nhất tôi có là ném các sự kiện :) nhưng điều đó có thể dễ dàng thay đổi. Ngoài ra, tôi đã thực hiện kiểm tra thông lượng với mã này và trên opteron lõi kép 2Ghz có thể đạt tối đa 100Mbps ethernet và đã thêm một lớp mã hóa lên trên mã này.
Kevin Nisbet

1
@KevinNisbet Tôi biết điều này khá muộn, nhưng đối với bất kỳ ai sử dụng câu trả lời này để thiết kế máy chủ của riêng họ - việc gửi cũng không đồng bộ, vì nếu không, bạn sẽ tự mở ra khả năng bế tắc. Nếu cả hai bên ghi dữ liệu lấp đầy bộ đệm tương ứng của họ, các Sendphương thức sẽ chặn vô thời hạn ở cả hai bên, vì không có ai đọc dữ liệu đầu vào.
Luaan

83

Có nhiều cách để thực hiện các hoạt động mạng trong C #. Tất cả chúng sử dụng các cơ chế khác nhau dưới mui xe, và do đó chịu các vấn đề hiệu suất lớn với tính đồng thời cao. Các hoạt động bắt đầu * là một trong những hoạt động mà nhiều người thường nhầm lẫn là cách nhanh nhất / nhanh nhất để thực hiện kết nối mạng.

Để giải quyết những vấn đề này, họ đã giới thiệu bộ phương pháp * Async: Từ MSDN http://msdn.microsoft.com/en-us/l Library / system.net.sockets.socketasynceventargs.aspx

Lớp SocketAsyncEventArss là một phần của tập hợp các cải tiến cho System.Net.Sockets .. ::. Lớp socket cung cấp một mẫu không đồng bộ thay thế có thể được sử dụng bởi các ứng dụng ổ cắm hiệu suất cao chuyên dụng. Lớp này được thiết kế đặc biệt cho các ứng dụng máy chủ mạng đòi hỏi hiệu năng cao. Một ứng dụng có thể sử dụng mẫu không đồng bộ nâng cao độc quyền hoặc chỉ trong các khu vực nóng được nhắm mục tiêu (ví dụ: khi nhận được lượng lớn dữ liệu).

Tính năng chính của những cải tiến này là tránh việc phân bổ lặp lại và đồng bộ hóa các đối tượng trong quá trình I / O ổ cắm không đồng bộ khối lượng lớn. Mẫu thiết kế Bắt đầu / Kết thúc hiện đang được thực hiện bởi System.Net.Sockets .. ::. Lớp socket yêu cầu một đối tượng System .. ::. IAsyncResult được phân bổ cho mỗi hoạt động của ổ cắm không đồng bộ.

Trong vỏ bọc, API * Async sử dụng các cổng hoàn thành IO là cách nhanh nhất để thực hiện các hoạt động mạng, xem http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

Và chỉ để giúp bạn hiểu, tôi bao gồm mã nguồn cho máy chủ telnet mà tôi đã viết bằng API * Async. Tôi chỉ bao gồm các phần có liên quan. Cũng cần lưu ý, thay vì xử lý dữ liệu nội tuyến, thay vào đó tôi chọn cách đẩy nó lên hàng đợi khóa miễn phí (chờ miễn phí) được xử lý trên một luồng riêng biệt. Lưu ý rằng tôi không bao gồm lớp Pool tương ứng chỉ là một nhóm đơn giản sẽ tạo một đối tượng mới nếu nó trống và lớp Buffer chỉ là một bộ đệm tự mở rộng không thực sự cần thiết trừ khi bạn nhận được một thứ không xác định Số lượng dữ liệu. Nếu bạn muốn biết thêm thông tin, vui lòng gửi cho tôi một 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
    }
}

Đây là khá thẳng về phía trước, và một ví dụ đơn giản. Cảm ơn. Tôi sẽ phải đánh giá các ưu và nhược điểm của từng phương pháp.
Erik Funkenbusch

Tôi đã không có cơ hội để kiểm tra nó nhưng tôi cảm thấy mơ hồ về tình trạng chủng tộc ở đây vì một số lý do. Đầu tiên, nếu bạn nhận được nhiều tin nhắn, tôi không biết rằng các sự kiện sẽ được xử lý theo thứ tự (có thể không quan trọng đối với ứng dụng của người dùng, nhưng cần lưu ý) hoặc tôi có thể sai và các sự kiện sẽ được xử lý theo thứ tự. Thứ hai là tôi có thể đã bỏ lỡ nó nhưng không có nguy cơ bộ đệm bị ghi đè bị xóa trong khi DataReceured vẫn đang chạy nếu mất nhiều thời gian? Nếu những mối quan tâm không chính đáng này được giải quyết, tôi nghĩ rằng đây là một giải pháp hiện đại rất tốt.
Kevin Nisbet

1
Trong trường hợp của tôi, đối với máy chủ telnet của tôi, 100%, CÓ, chúng theo thứ tự. Khóa này đang thiết lập phương thức gọi lại phù hợp trước khi gọi AcceptAsync, receiveAsync, v.v. nó sẽ cần phải được sửa đổi.
esac

1
Điểm # 2 cũng là điều bạn sẽ cần xem xét. Tôi đang lưu trữ đối tượng 'Kết nối' của mình trong ngữ cảnh SocketAsyncEventArss. Điều này có nghĩa là tôi chỉ có một bộ đệm nhận cho mỗi kết nối. Tôi sẽ không đăng một nhận khác với SocketAsyncEventArss này cho đến khi DataReceured hoàn tất, vì vậy không có dữ liệu nào có thể được đọc về điều này cho đến khi hoàn thành. Tôi TUYỆT VỜI rằng không có thao tác dài nào được thực hiện trên dữ liệu này. Tôi thực sự di chuyển toàn bộ bộ đệm của tất cả dữ liệu nhận được vào hàng đợi không khóa và sau đó xử lý nó trên một luồng riêng biệt. Điều này đảm bảo độ trễ thấp trên phần mạng.
esac

1
Bên cạnh đó, tôi đã viết các bài kiểm tra đơn vị và kiểm tra tải cho mã này và khi tôi tăng tải người dùng từ 1 người dùng lên 250 người dùng (trên hệ thống lõi kép đơn, 4GB RAM), thời gian đáp ứng cho 100 byte (1 gói) và 10000 byte (3 gói) giữ nguyên trong toàn bộ đường cong tải của người dùng.
esac

46

Đã từng có một cuộc thảo luận thực sự tốt về TCP / IP có thể mở rộng bằng .NET được viết bởi Chris Mullins của Coverant, thật không may là blog của anh ta đã biến mất khỏi vị trí trước đó, vì vậy tôi sẽ cố gắng ghép lời khuyên của anh ta khỏi bộ nhớ (một số bình luận hữu ích về sự xuất hiện của anh ấy trong chủ đề này: C ++ so với C #: Phát triển máy chủ IOCP có khả năng mở rộng cao )

Trước hết, lưu ý rằng cả việc sử dụng Begin/Endvà các Asyncphương thức trên Socketlớp đều sử dụng Cổng hoàn thành IO (IOCP) để cung cấp khả năng mở rộng. Điều này tạo ra sự khác biệt lớn hơn nhiều (khi được sử dụng một cách chính xác; xem bên dưới) về khả năng mở rộng so với hai phương pháp bạn thực sự chọn để thực hiện giải pháp của mình.

Các bài đăng của Chris Mullins được dựa trên việc sử dụng Begin/End, đó là bài viết mà cá nhân tôi có kinh nghiệm. Lưu ý rằng Chris đã kết hợp một giải pháp dựa trên giải pháp này có tỷ lệ lên tới 10.000 kết nối máy khách đồng thời trên máy 32 bit có bộ nhớ 2GB và thành 100.000 trên nền tảng 64 bit có đủ bộ nhớ. Từ kinh nghiệm của bản thân với kỹ thuật này (không có nơi nào gần loại tải này), tôi không có lý do gì để nghi ngờ những con số chỉ định này.

IOCP so với luồng trên mỗi kết nối hoặc 'chọn' nguyên thủy

Lý do bạn muốn sử dụng cơ chế sử dụng IOCP trong chương trình này là vì nó sử dụng nhóm luồng Windows cấp độ rất thấp không đánh thức bất kỳ luồng nào cho đến khi có dữ liệu thực tế trên kênh IO mà bạn đang cố đọc từ ( lưu ý rằng IOCP cũng có thể được sử dụng cho tệp IO). Lợi ích của việc này là Windows không phải chuyển sang một luồng chỉ để thấy rằng dù sao vẫn chưa có dữ liệu, vì vậy điều này làm giảm số lượng chuyển đổi ngữ cảnh mà máy chủ của bạn sẽ phải thực hiện ở mức tối thiểu cần thiết.

Chuyển mạch ngữ cảnh là thứ chắc chắn sẽ giết chết cơ chế 'luồng trên mỗi kết nối', mặc dù đây là một giải pháp khả thi nếu bạn chỉ xử lý vài chục kết nối. Cơ chế này tuy nhiên không có khả năng mở rộng trí tưởng tượng 'có thể mở rộng'.

Những cân nhắc quan trọng khi sử dụng IOCP

Ký ức

Đầu tiên và quan trọng nhất là phải hiểu rằng IOCP có thể dễ dàng dẫn đến các vấn đề về bộ nhớ trong .NET nếu việc triển khai của bạn quá ngây thơ. Mỗi BeginReceivecuộc gọi IOCP sẽ dẫn đến việc "ghim" bộ đệm bạn đang đọc vào. Để biết giải thích tốt về lý do tại sao đây là một vấn đề, hãy xem: Weblog của Yun Jin: OutOfMemoryException và Ghim .

May mắn là vấn đề này có thể tránh được, nhưng nó đòi hỏi một chút đánh đổi. Giải pháp được đề xuất là phân bổ một byte[]bộ đệm lớn khi khởi động ứng dụng (hoặc đóng lại), ít nhất là 90KB (như .NET 2, kích thước yêu cầu có thể lớn hơn trong các phiên bản sau). Lý do để làm điều này là việc phân bổ bộ nhớ lớn sẽ tự động kết thúc trong một phân đoạn bộ nhớ không nén (Heap đối tượng lớn) được ghim tự động một cách hiệu quả. Bằng cách phân bổ một bộ đệm lớn khi khởi động, bạn đảm bảo rằng khối bộ nhớ không thể di chuyển này nằm ở một "địa chỉ thấp", nơi nó sẽ không cản trở và gây ra sự phân mảnh.

Sau đó, bạn có thể sử dụng offset để phân đoạn một bộ đệm lớn này thành các khu vực riêng biệt cho mỗi kết nối cần đọc một số dữ liệu. Đây là nơi một sự đánh đổi diễn ra; vì bộ đệm này cần được phân bổ trước, bạn sẽ phải quyết định dung lượng bộ đệm bạn cần cho mỗi kết nối và giới hạn trên mà bạn muốn đặt cho số lượng kết nối bạn muốn chia tỷ lệ (hoặc, bạn có thể thực hiện trừu tượng hóa có thể phân bổ bộ đệm được ghim bổ sung khi bạn cần chúng).

Giải pháp đơn giản nhất là gán cho mỗi kết nối một byte ở mức bù duy nhất trong bộ đệm này. Sau đó, bạn có thể thực hiện BeginReceivecuộc gọi cho một byte duy nhất được đọc và thực hiện phần còn lại của việc đọc là kết quả của cuộc gọi lại mà bạn nhận được.

Chế biến

Khi bạn nhận được cuộc gọi lại từ Begincuộc gọi bạn đã thực hiện, điều rất quan trọng là phải nhận ra rằng mã trong cuộc gọi lại sẽ thực thi trên luồng IOCP cấp thấp. Điều tuyệt đối cần thiết là bạn tránh các thao tác kéo dài trong cuộc gọi lại này. Sử dụng các luồng này để xử lý phức tạp sẽ giết chết khả năng mở rộng của bạn hiệu quả tương đương với việc sử dụng 'luồng trên mỗi kết nối'.

Giải pháp được đề xuất là chỉ sử dụng gọi lại để xếp hàng một mục công việc để xử lý dữ liệu đến, sẽ được thực hiện trên một số luồng khác. Tránh mọi hoạt động có khả năng chặn bên trong cuộc gọi lại để luồng IOCP có thể quay lại nhóm của nó càng nhanh càng tốt. Trong .NET 4.0, tôi đề xuất giải pháp đơn giản nhất là sinh ra a Task, cung cấp cho nó một tham chiếu đến ổ cắm máy khách và một bản sao của byte đầu tiên đã được đọc bởi BeginReceivecuộc gọi. Nhiệm vụ này sau đó chịu trách nhiệm đọc tất cả dữ liệu từ ổ cắm thể hiện yêu cầu bạn đang xử lý, thực hiện nó và sau đó thực hiện một BeginReceivecuộc gọi mới để xếp hàng ổ cắm cho IOCP một lần nữa. Trước .NET 4.0, bạn có thể sử dụng ThreadPool hoặc tạo triển khai hàng đợi công việc theo luồng của riêng bạn.

Tóm lược

Về cơ bản, tôi khuyên bạn nên sử dụng mã mẫu của Kevin cho giải pháp này, với các cảnh báo được thêm sau:

  • Đảm bảo bộ đệm bạn chuyển đến BeginReceiveđã được 'ghim'
  • Hãy chắc chắn rằng cuộc gọi lại mà bạn chuyển đến BeginReceivekhông làm gì khác hơn là xếp hàng một tác vụ để xử lý việc xử lý thực tế của dữ liệu đến

Khi bạn làm điều đó, tôi chắc chắn rằng bạn có thể sao chép kết quả của Chris trong việc nhân rộng lên hàng trăm ngàn khách hàng đồng thời (được cung cấp phần cứng phù hợp và triển khai hiệu quả mã xử lý của chính bạn;)


1
Để ghim một khối bộ nhớ nhỏ hơn, phương thức Alloc của đối tượng GCHandle có thể được sử dụng để ghim bộ đệm. Khi điều này được thực hiện, UnsafeAddrOfPinnedArrayEuity của đối tượng Marshal có thể được sử dụng để lấy một con trỏ tới bộ đệm. Ví dụ: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayEuity (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPulum ();
Bob Bryan

@BobBryan Trừ khi tôi bỏ lỡ một điểm tinh tế mà bạn đang cố gắng thực hiện, cách tiếp cận đó không thực sự giúp giải quyết vấn đề mà giải pháp của tôi đang cố gắng giải quyết bằng cách phân bổ các khối lớn, cụ thể là khả năng phân mảnh bộ nhớ lớn vốn có trong việc phân bổ lặp lại các khối nhỏ được ghim của bộ nhớ.
jerryjvl

Chà, vấn đề là bạn không phải phân bổ một khối lớn để giữ cho nó được ghim trong bộ nhớ. Bạn có thể phân bổ các khối nhỏ hơn và sử dụng kỹ thuật trên để ghim chúng vào bộ nhớ để tránh việc gc di chuyển chúng. Bạn có thể giữ một tham chiếu đến từng khối nhỏ hơn, giống như bạn giữ một tham chiếu đến một khối lớn hơn và sử dụng lại chúng khi cần. Một trong hai cách tiếp cận là hợp lệ - Tôi chỉ chỉ ra rằng bạn không phải sử dụng một bộ đệm rất lớn. Nhưng, đã nói rằng đôi khi sử dụng một bộ đệm rất lớn là cách tốt nhất vì gc sẽ xử lý nó hiệu quả hơn.
Bob Bryan

@BobBryan kể từ khi ghim bộ đệm xảy ra tự động khi bạn gọi BeginReceive, việc ghim không thực sự là điểm nổi bật ở đây; hiệu quả là;) ... và điều này đặc biệt đáng quan tâm khi cố gắng viết một máy chủ có thể mở rộng, do đó cần phải phân bổ các khối lớn để sử dụng cho không gian bộ đệm.
jerryjvl

@jerryjvl Xin lỗi vì đã đưa ra một câu hỏi thực sự cũ, tuy nhiên gần đây tôi đã phát hiện ra vấn đề chính xác này với các phương pháp asynch BeginXXX / EndXXX. Đây là một bài viết tuyệt vời, nhưng đã mất rất nhiều đào để tìm. Tôi thích giải pháp được đề xuất của bạn nhưng không hiểu một phần của nó: "Sau đó, bạn có thể thực hiện cuộc gọi BeginReceive để đọc một byte duy nhất và thực hiện phần còn lại của việc đọc do kết quả của cuộc gọi lại mà bạn nhận được." Bạn có ý nghĩa gì khi thực hiện phần còn lại của sự sẵn sàng như là kết quả của cuộc gọi lại mà bạn nhận được?
Mausimo

22

Bạn đã có hầu hết các câu trả lời thông qua các mẫu mã ở trên. Sử dụng hoạt động IO không đồng bộ hoàn toàn là cách để đi đến đây. Async IO là cách Win32 được thiết kế nội bộ theo tỷ lệ. Hiệu suất tốt nhất có thể bạn có thể đạt được bằng cách sử dụng Cổng hoàn thành, ràng buộc các ổ cắm của bạn với các cổng hoàn thành và có một nhóm luồng đang chờ hoàn thành cổng. Sự khôn ngoan phổ biến là có 2-4 luồng trên mỗi CPU (lõi) đang chờ hoàn thành. Tôi đặc biệt khuyên bạn nên xem qua ba bài viết này của Rick Vicik từ nhóm Hiệu suất Windows:

  1. Thiết kế ứng dụng cho hiệu suất - Phần 1
  2. Thiết kế ứng dụng cho hiệu suất - Phần 2
  3. Thiết kế ứng dụng cho hiệu suất - Phần 3

Các bài viết nói trên chủ yếu bao gồm API Windows gốc, nhưng chúng phải đọc cho bất kỳ ai đang cố gắng nắm bắt khả năng mở rộng và hiệu suất. Họ cũng có một số tóm tắt về mặt quản lý của mọi thứ quá.

Điều thứ hai bạn sẽ cần làm là đảm bảo bạn xem qua cuốn sách Cải thiện hiệu suất và khả năng mở rộng ứng dụng .NET , có sẵn trực tuyến. Bạn sẽ tìm thấy lời khuyên phù hợp và hợp lệ xung quanh việc sử dụng chuỗi, cuộc gọi và khóa không đồng bộ trong Chương 5. Nhưng những viên ngọc thực sự nằm trong Chương 17, nơi bạn sẽ tìm thấy những điều tốt đẹp như hướng dẫn thực tế về điều chỉnh nhóm luồng của bạn. Các ứng dụng của tôi có một số vấn đề nghiêm trọng cho đến khi tôi điều chỉnh maxIothreads / maxWorkerThreads theo các khuyến nghị trong chương này.

Bạn nói rằng bạn muốn làm một máy chủ TCP thuần túy, vì vậy điểm tiếp theo của tôi là giả mạo. Tuy nhiên , nếu bạn thấy mình bị dồn vào chân tường và sử dụng lớp WebRequest và các dẫn xuất của nó, hãy cảnh báo rằng có một con rồng bảo vệ cánh cửa đó: ServicePointManager . Đây là một lớp cấu hình có một mục đích trong cuộc sống: làm hỏng hiệu suất của bạn. Hãy chắc chắn rằng bạn giải phóng máy chủ của mình khỏi ServicePoint.ConnectionLimit nhân tạo hoặc ứng dụng của bạn sẽ không bao giờ mở rộng (Tôi cho phép bạn khám phá xem giá trị mặc định là gì ...). Bạn cũng có thể xem xét lại chính sách mặc định gửi tiêu đề Expect100Continue trong các yêu cầu http.

Bây giờ về API lõi được quản lý ổ cắm, mọi thứ khá dễ dàng ở phía Gửi, nhưng chúng phức tạp hơn đáng kể ở phía Nhận. Để đạt được thông lượng và quy mô cao, bạn phải đảm bảo rằng ổ cắm không bị kiểm soát dòng chảy vì bạn không có bộ đệm được đăng để nhận. Lý tưởng cho hiệu suất cao, bạn nên đăng trước 3-4 bộ đệm và đăng bộ đệm mới ngay sau khi bạn lấy lại ( trước khi bạn xử lý lại bộ đệm ) để bạn đảm bảo rằng ổ cắm luôn có nơi để gửi dữ liệu từ mạng. Bạn sẽ thấy lý do tại sao bạn có thể sẽ không thể đạt được điều này trong thời gian ngắn.

Sau khi bạn chơi xong với API BeginRead / BeginWrite và bắt đầu công việc nghiêm túc, bạn sẽ nhận ra rằng bạn cần bảo mật cho lưu lượng truy cập của mình, tức là. Xác thực NTLM / Kerberos và mã hóa lưu lượng, hoặc ít nhất là bảo vệ giả mạo lưu lượng. Cách bạn làm điều này là bạn sử dụng tích hợp trong System.Net.Security.NegotiateStream (hoặc SslStream nếu bạn cần chuyển qua các tên miền khác nhau). Điều này có nghĩa là thay vì dựa vào các hoạt động không đồng bộ của ổ cắm thẳng, bạn sẽ dựa vào các hoạt động không đồng bộ AuthenticatedStream. Ngay sau khi bạn nhận được một ổ cắm (từ kết nối trên máy khách hoặc từ chấp nhận trên máy chủ), bạn tạo một luồng trên ổ cắm và gửi nó để xác thực, bằng cách gọi BeginAuthenticateAsClient hoặc BeginAuthenticateAsServer. Sau khi xác thực hoàn tất (ít nhất là sự an toàn của bạn khỏi sự điên rồ InitiateSecurityContext / AcceptSecurityContext ...), bạn sẽ thực hiện ủy quyền của mình bằng cách kiểm tra thuộc tính RemoteIdentity của luồng Xác thực của bạn và thực hiện bất kỳ xác minh ACL nào mà sản phẩm của bạn phải hỗ trợ. Sau đó, bạn sẽ gửi tin nhắn bằng BeginWrite và bạn sẽ nhận được chúng bằng BeginRead. Đây là vấn đề tôi đã nói trước đây rằng bạn sẽ không thể đăng nhiều bộ đệm nhận, bởi vì các lớp xác thực không hỗ trợ điều này. Hoạt động BeginRead quản lý nội bộ tất cả IO cho đến khi bạn nhận được toàn bộ khung, nếu không nó không thể xử lý xác thực thư (khung giải mã và xác thực chữ ký trên khung). Mặc dù theo kinh nghiệm của tôi, công việc được thực hiện bởi các lớp AuthenticatedStream là khá tốt và không nên có bất kỳ vấn đề nào với nó. I E. bạn sẽ có thể bão hòa mạng GB chỉ với 4-5% CPU. Các lớp AuthenticatedStream cũng sẽ áp đặt cho bạn các giới hạn kích thước khung cụ thể của giao thức (16k cho SSL, 12k cho Kerberos).

Điều này sẽ giúp bạn bắt đầu đi đúng hướng. Tôi sẽ không đăng mã ở đây, có một ví dụ hoàn toàn tốt về MSDN . Tôi đã thực hiện nhiều dự án như thế này và tôi có thể mở rộng tới khoảng 1000 người dùng được kết nối mà không gặp vấn đề gì. Ở trên, bạn sẽ cần sửa đổi các khóa đăng ký để cho phép kernel cho nhiều tay cầm ổ cắm hơn. và đảm bảo bạn triển khai trên một HĐH máy chủ , đó là W2K3 chứ không phải XP hoặc Vista (tức là HĐH máy khách), điều này tạo ra sự khác biệt lớn.

BTW đảm bảo rằng nếu bạn có các hoạt động cơ sở dữ liệu trên máy chủ hoặc tệp IO, bạn cũng sử dụng hương vị không đồng bộ cho chúng hoặc bạn sẽ thoát khỏi nhóm luồng ngay lập tức. Đối với các kết nối Máy chủ SQL, đảm bảo bạn thêm 'Asyncronous Treatment = true' vào chuỗi kết nối.


Có một số thông tin tuyệt vời ở đây. Tôi ước tôi có thể trao thưởng cho nhiều người tiền thưởng. Tuy nhiên, tôi đã nâng cao bạn. Tốt thứ ở đây, cảm ơn.
Erik Funkenbusch

11

Tôi đã có một máy chủ như vậy chạy trong một số giải pháp của tôi. Dưới đây là một lời giải thích rất chi tiết về các cách khác nhau để thực hiện điều đó trong .net: Đến gần hơn với dây có ổ cắm hiệu suất cao trong .NET

Gần đây tôi đã tìm cách cải thiện mã của mình và sẽ xem xét điều này: " Cải tiến hiệu suất ổ cắm trong phiên bản 3.5 " được bao gồm cụ thể "để sử dụng cho các ứng dụng sử dụng I / O mạng không đồng bộ để đạt được hiệu suất cao nhất".

"Tính năng chính của các cải tiến này là tránh việc phân bổ và đồng bộ hóa các đối tượng lặp đi lặp lại trong I / O ổ cắm không đồng bộ khối lượng lớn. Mẫu thiết kế Begin / End hiện được lớp socket cho I / O của ổ cắm không đồng bộ yêu cầu phải có Hệ thống. Đối tượng IAsyncResult được phân bổ cho từng hoạt động của ổ cắm không đồng bộ. "

Bạn có thể tiếp tục đọc nếu bạn theo liên kết. Cá nhân tôi sẽ kiểm tra mã mẫu của họ vào ngày mai để đánh giá nó so với những gì tôi đã có.

Chỉnh sửa: Tại đây bạn có thể tìm thấy mã làm việc cho cả máy khách và máy chủ bằng cách sử dụng 3,5 SocketAsyncEventArss mới để bạn có thể kiểm tra mã trong vòng vài phút và chuyển mã. Đó là một cách tiếp cận đơn giản nhưng là cơ sở để bắt đầu thực hiện lớn hơn nhiều. Ngoài ra đây bài báo từ gần hai năm trước đây trong tạp chí MSDN là một đọc thú vị.



9

Bạn đã xem xét chỉ sử dụng ràng buộc TCP mạng WCF và mẫu xuất bản / đăng ký chưa? WCF sẽ cho phép bạn tập trung [chủ yếu] vào tên miền của bạn thay vì hệ thống ống nước ..

Có rất nhiều mẫu WCF và thậm chí một khung xuất bản / đăng ký có sẵn trên phần tải xuống của IDesign có thể hữu ích: http://www.idesign.net


8

Tôi đang tự hỏi về một điều:

Tôi chắc chắn không muốn bắt đầu một chủ đề cho mỗi kết nối.

Tại sao vậy? Windows có thể xử lý hàng trăm luồng trong một ứng dụng kể từ ít nhất là Windows 2000. Tôi đã thực hiện nó, thật dễ dàng để làm việc nếu các luồng không cần phải được đồng bộ hóa. Đặc biệt là bạn đang thực hiện nhiều I / O (vì vậy bạn không bị ràng buộc CPU và rất nhiều luồng sẽ bị chặn trên cả giao tiếp trên đĩa hoặc mạng), tôi không hiểu hạn chế này.

Bạn đã thử nghiệm cách đa luồng và thấy nó thiếu một cái gì đó? Bạn có dự định cũng có kết nối cơ sở dữ liệu cho từng luồng (điều đó sẽ giết chết máy chủ cơ sở dữ liệu, vì vậy đó là một ý tưởng tồi, nhưng nó dễ dàng được giải quyết với thiết kế 3 tầng). Bạn có lo lắng rằng bạn sẽ có hàng ngàn khách hàng thay vì hàng trăm, và sau đó bạn sẽ thực sự gặp vấn đề? (Mặc dù tôi đã thử một nghìn luồng hoặc thậm chí mười nghìn nếu tôi có hơn 32 GB RAM - một lần nữa, với điều kiện là bạn không bị ràng buộc CPU, thời gian chuyển đổi luồng sẽ hoàn toàn không liên quan.)

Đây là mã - để xem giao diện này chạy như thế nào, hãy truy cập http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html và nhấp vào hình ảnh.

Lớp máy chủ:

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

Chương trình chính của máy chủ:

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

Lớp khách hàng:

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

Chương trình chính của khách hàng:

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 có thể xử lý nhiều luồng, nhưng .NET không thực sự được thiết kế để xử lý chúng. Mỗi miền appdomain có một nhóm luồng và bạn không muốn làm cạn kiệt luồng xử lý đó. Tôi không chắc chắn nếu bạn bắt đầu một Chủ đề bằng tay nếu nó đến từ luồng đó hay không. Tuy nhiên, hàng trăm chủ đề không làm gì trong phần lớn thời gian là một sự lãng phí tài nguyên lớn.
Erik Funkenbusch

1
Tôi tin rằng bạn có một cái nhìn không chính xác của chủ đề. Chủ đề chỉ đến từ nhóm chủ đề nếu bạn thực sự muốn điều đó - Chủ đề thông thường thì không. Hàng trăm chủ đề không làm gì lãng phí chính xác không có gì :) (Chà, một chút bộ nhớ, nhưng bộ nhớ quá rẻ không thực sự là vấn đề nữa.) Tôi sẽ viết một vài ứng dụng mẫu cho việc này, tôi sẽ đăng một URL lên nó đã hoàn thành Trong lúc này, tôi khuyên bạn nên xem lại những gì tôi đã viết ở trên và cố gắng trả lời câu hỏi của tôi.
Marcel Popescu

1
Mặc dù tôi đồng ý với nhận xét của Marcel về quan điểm của các chủ đề trong đó các chủ đề được tạo không xuất phát từ chủ đề, phần còn lại của tuyên bố là không chính xác. Bộ nhớ không phải là về số lượng cài đặt trong máy, tất cả các ứng dụng trên windows chạy trong không gian địa chỉ ảo và trên hệ thống 32 bit cung cấp cho bạn 2GB dữ liệu cho ứng dụng của bạn (không quan trọng là cài đặt bao nhiêu ram trên hộp). Họ vẫn phải được quản lý bởi thời gian chạy. Làm async IO không sử dụng một luồng để chờ (nó sử dụng IOCP cho phép IO chồng chéo) và là một giải pháp tốt hơn và sẽ mở rộng NHIỀU tốt hơn.
Brian ONeil

7
Khi chạy nhiều luồng, nó không phải là bộ nhớ mà là CPU. Chuyển đổi ngữ cảnh giữa các luồng là một hoạt động tương đối tốn kém và các luồng hoạt động nhiều hơn bạn có nhiều chuyển đổi ngữ cảnh sẽ xảy ra. Vài năm trước tôi đã chạy thử nghiệm trên PC của mình với ứng dụng bảng điều khiển C # và với khoảng. 500 luồng CPU của tôi là 100%, các luồng không làm gì đáng kể. Đối với comms mạng, tốt hơn là giữ số lượng chủ đề.
sipwiz

1
Tôi sẽ đi với một giải pháp Nhiệm vụ hoặc sử dụng async / await. Giải pháp Nhiệm vụ có vẻ đơn giản hơn trong khi async / await có khả năng mở rộng hơn (chúng đặc biệt dành cho các tình huống ràng buộc IO).
Marcel Popescu

5

Sử dụng Async IO tích hợp của .NET (BeginRead , v.v.) là một ý tưởng tốt nếu bạn có thể có được tất cả các chi tiết đúng. Khi bạn thiết lập đúng cách xử lý ổ cắm / tệp, nó sẽ sử dụng triển khai IOCP bên dưới của HĐH, cho phép các hoạt động của bạn hoàn thành mà không cần sử dụng bất kỳ luồng nào (hoặc trong trường hợp xấu nhất, sử dụng một luồng mà tôi tin là đến từ nhóm luồng IO của kernel của nhóm luồng của .NET, giúp giảm bớt tắc nghẽn luồng.)

Gotcha chính là để đảm bảo rằng bạn mở ổ cắm / tệp của mình ở chế độ không chặn. Hầu hết các chức năng tiện lợi mặc định (nhưFile.OpenRead ) không làm điều này, vì vậy bạn sẽ cần phải tự viết.

Một trong những mối quan tâm chính khác là xử lý lỗi - xử lý lỗi đúng khi viết mã I / O không đồng bộ khó hơn nhiều so với thực hiện mã đồng bộ. Nó cũng rất dễ dàng để kết thúc với các điều kiện cuộc đua và bế tắc mặc dù bạn có thể không sử dụng các chủ đề trực tiếp, vì vậy bạn cần phải nhận thức được điều này.

Nếu có thể, bạn nên thử và sử dụng thư viện tiện lợi để giảm bớt quá trình thực hiện IO không đồng bộ có thể mở rộng.

Thời gian chạy phối hợp đồng thời của Microsoft là một ví dụ về thư viện .NET được thiết kế để giảm bớt khó khăn khi thực hiện loại lập trình này. Nó trông rất tuyệt, nhưng khi tôi chưa sử dụng nó, tôi không thể nhận xét mức độ của nó.

Đối với các dự án cá nhân của tôi cần thực hiện I / O mạng không đồng bộ, tôi sử dụng một bộ công cụ đồng thời .NET / IO mà tôi đã xây dựng trong năm qua, được gọi là Squared.Task . Nó lấy cảm hứng từ các thư viện như imvu.taskxoắn , và tôi đã bao gồm một số ví dụ hoạt động trong kho lưu trữ mạng I / O. Tôi cũng đã sử dụng nó trong một vài ứng dụng tôi đã viết - ứng dụng được phát hành công khai lớn nhất là NDexer (sử dụng nó cho I / O không có đĩa). Thư viện được viết dựa trên kinh nghiệm của tôi với imvu.task và có một bộ các bài kiểm tra đơn vị khá toàn diện, vì vậy tôi rất khuyến khích bạn dùng thử. Nếu bạn có bất kỳ vấn đề nào với nó, tôi rất vui lòng cung cấp cho bạn một số trợ giúp.

Theo tôi, dựa trên kinh nghiệm của tôi khi sử dụng IO không đồng bộ / không luồng thay vì các luồng là một nỗ lực đáng giá trên nền tảng .NET, miễn là bạn sẵn sàng đối phó với đường cong học tập. Nó cho phép bạn tránh các rắc rối về khả năng mở rộng do chi phí của các đối tượng Thread và trong nhiều trường hợp, bạn hoàn toàn có thể tránh việc sử dụng các khóa và mutexes bằng cách sử dụng cẩn thận các nguyên hàm đồng thời như Futures / Promising.


Thông tin tuyệt vời, tôi sẽ kiểm tra tài liệu tham khảo của bạn và xem điều gì có ý nghĩa.
Erik Funkenbusch

3

Tôi đã sử dụng giải pháp của Kevin nhưng anh ấy nói rằng giải pháp đó thiếu mã để sắp xếp lại các tin nhắn. Các nhà phát triển có thể sử dụng mã này để tập hợp lại các thông điệp:

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

Bạn có thể thử sử dụng một khung gọi là ACE (Môi trường truyền thông thích ứng), là khung C ++ chung cho các máy chủ mạng. Đây là một sản phẩm rất chắc chắn, trưởng thành và được thiết kế để hỗ trợ các ứng dụng có độ tin cậy cao, khối lượng lớn lên đến cấp độ telco.

Khung này liên quan đến một loạt các mô hình tương tranh và có thể có một mô hình phù hợp cho ứng dụng của bạn. Điều này sẽ làm cho hệ thống dễ gỡ lỗi hơn vì hầu hết các vấn đề tương tranh khó chịu đã được sắp xếp. Sự đánh đổi ở đây là khung được viết bằng C ++ và không phải là cơ sở mã ấm áp và mượt mà nhất. Mặt khác, bạn được kiểm tra, cơ sở hạ tầng mạng công nghiệp và kiến ​​trúc có khả năng mở rộng cao.


2
Đó là một gợi ý hay, nhưng từ các thẻ của câu hỏi tôi tin rằng OP sẽ sử dụng C #
JPCosta

Tôi nhận thấy rằng; gợi ý là điều này có sẵn cho C ++ và tôi không biết bất cứ điều gì tương đương với C #. Việc gỡ lỗi loại hệ thống này không dễ dàng vào thời điểm tốt nhất và bạn có thể nhận được lợi nhuận từ việc chuyển sang khung này mặc dù điều đó có nghĩa là chuyển sang C ++.
Mối quan tâmOfTunbridgeWells

Vâng, đây là C #. Tôi đang tìm kiếm các giải pháp dựa trên .net tốt. Tôi nên rõ ràng hơn, nhưng tôi cho rằng mọi người sẽ đọc các thẻ
Erik Funkenbusch


1

Chà, ổ cắm .NET dường như cung cấp select () - đó là cách tốt nhất để xử lý đầu vào. Đối với đầu ra, tôi có một nhóm các luồng của trình soạn thảo ổ cắm đang nghe trên hàng đợi công việc, chấp nhận mô tả / đối tượng ổ cắm như một phần của mục công việc, vì vậy bạn không cần một luồng trên mỗi ổ cắm.


1

Tôi sẽ sử dụng các phương thức AcceptAsync / ConnectAsync / receiveAsync / SendAsync đã được thêm vào .Net 3.5. Tôi đã thực hiện một điểm chuẩn và chúng nhanh hơn khoảng 35% (thời gian phản hồi và tốc độ bit) với 100 người dùng liên tục gửi và nhận dữ liệu.


1

để mọi người sao chép dán câu trả lời được chấp nhận, bạn có thể viết lại phương thức acceptCallback, xóa tất cả các cuộc gọi của _serverSocket.BeginAccept (AsyncCallback mới (acceptCallback), _serverSocket); và đặt nó trong mệnh đề {} cuối cùng, theo cách này:

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

bạn thậm chí có thể xóa lần bắt đầu tiên vì nội dung của nó giống nhau nhưng đó là phương thức mẫu và bạn nên sử dụng ngoại lệ được nhập để xử lý tốt hơn các ngoại lệ và hiểu nguyên nhân gây ra lỗi, vì vậy chỉ cần thực hiện các lần bắt đó với một số mã hữu ích


0

Tôi khuyên bạn nên đọc những cuốn sách này trên ACE

để có ý tưởng về các mẫu cho phép bạn tạo một máy chủ hiệu quả.

Mặc dù ACE được triển khai trong C ++, các cuốn sách bao gồm rất nhiều mẫu hữu ích có thể được sử dụng trong bất kỳ ngôn ngữ lập trình nào.


-1

Để rõ ràng, tôi đang tìm kiếm các giải pháp dựa trên .net (C # nếu có thể, nhưng bất kỳ ngôn ngữ .net nào cũng sẽ hoạt động)

Bạn sẽ không có được khả năng mở rộng cao nhất nếu bạn hoàn toàn sử dụng .NET. Tạm dừng GC có thể cản trở độ trễ.

Tôi sẽ cần bắt đầu ít nhất một luồng cho dịch vụ. Tôi đang xem xét sử dụng API Asynch (BeginRecieve, v.v.) vì tôi không biết mình sẽ kết nối được bao nhiêu khách hàng tại bất kỳ thời điểm nào (có thể hàng trăm). Tôi chắc chắn không muốn bắt đầu một chủ đề cho mỗi kết nối.

IO chồng chéo thường được coi là API nhanh nhất của Windows để liên lạc qua mạng. Tôi không biết nếu điều này giống với API Asynch của bạn. Không sử dụng select vì mỗi cuộc gọi cần kiểm tra mọi ổ cắm đang mở thay vì có cuộc gọi lại trên các socket đang hoạt động.


1
Tôi không hiểu nhận xét tạm dừng GC của bạn .. Tôi chưa bao giờ thấy một hệ thống có vấn đề về khả năng mở rộng có liên quan trực tiếp đến GC.
Markt

4
Nhiều khả năng là bạn xây dựng một ứng dụng không thể mở rộng vì kiến ​​trúc kém hơn vì tồn tại. Các hệ thống biểu diễn + có khả năng mở rộng rất lớn đã được xây dựng với cả .NET và Java. Trong cả hai liên kết mà bạn đưa ra, nguyên nhân không phải là bộ sưu tập rác trực tiếp .. mà liên quan đến việc hoán đổi đống. Tôi nghi ngờ rằng nó thực sự là một vấn đề với kiến trúc mà có thể tránh được .. Nếu bạn có thể chỉ cho tôi một ngôn ngữ mà nó không phải là có thể xây dựng một hệ thống mà không thể mở rộng quy mô, tôi sẽ sẵn sàng sử dụng nó;)
Markt

1
Tôi không đồng ý với nhận xét này. Không biết, các câu hỏi bạn tham khảo là Java và chúng đặc biệt xử lý việc phân bổ bộ nhớ lớn hơn và cố gắng ép gc bằng tay. Tôi thực sự sẽ không có số lượng lớn phân bổ bộ nhớ đang diễn ra ở đây. Đây không chỉ là một vấn đề. Nhưng cảm ơn. Có, Mô hình lập trình không đồng bộ thường được triển khai trên IO chồng chéo.
Erik Funkenbusch

1
Trên thực tế, thực tiễn tốt nhất là không được liên tục thủ công buộc phải thu thập GC. Điều này rất có thể làm cho ứng dụng của bạn hoạt động kém hơn. .NET GC là một thế hệ GC sẽ điều chỉnh theo cách sử dụng ứng dụng của bạn. Nếu bạn thực sự nghĩ rằng bạn cần phải gọi thủ công GC.Collect, tôi sẽ nói rằng mã của bạn rất có thể cần phải được viết theo cách khác ..
markt

1
@markt, đó là một nhận xét cho những người không thực sự biết gì về bộ sưu tập rác. Nếu bạn có thời gian nhàn rỗi, không có gì sai khi thực hiện một bộ sưu tập thủ công. Nó sẽ không làm cho ứng dụng của bạn tồi tệ hơn khi nó kết thúc. Các bài báo học thuật cho thấy rằng các thế hệ GC hoạt động vì nó gần đúng với thời gian tồn tại của các đối tượng của bạn. Rõ ràng đây không phải là một đại diện hoàn hảo. Trên thực tế, có một nghịch lý là thế hệ "già nhất" thường có tỷ lệ rác cao nhất vì nó không bao giờ là rác được thu gom.
Không biết

-1

Bạn có thể sử dụng khung mã nguồn mở Push Framework để phát triển máy chủ hiệu suất cao. Nó được xây dựng trên IOCP và phù hợp cho các kịch bản đẩy và phát thông điệp.

http://www.pushframework.com


1
Bài đăng này đã được gắn thẻ C # và .net. Tại sao bạn đề xuất một khung C ++?
Erik Funkenbusch

Có lẽ bởi vì anh ấy đã viết nó. potatosoftware.com/ Phúc
quillbreaker 21/07

Pushframework có hỗ trợ nhiều trường hợp máy chủ không? Nếu không, làm thế nào để nó quy mô?
esskar
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.