2009-07-22 35 views
29

Tôi đã thấy nhiều tài nguyên ở đây về SO về Ổ cắm. Tôi tin rằng không ai trong số họ bao gồm các chi tiết mà tôi muốn biết. Trong ứng dụng của tôi, máy chủ thực hiện tất cả việc xử lý và gửi các bản cập nhật định kỳ cho các máy khách.Bắt đầu lập trình socket trong C# - Các phương pháp hay nhất

Mục đích của bài đăng này là bao gồm tất cả các ý tưởng cơ bản cần thiết khi phát triển ứng dụng socket và thảo luận các phương pháp hay nhất. Dưới đây là những điều cơ bản mà bạn sẽ thấy với hầu như tất cả các ứng dụng dựa trên socket.

1 - Binding và lắng nghe trên một ổ cắm

Tôi đang sử dụng đoạn mã sau. Nó hoạt động tốt trên máy của tôi. Tôi có cần phải quan tâm đến điều gì khác khi triển khai trên máy chủ thực không?

IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName()); 
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444); 

serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
        ProtocolType.Tcp); 
serverSocket.Bind(endPoint); 
serverSocket.Listen(10); 

2 - dữ liệu nhận

Tôi đã sử dụng một mảng byte kích thước 255. Vì vậy, khi tôi nhận dữ liệu lớn hơn 255 byte, tôi cần gọi phương thức nhận cho đến khi tôi nhận được dữ liệu đầy đủ, đúng không? Khi tôi nhận được dữ liệu đầy đủ, tôi cần phải nối thêm tất cả các byte nhận được cho đến nay để nhận được thông báo đầy đủ. Đúng không? Hoặc là có một cách tiếp cận tốt hơn?

3 - gửi dữ liệu và xác định độ dài dữ liệu

Kể từ khi không có cách nào trong giao thức TCP để tìm độ dài của tin nhắn để nhận, tôi đang lên kế hoạch để thêm chiều dài vào tin nhắn. Đây sẽ là byte đầu tiên của gói. Vì vậy, hệ thống khách hàng biết có bao nhiêu dữ liệu có sẵn để đọc.

Bất kỳ cách tiếp cận nào khác tốt hơn?

4 - Đóng client

Khi khách hàng được đóng lại, nó sẽ gửi một thông điệp tới máy chủ cho thấy sự gần gũi. Máy chủ sẽ xóa các chi tiết khách hàng khỏi danh sách khách hàng của nó. Sau đây là mã được sử dụng ở phía máy khách để ngắt kết nối ổ cắm (phần nhắn tin không được hiển thị).

client.Shutdown(SocketShutdown.Both); 
client.Close(); 

Bất kỳ đề xuất hoặc vấn đề nào?

5 - Đóng server

Server gửi thông điệp tới tất cả khách hàng cho thấy tắt máy. Mỗi máy khách sẽ ngắt kết nối socket khi nó nhận được thông báo này. Khách hàng sẽ gửi tin nhắn đóng đến máy chủ và đóng. Khi máy chủ nhận được tin nhắn gần gũi từ tất cả các máy khách, nó sẽ ngắt kết nối ổ cắm và ngừng nghe. Gọi Vứt bỏ trên mỗi ổ cắm máy khách để giải phóng tài nguyên. Đó có phải là cách tiếp cận chính xác không?

6 - disconnections client Unknown

Đôi khi, một khách hàng có thể ngắt kết nối mà không cần thông báo cho máy chủ. Kế hoạch của tôi để xử lý này là: Khi máy chủ gửi tin nhắn đến tất cả các máy khách, hãy kiểm tra trạng thái ổ cắm. Nếu nó không được kết nối, loại bỏ khách hàng đó khỏi danh sách khách hàng và đóng socket cho máy khách đó.

Mọi trợ giúp sẽ tuyệt vời!

+0

Tôi đã có vấn đề với IPAddress lắng nghe trên một số phiên bản cũ của cửa sổ. Sử dụng IPAddress ipAddress = new IPAddress (byte mới [] {0, 0, 0, 0}); dường như là tốt nhất cho tôi! – clamchoda

Trả lời

24

Vì đây là 'bắt đầu', câu trả lời của tôi sẽ gắn liền với triển khai đơn giản hơn là triển khai đơn giản. Tốt nhất là trước tiên hãy cảm thấy thoải mái với cách tiếp cận đơn giản trước khi làm mọi thứ trở nên phức tạp hơn.

1 - Binding và nghe
Mã của bạn có vẻ tốt với tôi, cá nhân tôi sử dụng:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Thay vì đi con đường DNS, nhưng tôi không nghĩ rằng đó là một vấn đề thực sự, hoặc đường.

1,5 - Chấp nhận các kết nối client
Chỉ cần nhắc đến này cho đầy đủ vì ... Tôi giả sử bạn đang làm điều này nếu không bạn sẽ không nhận được để bước 2.

2 - dữ liệu nhận
Tôi sẽ làm cho bộ đệm dài hơn 255 byte, trừ khi bạn có thể mong đợi tất cả các thư của máy chủ của bạn tối đa là 255 byte. Tôi nghĩ rằng bạn muốn một bộ đệm có khả năng lớn hơn kích thước gói TCP để bạn có thể tránh thực hiện nhiều lần đọc để nhận một khối dữ liệu duy nhất.

Tôi muốn nói chọn 1500 byte sẽ ổn, hoặc thậm chí có thể là 2048 cho số vòng đẹp.

Cách khác, có lẽ bạn có thể tránh sử dụng một byte[] để lưu trữ các mảnh dữ liệu, và thay vào đó quấn client socket server-side của bạn trong một NetworkStream, bọc trong một BinaryReader, do đó bạn có thể đọc các thành phần của thông điệp của bạn direclty từ các ổ cắm mà không lo lắng về kích thước bộ đệm.

3 - Gửi dữ liệu và xác định chiều dài dữ liệu
cách tiếp cận của bạn sẽ chỉ làm việc tốt, nhưng nó không rõ ràng đòi hỏi rằng nó rất dễ dàng để tính toán chiều dài của gói tin trước khi bạn bắt đầu gửi nó.

Cách khác, nếu định dạng tin nhắn (thứ tự các thành phần của nó) được thiết kế theo thời gian sao cho khách hàng có thể xác định xem có nhiều dữ liệu hơn không (ví dụ, mã 0x01 một int và một chuỗi, mã 0x02 có nghĩa là tiếp theo sẽ là 16 byte, vv, vv). Kết hợp với cách tiếp cận NetworkStream ở phía khách hàng, đây có thể là một cách tiếp cận rất hiệu quả.

Để an toàn, bạn có thể muốn thêm xác nhận hợp lệ các thành phần được nhận để đảm bảo rằng bạn chỉ xử lý các giá trị sane. Ví dụ, nếu bạn nhận được một dấu hiệu cho một chuỗi có chiều dài 1TB, bạn có thể đã bị hỏng gói ở đâu đó và có thể an toàn hơn để đóng kết nối và buộc khách hàng phải kết nối lại và 'bắt đầu lại'. Cách tiếp cận này mang đến cho bạn một hành vi bắt tất cả rất tốt trong trường hợp thất bại bất ngờ.

4/5 - Kết thúc client và server
Cá nhân tôi sẽ lựa chọn chỉ Close mà không cần thông điệp hơn nữa; khi một kết nối được đóng, bạn sẽ nhận được một ngoại lệ trên bất kỳ chặn đọc/ghi ở đầu kia của kết nối mà bạn sẽ phải phục vụ cho.

Vì bạn phải phục vụ cho 'ngắt kết nối không xác định' để có được một giải pháp mạnh mẽ, làm cho việc ngắt kết nối phức tạp hơn thường là vô nghĩa.

6 - disconnections Unknown
tôi sẽ không tin tưởng ngay cả những tình trạng ổ cắm ... nó có thể cho một kết nối đến chết ở đâu đó dọc theo con đường giữa client/server mà không cần một trong hai khách hàng hoặc Nhận ra máy chủ.

Cách duy nhất được bảo đảm để cho biết kết nối đã chết đột ngột là khi bạn cố gắng tiếp theo gửi kết nối nào đó. Tại thời điểm đó bạn sẽ luôn nhận được một ngoại lệ cho biết thất bại nếu có bất cứ điều gì đã đi sai với kết nối. Kết quả là, cách chống lừa đảo duy nhất để phát hiện tất cả các kết nối không mong muốn là thực hiện cơ chế 'ping', lý tưởng là máy khách và máy chủ định kỳ sẽ gửi thư đến đầu kia chỉ dẫn đến phản hồi thông báo cho biết rằng 'ping' đã được nhận.

Để tối ưu hóa các ping không cần thiết, bạn có thể muốn có cơ chế 'hết giờ' chỉ gửi ping khi không có lưu lượng truy cập nào khác nhận được từ đầu kia trong một khoảng thời gian nhất định (ví dụ: nếu tin nhắn cuối cùng từ máy chủ là hơn x giây, khách hàng sẽ gửi một ping để đảm bảo kết nối đã không chết mà không cần thông báo).

More tiên tiến
Nếu bạn muốn khả năng mở rộng cao, bạn sẽ phải nhìn vào các phương pháp không đồng bộ cho tất cả các hoạt động ổ cắm (Chấp nhận/gửi/nhận). Đây là những biến thể 'Bắt ​​đầu/Kết thúc', nhưng chúng phức tạp hơn nhiều khi sử dụng.

Tôi khuyên bạn nên thử điều này cho đến khi bạn có phiên bản đơn giản và hoạt động.

Cũng lưu ý rằng nếu bạn không có kế hoạch mở rộng hơn một vài chục khách hàng thì điều này không thực sự là một vấn đề bất kể. Các kỹ thuật không đồng bộ chỉ thực sự cần thiết nếu bạn có ý định mở rộng quy mô thành hàng nghìn hoặc hàng trăm nghìn khách hàng được kết nối trong khi không có máy chủ của bạn chết hoàn toàn.

tôi có lẽ đã quên một bó toàn bộ gợi ý quan trọng khác, nhưng điều này là đủ để giúp bạn có được một thực hiện khá mạnh mẽ và đáng tin cậy để bắt đầu với

+1

Đó là một lời giải thích tuyệt vời. Cảm ơn –

+0

Chỉ cần nhớ rằng một khi bạn đã làm tất cả những điều đó, có lẽ nhiều điều cần xem xét mà tôi đã quên đề cập đến ... – jerryjvl

13

1 - Binding và lắng nghe trên một ổ cắm

Có vẻ ổn với tôi. Mã của bạn sẽ ràng buộc socket chỉ với một địa chỉ IP. Nếu bạn chỉ muốn nghe trên bất kỳ giao diện địa chỉ IP/mạng, sử dụng IPAddress.Any:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Để có bằng chứng trong tương lai, bạn có thể muốn để hỗ trợ IPv6. Để nghe trên bất kỳ địa chỉ IPv6 nào, hãy sử dụng IPAddress.IPv6Any thay cho số IPAddress.Any.

Lưu ý rằng bạn không thể nghe trên bất kỳ IPv4 và bất kỳ địa chỉ IPv6 nào cùng một lúc, trừ khi bạn sử dụng Dual-Stack Socket.Điều này sẽ yêu cầu bạn phải bỏ đặt tùy chọn IPV6_V6ONLY ổ cắm:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0); 

Để kích hoạt Teredo với ổ cắm của bạn, bạn cần phải thiết lập các tùy chọn PROTECTION_LEVEL_UNRESTRICTED ổ cắm:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10); 

2 - dữ liệu nhận

Tôi khuyên bạn nên sử dụng NetworkStream để kết thúc tốt nhất ổ cắm trong một Stream thay vì đọc các khối theo cách thủ công.

Đọc một số cố định của byte là một chút lúng túng mặc dù:

using (var stream = new NetworkStream(serverSocket)) { 
    var buffer = new byte[MaxMessageLength]; 
    while (true) { 
     int type = stream.ReadByte(); 
     if (type == BYE) break; 
     int length = stream.ReadByte(); 
     int offset = 0; 
     do 
     offset += stream.Read(buffer, offset, length - offset); 
     while (offset < length); 
     ProcessMessage(type, buffer, 0, length); 
    } 
} 

đâu NetworkStream thực sự tỏa sáng là bạn có thể sử dụng nó như bất kỳ Stream khác. Nếu bảo mật là quan trọng, chỉ cần quấn NetworkStream vào một số SslStream để xác thực máy chủ và (tùy chọn) khách hàng có chứng chỉ X.509. Nén hoạt động theo cùng một cách.

var sslStream = new SslStream(stream, false); 
sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true); 
// receive/send data SSL secured 

3 - gửi dữ liệu và xác định độ dài dữ liệu

cách tiếp cận của bạn nên làm việc, mặc dù bạn có thể có thể không muốn đi xuống đường để reinventing the wheel và thiết kế một giao thức mới cho việc này. Có một cái nhìn tại BEEP hoặc thậm chí có thể một cái gì đó đơn giản như protobuf.

Tùy thuộc vào mục tiêu của bạn, có thể đáng suy nghĩ về việc chọn một ổ cắm trừu tượng phía trên như WCF hoặc một số cơ chế RPC khác.

4/5/6 - Đóng & Unknown disconnections

jerryjvl nói :-) duy nhất cơ chế phát hiện đáng tin cậy là ping hoặc gửi giữ-alives khi kết nối được nhàn rỗi.

Trong khi bạn phải đối phó với các ngắt kết nối không xác định trong mọi trường hợp, tôi sẽ giữ một số yếu tố giao thức để đóng kết nối trong thỏa thuận thay vì chỉ đóng nó mà không cần cảnh báo.

+0

Altho nó không thể làm tổn thương, tôi thấy rằng trong thực tế tập trung vào phục hồi mạnh mẽ từ những bất ngờ hoạt động tốt hơn thông điệp 'ngắt kết nối duyên dáng' ... YMMV :) – jerryjvl

+0

Bài đăng tuyệt vời. Cảm ơn. Bạn có thêm thông tin về việc sử dụng NetworkStream không? Bất kỳ liên kết nào cũng có thể làm được. –

+0

Liên kết và ví dụ được thêm vào để trả lời. – dtb

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