Cara menulis server berbasis Tcp / Ip yang dapat diskalakan


148

Saya dalam tahap desain menulis aplikasi Layanan Windows baru yang menerima koneksi TCP / IP untuk koneksi yang berjalan lama (yaitu ini tidak seperti HTTP di mana ada banyak koneksi pendek, melainkan klien menghubungkan dan tetap terhubung selama berjam-jam atau berhari-hari atau bahkan berminggu-minggu).

Saya sedang mencari ide cara terbaik untuk merancang arsitektur jaringan. Saya perlu memulai setidaknya satu utas untuk layanan ini. Saya sedang mempertimbangkan untuk menggunakan Asynch API (BeginRecieve, dll.) Karena saya tidak tahu berapa banyak klien yang akan saya sambungkan pada waktu tertentu (mungkin ratusan). Saya pasti tidak ingin memulai utas untuk setiap koneksi.

Data terutama akan mengalir ke klien dari server saya, tetapi akan ada beberapa perintah yang dikirim dari klien pada kesempatan. Ini terutama merupakan aplikasi pemantauan di mana server saya mengirim data status secara berkala ke klien.

Adakah saran tentang cara terbaik untuk membuat ini scalable mungkin? Alur kerja dasar? Terima kasih.

EDIT: Agar lebih jelas, saya sedang mencari solusi berbasis .net (C # jika memungkinkan, tetapi bahasa .net apa pun akan berfungsi)

CATATAN BOUNTY: Untuk mendapatkan hadiah, saya mengharapkan lebih dari jawaban sederhana. Saya akan membutuhkan contoh solusi yang berfungsi, baik sebagai penunjuk ke sesuatu yang dapat saya unduh atau contoh singkat in-line. Dan itu harus berbasis .net dan Windows (bahasa .net apa pun dapat diterima)

EDIT: Saya ingin mengucapkan terima kasih kepada semua orang yang memberikan jawaban yang baik. Sayangnya, saya hanya bisa menerima satu, dan saya memilih untuk menerima metode Begin / End yang lebih terkenal. Solusi Esac mungkin lebih baik, tetapi masih cukup baru sehingga saya tidak tahu pasti bagaimana cara kerjanya.

Saya telah mengangkat semua jawaban yang saya pikir baik, saya berharap bisa melakukan lebih banyak untuk kalian. Terima kasih lagi.


1
Apakah Anda benar-benar yakin bahwa itu perlu koneksi yang berjalan lama? Sulit untuk mengatakan dari info terbatas yang disediakan, tapi aku hanya akan melakukannya jika benar-benar diperlukan ..
markt

Ya, itu harus berjalan lama. Data harus diperbarui secara real-time, jadi saya tidak bisa melakukan polling berkala, data harus didorong ke klien saat itu terjadi, yang berarti koneksi konstan.
Erik Funkenbusch

1
Itu bukan alasan yang valid. Http mendukung koneksi yang berjalan lama dengan baik. Anda cukup membuka koneksi dan menunggu repsonse (polling macet). Ini berfungsi dengan baik untuk banyak aplikasi gaya AJAX dll. Bagaimana menurut Anda gmail bekerja :-)
TFD

2
Gmail bekerja dengan polling secara berkala untuk email, itu tidak menjaga koneksi berjalan lama. Ini bagus untuk email, di mana respons waktu nyata tidak diperlukan.
Erik Funkenbusch

2
Polling, atau menarik, berskala baik tetapi mengembangkan latensi dengan cepat. Mendorong juga tidak terjadi, tetapi membantu mengurangi atau menghilangkan latensi.
andrewbadera

Jawaban:


92

Saya sudah menulis sesuatu yang mirip dengan ini di masa lalu. Dari penelitian saya tahun lalu menunjukkan bahwa menulis implementasi soket Anda sendiri adalah yang terbaik, menggunakan soket Asynchronous. Ini berarti bahwa klien tidak benar-benar melakukan sesuatu sebenarnya membutuhkan sumber daya yang relatif sedikit. Apa pun yang terjadi ditangani oleh kumpulan .net thread.

Saya menulisnya sebagai kelas yang mengelola semua koneksi untuk server.

Saya hanya menggunakan daftar untuk menampung semua koneksi klien, tetapi jika Anda membutuhkan pencarian yang lebih cepat untuk daftar yang lebih besar, Anda dapat menulisnya sesuka Anda.

private List<xConnection> _sockets;

Anda juga perlu soket yang benar-benar mendengarkan untuk koneksi yang masuk.

private System.Net.Sockets.Socket _serverSocket;

Metode mulai sebenarnya memulai soket server dan mulai mendengarkan untuk setiap koneksi yang masuk.

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

Saya hanya ingin mencatat kode penanganan pengecualian terlihat buruk, tetapi alasannya adalah saya memiliki kode penekan pengecualian di sana sehingga pengecualian akan ditekan dan dikembalikan false jika opsi konfigurasi diatur, tetapi saya ingin menghapusnya untuk demi singkatnya.

The _serverSocket.BeginAccept (AsyncCallback baru (acceptCallback)), _serverSocket) di atas pada dasarnya menetapkan soket server kami untuk memanggil metode acceptCallback setiap kali pengguna terhubung. Metode ini berjalan dari .Net threadpool, yang secara otomatis menangani pembuatan utas pekerja tambahan jika Anda memiliki banyak operasi pemblokiran. Ini harus secara optimal menangani setiap beban di server.

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

Kode di atas pada dasarnya baru saja selesai menerima koneksi yang masuk, antrian BeginReceiveyang merupakan panggilan balik yang akan berjalan ketika klien mengirim data, dan kemudian antrian berikutnya acceptCallbackyang akan menerima koneksi klien berikutnya yang masuk.

Pemanggilan BeginReceivemetode adalah apa yang memberitahu socket apa yang harus dilakukan ketika menerima data dari klien. Untuk BeginReceive, Anda harus memberikan array byte, di mana ia akan menyalin data ketika klien mengirim data. The ReceiveCallbackmetode akan dipanggil, yang adalah bagaimana kita menangani menerima data.

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: Dalam pola ini saya lupa menyebutkan bahwa di area kode ini:

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

Apa yang biasanya saya lakukan adalah dalam kode apa pun yang Anda inginkan, adalah melakukan pemasangan kembali paket menjadi pesan, dan kemudian membuatnya sebagai pekerjaan di kumpulan utas. Dengan cara ini BeginReceive dari blok berikutnya dari klien tidak ditunda sementara kode pemrosesan pesan apa pun berjalan.

Panggilan balik yang diterima selesai membaca soket data dengan menelepon dan menerima. Ini mengisi buffer yang disediakan di fungsi begin accept. Setelah Anda melakukan apa pun yang Anda inginkan di mana saya meninggalkan komentar, kami memanggil BeginReceivemetode berikutnya yang akan menjalankan panggilan balik lagi jika klien mengirim data lagi. Sekarang inilah bagian yang sangat sulit, ketika klien mengirim data, panggilan balik Anda mungkin hanya dipanggil dengan bagian dari pesan. Reassembly bisa menjadi sangat sangat rumit. Saya menggunakan metode saya sendiri dan membuat semacam protokol kepemilikan untuk melakukan ini. Saya meninggalkannya, tetapi jika Anda memintanya, saya dapat menambahkannya. Handler ini sebenarnya adalah kode yang paling rumit yang pernah saya tulis.

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

Metode pengiriman di atas sebenarnya menggunakan Sendpanggilan sinkron , bagi saya itu baik-baik saja karena ukuran pesan dan sifat multithreaded dari aplikasi saya. Jika Anda ingin mengirim ke setiap klien, Anda hanya perlu mengulang daftar _sockets.

Kelas xConnection yang Anda lihat dirujuk di atas pada dasarnya adalah pembungkus sederhana untuk sebuah soket untuk memasukkan buffer byte, dan dalam implementasi saya beberapa tambahan.

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

Juga untuk referensi di sini adalah usings saya sertakan karena saya selalu kesal ketika mereka tidak termasuk.

using System.Net.Sockets;

Saya harap itu membantu, mungkin bukan kode terbersih, tetapi berfungsi. Ada juga beberapa nuansa pada kode yang Anda harus lelah untuk mengubahnya. Untuk satu, hanya memiliki satu panggilan BeginAcceptpada satu waktu. Dulu ada bug .net yang sangat mengganggu di sekitar ini, yang bertahun-tahun lalu jadi saya tidak ingat detailnya.

Juga, dalam ReceiveCallbackkode, kami memproses apa pun yang diterima dari soket sebelum kami mengantri pada penerimaan berikutnya. Ini berarti bahwa untuk satu soket, kami hanya ReceiveCallbacksekali dalam satu waktu, dan kami tidak perlu menggunakan sinkronisasi utas. Namun, jika Anda memesan ulang untuk memanggil penerima berikutnya segera setelah menarik data, yang mungkin sedikit lebih cepat, Anda harus memastikan Anda menyinkronkan utas dengan benar.

Juga, saya meretas banyak kode saya, tetapi meninggalkan esensi dari apa yang terjadi di tempat. Ini harus menjadi awal yang baik untuk desain Anda. Tinggalkan komentar jika Anda memiliki pertanyaan lain tentang ini.


1
Ini adalah jawaban yang bagus Kevin .. sepertinya Anda berada di jalur untuk mendapatkan hadiah. :)
Erik Funkenbusch

6
Saya tidak tahu mengapa ini adalah jawaban pilihan tertinggi. Begin * End * bukan cara tercepat untuk melakukan networking di C #, juga bukan yang paling skalabel. Ini IS lebih cepat daripada sinkron, tetapi ada banyak operasi yang berjalan di bawah tenda di Windows yang benar-benar memperlambat jalur jaringan ini.
esac

6
Ingatlah apa yang esac tulis dalam komentar sebelumnya. Pola awal-akhir mungkin akan bekerja untuk Anda sampai titik tertentu, heck kode saya saat ini menggunakan awal-akhir, tetapi ada peningkatan keterbatasannya di .net 3.5. Saya tidak peduli dengan hadiahnya, tetapi saya sarankan Anda membaca tautan dalam jawaban saya, bahkan jika Anda menerapkan pendekatan ini. "Peningkatan Kinerja Socket di Versi 3.5"
jvanderh

1
Saya hanya ingin melempar karena mereka mungkin tidak cukup jelas, ini adalah .net 2.0 era code di mana saya percaya ini adalah pola yang sangat layak. Namun, jawaban esac memang terlihat sedikit lebih modern jika menargetkan .net 3.5, satu-satunya nitpick yang saya miliki adalah melempar peristiwa :) tetapi itu dapat dengan mudah diubah. Juga, saya melakukan pengujian throughput dengan kode ini dan pada dual core opteron 2GHz mampu memaksimalkan ethernet 100Mbps, dan yang menambahkan lapisan enkripsi di atas kode ini.
Kevin Nisbet

1
@KevinNisbet Saya tahu ini cukup terlambat, tetapi bagi siapa pun yang menggunakan jawaban ini untuk merancang server mereka sendiri - pengiriman juga harus asinkron, karena jika tidak, Anda membuka diri untuk kemungkinan kebuntuan. Jika kedua sisi menulis data yang mengisi buffer masing-masing, Sendmetode akan memblokir tanpa batas di kedua sisi, karena tidak ada yang membaca data input.
Luaan

83

Ada banyak cara melakukan operasi jaringan di C #. Semua dari mereka menggunakan mekanisme yang berbeda di bawah tenda, dan karenanya menderita masalah kinerja utama dengan konkurensi tinggi. Operasi Begin * adalah salah satu di antaranya yang sering disalahartikan oleh banyak orang sebagai cara tercepat / tercepat dalam membangun jaringan.

Untuk mengatasi masalah ini, mereka memperkenalkan set metode * Async: Dari MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

Kelas SocketAsyncEventArgs adalah bagian dari serangkaian perangkat tambahan untuk System.Net.Sockets .. ::. Kelas soket yang memberikan pola asinkron alternatif yang dapat digunakan oleh aplikasi soket kinerja tinggi khusus. Kelas ini dirancang khusus untuk aplikasi server jaringan yang membutuhkan kinerja tinggi. Aplikasi dapat menggunakan pola asinkron yang ditingkatkan secara eksklusif atau hanya di area panas yang ditargetkan (misalnya, saat menerima data dalam jumlah besar).

Fitur utama dari peningkatan ini adalah menghindari alokasi berulang dan sinkronisasi objek selama I / O soket sinkron volume tinggi. Pola desain Mulai / Akhir saat ini diimplementasikan oleh System.Net.Sockets .. ::. Socket kelas membutuhkan Sistem .. ::. Objek IAsyncResult dialokasikan untuk setiap operasi soket asinkron.

Di bawah penutup, * Async API menggunakan port penyelesaian IO yang merupakan cara tercepat untuk melakukan operasi jaringan, lihat http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

Dan hanya untuk membantu Anda, saya memasukkan kode sumber untuk server telnet yang saya tulis menggunakan * Async API. Saya hanya memasukkan bagian yang relevan. Juga untuk dicatat, alih-alih memproses data inline, saya malah memilih untuk mendorongnya ke antrian kunci (tunggu gratis) yang diproses pada utas terpisah. Perhatikan bahwa saya tidak termasuk kelas Pool yang sesuai yang hanya pool sederhana yang akan membuat objek baru jika kosong, dan kelas Buffer yang hanya merupakan buffer yang dikembangkan sendiri yang tidak benar-benar diperlukan kecuali jika Anda menerima ketidakpastian jumlah data. Jika Anda ingin informasi lebih lanjut, silakan kirimi saya 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
    }
}

Ini cukup lurus ke depan, dan contoh sederhana. Terima kasih. Saya harus mengevaluasi pro dan kontra dari setiap metode.
Erik Funkenbusch

Saya belum memiliki kesempatan untuk mengujinya, tetapi saya merasa tidak jelas akan kondisi balapan di sini karena beberapa alasan. Pertama, Jika Anda mendapatkan banyak pesan, saya tidak tahu bahwa acara akan diproses secara berurutan (mungkin tidak penting untuk aplikasi pengguna, tetapi harus dicatat) atau saya bisa saja salah dan acara akan diproses secara berurutan. Kedua, apakah saya mungkin melewatkannya tetapi tidakkah ada risiko buffer ditimpa dihapus sementara DataReceived masih berjalan jika membutuhkan waktu lama? Jika masalah yang mungkin tidak dapat dibenarkan ini diatasi, saya pikir ini adalah solusi modern yang sangat baik.
Kevin Nisbet

1
Dalam kasus saya, untuk server telnet saya, 100%, YA mereka sudah beres. Kuncinya adalah mengatur metode panggilan balik yang tepat sebelum memanggil AcceptAsync, ReceiveAsync, dll. Dalam kasus saya, saya melakukan SendAsync pada utas terpisah, jadi jika ini diubah untuk melakukan pola Terima / Kirim / Terima / Kirim / Terima / Terima / Putuskan, kemudian itu perlu dimodifikasi.
esac

1
Poin # 2 juga sesuatu yang perlu Anda pertimbangkan. Saya menyimpan objek 'Koneksi' saya dalam konteks SocketAsyncEventArgs. Artinya, saya hanya punya satu buffer penerima per koneksi. Saya tidak memposting penerimaan lain dengan SocketAsyncEventArgs ini hingga DataReceived selesai, jadi tidak ada data lebih lanjut yang dapat dibaca tentang ini sampai selesai. Saya yakin bahwa tidak ada operasi lama yang dilakukan pada data ini. Saya benar-benar memindahkan seluruh buffer dari semua data yang diterima ke antrian lockfree dan kemudian memprosesnya pada utas terpisah. Ini memastikan latensi rendah pada bagian jaringan.
esac

1
Sebagai catatan, saya menulis unit test dan memuat tes untuk kode ini, dan ketika saya meningkatkan beban pengguna dari 1 pengguna menjadi 250 pengguna (pada sistem dual core tunggal, 4GB RAM), waktu respons untuk 100 byte (1 paket) dan 10.000 byte (3 paket) tetap sama di seluruh kurva beban pengguna.
esac

46

Dulu ada diskusi yang sangat bagus tentang scalable TCP / IP menggunakan .NET yang ditulis oleh Chris Mullins dari Coversant, sayangnya tampaknya blognya telah menghilang dari lokasi sebelumnya, jadi saya akan mencoba untuk mengumpulkan sarannya dari memori (beberapa komentar yang berguna dari kemunculannya di utas ini: C ++ vs. C #: Mengembangkan server IOCP yang sangat skalabel )

Pertama dan terutama, perhatikan bahwa baik menggunakan Begin/Enddan Asyncmetode di Socketkelas memanfaatkan IO Completion Ports (IOCP) untuk memberikan skalabilitas. Ini membuat perbedaan yang jauh lebih besar (bila digunakan dengan benar; lihat di bawah) untuk skalabilitas daripada yang mana dari dua metode yang Anda pilih untuk mengimplementasikan solusi Anda.

Posting Chris Mullins didasarkan pada penggunaan Begin/End, yang merupakan pengalaman pribadi saya. Perhatikan bahwa Chris menyusun solusi berdasarkan ini yang meningkatkan hingga 10.000-an koneksi klien bersamaan pada mesin 32-bit dengan memori 2GB, dan hingga 100.000-an pada platform 64-bit dengan memori yang cukup. Dari pengalaman saya sendiri dengan teknik ini (juga tidak ada di dekat beban semacam ini) saya tidak punya alasan untuk meragukan angka-angka indikatif ini.

IOCP versus thread-per-koneksi atau primitif 'select'

Alasan Anda ingin menggunakan mekanisme yang menggunakan IOCP di bawah tenda adalah karena ia menggunakan kumpulan utas Windows tingkat sangat rendah yang tidak membangun utas apa pun sampai ada data aktual pada saluran IO yang Anda coba baca dari ( perhatikan bahwa IOCP dapat digunakan untuk file IO juga). Manfaat dari hal ini adalah bahwa Windows tidak harus beralih ke utas hanya untuk menemukan bahwa belum ada data, jadi ini mengurangi jumlah konteks switch server Anda harus membuat ke minimum yang diperlukan.

Switch konteks adalah apa yang pasti akan mematikan mekanisme 'thread-per-koneksi', meskipun ini adalah solusi yang layak jika Anda hanya berurusan dengan beberapa lusin koneksi. Namun mekanisme ini sama sekali bukan imajinasi 'scalable'.

Pertimbangan penting saat menggunakan IOCP

Penyimpanan

Pertama dan terpenting adalah penting untuk memahami bahwa IOCP dapat dengan mudah mengakibatkan masalah memori di bawah. NET jika implementasi Anda terlalu naif. Setiap BeginReceivepanggilan IOCP akan menghasilkan "menyematkan" buffer yang Anda baca. Untuk penjelasan yang baik tentang mengapa ini menjadi masalah, lihat: Weblog Yun Jin: OutOfMemoryException dan Pinning .

Untungnya masalah ini dapat dihindari, tetapi membutuhkan sedikit pertukaran. Solusi yang disarankan adalah mengalokasikan besarbyte[] buffer pada saat permulaan aplikasi (atau menutupnya), setidaknya 90KB atau lebih (pada. NET 2, ukuran yang diperlukan mungkin lebih besar di versi yang lebih baru). Alasan untuk melakukan ini adalah bahwa alokasi memori yang besar secara otomatis berakhir di segmen memori yang tidak dipadatkan (Tumpukan Objek Besar) yang secara efektif disematkan. Dengan mengalokasikan satu buffer besar pada saat start-up Anda memastikan bahwa blok memori yang tidak bisa dipindahkan ini berada pada 'alamat yang relatif rendah' ​​di mana ia tidak akan menghalangi dan menyebabkan fragmentasi.

Anda kemudian dapat menggunakan offset untuk mengelompokkan buffer besar ini ke area terpisah untuk setiap koneksi yang perlu membaca beberapa data. Di sinilah trade-off ikut bermain; karena buffer ini perlu dialokasikan sebelumnya, Anda harus memutuskan berapa banyak ruang buffer yang Anda butuhkan per koneksi, dan batas atas apa yang ingin Anda atur pada jumlah koneksi yang ingin Anda skalakan (atau, Anda dapat mengimplementasikan abstraksi yang dapat mengalokasikan buffer disematkan tambahan setelah Anda membutuhkannya).

Solusi paling sederhana adalah dengan menetapkan setiap koneksi satu byte pada offset unik dalam buffer ini. Kemudian Anda dapat membuat BeginReceivepanggilan untuk byte tunggal untuk dibaca, dan melakukan sisa pembacaan sebagai hasil dari panggilan balik yang Anda dapatkan.

Pengolahan

Ketika Anda mendapatkan panggilan balik dari Beginpanggilan yang Anda buat, sangat penting untuk menyadari bahwa kode dalam panggilan balik akan dijalankan pada utas IOCP tingkat rendah. Sangat penting bahwa Anda menghindari operasi yang panjang dalam panggilan balik ini. Menggunakan utas ini untuk pemrosesan kompleks akan mematikan skalabilitas Anda sama efektifnya dengan menggunakan 'utas per koneksi'.

Solusi yang disarankan adalah menggunakan callback hanya untuk mengantri item kerja untuk memproses data yang masuk, yang akan dieksekusi pada beberapa utas lainnya. Hindari operasi yang berpotensi memblokir di dalam callback sehingga utas IOCP dapat kembali ke kelompoknya secepat mungkin. Dalam. NET 4.0 saya akan menyarankan solusi termudah untuk menelurkan Task, memberikan referensi ke soket klien dan salinan byte pertama yang sudah dibaca oleh BeginReceivepanggilan. Tugas ini kemudian bertanggung jawab untuk membaca semua data dari soket yang mewakili permintaan yang Anda proses, jalankan, dan kemudian membuat BeginReceivepanggilan baru untuk mengantri soket untuk IOCP sekali lagi. Pra .NET 4.0, Anda dapat menggunakan ThreadPool, atau membuat implementasi antrian kerja berulir Anda sendiri.

Ringkasan

Pada dasarnya, saya sarankan menggunakan kode sampel Kevin untuk solusi ini, dengan peringatan tambahan berikut:

  • Pastikan buffer yang Anda berikan BeginReceivesudah 'disematkan'
  • Pastikan panggilan balik yang Anda BeginReceivelakukan tidak lebih dari mengantri tugas untuk menangani pemrosesan aktual dari data yang masuk

Ketika Anda melakukan itu, saya tidak ragu Anda dapat meniru hasil Chris dalam meningkatkan hingga ratusan ribu klien simultan (diberi perangkat keras yang tepat dan implementasi efisien dari kode pemrosesan Anda sendiri tentu saja;)


1
Untuk menyematkan blok memori yang lebih kecil, metode Allch objek GCHandle dapat digunakan untuk menyematkan buffer. Setelah ini dilakukan, UnsafeAddrOfPinnedArrayElement dari objek Marshal dapat digunakan untuk mendapatkan pointer ke buffer. Misalnya: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan

@ BobBryan Kecuali saya kehilangan titik halus yang Anda coba buat, pendekatan itu tidak benar-benar membantu masalah yang solusi saya coba atasi dengan mengalokasikan blok besar, yaitu potensi fragmentasi memori dramatis yang melekat dalam alokasi berulang blok kecil yang disematkan memori.
jerryjvl

Yah, intinya adalah bahwa Anda tidak harus mengalokasikan blok besar untuk tetap tersemat di memori. Anda dapat mengalokasikan blok yang lebih kecil dan menggunakan teknik di atas untuk menyematkannya dalam memori untuk menghindari gc memindahkannya. Anda dapat menyimpan referensi ke masing-masing blok yang lebih kecil, seperti Anda menyimpan referensi ke satu blok yang lebih besar, dan menggunakannya kembali sesuai kebutuhan. Apa pun pendekatan yang valid - saya baru saja menunjukkan bahwa Anda tidak harus menggunakan buffer yang sangat besar. Tetapi, setelah mengatakan bahwa kadang-kadang menggunakan buffer yang sangat besar adalah cara terbaik untuk pergi karena gc akan memperlakukannya lebih efisien.
Bob Bryan

@ BobBryan sejak menjepit buffer terjadi secara otomatis ketika Anda memanggil BeginReceive, pinning sebenarnya bukan poin yang menonjol di sini; efisiensi adalah;) ... dan ini terutama menjadi perhatian ketika mencoba untuk menulis server yang skalabel, maka kebutuhan untuk mengalokasikan blok besar untuk digunakan untuk ruang buffer.
jerryjvl

@ jerryjvl Maaf untuk mengajukan pertanyaan yang sangat lama, namun saya baru-baru ini menemukan masalah yang tepat dengan metode asynch BeginXXX / EndXXX. Ini adalah pos yang bagus, tetapi perlu banyak penggalian untuk menemukannya. Saya menyukai solusi yang Anda sarankan tetapi tidak mengerti sebagian dari itu: "Maka Anda dapat membuat panggilan BeginReceive agar satu byte untuk dibaca, dan melakukan sisa pembacaan sebagai hasil dari panggilan balik yang Anda dapatkan." Apa yang Anda maksud dengan melakukan sisa persiapan sebagai akibat dari panggilan balik yang Anda dapatkan?
Mausimo

22

Anda sudah mendapatkan sebagian besar jawaban melalui contoh kode di atas. Menggunakan operasi IO asynchronous benar-benar cara untuk pergi ke sini. Async IO adalah cara Win32 dirancang secara internal untuk skala. Performa terbaik yang bisa Anda dapatkan dicapai dengan menggunakan Port Penyelesaian, mengikat soket Anda ke port penyelesaian dan memiliki kumpulan utas menunggu penyelesaian port penyelesaian. Kebijaksanaan umum adalah memiliki 2-4 utas per CPU (inti) menunggu penyelesaian. Saya sangat merekomendasikan untuk membaca tiga artikel ini oleh Rick Vicik dari tim Windows Performance:

  1. Merancang Aplikasi untuk Kinerja - Bagian 1
  2. Merancang Aplikasi untuk Kinerja - Bagian 2
  3. Merancang Aplikasi untuk Kinerja - Bagian 3

Artikel-artikel tersebut sebagian besar mencakup API Windows asli, tetapi harus dibaca untuk siapa pun yang ingin memahami skalabilitas dan kinerja. Mereka memang memiliki beberapa celana dalam hal-hal yang dikelola juga.

Hal kedua yang perlu Anda lakukan adalah memastikan Anda membaca buku Peningkatan Kinerja dan Skalabilitas Aplikasi .NET , yang tersedia secara online. Anda akan menemukan saran yang relevan dan valid di sekitar penggunaan utas, panggilan dan kunci asinkron di Bab 5. Tetapi permata asli ada di Bab 17 di mana Anda akan menemukan barang seperti panduan praktis untuk menyetel kumpulan utas Anda. Aplikasi saya memiliki beberapa masalah serius sampai saya menyesuaikan maxIothreads / maxWorkerThreads sesuai rekomendasi dalam bab ini.

Anda mengatakan bahwa Anda ingin melakukan server TCP murni, jadi poin saya berikutnya adalah palsu. Namun , jika Anda menemukan diri Anda terpojok dan menggunakan kelas WebRequest dan turunannya, berhati-hatilah bahwa ada naga yang menjaga pintu itu: ServicePointManager . Ini adalah kelas konfigurasi yang memiliki satu tujuan dalam hidup: untuk merusak kinerja Anda. Pastikan Anda membebaskan server Anda dari ServicePoint.ConnectionLimit yang dibuat secara buatan atau aplikasi Anda tidak akan pernah menskala (saya membiarkan Anda menemukan sendiri apa nilai defaultnya ...). Anda juga dapat mempertimbangkan kembali kebijakan default untuk mengirim header Expect100Continue dalam permintaan http.

Sekarang tentang soket inti yang dikelola, hal-hal yang cukup mudah di sisi Kirim, tetapi mereka secara signifikan lebih kompleks di sisi Terima. Untuk mencapai throughput dan skala tinggi, Anda harus memastikan bahwa soket tidak mengalir terkontrol karena Anda tidak memiliki buffer yang dipasang untuk menerima. Idealnya untuk kinerja tinggi Anda harus memposting 3-4 buffer ke depan dan memposting buffer baru segera setelah Anda mendapatkan satu buffer ( sebelum Anda memprosesnya kembali) sehingga Anda memastikan bahwa soket selalu memiliki tempat untuk menyimpan data yang datang dari jaringan. Anda akan melihat mengapa Anda mungkin tidak akan dapat mencapai hal ini dalam waktu dekat.

Setelah selesai bermain dengan BeginRead / BeginWrite API dan mulai pekerjaan serius Anda akan menyadari bahwa Anda memerlukan keamanan pada lalu lintas Anda, yaitu. Otentikasi NTLM / Kerberos dan enkripsi lalu lintas, atau setidaknya perlindungan gangguan lalu lintas. Cara Anda melakukan ini adalah Anda menggunakan built in System.Net.Security.NegotiateStream (atau SslStream jika Anda perlu pergi lintas domain yang berbeda). Ini berarti bahwa alih-alih mengandalkan operasi asinkron soket lurus, Anda akan mengandalkan operasi asinkron AuthenticatedStream. Segera setelah Anda mendapatkan soket (baik dari terhubung pada klien atau dari menerima di server) Anda membuat aliran pada soket dan mengirimkannya untuk otentikasi, dengan memanggil baik BeginAuthenticateAsClient atau BeginAuthenticateAsServer. Setelah otentikasi selesai (setidaknya keamanan Anda dari kegilaan InitiateSecurityContext / AcceptSecurityContext asli ...), Anda akan melakukan otorisasi dengan memeriksa properti RemoteIdentity dari aliran Otentikasi Anda dan melakukan verifikasi ACL apa pun yang harus didukung oleh produk Anda. Setelah itu Anda akan mengirim pesan menggunakan BeginWrite dan Anda akan menerimanya dengan BeginRead. Ini adalah masalah yang saya bicarakan sebelumnya bahwa Anda tidak akan dapat memposting beberapa buffer penerima, karena kelas AuthenticateStream tidak mendukung ini. Operasi BeginRead mengelola semua IO secara internal sampai Anda telah menerima seluruh bingkai, jika tidak, ia tidak dapat menangani otentikasi pesan (dekripsi bingkai dan validasi tanda tangan pada bingkai). Meskipun dalam pengalaman saya pekerjaan yang dilakukan oleh kelas AuthenticatedStream cukup baik dan seharusnya tidak memiliki masalah dengan itu. Yaitu. Anda harus dapat memenuhi jaringan GB hanya dengan CPU 4-5%. Kelas AuthenticatedStream juga akan memberlakukan pada Anda batasan ukuran bingkai protokol khusus (16k untuk SSL, 12k untuk Kerberos).

Ini akan membantu Anda memulai di jalur yang benar. Saya tidak akan memposting kode di sini, ada contoh yang sangat bagus di MSDN . Saya telah melakukan banyak proyek seperti ini dan saya dapat mengukur sekitar 1000 pengguna yang terhubung tanpa masalah. Di atas itu Anda perlu memodifikasi kunci registri untuk memungkinkan kernel untuk menangani soket lebih banyak. dan pastikan Anda menggunakan server OS, yaitu W2K3 bukan XP atau Vista (mis. OS klien), itu membuat perbedaan besar.

BTW pastikan jika Anda memiliki operasi database di server atau file IO Anda juga menggunakan rasa async untuk mereka, atau Anda akan mengeringkan kumpulan utas dalam waktu singkat. Untuk koneksi SQL Server pastikan Anda menambahkan 'Asyncronous Processing = true' ke string koneksi.


Ada beberapa informasi hebat di sini. Saya berharap saya bisa menghadiahkan banyak orang hadiah. Namun, saya telah meningkatkan Anda. Barang bagus di sini, terima kasih.
Erik Funkenbusch

11

Saya punya server yang berjalan di beberapa solusi saya. Berikut adalah penjelasan yang sangat detail tentang berbagai cara untuk melakukannya di .net: Dapatkan Lebih Dekat dengan Kawat dengan Soket Berkinerja Tinggi di .NET

Akhir-akhir ini saya telah mencari cara untuk meningkatkan kode kami dan akan mencari ke dalam ini: " Peningkatan Kinerja Socket di Versi 3.5 " yang dimasukkan secara khusus "untuk digunakan oleh aplikasi yang menggunakan jaringan I / O asinkron untuk mencapai kinerja tertinggi".

"Fitur utama dari peningkatan ini adalah menghindari alokasi berulang dan sinkronisasi objek selama I / O soket sinkron volume tinggi. Pola desain Awal / Akhir yang saat ini diterapkan oleh kelas Socket untuk soket asinkron I / O membutuhkan Sistem. Objek IAsyncResult dialokasikan untuk setiap operasi soket asinkron. "

Anda dapat terus membaca jika Anda mengikuti tautan. Saya pribadi akan menguji kode sampel mereka besok untuk membandingkannya dengan apa yang saya dapatkan.

Sunting: Di sini Anda dapat menemukan kode yang berfungsi untuk klien dan server menggunakan 3,5 SocketAsyncEventArgs baru sehingga Anda dapat mengujinya dalam beberapa menit dan melalui kode. Ini adalah pendekatan yang sederhana tetapi merupakan dasar untuk memulai implementasi yang jauh lebih besar. Juga artikel ini dari hampir dua tahun lalu di Majalah MSDN adalah bacaan yang menarik.



9

Sudahkah Anda mempertimbangkan hanya menggunakan WCF net TCP binding dan pola publish / subscribe? WCF akan memungkinkan Anda untuk fokus [sebagian besar] pada domain Anda alih-alih plumbing ..

Ada banyak sampel WCF & bahkan kerangka publikasi / berlangganan yang tersedia di bagian unduhan IDesign yang mungkin berguna: http://www.idesign.net


8

Saya bertanya-tanya tentang satu hal:

Saya pasti tidak ingin memulai utas untuk setiap koneksi.

Mengapa demikian? Windows dapat menangani ratusan utas dalam suatu aplikasi sejak setidaknya Windows 2000. Saya sudah melakukannya, sangat mudah untuk bekerja jika utas tidak perlu disinkronkan. Terutama mengingat bahwa Anda melakukan banyak I / O (jadi Anda tidak terikat CPU, dan banyak utas akan diblokir pada disk atau komunikasi jaringan), saya tidak mengerti batasan ini.

Sudahkah Anda menguji cara multi-utas dan menemukannya kurang dalam sesuatu? Apakah Anda bermaksud juga memiliki koneksi database untuk setiap utas (yang akan membunuh server database, jadi itu ide yang buruk, tetapi mudah diselesaikan dengan desain 3-tier). Apakah Anda khawatir bahwa Anda akan memiliki ribuan klien, bukannya ratusan, dan kemudian Anda benar-benar akan memiliki masalah? (Meskipun saya akan mencoba seribu utas atau bahkan sepuluh ribu jika saya memiliki 32+ GB RAM - sekali lagi, mengingat Anda tidak terikat CPU, waktu sakelar thread harus benar-benar tidak relevan.)

Berikut adalah kode - untuk melihat bagaimana ini terlihat berjalan, kunjungi http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html dan klik pada gambar.

Kelas server:

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

Program utama server:

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

Kelas klien:

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

Program utama klien:

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 dapat menangani banyak utas, tetapi .NET tidak benar-benar dirancang untuk menanganinya. Setiap .NET appdomain memiliki kumpulan utas, dan Anda tidak ingin menguras kumpulan utas itu. Saya tidak yakin apakah Anda memulai Thread secara manual apakah itu berasal dari threadpool atau tidak. Namun, ratusan utas yang tidak melakukan apa-apa hampir sepanjang waktu merupakan pemborosan sumber daya yang besar.
Erik Funkenbusch

1
Saya yakin Anda memiliki pandangan salah tentang utas. Utas hanya datang dari kumpulan utas jika Anda benar-benar menginginkannya - Utas biasa tidak. Ratusan utas yang tidak melakukan apa-apa sama sekali sia-sia :) (Yah, sedikit memori, tapi memori sangat murah sehingga tidak benar-benar masalah lagi.) Saya akan menulis beberapa contoh aplikasi untuk ini, saya akan mengirim URL ke begitu aku selesai. Sementara itu, saya sarankan Anda memeriksa kembali apa yang saya tulis di atas dan mencoba menjawab pertanyaan saya.
Marcel Popescu

1
Sementara saya setuju dengan komentar Marcel tentang pandangan utas dalam utas yang dibuat tidak berasal dari utas, sisa pernyataan itu tidak benar. Memori bukan tentang berapa banyak yang diinstal di mesin, semua aplikasi di windows berjalan di ruang alamat virtual dan pada sistem 32bit yang memberi Anda 2GB data untuk aplikasi Anda (tidak peduli berapa banyak ram yang diinstal pada kotak). Mereka masih harus dikelola oleh runtime. Melakukan async IO tidak menggunakan utas untuk menunggu (menggunakan IOCP yang memungkinkan IO tumpang tindih) dan merupakan solusi yang lebih baik dan akan skala JAUH lebih baik.
Brian ONeil

7
Saat menjalankan banyak utas, bukan memori yang menjadi masalah melainkan CPU. Peralihan konteks di antara utas adalah operasi yang relatif mahal dan utas yang lebih aktif Anda memiliki lebih banyak sakelar konteks yang akan terjadi. Beberapa tahun yang lalu saya menjalankan tes pada PC saya dengan aplikasi konsol C # dan dengan kira-kira. 500 utas CPU saya 100%, utas tidak melakukan apa pun yang signifikan. Untuk jaringan comms, lebih baik untuk menjaga jumlah utas.
sipwiz

1
Saya akan pergi dengan solusi Tugas atau menggunakan async / menunggu. Solusi Tugas tampaknya lebih sederhana sementara async / menunggu cenderung lebih skalabel (mereka secara khusus dimaksudkan untuk situasi yang terikat IO).
Marcel Popescu

5

Menggunakan .NET's Iync IO terintegrasi (BeginRead , dll) adalah ide yang bagus jika Anda bisa mendapatkan semua detail dengan benar. Ketika Anda dengan benar mengatur soket / file Anda menangani itu akan menggunakan implementasi IOCP OS yang mendasari, memungkinkan operasi Anda untuk menyelesaikan tanpa menggunakan utas (atau, dalam kasus terburuk, menggunakan utas yang saya percaya berasal dari kumpulan thread IO kernel sebagai gantinya dari .NET's thread pool, yang membantu meringankan kemacetan threadpool.)

Gotcha utama adalah untuk memastikan bahwa Anda membuka soket / file dalam mode non-blocking. Sebagian besar fungsi kenyamanan default (sepertiFile.OpenRead ) tidak melakukan ini, jadi Anda harus menulis sendiri.

Salah satu masalah utama lainnya adalah penanganan kesalahan - penanganan kesalahan dengan benar saat menulis kode I / O asinkron jauh lebih sulit daripada melakukannya dalam kode sinkron. Juga sangat mudah untuk berakhir dengan kondisi balapan dan kebuntuan meskipun Anda mungkin tidak menggunakan utas secara langsung, jadi Anda perlu mengetahui hal ini.

Jika memungkinkan, Anda harus mencoba dan menggunakan perpustakaan praktis untuk memudahkan proses melakukan IO asinkron yang dapat diskalakan.

Microsoft Concurrency Coordination Runtime adalah salah satu contoh perpustakaan .NET yang dirancang untuk memudahkan kesulitan melakukan pemrograman jenis ini. Ini terlihat hebat, tetapi karena saya belum menggunakannya, saya tidak bisa berkomentar seberapa baik skala itu.

Untuk proyek pribadi saya yang perlu melakukan jaringan atau disk I / O asinkron, saya menggunakan seperangkat .NET concurrency / IO tools yang saya buat selama setahun terakhir, yang disebut Squared.Task . Ini terinspirasi oleh perpustakaan seperti imvu.task dan twisted , dan saya telah menyertakan beberapa contoh yang berfungsi dalam repositori yang melakukan I / O jaringan. Saya juga telah menggunakannya dalam beberapa aplikasi yang saya tulis - yang dirilis secara publik adalah NDexer (yang menggunakannya untuk disk I / O threadless). Perpustakaan ditulis berdasarkan pengalaman saya dengan imvu.task dan memiliki serangkaian tes unit yang cukup komprehensif, jadi saya sangat menyarankan Anda untuk mencobanya. Jika Anda memiliki masalah dengan itu, saya akan dengan senang hati menawarkan bantuan kepada Anda.

Menurut pendapat saya, berdasarkan pengalaman saya menggunakan asynchronous / threadless IO bukannya thread adalah upaya yang bermanfaat pada platform .NET, selama Anda siap untuk berurusan dengan kurva belajar. Hal ini memungkinkan Anda untuk menghindari kerepotan skalabilitas yang dikenakan oleh biaya objek Thread, dan dalam banyak kasus, Anda dapat sepenuhnya menghindari penggunaan kunci dan mutex dengan menggunakan hati-hati primitif concurrency seperti Futures / Promises.


Info hebat, saya akan memeriksa referensi Anda dan melihat apa yang masuk akal.
Erik Funkenbusch

3

Saya menggunakan solusi Kevin tetapi dia mengatakan solusi itu tidak memiliki kode untuk pemasangan kembali pesan. Pengembang dapat menggunakan kode ini untuk menyusun kembali pesan:

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

Anda bisa mencoba menggunakan kerangka kerja yang disebut ACE (Adaptive Communications Environment) yang merupakan kerangka C ++ umum untuk server jaringan. Ini adalah produk yang sangat solid, matang dan dirancang untuk mendukung aplikasi bervolume tinggi dengan keandalan tinggi, hingga kelas telekomunikasi.

Kerangka kerja ini berkaitan dengan berbagai model konkurensi dan mungkin memiliki satu yang cocok untuk aplikasi Anda di luar kotak. Ini akan membuat sistem lebih mudah untuk di-debug karena sebagian besar masalah konkurensi jahat telah diselesaikan. Trade-off di sini adalah bahwa framework ditulis dalam C ++ dan bukan basis kode yang paling hangat dan halus. Di sisi lain, Anda diuji, infrastruktur jaringan kelas industri dan arsitektur yang sangat skalabel.


2
Itu saran yang bagus, tetapi dari tag pertanyaan saya percaya OP akan menggunakan C #
JPCosta

Aku tahu itu; sarannya adalah ini tersedia untuk C ++ dan saya tidak mengetahui apa pun yang setara untuk C #. Debugging sistem semacam ini tidak mudah pada saat terbaik dan Anda mungkin mendapatkan pengembalian dari pergi ke kerangka kerja ini meskipun itu berarti beralih ke C ++.
ConcernedOfTunbridgeWells

Ya, ini C #. Saya mencari solusi berbasis internet yang bagus. Saya seharusnya lebih jelas, tetapi saya berasumsi orang akan membaca tag
Erik Funkenbusch


1

Nah, .NET sockets tampaknya menyediakan select () - itu yang terbaik untuk menangani input. Untuk output, saya akan memiliki kumpulan thread-penulis thread mendengarkan pada antrian kerja, menerima socket deskriptor / objek sebagai bagian dari item pekerjaan, jadi Anda tidak perlu thread per socket.


1

Saya akan menggunakan metode AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync yang ditambahkan dalam .Net 3.5. Saya telah melakukan benchmark dan mereka kira-kira 35% lebih cepat (waktu respons dan bitrate) dengan 100 pengguna terus-menerus mengirim dan menerima data.


1

untuk orang-orang menyalin menempel jawaban yang diterima, Anda dapat menulis ulang metode acceptCallback, menghapus semua panggilan _serverSocket.BeginAccept (AsyncCallback baru (acceptCallback), _serverSocket); dan letakkan di akhirnya {} klausa, dengan cara ini:

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

Anda bahkan dapat menghapus tangkapan pertama karena isinya sama tetapi ini merupakan metode templat dan Anda harus menggunakan pengecualian yang diketik untuk menangani pengecualian dengan lebih baik dan memahami apa yang menyebabkan kesalahan, jadi cukup terapkan tangkapan tersebut dengan beberapa kode yang berguna



-1

Agar lebih jelas, saya sedang mencari solusi berbasis .net (C # jika memungkinkan, tetapi bahasa .net apa pun akan berfungsi)

Anda tidak akan mendapatkan skalabilitas tingkat tertinggi jika Anda menggunakan murni .NET. Jeda GC dapat menghambat latensi.

Saya perlu memulai setidaknya satu utas untuk layanan ini. Saya sedang mempertimbangkan untuk menggunakan Asynch API (BeginRecieve, dll.) Karena saya tidak tahu berapa banyak klien yang akan saya sambungkan pada waktu tertentu (mungkin ratusan). Saya pasti tidak ingin memulai utas untuk setiap koneksi.

IO yang tumpang tindih umumnya dianggap sebagai API tercepat Windows untuk komunikasi jaringan. Saya tidak tahu apakah ini sama dengan API Asynch Anda. Jangan gunakan pilih karena setiap panggilan harus memeriksa setiap soket yang terbuka alih-alih memiliki panggilan balik pada soket aktif.


1
Saya tidak mengerti komentar jeda GC Anda .. Saya belum pernah melihat sistem dengan masalah skalabilitas yang terkait langsung dengan GC.
markt

4
Jauh lebih mungkin bahwa Anda membangun aplikasi yang tidak dapat menskala karena arsitektur yang buruk daripada karena GC ada. Sistem dengan performansi scalable + besar telah dibangun dengan .NET dan Java. Di kedua tautan yang Anda berikan, penyebabnya bukan pengumpulan sampah secara langsung .. tetapi terkait dengan penumpukan tumpukan. Saya menduga bahwa itu benar-benar masalah dengan arsitektur yang bisa dihindari .. Jika Anda dapat menunjukkan kepada saya bahasa yang tidak mungkin untuk membangun sistem yang tidak dapat skala, saya dengan senang hati akan menggunakannya;)
markt

1
Saya tidak setuju dengan komentar ini. Tidak diketahui, pertanyaan yang Anda rujuk adalah Java, dan mereka secara khusus berurusan dengan alokasi memori yang lebih besar dan mencoba untuk memaksa gc secara manual. Saya tidak benar-benar akan mengalami alokasi memori yang sangat besar di sini. Ini bukan masalah. Tapi terima kasih. Ya, Model Pemrograman Asinkron biasanya diterapkan di atas IO yang Tumpang tindih.
Erik Funkenbusch

1
Sebenarnya, praktik terbaik adalah tidak terus-menerus secara manual memaksa GC untuk mengumpulkan. Ini bisa membuat aplikasi Anda berkinerja lebih buruk. .NET GC adalah GC generasi yang akan menyesuaikan dengan penggunaan aplikasi Anda. Jika Anda benar-benar berpikir bahwa Anda perlu secara manual memanggil GC.Collect, saya akan mengatakan bahwa kode Anda kebutuhan yang paling mungkin ditulis dengan cara lain ..
markt

1
@ markt, itu adalah komentar untuk orang yang tidak benar-benar tahu apa-apa tentang pengumpulan sampah. Jika Anda memiliki waktu idle, tidak ada yang salah dengan melakukan koleksi manual. Itu tidak akan membuat aplikasi Anda lebih buruk ketika selesai. Makalah akademis menunjukkan bahwa GC generasi berfungsi karena merupakan perkiraan masa hidup objek Anda. Jelas ini bukan representasi yang sempurna. Bahkan, ada paradoks di mana generasi "tertua" sering memiliki rasio sampah tertinggi karena tidak pernah mengumpulkan sampah.
Tidak diketahui

-1

Anda dapat menggunakan kerangka kerja sumber terbuka Push Framework untuk pengembangan server berkinerja tinggi. Itu dibangun di atas IOCP dan cocok untuk skenario push dan siaran pesan.

http://www.pushframework.com


1
Posting ini ditandai C # dan .net. Mengapa Anda menyarankan kerangka kerja C ++?
Erik Funkenbusch

Mungkin karena dia yang menulisnya. potatosoftware.com/…
quillbreaker

apakah pushframework mendukung beberapa instance server? jika tidak, bagaimana skala?
esskar
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.