2009-10-08 41 views
7

Tôi muốn tạo một máy chủ socket không đồng bộ bằng cách sử dụng sự kiện SocketAsyncEventArgs.Thiết kế máy chủ bằng cách sử dụng SocketAsyncEventArgs

Máy chủ sẽ quản lý khoảng 1000 kết nối cùng một lúc. Cách tốt nhất để xử lý logic cho mỗi gói là gì?

Thiết kế máy chủ dựa trên this MSDN example, vì vậy mọi ổ cắm sẽ có SocketAsyncEventArgs của riêng mình để nhận dữ liệu.

  1. Thực hiện công cụ logic bên trong chức năng nhận. Không có phí sẽ được tạo, nhưng vì cuộc gọi ReceiveAsync() tiếp theo sẽ không được thực hiện trước khi logic hoàn tất, không thể đọc dữ liệu mới từ ổ cắm. Hai câu hỏi chính của tôi là: Nếu khách hàng gửi rất nhiều dữ liệu và xử lý logic nặng, hệ thống sẽ xử lý nó như thế nào (các gói bị mất vì bộ đệm đầy)? Ngoài ra, nếu tất cả khách hàng gửi dữ liệu cùng một lúc, sẽ có 1000 luồng hoặc có giới hạn nội bộ và chuỗi mới không thể bắt đầu trước khi một chuỗi khác hoàn tất việc thực thi không?

  2. Sử dụng hàng đợi. Chức năng nhận sẽ rất ngắn và thực thi nhanh, nhưng bạn sẽ có chi phí khá cao vì hàng đợi. Vấn đề là, nếu chủ đề công nhân của bạn không đủ nhanh dưới tải máy chủ nặng, hàng đợi của bạn có thể nhận được đầy đủ, vì vậy có thể bạn phải ép buộc gói tin. Bạn cũng gặp vấn đề về Nhà sản xuất/Người tiêu dùng, điều này có thể làm chậm toàn bộ hàng đợi với nhiều khóa.

Vì vậy, thiết kế, logic trong chức năng nhận, logic trong chủ đề công nhân hay bất cứ điều gì hoàn toàn khác tôi đã bỏ lỡ cho đến nay.

Một nhiệm vụ khác liên quan đến gửi dữ liệu. Có tốt hơn nếu có một SocketAsyncEventArgs gắn với một ổ cắm (tương tự như sự kiện nhận) và sử dụng một hệ thống đệm để thực hiện một cuộc gọi gửi cho một vài gói nhỏ (giả sử các gói khác đôi khi gửi trực tiếp một gói sau khi gửi đi.). khác) hoặc sử dụng một SocketAsyncEventArgs khác nhau cho mỗi gói và lưu trữ chúng trong một hồ bơi để tái sử dụng chúng?

Trả lời

10

Để thực thi hiệu quả các ổ cắm không đồng bộ, mỗi socket sẽ cần nhiều hơn 1 SocketAsyncEventArgs. Ngoài ra còn có một vấn đề với bộ đệm byte [] trong mỗi SocketAsyncEventArgs. Tóm lại, các bộ đệm byte sẽ được ghim bất cứ khi nào một chuyển đổi được quản lý - bản địa xảy ra (gửi/nhận). Nếu bạn phân bổ bộ đệm SocketAsyncEventArgs và byte khi cần thiết, bạn có thể chạy vào OutOfMemoryExceptions với nhiều máy khách do phân đoạn và không có khả năng của GC để nén bộ nhớ nhỏ gọn.

Cách tốt nhất để xử lý việc này là tạo một lớp SocketBufferPool sẽ phân bổ một số lượng lớn byte và SocketAsyncEventArgs khi ứng dụng được khởi động lần đầu, cách này bộ nhớ được ghim sẽ tiếp giáp. Sau đó, chỉ đơn giản là tái sử dụng các bộ đệm từ hồ bơi khi cần thiết.

Trong thực tế, tôi đã tìm thấy nó tốt nhất để tạo ra một lớp bao bọc xung quanh SocketAsyncEventArgs và một lớp SocketBufferPool để quản lý việc phân phối tài nguyên.

Như một ví dụ, đây là mã cho một phương pháp BeginReceive:

private void BeginReceive(Socket socket) 
    { 
     Contract.Requires(socket != null, "socket"); 

     SocketEventArgs e = SocketBufferPool.Instance.Alloc(); 
     e.Socket = socket; 
     e.Completed += new EventHandler<SocketEventArgs>(this.HandleIOCompleted); 

     if (!socket.ReceiveAsync(e.AsyncEventArgs)) { 
      this.HandleIOCompleted(null, e); 
     } 
    } 

Và đây là phương pháp HandleIOCompleted:

private void HandleIOCompleted(object sender, SocketEventArgs e) 
    { 
     e.Completed -= this.HandleIOCompleted; 
     bool closed = false; 

     lock (this.sequenceLock) { 
      e.SequenceNumber = this.sequenceNumber++; 
     } 

     switch (e.LastOperation) { 
      case SocketAsyncOperation.Send: 
      case SocketAsyncOperation.SendPackets: 
      case SocketAsyncOperation.SendTo: 
       if (e.SocketError == SocketError.Success) { 
        this.OnDataSent(e); 
       } 
       break; 
      case SocketAsyncOperation.Receive: 
      case SocketAsyncOperation.ReceiveFrom: 
      case SocketAsyncOperation.ReceiveMessageFrom: 
       if ((e.BytesTransferred > 0) && (e.SocketError == SocketError.Success)) { 
        this.BeginReceive(e.Socket); 
        if (this.ReceiveTimeout > 0) { 
         this.SetReceiveTimeout(e.Socket); 
        } 
       } else { 
        closed = true; 
       } 

       if (e.SocketError == SocketError.Success) { 
        this.OnDataReceived(e); 
       } 
       break; 
      case SocketAsyncOperation.Disconnect: 
       closed = true; 
       break; 
      case SocketAsyncOperation.Accept: 
      case SocketAsyncOperation.Connect: 
      case SocketAsyncOperation.None: 
       break; 
     } 

     if (closed) { 
      this.HandleSocketClosed(e.Socket); 
     } 

     SocketBufferPool.Instance.Free(e); 
    } 

Đoạn mã trên được chứa trong một lớp học TcpSocket rằng sẽ nâng cao DataReceived & Sự kiện DataSent. Một điều cần lưu ý là trường hợp SocketAsyncOperation.ReceiveMessageFrom: block; nếu socket không có lỗi, nó sẽ bắt đầu ngay lập tức một BeginReceive() khác sẽ phân bổ một SocketEventArgs khác từ pool.

Một lưu ý quan trọng khác là thuộc tính SocketEventArgs SequenceNumber được đặt trong phương thức HandleIOComplete. Mặc dù các yêu cầu không đồng bộ sẽ hoàn thành theo thứ tự xếp hàng đợi, bạn vẫn phải tuân theo các điều kiện chủng tộc khác. Kể từ khi mã gọi BeginReceive trước khi nâng cao sự kiện DataReceived có khả năng là thread phục vụ IOCP gốc sẽ chặn sau khi gọi BeginReceive nhưng trước khi rasing sự kiện trong khi async thứ hai nhận được hoàn thành trên một luồng mới làm tăng sự kiện DataReceived trước. Mặc dù đây là một trường hợp khá hiếm có thể xảy ra và thuộc tính SequenceNumber cung cấp cho ứng dụng tiêu thụ khả năng đảm bảo rằng dữ liệu được xử lý theo đúng thứ tự.

Một khu vực khác cần lưu ý là không đồng bộ gửi. Thông thường, các yêu cầu gửi không đồng bộ sẽ hoàn thành đồng bộ (SendAsync sẽ trả về false nếu cuộc gọi hoàn thành đồng bộ) và có thể làm giảm hiệu suất nghiêm trọng. Các chi phí bổ sung của cuộc gọi async quay trở lại trên IOCP có thể trong thực tế gây ra hiệu suất tồi tệ hơn chỉ đơn giản bằng cách sử dụng cuộc gọi đồng bộ. Cuộc gọi async yêu cầu hai cuộc gọi hạt nhân và phân bổ đống trong khi cuộc gọi đồng bộ xảy ra trên ngăn xếp.

Hope this helps, Bill

-1

Trong code của bạn, bạn làm như sau:

if (!socket.ReceiveAsync(e.AsyncEventArgs)) { 
    this.HandleIOCompleted(null, e); 
} 

Nhưng đó là một lỗi để làm điều đó. Có một lý do tại sao cuộc gọi lại không được gọi khi nó kết thúc đồng bộ, hành động như vậy có thể lấp đầy ngăn xếp.

Hãy tưởng tượng rằng mỗi ReceiveAsync luôn quay trở lại đồng bộ. Nếu HandleIOCompleted của bạn trong một thời gian, bạn có thể xử lý kết quả trả về đồng bộ ở cùng một mức stack. Nếu nó không trả về đồng bộ, bạn sẽ phá vỡ thời gian. Nhưng, bằng cách thực hiện bạn bạn làm, bạn kết thúc việc tạo một mục mới trong ngăn xếp ... vì vậy nếu bạn có đủ may mắn, bạn sẽ gây ra các ngoại lệ tràn ngăn xếp.

Các vấn đề liên quan