วิธีเขียนเซิร์ฟเวอร์ที่ใช้ Tcp / Ip ที่ปรับขนาดได้


148

ฉันอยู่ในขั้นตอนการออกแบบการเขียนแอปพลิเคชัน Windows Service ใหม่ที่ยอมรับการเชื่อมต่อ TCP / IP สำหรับการเชื่อมต่อที่ใช้เวลานาน (นั่นคือไม่เหมือน HTTP ที่มีการเชื่อมต่อระยะสั้นจำนวนมาก แต่เป็นไคลเอนต์ที่เชื่อมต่อ แม้สัปดาห์)

ฉันกำลังมองหาแนวคิดสำหรับวิธีที่ดีที่สุดในการออกแบบสถาปัตยกรรมเครือข่าย ฉันจะต้องเริ่มต้นอย่างน้อยหนึ่งเธรดสำหรับบริการ ฉันกำลังพิจารณาใช้ Asynch API (BeginRecieve และอื่น ๆ ) เนื่องจากฉันไม่ทราบว่ามีลูกค้ากี่รายที่ฉันจะเชื่อมต่อในเวลาใดก็ตาม (อาจเป็นร้อย) แน่นอนฉันไม่ต้องการเริ่มหัวข้อสำหรับแต่ละการเชื่อมต่อ

ข้อมูลจะไหลออกไปยังไคลเอนต์จากเซิร์ฟเวอร์ของฉันเป็นหลัก แต่จะมีบางคำสั่งที่ส่งจากลูกค้าในบางโอกาส นี่เป็นแอปพลิเคชันการตรวจสอบเบื้องต้นที่เซิร์ฟเวอร์ของฉันส่งข้อมูลสถานะเป็นระยะให้กับลูกค้า

คำแนะนำใด ๆ เกี่ยวกับวิธีที่ดีที่สุดในการปรับขนาดให้ได้มากที่สุด เวิร์กโฟลว์พื้นฐาน ขอบคุณ

แก้ไข: เพื่อความชัดเจนฉันกำลังมองหา. based based solutions (C # ถ้าเป็นไปได้ แต่. net ภาษาใด ๆ จะทำงาน)

BOUNTY NOTE: เพื่อรับรางวัลโปรดปรานฉันคาดหวังมากกว่าคำตอบง่ายๆ ฉันต้องการตัวอย่างการทำงานของโซลูชันไม่ว่าจะเป็นตัวชี้ไปยังสิ่งที่ฉันสามารถดาวน์โหลดหรือตัวอย่างสั้น ๆ ในบรรทัด และต้องเป็น. net และ Windows (ภาษาใด ๆ . net เป็นที่ยอมรับ)

แก้ไข: ฉันอยากจะขอบคุณทุกคนที่ให้คำตอบที่ดี น่าเสียดายที่ฉันสามารถยอมรับได้เพียงวิธีเดียวเท่านั้นและฉันเลือกที่จะยอมรับวิธีการเริ่มต้น / สิ้นสุดที่เป็นที่รู้จักมากขึ้น วิธีแก้ปัญหาของ Esac อาจจะดีกว่า แต่ก็ยังใหม่พอที่ฉันไม่รู้ว่ามันจะออกมาดีอย่างไร

ฉันได้ตอบคำถามทั้งหมดที่ฉันคิดว่าดีมาแล้วฉันหวังว่าฉันจะทำเพื่อคุณได้มากขึ้น ขอบคุณอีกครั้ง.


1
คุณแน่ใจหรือไม่ว่าจำเป็นต้องใช้การเชื่อมต่อที่ยาวนาน มันยากที่จะบอกได้จากข้อมูลที่ จำกัด ให้ แต่ผมเท่านั้นที่จะทำเช่นนั้นจำเป็นอย่างยิ่ง .. ถ้า
Markt

ใช่มันต้องใช้เวลานาน ข้อมูลต้องได้รับการอัพเดตแบบเรียลไทม์ดังนั้นฉันไม่สามารถทำการสำรวจเป็นระยะได้ข้อมูลจะต้องถูกส่งไปยังลูกค้าเมื่อมันเกิดขึ้นซึ่งหมายถึงการเชื่อมต่อคงที่
Erik Funkenbusch

1
นั่นไม่ใช่เหตุผลที่ถูกต้อง Http รองรับการเชื่อมต่อที่ใช้งานได้นาน คุณเพิ่งเปิดการเชื่อมต่อและรอ repsonse (สำรวจความคิดเห็นค้าง) งานนี้ดีสำหรับรูปแบบ AJAX หลายปพลิเคชันอื่น ๆ คุณจะทำอย่างไรคิดว่าผลงาน Gmail :-)
TFD

2
Gmail ทำงานโดยการหยั่งเสียงเป็นระยะ ๆ สำหรับอีเมลมันไม่ได้ทำให้การเชื่อมต่อทำงานนาน นี่เป็นเรื่องปกติสำหรับอีเมลซึ่งไม่จำเป็นต้องตอบกลับตามเวลาจริง
Erik Funkenbusch

2
การสำรวจหรือการดึงสเกลทำได้ดี แต่พัฒนาความล่าช้าอย่างรวดเร็ว การกดไม่ได้ปรับขนาดเช่นกัน แต่ช่วยลดหรือขจัดความล่าช้า
andrewbadera

คำตอบ:


92

ฉันเคยเขียนบางสิ่งที่คล้ายกับสิ่งนี้ในอดีต จากงานวิจัยของฉันเมื่อหลายปีก่อนแสดงให้เห็นว่าการเขียนการติดตั้งซ็อกเก็ตของคุณเองเป็นทางออกที่ดีที่สุดโดยใช้ซ็อกเก็ตอะซิงโครนัส นั่นหมายความว่าลูกค้าไม่ได้ทำสิ่งใดต้องการทรัพยากรที่ค่อนข้างน้อย สิ่งใดที่เกิดขึ้นได้รับการจัดการโดย. เธรดพูล

ฉันเขียนเป็นคลาสที่จัดการการเชื่อมต่อทั้งหมดสำหรับเซิร์ฟเวอร์

ฉันใช้ลิสต์เพื่อหยุดการเชื่อมต่อไคลเอนต์ทั้งหมด แต่ถ้าคุณต้องการการค้นหาที่เร็วขึ้นสำหรับลิสต์ที่ใหญ่ขึ้นคุณสามารถเขียนได้ตามที่คุณต้องการ

private List<xConnection> _sockets;

นอกจากนี้คุณต้องใช้ซ็อกเก็ตจริงฟังเพื่อเริ่มต้นการเชื่อมต่อ

private System.Net.Sockets.Socket _serverSocket;

วิธีการเริ่มต้นจะเริ่มต้นซ็อกเก็ตเซิร์ฟเวอร์และเริ่มฟังการเชื่อมต่อที่ไม่คาดคิด

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

ฉันแค่ต้องการจะทราบว่ารหัสการจัดการข้อยกเว้นนั้นดูไม่ดี แต่สาเหตุที่เป็นเพราะฉันมีรหัสการยกเว้นในนั้นเพื่อให้ข้อยกเว้นใด ๆ จะถูกระงับและส่งคืนfalseหากมีการตั้งค่าตัวเลือกการตั้งค่าไว้ แต่ฉันต้องการลบ เห็นแก่ความกะทัดรัด

_serverSocket.BeginAccept (ใหม่ AsyncCallback (acceptCallback)), _serverSocket) ด้านบนตั้งค่าซ็อกเก็ตเซิร์ฟเวอร์ของเราเป็นหลักเพื่อเรียกเมธอด acceptCallback เมื่อใดก็ตามที่ผู้ใช้เชื่อมต่อ เมธอดนี้รันจาก. Net threadpool ซึ่งจัดการการสร้างเธรดผู้ทำงานเพิ่มเติมโดยอัตโนมัติหากคุณมีการดำเนินการบล็อกจำนวนมาก สิ่งนี้ควรจัดการโหลดบนเซิร์ฟเวอร์อย่างเหมาะสมที่สุด

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

รหัสด้านบนเป็นหลักเพิ่งจะยอมรับการเชื่อมต่อที่เข้ามาคิวBeginReceiveซึ่งเป็นการเรียกกลับที่จะทำงานเมื่อไคลเอนต์ส่งข้อมูลจากนั้นรอคิวถัดไปacceptCallbackซึ่งจะยอมรับการเชื่อมต่อไคลเอ็นต์ถัดไปที่เข้ามา

BeginReceiveเรียกวิธีคือสิ่งที่บอกซ็อกเก็ตว่าจะทำอย่างไรเมื่อได้รับข้อมูลจากลูกค้า สำหรับBeginReceiveคุณจะต้องให้มันเป็นอาร์เรย์ไบต์ซึ่งเป็นที่ที่มันจะคัดลอกข้อมูลเมื่อลูกค้าส่งข้อมูล ReceiveCallbackวิธีการจะได้รับการเรียกว่าซึ่งเป็นวิธีการที่เราจัดการกับข้อมูลที่ได้รับ

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

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

แก้ไข: ในรูปแบบนี้ฉันลืมที่จะพูดถึงว่าในรหัสพื้นที่นี้:

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

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

โดยทั่วไปสิ่งที่ฉันจะทำคืออะไรก็ได้ที่คุณต้องการในรหัสคือทำการรวมกันอีกครั้งของแพ็กเก็ตลงในข้อความและสร้างมันเป็นงานบนเธรดพูล วิธีนี้ BeginReceive ของบล็อกถัดไปจากไคลเอ็นต์จะไม่ล่าช้าในขณะที่รหัสการประมวลผลข้อความกำลังทำงานอยู่

การเรียกกลับยอมรับเสร็จสิ้นการอ่านซ็อกเก็ตข้อมูลโดยการเรียกรับสิ้นสุด สิ่งนี้จะเติมบัฟเฟอร์ที่มีให้ในฟังก์ชั่นเริ่มต้นรับ เมื่อคุณทำสิ่งที่คุณต้องการในที่ที่ฉันได้แสดงความคิดเห็นเราจะเรียกBeginReceiveวิธีการถัดไปซึ่งจะเรียกใช้การเรียกกลับอีกครั้งหากลูกค้าส่งข้อมูลเพิ่มเติม ตอนนี้เป็นส่วนที่ยากมากเมื่อลูกค้าส่งข้อมูลการรับสายของคุณอาจถูกเรียกโดยใช้เพียงบางส่วนของข้อความ การประกอบซ้ำนั้นซับซ้อนมาก ฉันใช้วิธีการของตัวเองและสร้างโปรโตคอลที่เป็นกรรมสิทธิ์เพื่อทำสิ่งนี้ ฉันทิ้งมันไว้ แต่ถ้าคุณร้องขอฉันสามารถเพิ่มมันเข้าไปได้ตัวจัดการนี้จริงๆแล้วเป็นโค้ดที่ซับซ้อนที่สุดที่ฉันเคยเขียน

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

วิธีการส่งด้านบนใช้การSendโทรแบบซิงโครนัสสำหรับฉันซึ่งใช้ได้เนื่องจากขนาดข้อความและลักษณะมัลติเธรดของแอปพลิเคชันของฉัน หากคุณต้องการส่งไปยังลูกค้าทุกคนคุณเพียงแค่ต้องวนลูปผ่านรายการ _sockets

คลาส xConnection ที่คุณเห็นอ้างอิงข้างต้นนั้นเป็น wrapper ง่ายๆสำหรับซ็อกเก็ตที่จะรวมบัฟเฟอร์ไบต์และในการใช้งานของฉันพิเศษบางอย่าง

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

นอกจากนี้สำหรับการอ้างอิงที่นี่เป็นสิ่งusingที่ฉันรวมไว้เพราะฉันมักจะรำคาญเมื่อพวกเขาไม่รวม

using System.Net.Sockets;

ฉันหวังว่าจะเป็นประโยชน์อาจไม่ได้เป็นรหัสที่สะอาดที่สุด แต่ใช้งานได้ นอกจากนี้ยังมีความแตกต่างบางอย่างของรหัสที่คุณควรจะเบื่อหน่ายเกี่ยวกับการเปลี่ยนแปลง สำหรับหนึ่งมีเพียงหนึ่งเดียวที่BeginAcceptเรียกในเวลาใดก็ได้ เคยมีข้อผิดพลาด. net ที่น่ารำคาญรอบ ๆ สิ่งนี้ซึ่งเป็นเมื่อหลายปีก่อนดังนั้นฉันจึงไม่จำรายละเอียดได้

นอกจากนี้ในReceiveCallbackรหัสเราประมวลผลทุกอย่างที่ได้รับจากซ็อกเก็ตก่อนที่เราจะได้รับคิวต่อไป ซึ่งหมายความว่าสำหรับซ็อกเก็ตเดียวเราจริง ๆ แล้วได้ตลอด ReceiveCallbackเวลาในเวลาใดก็ได้และเราไม่จำเป็นต้องใช้การซิงโครไนส์เธรด อย่างไรก็ตามหากคุณเรียงลำดับสิ่งนี้เพื่อเรียกรับครั้งต่อไปทันทีหลังจากดึงข้อมูลซึ่งอาจเร็วขึ้นเล็กน้อยคุณจะต้องตรวจสอบให้แน่ใจว่าคุณซิงโครไนซ์เธรดอย่างถูกต้อง

นอกจากนี้ฉันแฮ็คโค้ดจำนวนมาก แต่ทิ้งสาระสำคัญของสิ่งที่เกิดขึ้น นี่ควรเป็นการเริ่มต้นที่ดีสำหรับการออกแบบของคุณ แสดงความคิดเห็นหากคุณมีคำถามเพิ่มเติมเกี่ยวกับเรื่องนี้


1
นี่เป็นคำตอบที่ดีเควิน .. ดูเหมือนว่าคุณกำลังจะได้รับรางวัล :)
Erik Funkenbusch

6
ฉันไม่รู้ว่าทำไมนี่คือคำตอบที่ได้รับการโหวตสูงสุด เริ่มต้น * สิ้นสุด * ไม่ใช่วิธีที่เร็วที่สุดในการทำเครือข่ายใน C # หรือปรับขนาดได้สูงที่สุด มันเร็วกว่าแบบซิงโครนัส แต่มีการดำเนินการมากมายที่ดำเนินต่อไปภายใต้ประทุนใน Windows ที่ช้าลงเส้นทางเครือข่ายนี้
esac

6
โปรดจำไว้ว่าสิ่งที่ esac เขียนไว้ในความคิดเห็นก่อนหน้า รูปแบบการเริ่มต้นอาจเหมาะกับคุณถึงจุดหนึ่งขณะนี้รหัสของฉันกำลังใช้การเริ่มต้น แต่มีการปรับปรุงข้อ จำกัด ใน. net 3.5 ฉันไม่สนใจเงินรางวัล แต่อยากจะแนะนำให้คุณอ่านลิงก์ในคำตอบของฉันแม้ว่าคุณจะใช้วิธีการนี้ "การปรับปรุงประสิทธิภาพของซ็อกเก็ตในเวอร์ชัน 3.5"
jvanderh

1
ฉันแค่อยากจะโยนพวกเขาเนื่องจากฉันอาจยังไม่ชัดเจนพอนี่คือรหัสยุค. net 2.0 ที่ฉันเชื่อว่านี่เป็นรูปแบบที่ทำงานได้ดีมาก อย่างไรก็ตามคำตอบของ esac ดูเหมือนจะค่อนข้างทันสมัยกว่าหากกำหนดเป้าหมายเป็น. net 3.5 nitpick เดียวที่ฉันมีคือการขว้างเหตุการณ์ :) แต่นั่นสามารถเปลี่ยนแปลงได้อย่างง่ายดาย นอกจากนี้ฉันได้ทำการทดสอบปริมาณงานด้วยรหัสนี้และใน dual core opteron 2Ghz ก็สามารถใช้งานอีเธอร์เน็ตได้สูงสุด 100Mbps และเพิ่มชั้นการเข้ารหัสที่ด้านบนของรหัสนี้
Kevin Nisbet

1
@KevinNisbet ฉันรู้ว่ามันค่อนข้างช้า แต่สำหรับใครก็ตามที่ใช้คำตอบนี้ในการออกแบบเซิร์ฟเวอร์ของตัวเอง - การส่งควรเป็นแบบอะซิงโครนัสเพราะมิฉะนั้นคุณจะเปิดตัวเองเพื่อความเป็นไปได้ของการหยุดชะงัก หากทั้งสองฝั่งเขียนข้อมูลที่เติมบัฟเฟอร์ตามลำดับSendวิธีการนั้นจะบล็อกทั้งสองข้างอย่างไม่มีกำหนดเพราะไม่มีใครอ่านข้อมูลอินพุต
Luaan

83

มีหลายวิธีในการดำเนินการเครือข่ายใน C # พวกเขาทั้งหมดใช้กลไกที่แตกต่างกันภายใต้ประทุนและทำให้ประสบปัญหาประสิทธิภาพการทำงานที่สำคัญพร้อมกันสูง การดำเนินการเริ่มต้น * เป็นหนึ่งในสิ่งเหล่านี้ที่หลายคนมักจะเข้าใจผิดว่าเป็นวิธีที่เร็วกว่า / เร็วที่สุดในการทำเครือข่าย

เมื่อต้องการแก้ไขปัญหาเหล่านี้พวกเขาแนะนำวิธีการตั้งค่า * Async: จาก MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasocketasynceventargs.aspx

คลาส SocketAsyncEventArgs เป็นส่วนหนึ่งของชุดการปรับปรุงเพื่อ System.Net.Sockets .. ::. คลาสซ็อกเก็ตที่ให้รูปแบบอะซิงโครนัสทางเลือกที่สามารถใช้งานได้โดยแอปพลิเคชันซ็อกเก็ตประสิทธิภาพสูงพิเศษ คลาสนี้ได้รับการออกแบบมาโดยเฉพาะสำหรับแอปพลิเคชันเซิร์ฟเวอร์เครือข่ายที่ต้องการประสิทธิภาพสูง แอปพลิเคชันสามารถใช้รูปแบบอะซิงโครนัสที่ปรับปรุงแล้วเฉพาะหรือเฉพาะในพื้นที่ร้อนที่กำหนดเป้าหมาย (ตัวอย่างเช่นเมื่อรับข้อมูลจำนวนมาก)

คุณสมบัติหลักของการปรับปรุงเหล่านี้คือการหลีกเลี่ยงการจัดสรรซ้ำและการซิงโครไนซ์วัตถุระหว่าง I / O ซ็อกเก็ตอะซิงโครนัสปริมาณสูง รูปแบบการออกแบบเริ่มต้น / สิ้นสุดที่ใช้งานในปัจจุบันโดย System.Net.Sockets .. ::. คลาสซ็อกเก็ตต้องการระบบ .. ::. วัตถุ IAsyncResult ถูกจัดสรรสำหรับการดำเนินงานซ็อกเก็ตแบบอะซิงโครนัส

ภายใต้หน้าปก, * Async API ใช้พอร์ตเสร็จ IO ซึ่งเป็นวิธีที่เร็วที่สุดในการดำเนินการเครือข่ายดูhttp://msdn.microsoft.com/en-us/magazine/cc302334.aspx

และเพื่อช่วยคุณฉันรวมถึงซอร์สโค้ดสำหรับเซิร์ฟเวอร์เทลเน็ตที่ฉันเขียนโดยใช้ * Async API ฉันรวมเฉพาะบางส่วนที่เกี่ยวข้องเท่านั้น นอกจากนี้เพื่อทราบแทนการประมวลผลข้อมูลแบบอินไลน์ฉันแทนที่จะเลือกที่จะผลักมันลงบนคิวล็อคฟรี (รอฟรี) ที่ถูกประมวลผลในหัวข้อที่แยกต่างหาก โปรดทราบว่าฉันไม่ได้รวมคลาส Pool ที่สอดคล้องกันซึ่งเป็นเพียงพูลธรรมดาที่จะสร้างวัตถุใหม่ถ้าว่างเปล่าและคลาสบัฟเฟอร์ซึ่งเป็นเพียงบัฟเฟอร์การขยายตัวเองซึ่งไม่จำเป็นจริงๆเว้นแต่คุณจะได้รับการกำหนด ปริมาณข้อมูล หากคุณต้องการข้อมูลอีกต่อไปอย่าลังเลที่จะส่ง 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
    }
}

นี่เป็นสิ่งที่ตรงไปตรงมาและเป็นตัวอย่างง่ายๆ ขอบคุณ ฉันจะต้องประเมินข้อดีข้อเสียของแต่ละวิธี
Erik Funkenbusch

ฉันไม่ได้มีโอกาสทดสอบ แต่ฉันได้รับความรู้สึกที่คลุมเครือของสภาพการแข่งขันที่นี่ด้วยเหตุผลบางอย่าง ก่อนอื่นหากคุณได้รับข้อความจำนวนมากฉันไม่ทราบว่าจะมีการดำเนินการตามลำดับเหตุการณ์ (อาจไม่สำคัญสำหรับแอปของผู้ใช้ แต่ควรจดบันทึกไว้) หรือฉันอาจผิดและเหตุการณ์จะดำเนินการตามลำดับ ที่สองคือฉันอาจพลาด แต่ไม่มีความเสี่ยงของบัฟเฟอร์ที่ถูกเขียนทับในขณะที่ DataReceived ยังคงทำงานอยู่หากใช้เวลานานหรือไม่ หากปัญหาเหล่านี้ได้รับการแก้ไขแล้วฉันคิดว่านี่เป็นวิธีแก้ปัญหาที่ทันสมัยมาก
Kevin Nisbet

1
ในกรณีของฉันสำหรับเซิร์ฟเวอร์เทลเน็ตของฉัน 100% มีใช่อยู่ในลำดับ ที่สำคัญคือการตั้งค่าวิธีการโทรกลับที่เหมาะสมก่อนที่จะโทร AcceptAsync, ReceiveAsync ฯลฯ ในกรณีของฉันฉันทำ SendAsync ในหัวข้อที่แยกต่างหากดังนั้นถ้าสิ่งนี้ถูกปรับเปลี่ยนให้ทำแบบยอมรับ / ส่ง / รับ / ส่ง / รับ / ตัดการเชื่อมต่อจากนั้น มันจะต้องมีการแก้ไข
esac

1
จุดที่ 2 เป็นสิ่งที่คุณจะต้องพิจารณาด้วย ฉันกำลังจัดเก็บวัตถุ 'การเชื่อมต่อ' ในบริบท SocketAsyncEventArgs สิ่งนี้หมายความว่าฉันมีเพียงหนึ่งรับบัฟเฟอร์ต่อการเชื่อมต่อ ฉันไม่ได้โพสต์อีกรับด้วย SocketAsyncEventArgs นี้จนกระทั่ง DataReceived เสร็จสมบูรณ์ดังนั้นจึงไม่สามารถอ่านข้อมูลเพิ่มเติมได้จนกว่าจะเสร็จสมบูรณ์ ฉันขอแนะนำว่าจะไม่มีการดำเนินการที่ยาวนานกับข้อมูลนี้ ฉันย้ายบัฟเฟอร์ทั้งหมดของข้อมูลทั้งหมดที่ได้รับไปยังคิว lockfree แล้วประมวลผลบนเธรดแยกต่างหาก สิ่งนี้ช่วยให้มั่นใจว่า latency ต่ำในส่วนของเครือข่าย
esac

1
ในหมายเหตุด้านข้างฉันเขียนการทดสอบหน่วยและการทดสอบโหลดสำหรับโค้ดนี้และเมื่อฉันเพิ่มการโหลดผู้ใช้จาก 1 ผู้ใช้เป็น 250 ผู้ใช้ (ในระบบ Dual Core ระบบเดียว 4GB ของ RAM) เวลาตอบสนอง 100 ไบต์ (1 แพ็คเก็ต) และ 10000 ไบต์ (3 แพ็คเก็ต) ยังคงเหมือนเดิมตลอดช่วงเวลาการโหลดของผู้ใช้ทั้งหมด
esac

46

เคยมีการพูดคุยที่ดีจริง ๆ ของ TCP / IP ที่ปรับขนาดได้โดยใช้. NET ที่เขียนโดย Chris Mullins จาก Coversant โชคไม่ดีที่บล็อกของเขาหายไปจากตำแหน่งเดิมดังนั้นฉันจะพยายามรวบรวมคำแนะนำจากหน่วยความจำ ของการปรากฏตัวของเขาในหัวข้อนี้: C ++ กับ C #: การพัฒนาเซิร์ฟเวอร์ IOCP ที่ปรับขนาดได้สูง )

ก่อนอื่นให้สังเกตว่าทั้งการใช้Begin/EndและAsyncวิธีการในSocketชั้นเรียนนั้นใช้ประโยชน์จากพอร์ต IO Completion (IOCP) เพื่อให้เกิดความยืดหยุ่น สิ่งนี้สร้างความแตกต่างที่ยิ่งใหญ่กว่า (เมื่อใช้อย่างถูกต้องดูด้านล่าง) เพื่อขยายขีดความสามารถมากกว่าวิธีการสองวิธีที่คุณเลือกเพื่อนำไปใช้แก้ปัญหา

โพสต์ของ Chris Mullins นั้นมาจากการใช้Begin/Endงานซึ่งเป็นสิ่งที่ฉันมีประสบการณ์ โปรดทราบว่า Chris รวบรวมวิธีการแก้ปัญหาโดยปรับขนาดการเชื่อมต่อไคลเอนต์พร้อมกันบน 10,000 เครื่องบนเครื่อง 32- บิตที่มีหน่วยความจำ 2GB และเป็น 100,000 ในแพลตฟอร์ม 64 บิตที่มีหน่วยความจำเพียงพอ จากประสบการณ์ของฉันกับเทคนิคนี้ (ไม่มีที่ไหนเลยใกล้กับโหลดประเภทนี้) ฉันไม่มีเหตุผลที่จะสงสัยตัวเลขที่บ่งบอกถึงสิ่งเหล่านี้

IOCP กับเธรดต่อการเชื่อมต่อหรือดั้งเดิม 'เลือก'

เหตุผลที่คุณต้องการใช้กลไกที่ใช้ IOCP ภายใต้ประทุนคือมันใช้พูลเธรด Windows ระดับต่ำมากที่ไม่ปลุกเธรดใด ๆ จนกว่าจะมีข้อมูลจริงบนแชนเนล IO ที่คุณพยายามอ่านจาก ( โปรดทราบว่า IOCP สามารถใช้สำหรับไฟล์ IO ได้เช่นกัน) ประโยชน์ของการทำเช่นนี้คือ Windows ไม่จำเป็นต้องเปลี่ยนไปใช้เธรดเท่านั้นเพื่อค้นหาว่ายังไม่มีข้อมูลอยู่ดังนั้นการลดจำนวนการสลับบริบทของเซิร์ฟเวอร์ของคุณจะต้องทำอย่างน้อยที่สุด

การสลับบริบทเป็นสิ่งที่จะทำลายกลไก 'เธรดต่อการเชื่อมต่อ' อย่างแน่นอนแม้ว่านี่จะเป็นทางออกที่ทำงานได้หากคุณจัดการกับการเชื่อมต่อเพียงไม่กี่โหล อย่างไรก็ตามกลไกนี้ไม่สามารถขยายจินตนาการได้

ข้อควรพิจารณาที่สำคัญเมื่อใช้ IOCP

หน่วยความจำ

ก่อนอื่นสิ่งสำคัญคือต้องเข้าใจว่า IOCP สามารถทำให้เกิดปัญหาหน่วยความจำภายใต้. NET หากการใช้งานของคุณไร้เดียงสาเกินไป การBeginReceiveเรียกIOCP ทุกครั้งจะทำให้ "การตรึง" ของบัฟเฟอร์ที่คุณกำลังอ่านอยู่ สำหรับคำอธิบายที่ดีว่าทำไมปัญหานี้เป็นปัญหาดู: ยุนจินเว็บบล็อก: OutOfMemoryException และการตรึง

โชคดีที่ปัญหานี้สามารถหลีกเลี่ยงได้ แต่ก็ต้องมีการแลกเปลี่ยนกันเล็กน้อย โซลูชันที่แนะนำคือการจัดสรรbyte[]บัฟเฟอร์ขนาดใหญ่เมื่อเริ่มต้นแอปพลิเคชัน (หรือปิดดังกล่าว) อย่างน้อย 90KB หรืออย่างน้อย (ณ . NET 2 ขนาดที่ต้องการอาจใหญ่กว่าในรุ่นที่ใหม่กว่า) เหตุผลในการทำเช่นนี้คือการจัดสรรหน่วยความจำขนาดใหญ่จะสิ้นสุดโดยอัตโนมัติในเซ็กเมนต์หน่วยความจำที่ไม่ได้ทำการบีบอัด (The Large Object Heap) ที่ถูกตรึงอย่างมีประสิทธิภาพโดยอัตโนมัติ ด้วยการจัดสรรบัฟเฟอร์ขนาดใหญ่หนึ่งตัวเมื่อเริ่มต้นระบบคุณต้องแน่ใจว่าบล็อกของหน่วยความจำที่ไม่สามารถเคลื่อนย้ายได้นั้นอยู่ในสถานะ 'ที่อยู่ต่ำ' ซึ่งจะไม่เข้าทางและทำให้เกิดการแตกแฟรกเมนต์

จากนั้นคุณสามารถใช้ออฟเซ็ตเพื่อแบ่งเซกเมนต์ขนาดใหญ่ของบัฟเฟอร์นี้ออกเป็นส่วน ๆ สำหรับการเชื่อมต่อแต่ละครั้งที่จำเป็นต้องอ่านข้อมูลบางอย่าง นี่คือจุดเริ่มต้นของการแลกเปลี่ยน เนื่องจากบัฟเฟอร์นี้ต้องได้รับการจัดสรรล่วงหน้าคุณจะต้องตัดสินใจว่าต้องใช้พื้นที่บัฟเฟอร์เท่าไรต่อการเชื่อมต่อและขีด จำกัด สูงสุดที่คุณต้องการกำหนดในจำนวนการเชื่อมต่อที่คุณต้องการปรับขนาดเป็น (หรือคุณสามารถใช้นามธรรม ที่สามารถจัดสรรบัฟเฟอร์เพิ่มเติมที่ตรึงไว้เมื่อคุณต้องการ)

ทางออกที่ง่ายที่สุดคือการกำหนดให้ทุกการเชื่อมต่อเป็นไบต์เดียวที่ออฟเซ็ตที่ไม่ซ้ำกันภายในบัฟเฟอร์นี้ จากนั้นคุณสามารถBeginReceiveโทรหาหนึ่งไบต์เพื่ออ่านและดำเนินการอ่านส่วนที่เหลือจากการโทรกลับที่คุณได้รับ

การประมวลผล

เมื่อคุณได้รับการติดต่อกลับจากการBeginโทรที่คุณทำสิ่งสำคัญคือการตระหนักว่ารหัสในการติดต่อกลับจะดำเนินการในเธรด IOCP ระดับต่ำ จำเป็นอย่างยิ่งที่คุณหลีกเลี่ยงการดำเนินการที่ยาวนานในการติดต่อกลับนี้ การใช้เธรดเหล่านี้สำหรับการประมวลผลที่ซับซ้อนจะช่วยลดความสามารถในการขยายของคุณเช่นเดียวกับการใช้ 'การเชื่อมต่อแบบเธรดต่อ'

วิธีแก้ปัญหาที่แนะนำคือใช้การเรียกกลับเพื่อจัดคิวไอเท็มงานเพื่อประมวลผลข้อมูลขาเข้าซึ่งจะถูกดำเนินการในเธรดอื่น หลีกเลี่ยงการปิดกั้นการดำเนินการใด ๆ ที่อาจเกิดขึ้นภายในการเรียกกลับเพื่อให้เธรด IOCP สามารถกลับไปที่กลุ่มของมันโดยเร็วที่สุด ใน. NET 4.0 ฉันขอแนะนำทางออกที่ง่ายที่สุดคือวางไข่Taskโดยอ้างอิงถึงซ็อกเก็ตไคลเอ็นต์และสำเนาไบต์แรกที่อ่านแล้วโดยการBeginReceiveโทร ภารกิจนี้มีหน้าที่รับผิดชอบในการอ่านข้อมูลทั้งหมดจากซ็อกเก็ตที่แสดงถึงคำขอที่คุณกำลังประมวลผลเรียกใช้งานจากนั้นทำการBeginReceiveเรียกใหม่เพื่อจัดคิวซ็อกเก็ตสำหรับ IOCP อีกครั้ง ก่อน. NET 4.0 คุณสามารถใช้ ThreadPool หรือสร้างการใช้คิวงานของคุณเอง

สรุป

โดยพื้นฐานแล้วฉันขอแนะนำให้ใช้โค้ดตัวอย่างของ Kevin สำหรับโซลูชันนี้โดยมีคำเตือนเพิ่มเติมดังต่อไปนี้:

  • ตรวจสอบให้แน่ใจว่าบัฟเฟอร์ที่คุณส่งไปBeginReceiveนั้นถูก 'ตรึง' อยู่แล้ว
  • ตรวจสอบให้แน่ใจว่าการโทรกลับที่คุณส่งไปBeginReceiveนั้นไม่ได้ทำอะไรมากไปกว่าคิวงานเพื่อจัดการการประมวลผลข้อมูลขาเข้าจริง

เมื่อคุณทำเช่นนั้นฉันไม่สงสัยเลยว่าคุณสามารถทำซ้ำผลลัพธ์ของคริสในการขยายไปสู่ลูกค้าพร้อมกันหลายแสนคน (จากฮาร์ดแวร์ที่เหมาะสมและการใช้งานอย่างมีประสิทธิภาพของโค้ดประมวลผลของคุณเอง)


1
ในการตรึงบล็อกหน่วยความจำที่มีขนาดเล็กลงสามารถใช้วิธีการจัดสรรวัตถุ GCHandle เพื่อตรึงบัฟเฟอร์ เมื่อเสร็จสิ้นแล้ว UnsafeAddrOfPinnedArrayElement ของวัตถุ Marshal สามารถใช้เพื่อรับตัวชี้ไปยังบัฟเฟอร์ ตัวอย่างเช่น: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan

@ BobBryan ถ้าฉันไม่เข้าใจจุดที่คุณพยายามทำวิธีการดังกล่าวไม่ได้ช่วยแก้ปัญหาที่โซลูชันของฉันพยายามแก้ไขด้วยการจัดสรรบล็อคขนาดใหญ่นั่นคือศักยภาพในการกระจายตัวของหน่วยความจำอย่างมากในการจัดสรรบล็อกพินเล็ก ๆ ซ้ำ ๆ ของหน่วยความจำ
jerryjvl

ประเด็นก็คือคุณไม่ต้องจัดสรรบล็อกขนาดใหญ่เพื่อเก็บไว้ในหน่วยความจำ คุณสามารถจัดสรรบล็อกขนาดเล็กลงและใช้เทคนิคด้านบนเพื่อตรึงไว้ในหน่วยความจำเพื่อหลีกเลี่ยงการย้าย gc เหล่านั้น คุณสามารถอ้างอิงถึงบล็อกที่มีขนาดเล็กแต่ละบล็อกได้เช่นเดียวกับที่คุณอ้างอิงกับบล็อกที่มีขนาดใหญ่กว่านั้นและนำกลับมาใช้ใหม่ได้ตามต้องการ วิธีใดวิธีหนึ่งที่ถูกต้อง - ฉันเพิ่งชี้ให้เห็นว่าคุณไม่จำเป็นต้องใช้บัฟเฟอร์ที่มีขนาดใหญ่มาก แต่ต้องบอกว่าบางครั้งการใช้บัฟเฟอร์ที่มีขนาดใหญ่มากเป็นวิธีที่ดีที่สุดที่จะไปเนื่องจาก gc จะปฏิบัติต่อมันอย่างมีประสิทธิภาพมากขึ้น
บ๊อบไบรอัน

@ BobBryan ตั้งแต่การตรึงบัฟเฟอร์จะเกิดขึ้นโดยอัตโนมัติเมื่อคุณเรียก BeginReceive การปักไม่ใช่จุดสำคัญจริงๆที่นี่ ประสิทธิภาพคือ;) ... และนี่เป็นข้อกังวลโดยเฉพาะอย่างยิ่งเมื่อพยายามเขียนเซิร์ฟเวอร์ที่ปรับขนาดได้ดังนั้นจึงจำเป็นต้องจัดสรรบล็อกขนาดใหญ่เพื่อใช้สำหรับพื้นที่บัฟเฟอร์
jerryjvl

@jerryjvl ขออภัยที่ต้องถามคำถามที่เก่ามาก แต่เมื่อเร็ว ๆ นี้ฉันได้ค้นพบปัญหาที่แน่นอนนี้ด้วยวิธี asinch BeginXXX / EndXXX นี่คือโพสต์ที่ยอดเยี่ยม แต่เอาการขุดจำนวนมากเพื่อค้นหา ฉันชอบโซลูชันที่คุณแนะนำ แต่ไม่เข้าใจส่วนใดส่วนหนึ่งของมัน: "จากนั้นคุณสามารถโทรหา BeginReceive เพื่ออ่านไบต์เดียวและทำการอ่านส่วนที่เหลือจากการโทรกลับที่คุณได้รับ" คุณหมายถึงอะไรโดยทำส่วนที่เหลือของการเตรียมการอันเป็นผลมาจากการโทรกลับที่คุณได้รับ?
Mausimo

22

คุณได้คำตอบส่วนใหญ่ผ่านตัวอย่างโค้ดด้านบนแล้ว การใช้การดำเนินการแบบอะซิงโครนัส IO เป็นวิธีที่จะไปที่นี่อย่างแน่นอน Async IO เป็นวิธีที่ Win32 ได้รับการออกแบบภายใน ประสิทธิภาพที่ดีที่สุดเท่าที่จะเป็นไปได้คือการใช้พอร์ตที่สมบูรณ์โดยการเชื่อมซ็อกเก็ตของคุณกับพอร์ตที่เสร็จสมบูรณ์และมีเธรดพูลรอพอร์ตที่เสร็จสมบูรณ์ ภูมิปัญญาทั่วไปคือมี 2-4 เธรดต่อ CPU (หลัก) รอให้เสร็จ ฉันขอแนะนำให้อ่านบทความทั้งสามนี้โดย Rick Vicik จากทีม Windows Performance:

  1. การออกแบบแอพพลิเคชั่นเพื่อประสิทธิภาพ - ตอนที่ 1
  2. การออกแบบแอพพลิเคชั่นเพื่อประสิทธิภาพ - ตอนที่ 2
  3. การออกแบบแอพพลิเคชั่นเพื่อประสิทธิภาพ - ส่วนที่ 3

บทความดังกล่าวครอบคลุมส่วนใหญ่เป็น Windows API ดั้งเดิม แต่เป็นสิ่งที่ต้องอ่านสำหรับทุกคนที่พยายามเข้าใจถึงความสามารถในการปรับขนาดและประสิทธิภาพ พวกเขามีกางเกงในด้านการจัดการของสิ่งต่าง ๆ เช่นกัน

สิ่งที่สองที่คุณต้องทำคือให้แน่ใจว่าคุณได้อ่านหนังสือการปรับปรุง. NET Application Performance และ Scalabilityซึ่งมีให้ทางออนไลน์ คุณจะพบคำแนะนำที่เกี่ยวข้องและถูกต้องเกี่ยวกับการใช้เธรดการโทรและการล็อกแบบอะซิงโครนัสในบทที่ 5 แต่อัญมณีที่แท้จริงอยู่ในบทที่ 17 ที่คุณจะพบสินค้าเช่นคำแนะนำการปฏิบัติในการปรับพูลเธรด แอพของฉันมีปัญหาร้ายแรงบางอย่างจนกว่าฉันจะปรับ maxIothreads / maxWorkerThreads ตามคำแนะนำในบทนี้

คุณบอกว่าคุณต้องการที่จะทำเซิร์ฟเวอร์ TCP บริสุทธิ์ดังนั้นจุดต่อไปของฉันคือเก๊ แต่ถ้าคุณพบว่าตัวเองจนตรอกและใช้ WebRequest คลาสและอนุพันธ์ของมันได้รับการเตือนว่ามีมังกรเฝ้าประตูว่าที่: ServicePointManager นี่คือคลาสการกำหนดค่าที่มีจุดประสงค์เดียวในชีวิต: เพื่อทำลายประสิทธิภาพของคุณ ตรวจสอบให้แน่ใจว่าคุณปล่อยเซิร์ฟเวอร์ของคุณจาก ServicePoint.ConnectionLimit ที่สร้างขึ้นโดยไม่ได้ตั้งใจหรือแอปพลิเคชันของคุณจะไม่ขยายขนาด (ฉันให้คุณค้นพบด้วยตนเองว่าค่าเริ่มต้นคืออะไร ... ) นอกจากนี้คุณยังสามารถทบทวนนโยบายเริ่มต้นของการส่งส่วนหัว Expect100Continue ในคำขอ http

ตอนนี้เกี่ยวกับ core socket ที่จัดการ API นั้นค่อนข้างง่ายในฝั่ง Send แต่มันซับซ้อนกว่าในด้าน Receive เพื่อให้ได้ปริมาณงานและขนาดที่สูงคุณต้องมั่นใจว่าซ็อกเก็ตนั้นไม่ได้ควบคุมการไหลเนื่องจากคุณไม่มีบัฟเฟอร์ที่โพสต์เพื่อรับ เพื่อให้มีประสิทธิภาพสูงคุณควรโพสต์บัฟเฟอร์ 3-4 ล่วงหน้าและโพสต์บัฟเฟอร์ใหม่ทันทีที่คุณได้รับหนึ่งคืน ( ก่อนที่คุณจะประมวลผลได้รับคืน) ดังนั้นคุณจึงมั่นใจได้ว่าซ็อกเก็ตมีที่ใดที่หนึ่งเพื่อฝากข้อมูลที่มาจากเครือข่าย คุณจะเห็นว่าทำไมคุณอาจไม่สามารถทำได้ในไม่ช้า

หลังจากที่คุณเล่นกับ BeginRead / BeginWrite API และเริ่มงานอย่างจริงจังคุณจะรู้ว่าคุณต้องการความปลอดภัยในการรับส่งข้อมูลของคุณเช่น การรับรองความถูกต้อง NTLM / Kerberos และการเข้ารหัสปริมาณข้อมูลหรือการป้องกันการรับส่งข้อมูลอย่างน้อยที่สุด วิธีที่คุณทำคือใช้ตัวสร้างใน System.Net.Security.NegotiateStream (หรือ SslStream หากคุณต้องการข้ามโดเมนที่แตกต่างกัน) ซึ่งหมายความว่าแทนที่จะอาศัยการดำเนินการแบบอะซิงโครนัสซ็อกเก็ตแบบตรงคุณจะต้องใช้การดำเนินการแบบอะซิงโครนัส AuthenticatedStream ทันทีที่คุณได้รับซ็อกเก็ต (จากการเชื่อมต่อบนไคลเอ็นต์หรือจากการยอมรับบนเซิร์ฟเวอร์) คุณจะสร้างสตรีมบนซ็อกเก็ตและส่งเพื่อการตรวจสอบความถูกต้องโดยการเรียก BeginAuthenticateAsClient หรือ BeginAuthenticateAsServer หลังจากการรับรองความถูกต้องเสร็จสมบูรณ์ (อย่างน้อยคุณปลอดภัยจากความบ้าคลั่ง InitiateSecurityContext / AcceptSecurityContext ดั้งเดิม ... ) คุณจะทำการอนุญาตโดยการตรวจสอบคุณสมบัติ RemoteIdentity ของสตรีมที่ผ่านการรับรองความถูกต้องของคุณและทำการตรวจสอบ ACL ที่ผลิตภัณฑ์ของคุณต้องสนับสนุน หลังจากนั้นคุณจะส่งข้อความโดยใช้ BeginWrite และคุณจะได้รับข้อความเหล่านั้นด้วย BeginRead นี่เป็นปัญหาที่ฉันพูดก่อนหน้านี้ว่าคุณจะไม่สามารถโพสต์บัฟเฟอร์การรับหลายอันเนื่องจากคลาส AuthenticateStream ไม่สนับสนุนสิ่งนี้ การดำเนินการ BeginRead จัดการ IO ภายในทั้งหมดจนกว่าคุณจะได้รับเฟรมทั้งหมดมิฉะนั้นจะไม่สามารถจัดการการตรวจสอบข้อความ (ถอดรหัสเฟรมและตรวจสอบลายเซ็นบนเฟรม) แม้ว่าในประสบการณ์ของฉันงานที่ทำโดยคลาส AuthenticatedStream ค่อนข้างดีและไม่ควรมีปัญหาใด ๆ กล่าวคือ คุณควรจะสามารถทำให้เครือข่าย GB อิ่มตัวด้วย CPU เพียง 4-5% คลาส AuthenticatedStream จะกำหนดข้อ จำกัด ขนาดเฟรมเฉพาะให้กับคุณ (16k สำหรับ SSL, 12k สำหรับ Kerberos)

สิ่งนี้จะช่วยให้คุณเริ่มต้นในเส้นทางที่ถูกต้อง ฉันจะไม่โพสต์รหัสที่นี่มีตัวอย่างที่ดีได้อย่างสมบูรณ์แบบใน MSDN ฉันทำโครงการหลายอย่างเช่นนี้และฉันสามารถเพิ่มจำนวนผู้ใช้งานที่เชื่อมต่อได้ถึง 1,000 คนโดยไม่มีปัญหา ด้านบนนั้นคุณจะต้องแก้ไขรีจิสตรีคีย์เพื่อให้เคอร์เนลจัดการซ็อกเก็ตได้มากขึ้น และให้แน่ใจว่าคุณปรับใช้บนเซิร์ฟเวอร์ระบบปฏิบัติการนั่นคือ W2K3 ไม่ใช่ XP หรือ Vista (เช่นไคลเอนต์ระบบปฏิบัติการ) มันสร้างความแตกต่างอย่างมาก

BTW ตรวจสอบให้แน่ใจว่าคุณมีฐานข้อมูลการดำเนินงานบนเซิร์ฟเวอร์หรือไฟล์ IO คุณยังใช้รสชาติ async สำหรับพวกเขาหรือคุณจะระบายสระเธรดในเวลาไม่นาน สำหรับการเชื่อมต่อ SQL Server ตรวจสอบให้แน่ใจว่าคุณเพิ่ม 'Asyncronous Processing = true' เข้ากับสตริงการเชื่อมต่อ


มีข้อมูลที่ดีที่นี่ ฉันหวังว่าฉันจะได้รับรางวัลหลายคนจากความโปรดปราน อย่างไรก็ตามฉันได้โหวตคุณแล้ว สิ่งที่ดีที่นี่ขอบคุณ
Erik Funkenbusch

11

ฉันมีเซิร์ฟเวอร์ที่ทำงานในโซลูชันบางอย่างของฉัน นี่คือคำอธิบายโดยละเอียดเกี่ยวกับวิธีต่างๆใน. net: เข้า ใกล้สายกับซ็อกเก็ตประสิทธิภาพสูงใน. NET

เมื่อเร็ว ๆ นี้ฉันกำลังมองหาวิธีในการปรับปรุงรหัสของเราและจะพิจารณาในเรื่องนี้: " การปรับปรุงประสิทธิภาพซ็อกเก็ตในเวอร์ชัน 3.5 " ที่รวมไว้เป็นพิเศษ "สำหรับการใช้งานโดยแอปพลิเคชันที่ใช้ I / O เครือข่ายอะซิงโครนัส

"คุณสมบัติหลักของการปรับปรุงเหล่านี้คือการหลีกเลี่ยงการจัดสรรซ้ำและการซิงโครไนซ์วัตถุระหว่างซ็อกเก็ตอะซิงโครนัสปริมาณสูง I / O รูปแบบการออกแบบ Begin / End ที่ใช้งานในปัจจุบัน มีการจัดสรรวัตถุ IAsyncResult สำหรับแต่ละการดำเนินการซ็อกเก็ตแบบอะซิงโครนัส "

คุณสามารถอ่านต่อไปหากคุณไปที่ลิงค์ ฉันเองจะทำการทดสอบโค้ดตัวอย่างของพวกเขาในวันพรุ่งนี้เพื่อเปรียบเทียบกับสิ่งที่ฉันได้รับ

แก้ไข: ที่ นี่คุณสามารถค้นหารหัสการทำงานสำหรับทั้งไคลเอนต์และเซิร์ฟเวอร์โดยใช้ 3.5 SocketAsyncEventArgs ใหม่เพื่อให้คุณสามารถทดสอบได้ภายในไม่กี่นาทีและผ่านรหัส มันเป็นวิธีที่ง่าย แต่เป็นพื้นฐานสำหรับการเริ่มต้นการใช้งานที่มีขนาดใหญ่กว่ามาก นอกจากนี้บทความจากเกือบสองปีที่ผ่านมาในนิตยสาร MSDN เป็นที่น่าสนใจอ่าน



9

คุณได้พิจารณาเพียงแค่ใช้การเชื่อม TCP สุทธิ TCPF และรูปแบบการเผยแพร่ / สมัครสมาชิก? WCF จะช่วยให้คุณสามารถมุ่งเน้น [ส่วนใหญ่] ในโดเมนของคุณแทนการประปา ..

มีตัวอย่าง WCF มากมายและแม้แต่เฟรมเวิร์กการเผยแพร่ / สมัครสมาชิกที่มีอยู่ในส่วนการดาวน์โหลดของ IDesign ซึ่งอาจมีประโยชน์: http://www.idesign.net


8

ฉันสงสัยเกี่ยวกับสิ่งหนึ่ง:

แน่นอนฉันไม่ต้องการเริ่มหัวข้อสำหรับแต่ละการเชื่อมต่อ

ทำไมถึงเป็นอย่างนั้น? Windows สามารถจัดการเธรดนับร้อยในแอปพลิเคชันตั้งแต่อย่างน้อย Windows 2000 ฉันได้ทำมันเป็นเรื่องง่ายที่จะทำงานด้วยถ้าไม่จำเป็นต้องซิงโครไนซ์เธรด โดยเฉพาะอย่างยิ่งเมื่อคุณทำ I / O จำนวนมาก (ดังนั้นคุณจึงไม่ได้เชื่อมโยงกับ CPU และเธรดจำนวนมากจะถูกบล็อกบนการสื่อสารดิสก์หรือเครือข่าย) ฉันไม่เข้าใจข้อ จำกัด นี้

คุณได้ทดสอบวิธีแบบมัลติเธรดและพบว่ามันขาดอะไรหรือเปล่า? คุณตั้งใจจะมีการเชื่อมต่อฐานข้อมูลสำหรับแต่ละเธรด (ที่จะฆ่าเซิร์ฟเวอร์ฐานข้อมูลดังนั้นจึงเป็นความคิดที่ไม่ดี แต่แก้ไขได้อย่างง่ายดายด้วยการออกแบบ 3 ระดับ) คุณเป็นห่วงว่าคุณจะมีลูกค้าหลายพันคนแทนที่จะเป็นร้อยแล้วคุณจะมีปัญหาจริงๆหรือ (แม้ว่าฉันจะลองหนึ่งพันเธรดหรือแม้กระทั่งหมื่นถ้าฉันมี 32+ GB ของ RAM - อีกครั้งเนื่องจากคุณไม่ได้ผูก CPU เวลาเปลี่ยนเธรดควรไม่เกี่ยวข้องอย่างแน่นอน)

นี่คือรหัส - เพื่อดูว่ามันทำงานอย่างไรไปที่http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.htmlแล้วคลิกที่ภาพ

คลาสเซิร์ฟเวอร์:

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

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

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

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

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

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

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

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

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

โปรแกรมหลักของเซิร์ฟเวอร์:

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

ระดับลูกค้า:

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

      var msg = new byte[1024];

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

โปรแกรมหลักของลูกค้า:

using System;
using System.Threading;

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

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

Windows สามารถจัดการเธรดได้มากมาย แต่. NET ไม่ได้ออกแบบมาเพื่อจัดการเธรดเหล่านั้นจริงๆ แต่ละ. NET appdomain มีเธรดพูลและคุณไม่ต้องการใช้พูลเธรดนั้น ฉันไม่แน่ใจว่าคุณเริ่มกระทู้ด้วยตนเองถ้ามันมาจากเธรดพูลหรือไม่ ถึงกระนั้นหลายร้อยกระทู้ที่ไม่ได้ทำอะไรเลยส่วนใหญ่เป็นการสูญเสียทรัพยากรจำนวนมาก
Erik Funkenbusch

1
ฉันเชื่อว่าคุณมีมุมมองที่ไม่ถูกต้องของหัวข้อ เธรดมาจากเธรดพูลหากคุณต้องการเท่านั้น - เธรดปกติไม่ได้ หลายร้อยกระทู้ที่ไม่ทำอะไรเลยไม่ต้องเสียอะไรเลย :) (หน่วยความจำน้อย แต่หน่วยความจำราคาถูกมากมันไม่ได้เป็นปัญหาอีกต่อไปแล้ว) ฉันจะเขียนแอพตัวอย่างสองสามตัวสำหรับเรื่องนี้ฉันจะโพสต์ URL ไปที่ เมื่อฉันทำเสร็จแล้ว ในระหว่างนี้ฉันขอแนะนำให้คุณลองอ่านสิ่งที่ฉันเขียนไว้ด้านบนอีกครั้งและลองตอบคำถามของฉัน
Marcel Popescu

1
ในขณะที่ฉันเห็นด้วยกับความคิดเห็นของ Marcel เกี่ยวกับมุมมองของเธรดในเธรดที่สร้างขึ้นนั้นไม่ได้มาจากเธรดพูลส่วนที่เหลือของคำสั่งนั้นไม่ถูกต้อง หน่วยความจำไม่ได้เกี่ยวกับจำนวนการติดตั้งในเครื่องแอปพลิเคชันทั้งหมดบน windows ทำงานในพื้นที่ที่อยู่เสมือนและในระบบ 32 บิตที่ให้ข้อมูล 2GB สำหรับแอปของคุณ (ไม่สำคัญว่า RAM ติดตั้งบนกล่องเท่าใด) พวกเขายังคงต้องได้รับการจัดการโดยรันไทม์ การทำ async IO ไม่ได้ใช้เธรดเพื่อรอ (ใช้ IOCP ซึ่งทำให้ IO ที่ทับซ้อนกัน) และเป็นทางออกที่ดีกว่าและจะขยายขนาดได้ดีขึ้น
Brian ONeil

7
เมื่อใช้เธรดจำนวนมากไม่ใช่หน่วยความจำที่เป็นปัญหา แต่เป็น CPU การสลับบริบทระหว่างเธรดเป็นการดำเนินการที่ค่อนข้างแพงและยิ่งเธรดที่แอ็คทีฟมากขึ้นคุณจะมีการสลับบริบทเพิ่มเติมที่จะเกิดขึ้น ไม่กี่ปีที่ผ่านมาฉันทำการทดสอบบนพีซีด้วยแอพคอนโซล C # และประมาณ 500 เธรด CPU ของฉันคือ 100% เธรดไม่ได้ทำอะไรที่สำคัญ สำหรับเครือข่าย comms จะดีกว่าเพื่อให้จำนวนกระทู้ลง
sipwiz

1
ฉันจะไปกับโซลูชันงานหรือใช้ async / คอย วิธีแก้ปัญหาดูเหมือนง่ายกว่าในขณะที่ async / await มีแนวโน้มที่ปรับขนาดได้มากกว่า
Marcel Popescu

5

การใช้ Async IO ( BeginReadฯลฯ ) ของ. NET เป็นความคิดที่ดีถ้าคุณสามารถรับรายละเอียดทั้งหมดได้อย่างถูกต้อง เมื่อคุณตั้งค่าการจัดการซ็อกเก็ต / ไฟล์อย่างถูกต้องจะใช้ IOCP พื้นฐานของระบบปฏิบัติการอนุญาตให้การดำเนินการของคุณเสร็จสมบูรณ์โดยไม่ต้องใช้เธรดใด ๆ (หรือในกรณีที่แย่ที่สุดโดยใช้เธรดที่ฉันเชื่อว่ามาจาก IO thread pool ของเคอร์เนลแทน ของเธรดพูลของ. NET ซึ่งช่วยบรรเทาความแออัดของเธรดพูล)

gotcha หลักคือเพื่อให้แน่ใจว่าคุณเปิดซ็อกเก็ต / ไฟล์ของคุณในโหมดที่ไม่ปิดกั้น ฟังก์ชันความสะดวกสบายเริ่มต้นส่วนใหญ่ (เช่นFile.OpenRead) ไม่ทำเช่นนี้ดังนั้นคุณจะต้องเขียนด้วยตัวคุณเอง

ข้อกังวลหลักอีกข้อหนึ่งคือการจัดการข้อผิดพลาด - การจัดการข้อผิดพลาดอย่างถูกต้องเมื่อเขียนรหัส I / O แบบอะซิงโครนัสนั้นมากยากกว่าการทำในรหัสซิงโครนัส นอกจากนี้ยังง่ายมากที่จะจบลงด้วยสภาพการแข่งขันและการหยุดชะงักแม้ว่าคุณอาจไม่ได้ใช้เธรดโดยตรงดังนั้นคุณต้องระวังสิ่งนี้

ถ้าเป็นไปได้คุณควรลองใช้ไลบรารีอำนวยความสะดวกเพื่อลดขั้นตอนในการทำ IO แบบอะซิงโครนัสที่ปรับขนาดได้

Concurrency Coordination Runtimeของ Microsoft เป็นตัวอย่างหนึ่งของ. NET library ที่ออกแบบมาเพื่อลดความยุ่งยากในการเขียนโปรแกรมประเภทนี้ มันดูดี แต่เมื่อฉันไม่ได้ใช้มันฉันไม่สามารถแสดงความคิดเห็นได้ว่ามันจะปรับขนาดได้ดีแค่ไหน

สำหรับโครงการส่วนบุคคลของฉันที่จำเป็นในการทำเครือข่ายตรงกันหรือดิสก์ I / O ที่ผมใช้ชุดของ .NET เห็นพ้อง / เครื่องมือ IO ที่ผมได้สร้างขึ้นในช่วงปีที่ผ่านมาเรียกว่าSquared.Task มันได้รับแรงบันดาลใจจากห้องสมุดเช่นimvu.taskและtwistedและฉันได้รวมตัวอย่างการทำงานไว้ในที่เก็บที่ทำ I / O เครือข่าย ฉันยังใช้มันในแอปพลิเคชั่นไม่กี่ตัวที่ฉันเขียน - NDexer ที่ใหญ่ที่สุดที่เผยแพร่สู่สาธารณะ(ซึ่งใช้สำหรับดิสก์ที่ไม่มีเธรด I / O) ห้องสมุดเขียนขึ้นจากประสบการณ์ของฉันกับ imvu.task และมีชุดการทดสอบหน่วยที่ครอบคลุมพอสมควรดังนั้นฉันขอแนะนำให้คุณลองใช้ หากคุณมีปัญหาใด ๆ ฉันยินดีที่จะให้ความช่วยเหลือแก่คุณ

ในความคิดของฉันตามประสบการณ์ของฉันโดยใช้ asynchronous / threadless IO แทน thread เป็นความพยายามที่คุ้มค่าบนแพลตฟอร์ม. NET ตราบใดที่คุณพร้อมที่จะจัดการกับช่วงการเรียนรู้ ช่วยให้คุณหลีกเลี่ยงความยุ่งยากในการปรับขนาดที่กำหนดโดยต้นทุนของเธรดเธรดและในหลาย ๆ กรณีคุณสามารถหลีกเลี่ยงการใช้การล็อกและ mutexes ได้อย่างสมบูรณ์


ข้อมูลที่ดีฉันจะตรวจสอบการอ้างอิงของคุณและดูว่ามีเหตุผลอะไร
Erik Funkenbusch

3

ฉันใช้โซลูชันของ Kevin แต่เขาบอกว่าโซลูชันไม่มีรหัสสำหรับประกอบข้อความใหม่ นักพัฒนาสามารถใช้รหัสนี้เพื่อประกอบข้อความอีกครั้ง:

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

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

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

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

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

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

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

    ContinueReading(cInfo);
 }

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

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

    public Socket Soket { get; set; }

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

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

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

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

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

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

        }

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

}


1

คุณสามารถลองใช้เฟรมเวิร์กที่เรียกว่า ACE (Adaptive Communications Environment) ซึ่งเป็นเฟรมเวิร์ก C ++ ทั่วไปสำหรับเซิร์ฟเวอร์เครือข่าย มันเป็นผลิตภัณฑ์ที่แข็งแกร่งและเป็นผู้ใหญ่มากและได้รับการออกแบบมาเพื่อรองรับแอปพลิเคชันที่มีความน่าเชื่อถือสูงและมีปริมาณมากถึงระดับเทลโก

เฟรมเวิร์กเกี่ยวข้องกับโมเดลการทำงานพร้อมกันที่หลากหลายและอาจมีหนึ่งที่เหมาะสมสำหรับการคัดค้านของคุณออกจากกล่อง สิ่งนี้จะทำให้ระบบง่ายต่อการตรวจแก้จุดบกพร่องเนื่องจากปัญหาการทำงานพร้อมกันที่น่ารังเกียจส่วนใหญ่ได้ถูกแยกออกแล้ว การแลกเปลี่ยนที่นี่คือกรอบการเขียนใน C ++ และไม่อบอุ่นและปุยฐานรหัส ในอีกทางหนึ่งคุณจะได้รับการทดสอบโครงสร้างพื้นฐานเครือข่ายระดับอุตสาหกรรมและสถาปัตยกรรมที่ปรับขนาดได้อย่างรวดเร็ว


2
นั่นเป็นข้อเสนอแนะที่ดี แต่จากแท็กของคำถามที่ฉันเชื่อว่า OP จะใช้ C #
JPCosta

ฉันสังเกตว่า; ข้อเสนอแนะคือว่านี้สามารถใช้ได้สำหรับ C ++ และฉันไม่ได้ตระหนักถึงสิ่งที่เทียบเท่ากับ C # การดีบักระบบประเภทนี้ไม่ใช่เรื่องง่ายในช่วงเวลาที่ดีที่สุดและคุณอาจได้รับผลตอบแทนจากการไปที่เฟรมเวิร์กนี้แม้ว่าจะหมายถึงการเปลี่ยนเป็น C ++
ConcOfOfTunbridgeWells

ใช่นี่คือ C # ฉันกำลังมองหาโซลูชันที่ใช้. net ฉันควรจะได้รับความชัดเจนมากขึ้น แต่ผมถือว่าคนที่จะอ่านแท็ก
เอริค Funkenbusch

1

ฉันจะใช้SEDAหรือไลบรารีเธรดที่มีน้ำหนักเบา (erlang หรือ linux ที่ใหม่กว่าดูความสามารถในการขยาย NTPL ทางฝั่งเซิร์ฟเวอร์ ) การเข้ารหัส Async ยุ่งยากมากหากการสื่อสารของคุณไม่ใช่ :)


1

ซ็อกเก็ต. NET ดูเหมือนจะให้เลือก () - ซึ่งดีที่สุดสำหรับการจัดการอินพุต สำหรับผลลัพธ์ฉันมีกลุ่มของตัวเขียนซ็อกเก็ตที่ฟังในคิวงานยอมรับตัวอธิบายซ็อกเก็ต / วัตถุเป็นส่วนหนึ่งของรายการงานดังนั้นคุณไม่จำเป็นต้องใช้เธรดต่อซ็อกเก็ต


1

ฉันจะใช้เมธอด AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync ที่เพิ่มใน. Net 3.5 ฉันได้ทำการวัดประสิทธิภาพแล้วและเร็วกว่าประมาณ 35% (เวลาตอบสนองและบิตเรต) โดยมีผู้ใช้ 100 คนที่รับส่งข้อมูลอย่างต่อเนื่อง


1

สำหรับผู้ที่คัดลอกการวางคำตอบที่ยอมรับคุณสามารถเขียนเมธอด acceptCallback ได้อีกครั้งโดยลบการโทรทั้งหมดของ _serverSocket.BeginAccept (AsyncCallback ใหม่ (acceptCallback), _serverSocket); และวางไว้ในประโยคสุดท้าย {} ด้วยวิธีนี้:

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

คุณสามารถลบการจับครั้งแรกได้เนื่องจากเนื้อหาเหมือนกัน แต่เป็นวิธีการของแม่แบบและคุณควรใช้ข้อยกเว้นที่พิมพ์เพื่อจัดการกับข้อยกเว้นและเข้าใจสิ่งที่ทำให้เกิดข้อผิดพลาดได้ดีขึ้นดังนั้นเพียงใช้การจับเหล่านั้นด้วยรหัสที่มีประโยชน์


0

ฉันอยากจะแนะนำให้อ่านหนังสือเหล่านี้ใน ACE

เพื่อรับแนวคิดเกี่ยวกับรูปแบบที่ช่วยให้คุณสร้างเซิร์ฟเวอร์ที่มีประสิทธิภาพ

แม้ว่า ACE จะถูกนำมาใช้ใน C ++ หนังสือครอบคลุมรูปแบบที่มีประโยชน์มากมายที่สามารถใช้ในภาษาการเขียนโปรแกรมใด ๆ


-1

เพื่อความชัดเจนฉันกำลังมองหา. based based solutions (C # ถ้าเป็นไปได้ แต่. net ภาษาใด ๆ จะทำงาน)

คุณจะไม่ได้รับความยืดหยุ่นในระดับสูงสุดหากคุณใช้. NET เพียงอย่างเดียว การหยุด GC ชั่วคราวสามารถขัดขวางความหน่วง

ฉันจะต้องเริ่มต้นอย่างน้อยหนึ่งเธรดสำหรับบริการ ฉันกำลังพิจารณาใช้ Asynch API (BeginRecieve และอื่น ๆ ) เนื่องจากฉันไม่ทราบว่ามีลูกค้ากี่รายที่ฉันจะเชื่อมต่อในเวลาใดก็ตาม (อาจเป็นร้อย) แน่นอนฉันไม่ต้องการเริ่มหัวข้อสำหรับแต่ละการเชื่อมต่อ

โดยทั่วไปแล้วIO ที่ทับซ้อนกันจะถือว่าเป็น API ที่เร็วที่สุดของ Windows สำหรับการสื่อสารเครือข่าย ฉันไม่รู้ว่านี่เหมือนกับ Asynch API ของคุณหรือไม่ อย่าใช้การเลือกเนื่องจากการโทรแต่ละครั้งจะต้องตรวจสอบทุกซ็อกเก็ตที่เปิดอยู่แทนที่จะมีการโทรกลับบนซ็อกเก็ตที่ใช้งานอยู่


1
ฉันไม่เข้าใจความคิดเห็นเกี่ยวกับการหยุดชั่วคราวของ GC ของคุณ .. ฉันไม่เคยเห็นระบบที่มีปัญหาในการปรับขนาดที่เกี่ยวข้องโดยตรงกับ GC
markt

4
มีความเป็นไปได้มากกว่าที่คุณจะสร้างแอปที่ไม่สามารถปรับขนาดได้เนื่องจากสถาปัตยกรรมที่ไม่ดีกว่าเพราะมี GC อยู่ ระบบขนาดใหญ่ที่ปรับขนาดได้ + performant ถูกสร้างขึ้นด้วย. NET และ Java ในลิงก์ทั้งสองที่คุณให้ไว้สาเหตุไม่ได้เป็นการรวบรวมขยะโดยตรง แต่เกี่ยวข้องกับการแลกเปลี่ยนฮีป ฉันสงสัยว่ามันเป็นปัญหาของสถาปัตยกรรมที่สามารถหลีกเลี่ยงได้จริง ๆ ถ้าคุณสามารถแสดงภาษาที่ไม่สามารถสร้างระบบที่ไม่สามารถปรับขนาดได้ฉันจะใช้มันด้วยความยินดี;)
markt

1
ฉันไม่เห็นด้วยกับความคิดเห็นนี้ ไม่ทราบคำถามที่คุณอ้างอิงคือ Java และพวกเขากำลังจัดการกับการจัดสรรหน่วยความจำขนาดใหญ่โดยเฉพาะและพยายามบังคับ gc ด้วยตนเอง ฉันจะไม่ได้รับการจัดสรรหน่วยความจำจำนวนมากที่นี่ นี่ไม่ใช่ปัญหา แต่ขอบคุณ. ใช่รูปแบบการเขียนโปรแกรมแบบอะซิงโครนัสมักจะถูกนำไปใช้ด้านบนของ Overlapped IO
Erik Funkenbusch

1
ที่จริงแล้ววิธีปฏิบัติที่ดีที่สุดคือไม่ให้บังคับให้ GC รวบรวมด้วยตนเองตลอดเวลา นี่อาจทำให้แอปของคุณทำงานแย่ลง .NET GC เป็น GC generational ที่จะปรับการใช้งานแอปของคุณ จริงๆถ้าคุณคิดว่าคุณจะต้องมีการเรียก GC.Collect ตนเองฉันจะบอกว่ารหัสความต้องการของคุณได้มากที่สุดที่จะเขียนอีกทางหนึ่ง ..
Markt

1
@markt นั่นเป็นความคิดเห็นสำหรับคนที่ไม่รู้อะไรเลยเกี่ยวกับการเก็บขยะ หากคุณมีเวลาว่างไม่มีอะไรผิดปกติในการทำคอลเลกชันด้วยตนเอง มันจะไม่ทำให้ใบสมัครของคุณแย่ลงเมื่อเสร็จสิ้น เอกสารวิชาการแสดงว่า generational GCs ทำงานเพราะมันประมาณอายุการใช้งานของวัตถุของคุณ เห็นได้ชัดว่านี่ไม่ใช่ตัวแทนที่สมบูรณ์แบบ ในความเป็นจริงมีความขัดแย้งที่คนรุ่นเก่ามักจะมีอัตราส่วนของขยะสูงที่สุดเพราะไม่เคยเก็บขยะ
ไม่ทราบ

-1

คุณสามารถใช้เฟรมเวิร์กโอเพนซอร์ส Push Framework สำหรับการพัฒนาเซิร์ฟเวอร์ประสิทธิภาพสูง มันถูกสร้างขึ้นบน IOCP และเหมาะสำหรับสถานการณ์การผลักดันและการออกอากาศข้อความ

http://www.pushframework.com


1
โพสต์นี้ถูกติดแท็ก C # และ. net ทำไมคุณถึงแนะนำ C ++ framework
Erik Funkenbusch

อาจเป็นเพราะเขาเขียนมัน potatosoftware.com/…
quillbreaker

pushframework รองรับอินสแตนซ์ของเซิร์ฟเวอร์หลายตัวหรือไม่ ถ้าไม่ขนาดมันเป็นอย่างไร
esskar
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.