Làm cách nào để xử lý mất gói trong mô hình mạng máy khách-máy chủ?


7

Trong mô hình mạng máy khách-máy chủ, các máy khách chỉ gửi lệnh đến máy chủ (tức là tọa độ của một lần nhấp, súng bắn, v.v.) và sau đó máy chủ sẽ chạy các lệnh đó để tạo trạng thái trò chơi.

Nhưng chuyện gì sẽ xảy ra nếu khách hàng gửi một lệnh như "súng lửa" trong một gói bị rơi. Người dùng có trải nghiệm rằng anh ta đã nhấn nút bắn không có gì xảy ra hay có cách nào thông minh để đảm bảo rằng các lệnh đó được máy chủ nhận được bất kể mất gói không?

Tức là tiếp tục gửi lệnh "súng lửa" cũ nhiều lần để đảm bảo rằng ít nhất cuối cùng nó cũng bắn (gắn thẻ lệnh để máy chủ hiểu rằng đó là cùng một lệnh được gửi nhiều lần, không phải là lệnh được bắn đi lật lại) .

Tôi đã xem tài liệu về Mô hình mạng Quake3, Unreal và Nguồn nhưng tôi không thể tìm thấy bất cứ điều gì về vấn đề cụ thể này. Tôi tin rằng họ đã giải quyết nó bởi vì tôi chưa bao giờ bắn AWP của mình mà không thực sự bắn. Nếu có độ trễ liên quan, thì tôi chỉ mất một lúc trước khi bắn, nhưng tôi nghĩ nó không bao giờ mất lệnh.

EDIT: Tôi đã tìm thấy một tài nguyên giải thích một cách để xử lý vấn đề này tại đây: http://gafferongames.com/networking-for-game-programmers/relabilities-and-flow-control/

Họ giải quyết nó bằng cách thêm một loại ACK lỏng lẻo lên trên UDP. Tôi cảm thấy như có một cách thông minh hơn để đi về nó tuy nhiên. Cách xử lý này là ổn đối với máy chủ Tôi đoán rằng phần lớn cần phải đảm bảo rằng nó có kết nối trực tiếp với và để xử lý những thứ như tin nhắn trò chuyện (không thực sự nhạy cảm với thời gian nhưng không thể được phép bị mất) nhưng đối với khách hàng, tôi tin rằng một ACK tạo ra quá nhiều độ trễ khi mất gói tin (ít nhất là độ trễ toàn thời gian toàn vòng).


Tôi tin rằng câu trả lời đúng là "Đừng". Lý do tại sao UDP tốt cho những thứ như CS / LOL / ETC ... là bởi vì bạn liên tục gửi các bản cập nhật trạng thái ghi đè trạng thái trước đó. Rất giống với một luồng phim. Đây là một sự đánh đổi về độ trễ so với đảm bảo rằng trạng thái máy chủ được phân phối hoàn hảo và độ trễ tối thiểu dường như là ưu tiên hàng đầu của bạn.
ClassicThunder

@ClassicThunder Đối với máy chủ-> giao tiếp máy khách UDP hoạt động kỳ diệu vì đó là bản chất của trạng thái trò chơi như bạn mô tả. Tuy nhiên, máy khách không gửi trạng thái trò chơi đến máy chủ, nó sẽ gửi lệnh. Đây là những bản chất khác nhau vì chúng không ghi đè lệnh trước đó mà thay vào đó được xây dựng dựa trên nó một cách bổ sung. Do đó, mất gói là một mối quan tâm lớn trong giao tiếp máy khách-> máy chủ.
Willem

Một ACK lỏng lẻo thông qua UDP vẫn được sử dụng mà không có nhiều thay đổi vì nó hoạt động và được chứng minh theo thời gian. Bạn đang hỏi làm thế nào để đảm bảo máy chủ nhận được tin nhắn từ khách hàng ... mà không bao giờ thông báo lại cho khách hàng. Nếu bạn đã sử dụng UDP w / ACK hoặc TCP và thấy nó không đạt yêu cầu, thì trong những điều kiện nào nó không đáp ứng nhu cầu của bạn? Tôi là tất cả để điều tra học tập, nhưng đây có vẻ như là một trường hợp tối ưu hóa sớm.
LLL79

Câu trả lời:


8

Nó luôn làm tôi thất vọng về việc có bao nhiêu người quá đơn giản hóa sự khác biệt giữa TCP và UDP là "TCP đáng tin cậy nhưng chậm, trong khi UDP nhanh nhưng không đáng tin cậy".

Đây không phải là những gì TCP và UDP nói về. TCP và UDP là hai khái niệm trừu tượng khác nhau trên IP và được sử dụng cho những thứ khác nhau. Cả hai rất tốt trong lĩnh vực riêng của họ.

TCP là một giao thức định hướng luồng . Bạn sử dụng TCP khi bạn muốn chuyển nhiều dữ liệu tuần tự từ điểm này sang điểm khác, tối đa hóa thông lượng (nghĩa là giảm thiểu thời gian bạn phải gửi - tất cả dữ liệu). Việc tự động gửi lại các datagram bị mất và sắp xếp lại là một trong những tính năng của TCP, nhưng TCP cũng có các kết nối trạng thái, kiểm soát luồng và kiểm soát tắc nghẽn. Các tính năng này làm cho TCP trở thành một công cụ thực sự tốt để truyền một lượng lớn dữ liệu từ điểm này sang điểm khác. Các giao thức như HTTP và FTP nơi bạn muốn gửi khối dữ liệu lớn càng nhanh càng tốt để sử dụng TCP.

Mặt khác, UDP là một giao thức hướng thông điệp . Bạn sử dụng UDP khi bạn muốn gửi tin nhắn từ một điểm đến một hoặc nhiều điểm, giảm thiểu độ trễ (nghĩa là khoảng thời gian giữa một tin nhắn được gửi cho đến khi nhận được tin nhắn). Để giảm thiểu độ trễ, UDP không triển khai truyền lại hoặc sắp xếp lại datagram. Tuy nhiên, nếu bạn muốn ứng dụng dựa trên tin nhắn của mình hỗ trợ truyền lại và / hoặc sắp xếp lại, bạn có thể tự thực hiện điều đó và thực sự khá đơn giản! Ngoài ra, có một loạt những điều thú vị bạn có thể làm trong UDP mà bạn đơn giản không thể làm với TCP, như đục lỗ đa tuyến và UDP.

TCP và UDP đều là những công nghệ tuyệt vời dành cho những thứ khác nhau, vì vậy điều bạn muốn làm là tự hỏi liệu các giao tiếp trong chương trình của bạn được thể hiện tốt hơn dưới dạng một luồng byte hay một chuỗi các thông điệp và một quy tắc tốt Là:

  • Nếu chương trình của bạn là về việc gửi tin nhắn, hãy sử dụng UDP.

  • Nếu chương trình của bạn sắp gửi một luồng byte, hãy sử dụng TCP.

Đây là quyết định giữa TCP và UDP phải như thế nào, không phải về độ tin cậy, mà chỉ là một trong nhiều điểm khác biệt giữa chúng và theo tôi là quan trọng nhất trong tất cả chúng.

Giống như có thể thực hiện một sự trừu tượng hóa luồng trên UDP, có thể (và thật đáng buồn là rất phổ biến) để tạo một thông điệp trừu tượng hóa trên TCP.

Nếu bạn chọn tạo một thông điệp trừu tượng trên TCP, hãy nhớ rằng bạn sẽ phải sử dụng TCP theo cách không có nghĩa, điều đó có nghĩa là bạn sẽ gặp một số khó khăn. Đặc biệt:

  • Ranh giới thông báo: TCP coi dữ liệu là một luồng byte rất dài và do đó, không có khái niệm về gói hoặc thông điệp. Nếu bạn muốn gửi tin nhắn qua TCP, bạn sẽ phải tạo một số loại tiêu đề thư bao gồm độ dài của mỗi tin nhắn.

  • Bộ đệm khi gửi: TCP hy vọng bạn có thể tạo dữ liệu nhanh nhất có thể. Các datagram TCP có thể rất lớn và nếu điều kiện mạng tốt, một ngăn xếp TCP sẽ đệm các byte của bạn cho đến khi nó có thể gửi một datagram rất lớn do đó tối đa hóa thông lượng. Đây là một điều tốt cho các luồng, nhưng không quá nhiều cho các tin nhắn. Một khi quá nhiều lần mọi người kết thúc việc xóa bộ đệm trên mỗi lần gửi hoặc sử dụng các tùy chọn như TCP_NODELAY, và thậm chí sau đó bạn không có gì đảm bảo. Để trích dẫn Richard Stevens quá cố :

2.11. Làm thế nào tôi có thể buộc một ổ cắm để gửi dữ liệu trong bộ đệm của nó?

Bạn không thể ép buộc nó. Giai đoạn = Stage. TCP tự quyết định khi nào nó có thể gửi dữ liệu. Bây giờ, thông thường khi bạn gọi write () trên ổ cắm TCP, TCP thực sự sẽ gửi một phân đoạn, nhưng không có gì đảm bảo và không có cách nào để buộc điều này. Có rất nhiều lý do khiến TCP sẽ không gửi một phân đoạn: một cửa sổ đóng và thuật toán Nagle là hai điều cần chú ý ngay lập tức.

  • Bộ đệm khi nhận: Ngay cả khi bạn quản lý để xóa bộ đệm trên mỗi lần gửi, không có gì đảm bảo rằng bạn sẽ nhận được một lần nhận cho mỗi lần gửi mà bạn gửi. Bạn hoàn toàn có thể nhận được một số tin nhắn trong một recvcuộc gọi, bởi vì chúng được đệm ở đầu tiếp nhận (đặc biệt là trên các mạng có tốc độ mất gói cao). Sau đó, bạn phải triển khai hàng đợi nhận tin nhắn, đây là một việc khá ngu ngốc khi bạn sử dụng TCP.

  • Nhịp tim và phát hiện thả kết nối: TCP có cơ chế riêng để phát hiện các kết nối bị rớt, nhưng chúng thường quá chậm đối với các ứng dụng truyền tin nhắn (datagram giữ không thể cấu hình được gửi khoảng hai giờ một lần). Một khi có quá nhiều lần mọi người kết thúc việc thực hiện các giao thức tốc độ riêng của họ trong cùng một luồng TCP, và rồi cuối cùng họ lại hỏi "làm thế nào để đóng TIME_WAIT", "Cách phát hiện kết nối kín" hoặc đại loại như thế.

Không có vấn đề nào trong số này tồn tại nếu bạn tạo chương trình hướng thông điệp của mình bằng UDP:

  • Ranh giới tin nhắn: Mỗi tin nhắn là một datagram UDP riêng.

  • Bộ đệm khi gửi: Không tồn tại. Một sendtocuộc gọi ngay lập tức gửi một datagram.

  • Bộ đệm khi nhận: Không tồn tại. Bạn sẽ chỉ nhận được một datagram cho mỗi recvfromcuộc gọi.

  • Phát hiện thả kết nối: UDP không được định hướng kết nối. Bạn có thể thực hiện thời gian chờ đơn giản với các tham số của riêng bạn để xem xét một khách hàng không còn ở đó nữa.

Nếu bạn muốn sử dụng TCP để truyền tin nhắn vì bạn nghĩ nó đơn giản, đừng.

Bây giờ, đối với "vấn đề" độ tin cậy đáng sợ, bạn có thể thực hiện độ tin cậy đơn giản bằng cách bao gồm hai trường trên mỗi tin nhắn: trường đầu tiên là ID tin nhắn tăng vĩnh viễn và trường thứ hai là ID tin nhắn lớn nhất bạn nhận được từ một khách hàng cụ thể. Nếu số lượng nội bộ của bạn khác quá nhiều so với những gì khách hàng nói rằng anh ta đã nhận được, bạn chỉ cần gửi lại bắt đầu từ tin nhắn đầu tiên mà khách hàng chưa nhận được. Với tính năng bao bọc, bạn có thể thực hiện việc này chỉ với hai byte bổ sung cho mỗi tin nhắn.

Điều tốt nhất về lớp độ tin cậy của riêng bạn là bạn có thể điều chỉnh nó cho bất cứ nhu cầu nào của bạn: bạn có thể có một số loại thông báo có độ tin cậy trong đó, trong khi các loại khác không có nó. Bạn có thể có nhiều kênh khác nhau, mỗi kênh có số thứ tự riêng, bạn có thể sử dụng các số đó để dự đoán độ trễ và rất nhiều nội dung tuyệt vời khác.

Đừng chạy trốn khỏi UDP vì những người khác đã nói với bạn "thật khó". Có lẽ họ chưa bao giờ (nghiêm túc) sử dụng UDP và có lẽ thậm chí chưa (nghiêm túc) sử dụng TCP. Thật ra UDP đơn giản và dễ hiểu hơn nhiều so với TCP.

Đọc lập trình mạng Unix của Stevens. Và sau đó đọc lại nó.


Tôi đã chọn UDP vì khả năng tin nhắn không đáng tin cậy và vì chúng là tin nhắn. Tôi tin rằng WebRTC (đó là những gì tôi sẽ sử dụng) thực hiện kiểm soát luồng nên tôi sẽ không phải đặt nhiều logic lên trên triển khai mạng của mình ngoài các chi tiết cụ thể về mô hình mạng trò chơi.
Willem

1
Trong khi bài đăng này chứa rất nhiều thông tin nội bộ về các giao thức. Tôi chỉ không thể đồng ý với việc khuyên các lập trình viên mới triển khai lại các cơ chế TCP sử dụng UDP làm cơ sở. Lời khuyên này giống như bạn đã khuyên các lập trình viên mới sử dụng C thuần thay vì java là "bạn không phải lo lắng về các lớp" và "nó nhanh hơn vì nó không có bộ sưu tập rác".
wonderra

1
@wondra: Vì vậy, bạn chỉ nói rằng mọi người nên thực hiện lại các tính năng UDP như ranh giới tin nhắn và lạm dụng hoàn toàn các tính năng TCP như bộ đệm, chỉ vì một tính năng TCP mà bạn quan tâm? So sánh TCP với UDP khi bạn so sánh Java với C là không chính xác: cả TCP và UDP đều là các giao thức lớp vận chuyển cấp cao. Nếu so sánh được thực hiện giữa Java và Scheme: bạn chắc chắn có thể làm một cái gì đó tương tự như lập trình chức năng trong Java, cũng như một cái gì đó tương tự OOP trong Scheme, nhưng có những công cụ tốt hơn để làm những gì bạn muốn làm.
Panda Pyjama

@wondra: Ngoài ra, không có gì "nội bộ" hoặc mức độ thấp về những gì tôi đã viết. Định hướng theo luồng là tính năng chính của TCP và định hướng thông điệp là tính năng chính của UDP. Đây không phải là thông tin mơ hồ.
Panda Pyjama

Giao diện cơ bản cho cả TCP và UDP khá giống nhau ở nhiều ngôn ngữ. Những gì không có trong giao diện là nội bộ. Tôi chỉ nói rằng bạn sẽ không bao giờ thuyết phục tôi thực hiện nhiều dòng để kiểm tra lỗi (ack, đánh số và CRC). Tại sao triển khai thu gom rác thành ANSI C để có hiệu suất cao hơn một chút (= được tối ưu hóa tốt hơn cho chương trình đơn) qua java? Đây là câu hỏi mà mọi người phải tự trả lời. Tôi nói không với tối ưu hóa sớm (ít nhất là cho người mới bắt đầu). Nếu anh ta phải hỏi làm thế nào để làm điều đó, anh ta người mới bắt đầu.
wonderra

1

Để xử lý mất gói, trước tiên bạn cần quyết định giao thức mạng nào bạn sẽ sử dụng: TCP hoặc UDP.

  • TCP: đáng tin cậy, nhưng chậm gửi các tin nhắn đơn giản (có độ trễ cao hơn)

  • UDP: có độ trễ thấp, nhưng để đạt được mục tiêu của bạn là không để người dùng nhận thấy một số gói bị mất, bạn sẽ cần phát triển thêm một lớp sẽ làm chậm giao tiếp của bạn.

Lớp bổ sung này giống như sự khác biệt giữa UDP x TCP.

Bạn sẽ cần nghiên cứu chi tiết về giao thức TCP và thực hiện nhiều tính năng. Bạn sẽ cần ít nhất để phát triển các gói ACK và thứ tự gói. Nếu lớp của bạn không được phát triển tốt, nó thậm chí có thể chậm hơn TCP.

Có lẽ bạn có thể trộn cả hai giao thức trong cùng một trò chơi.

UDP chỉ nên được sử dụng khi bạn đang cung cấp dữ liệu có thể bị mất mà không gặp sự cố. Ví dụ: nếu bạn bỏ lỡ gói "kẻ thù 1 đang ở trên trường A", thì thỉnh thoảng bạn không lặp lại vị trí của kẻ thù. Nếu bạn nhận được gói thứ hai với "kẻ thù 1 ở trên trường A" hoặc "kẻ thù 1 ở trên trường B", thì nó không quan trọng ở đâu trước đó. Bạn sẽ chỉ đặt kẻ thù vào trường nhận được cuối cùng.

Bây giờ, nếu bạn không muốn trình phát của mình mất BẤT K shot ảnh nào, hãy sử dụng TCP (hoặc tin nhắn UDP được sửa đổi đáng tin cậy).


Nhưng nếu tôi không muốn sử dụng ACK? Tôi chỉ muốn xây dựng trong một số dự phòng để các lệnh không bị bỏ qua trừ khi người chơi bị chậm trễ. Hoặc có thể là mô hình mạng máy khách-máy chủ thường sử dụng TCP cho máy khách-> giao tiếp lệnh của máy chủ trong khi sử dụng UDP cho máy chủ-> giao tiếp máy khách của trạng thái trò chơi đầy đủ (có thể bị mất mà không gặp sự cố)?
Willem

Bạn có thể tránh sử dụng thuật toán ACK (hoặc TCP) và phát triển thuật toán của riêng bạn để dự phòng, nhưng tôi không đề xuất điều đó. Có phải tình huống tương tự nếu ai đó quyết định tạo ra thuật toán mật mã của riêng mình thay vì sử dụng tôi sử dụng rộng rãi như RSA.
Zanon

Đề nghị của tôi là trộn TCP (cho thông tin mà bạn không thể bỏ lỡ) với UDP (đối với thông tin sẽ không gây ra sự cố nếu bạn bỏ lỡ).
Zanon

Tôi nghĩ đó là một gợi ý hay nhưng lý do tôi nghĩ nên có cách tốt hơn với UDP là vì thông tin sẽ gây ra nhiều vấn đề nếu bạn bỏ lỡ đủ gói, điều đó có nghĩa là tôi chỉ cần có cách bảo vệ chống mất gói nhỏ mà không phải làm việc vì mất gói lớn vì dù sao thì trò chơi cũng cần phải tự thiết lập lại trong kịch bản đó.
Willem

Tôi cũng đã đọc rằng TCP gây ra mất gói trong UDP khi được sử dụng kết hợp. Thêm vào đó, các lệnh của người chơi cần đến máy chủ càng nhanh càng tốt vì nó xác định mức độ nhanh của trò chơi.
Willem

0

Trừ khi bạn quan tâm đến các chi tiết triển khai lớp tin cậy của riêng bạn bằng UDP, tôi sẽ khuyên bạn nên sử dụng RakNet , đặc biệt là vì nó đã được tạo thành nguồn mở. Đây sẽ là con đường để đi nếu bạn tập trung vào việc tạo ra một trò chơi hơn là một giao thức mạng.

Với RakNet, bạn có thể gửi các gói với các cờ có độ tin cậy nhất định, chẳng hạn như RELIABLE_ORDERED. Điều này đảm bảo mỗi gói sẽ đến (nó được gửi lại nếu không được nhận) và nó sử dụng một hệ thống đặt hàng cơ bản để các gói được xử lý theo thứ tự ở đầu nhận. Điều này sẽ rất quan trọng nếu bạn đang gửi đầu vào cho một thứ gì đó giống như một trò chơi chiến đấu trong đó trật tự là rất quan trọng để tính toán các bước di chuyển và như vậy. Đây là tất cả rất dễ dàng để thực hiện bằng cách sử dụng một thư viện như RakNet.

Tuy nhiên, điều này sẽ làm tăng băng thông, nhưng bạn có thể chọn một cách thích hợp những gói tin nào đáng tin cậy và được đặt hàng và những gói nào bạn có thể đủ khả năng để mất.

Chỉnh sửa: Cũng xem xét theo dõi các bài viết này để thực hiện nhiều người chơi có nhịp độ nhanh. Nó khá dễ dàng thực hiện với một thư viện mạnh mẽ như RakNet.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.