Cái gì tốt hơn Rất nhiều gói TCP nhỏ, hay một gói dài? [đóng cửa]


16

Tôi đang gửi khá nhiều dữ liệu đến và từ một máy chủ, cho một trò chơi tôi đang thực hiện.

Tôi hiện đang gửi dữ liệu vị trí như thế này:

sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));

Rõ ràng là nó đang gửi các giá trị X, Y và Z tương ứng.

Nó sẽ hiệu quả hơn để gửi dữ liệu như thế này?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));

2
Mất gói thường dưới 5%, theo kinh nghiệm hạn chế của tôi.
mucaho

5
SendToClient có thực sự gửi một gói không? Nếu vậy, làm thế nào bạn làm cho nó làm điều đó?
dùng253751

1
@mucaho Tôi chưa bao giờ tự đo nó hoặc bất cứ điều gì, nhưng tôi ngạc nhiên rằng TCP là xung quanh các cạnh. Tôi đã hy vọng cho một cái gì đó giống như 0,5% hoặc ít hơn.
Panzercrisis

1
@Panzercrisis Tôi phải đồng ý với bạn. Cá nhân tôi cảm thấy rằng mất 5% sẽ không thể chấp nhận được. Nếu bạn nghĩ về thứ gì đó như tôi gửi, giả sử, một con tàu mới xuất hiện trong trò chơi, thậm chí 1% khả năng gói tin đó không được nhận sẽ là thảm họa, bởi vì tôi sẽ nhận được những con tàu vô hình.
joehot200 20/03/2015

2
đừng làm mọi người thất vọng, ý tôi là 5% như một giới hạn trên :) trong thực tế nó tốt hơn nhiều, như được ghi nhận bởi các bình luận khác.
mucaho

Câu trả lời:


28

Một phân đoạn TCP có khá nhiều chi phí. Khi bạn gửi tin nhắn 10 byte với một gói TCP bạn thực sự gửi:

  • 16 byte tiêu đề IPv4 (sẽ tăng lên 40 byte khi IPv6 trở nên phổ biến)
  • 16 byte tiêu đề TCP
  • 10 byte tải trọng
  • chi phí bổ sung cho các giao thức liên kết dữ liệu và lớp vật lý được sử dụng

dẫn đến 42 byte lưu lượng để vận chuyển 10 byte dữ liệu. Vì vậy, bạn chỉ sử dụng ít hơn 25% băng thông có sẵn của mình. Và điều đó vẫn chưa tính đến chi phí mà các giao thức cấp thấp hơn như Ethernet hoặc PPPoE tiêu thụ (nhưng chúng khó ước tính vì có rất nhiều lựa chọn thay thế).

Ngoài ra, nhiều gói nhỏ gây căng thẳng hơn cho bộ định tuyến, tường lửa, thiết bị chuyển mạch và thiết bị hạ tầng mạng khác, vì vậy khi bạn, nhà cung cấp dịch vụ và người dùng của bạn không đầu tư vào phần cứng chất lượng cao, điều này có thể biến thành một nút cổ chai khác.

Vì lý do đó, bạn nên cố gắng gửi tất cả dữ liệu bạn có sẵn cùng một lúc trong một phân đoạn TCP.

Về xử lý mất gói : Khi bạn sử dụng TCP, bạn không cần phải lo lắng về điều đó. Bản thân giao thức đảm bảo rằng bất kỳ gói tin bị mất nào được gửi lại và các gói được xử lý theo thứ tự, vì vậy bạn có thể giả sử rằng tất cả các gói bạn gửi sẽ đến ở phía bên kia và chúng sẽ đến theo thứ tự bạn gửi chúng. Cái giá cho điều này là khi mất gói, trình phát của bạn sẽ gặp độ trễ đáng kể, vì một gói bị rơi sẽ tạm dừng toàn bộ luồng dữ liệu cho đến khi được yêu cầu lại và nhận được.

Khi đây là một vấn đề, bạn luôn có thể sử dụng UDP. Nhưng sau đó bạn cần phải tìm giải pháp của riêng bạn cho mất và out-of-trật tự thông điệp (nó ít nhất đảm bảo rằng những thông điệp mà làm đến nơi, đến đầy đủ và không bị hư hại).


1
Có phải chi phí từ cách TCP phục hồi mất gói có khác nhau tùy thuộc vào kích cỡ của gói không?
Panzercrisis

@Panzercrisis Chỉ trong chừng mực có một gói lớn hơn cần phải bực bội.
Philipp

8
Tôi sẽ lưu ý rằng hệ điều hành gần như chắc chắn sẽ áp dụng Thuật toán Nagles en.wikipedia.org/wiki/Nagle%27s_alacticm cho dữ liệu gửi đi, điều này có nghĩa là không có vấn đề gì nếu trong ứng dụng bạn viết riêng hoặc kết hợp chúng, nó sẽ kết hợp chúng trước khi thực sự chuyển chúng qua TCP.
Vality

1
@Vality Hầu hết các API của socket tôi đã sử dụng cho phép kích hoạt hoặc hủy kích hoạt cagle cho mỗi socket. Đối với hầu hết các trò chơi, tôi khuyên bạn nên tắt nó, vì độ trễ thấp thường quan trọng hơn việc bảo toàn băng thông.
Philipp

4
Thuật toán của Nagle là một, nhưng không phải là lý do duy nhất dữ liệu có thể được đệm ở phía gửi. Không có cách nào để buộc dữ liệu đáng tin cậy gửi. Ngoài ra, bộ đệm / phân mảnh có thể xảy ra ở bất cứ đâu sau khi dữ liệu được gửi, tại NAT, bộ định tuyến, proxy hoặc thậm chí là phía nhận. TCP không đảm bảo về kích thước và thời gian mà bạn nhận dữ liệu, chỉ có điều nó sẽ đến theo thứ tự và đáng tin cậy. Nếu bạn cần đảm bảo kích thước, sử dụng UDP. Thực tế là TCP có vẻ dễ hiểu hơn không làm cho nó trở thành công cụ tốt nhất cho mọi vấn đề!
Panda Pyjama

10

Một lớn (trong lý do) là tốt hơn.

Như bạn đã nói, mất gói là lý do chính. Các gói thường được gửi trong các khung có kích thước cố định, vì vậy tốt hơn là lấy một khung có thông điệp lớn hơn 10 khung với 10 khung nhỏ.

Tuy nhiên với TCP chuẩn, đây thực sự không phải là vấn đề, trừ khi bạn tắt nó. (Nó được gọi là thuật toán của Nagle và đối với các trò chơi, bạn nên vô hiệu hóa nó.) TCP sẽ đợi thời gian chờ cố định hoặc cho đến khi gói "đầy". Trong đó "đầy đủ" sẽ là một số ma thuật hơi, được xác định một phần bởi kích thước khung hình.


Tôi đã nghe nói về thuật toán của Nagle, nhưng nó thực sự là một ý tưởng tốt để vô hiệu hóa nó? Tôi vừa đến từ câu trả lời của StackOverflow khi có người nói rằng nó hiệu quả hơn (và vì lý do rõ ràng tôi muốn hiệu quả).
joehot200

6
@ joehot200 Câu trả lời đúng duy nhất cho điều đó là "nó phụ thuộc". Có hiệu quả hơn khi gửi nhiều dữ liệu, vâng, nhưng không phải cho phát trực tuyến thời gian thực mà các trò chơi có xu hướng cần.
D-side

4
@ joehot200: Thuật toán của Nagle tương tác kém với thuật toán xác nhận bị trì hoãn mà đôi khi các triển khai TCP nhất định sử dụng. Một số triển khai TCP sẽ trì hoãn việc gửi ACK sau khi họ nhận được một số dữ liệu nếu họ mong đợi nhiều dữ liệu sẽ được theo dõi ngay sau đó (vì việc thừa nhận gói tin sau này cũng sẽ ngầm thừa nhận dữ liệu trước đó). Thuật toán của Nagle nói rằng một đơn vị không nên gửi một phần gói nếu nó đã gửi một số dữ liệu nhưng không nghe thấy một xác nhận. Đôi khi hai cách tiếp cận tương tác xấu, với mỗi bên chờ đợi người kia làm gì đó, cho đến khi ...
supercat

2
... một "bộ đếm thời gian tỉnh táo" khởi động và giải quyết tình huống (theo thứ tự của một giây).
supercat

2
Thật không may, vô hiệu hóa thuật toán của Nagle sẽ không làm gì để ngăn chặn bộ đệm ở phía máy chủ khác. Vô hiệu hóa thuật toán của Nagle không đảm bảo bạn nhận được một recv()cuộc gọi cho mỗi send()cuộc gọi, đó là điều mà hầu hết mọi người đang tìm kiếm. Tuy nhiên, sử dụng một giao thức đảm bảo điều này, giống như UDP. "Khi tất cả những gì bạn có là TCP, mọi thứ trông giống như một luồng"
Panda Pajama

6

Tất cả các câu trả lời trước là không chính xác. Trong thực tế, không quan trọng bạn thực hiện một send()cuộc gọi dài hay một vài send()cuộc gọi nhỏ .

Như Phillip tuyên bố, một phân đoạn TCP có một số chi phí hoạt động, nhưng là một lập trình viên ứng dụng, bạn không có quyền kiểm soát đối với cách các phân đoạn được tạo. Nói một cách đơn giản:

Một send()cuộc gọi không nhất thiết phải dịch sang một phân đoạn TCP.

HĐH hoàn toàn miễn phí để đệm tất cả dữ liệu của bạn và gửi nó trong một phân đoạn, hoặc lấy dữ liệu dài và chia thành nhiều phân đoạn nhỏ.

Điều này có một số hàm ý, nhưng điều quan trọng nhất là:

Một send()cuộc gọi hoặc một phân đoạn TCP không nhất thiết phải dịch sang một recv()cuộc gọi thành công ở đầu bên kia

Lý do đằng sau điều này là TCP là một giao thức truyền phát . TCP coi dữ liệu của bạn là một chuỗi byte dài và hoàn toàn không có khái niệm về "gói". Với send()bạn thêm byte vào luồng đó và với recv()bạn sẽ nhận được byte ở phía bên kia. TCP sẽ tích cực đệm và phân chia dữ liệu của bạn bất cứ nơi nào nó thấy phù hợp để đảm bảo dữ liệu của bạn được chuyển sang phía bên kia càng nhanh càng tốt.

Nếu bạn muốn gửi và nhận "gói" với TCP, bạn phải triển khai các dấu bắt đầu gói, đánh dấu độ dài, v.v. Thay vào đó, sử dụng giao thức hướng thông báo như UDP thì sao? UDP đảm bảo một send()cuộc gọi chuyển thành một datagram đã gửi và một recv()cuộc gọi!

Khi tất cả những gì bạn có là TCP, mọi thứ trông giống như một luồng


1
Tiền tố mỗi tin nhắn có độ dài tin nhắn không quá khó khăn.
ysdx 20/03/2015

Bạn có một công tắc để lật khi nói đến tập hợp gói, cho dù Thuật toán của Nagle có hiệu lực hay không. Không có gì lạ khi nó tắt trong mạng trò chơi để đảm bảo cung cấp kịp thời các gói bị thiếu.
Lars Viklund

Đây hoàn toàn là hệ điều hành hoặc thậm chí là thư viện cụ thể. Ngoài ra, bạn có rất nhiều quyền kiểm soát - nếu bạn muốn. Đúng là bạn không có toàn quyền kiểm soát, TCP luôn được phép kết hợp hai tin nhắn hoặc tách một tin nhắn nếu nó không phù hợp với MTU, nhưng bạn vẫn có thể gợi ý nó theo đúng hướng. Đặt các cài đặt cấu hình khác nhau, gửi tin nhắn thủ công cách nhau 1 giây hoặc đệm dữ liệu và gửi nó trong một lần chụp.
Dorus

@ysdx: không, không phải ở phía gửi, nhưng có ở phía nhận. Vì bạn không có gì đảm bảo chính xác nơi bạn sẽ nhận được dữ liệu recv(), nên bạn cần phải thực hiện bộ đệm của riêng mình để bù đắp cho điều đó. Tôi sẽ xếp hạng nó trong cùng độ khó như triển khai độ tin cậy so với UDP.
Panda Pyjama

@Pagnda Pajama: Một triển khai ngây thơ của bên nhận là: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(bỏ qua xử lý lỗi và đọc một phần cho ngắn gọn nhưng nó không thay đổi nhiều). Làm điều này là selectkhông phức tạp hơn nhiều và nếu bạn đang sử dụng TCP, có lẽ bạn cần phải đệm các thông điệp một phần. Việc thực hiện độ tin cậy mạnh mẽ đối với UDP phức tạp hơn thế nhiều.
ysdx 20/03/2015

3

Nhiều gói nhỏ là tốt. Trong thực tế, nếu bạn lo lắng về chi phí TCP, chỉ cần chèn một bộ bufferstreamsưu tập lên tới 1500 ký tự (hoặc bất cứ MTU TCP nào của bạn, tốt nhất là yêu cầu nó một cách linh hoạt) và xử lý vấn đề ở một nơi. Làm như vậy sẽ tiết kiệm cho bạn chi phí ~ 40 byte cho mỗi gói bổ sung mà bạn đã tạo.

Điều đó nói rằng, vẫn tốt hơn để gửi ít dữ liệu hơn và xây dựng các đối tượng lớn hơn giúp đỡ ở đó. Tất nhiên là gửi nhỏ "UID:10|1|2|3hơn gửi UID:10;x:1UID:10;y:2UID:10;z:3. Trên thực tế, tại thời điểm này, bạn không nên phát minh lại bánh xe, hãy sử dụng một thư viện như protobuf có thể giảm dữ liệu như thế xuống chuỗi 10 byte hoặc ít hơn.

Điều duy nhất bạn không nên quên là chèn một Flushlệnh trên luồng của mình tại các vị trí có liên quan, bởi vì ngay khi bạn ngừng thêm dữ liệu vào luồng của mình, nó có thể đợi vô hạn trước khi nó gửi bất cứ thứ gì. Thực sự có vấn đề khi khách hàng của bạn đang chờ dữ liệu đó và máy chủ của bạn sẽ không gửi bất cứ điều gì mới cho đến khi khách hàng gửi lệnh tiếp theo.

Mất gói là một cái gì đó bạn có thể ảnh hưởng ở đây, lề. Mỗi byte bạn gửi có khả năng bị hỏng và TCP sẽ tự động yêu cầu truyền lại. Các gói nhỏ hơn có nghĩa là cơ hội thấp hơn cho mỗi gói bị hỏng, nhưng vì chúng cộng vào chi phí, bạn gửi nhiều byte hơn, làm tăng tỷ lệ của gói bị mất nhiều hơn. Khi một gói bị mất, TCP sẽ đệm tất cả dữ liệu thành công cho đến khi gói bị thiếu được gửi lại và nhận được. Điều này dẫn đến một độ trễ lớn (ping). Mặc dù tổng tổn thất về băng thông do mất gói có thể không đáng kể, ping cao hơn sẽ là điều không mong muốn đối với các trò chơi.

Điểm mấu chốt: Gửi càng ít dữ liệu càng tốt, gửi các gói lớn và không viết các phương thức cấp thấp của riêng bạn để làm như vậy, mà dựa vào các thư viện và phương thức nổi tiếng như bufferstreamvà protobuf để xử lý việc nâng vật nặng.


Trên thực tế đối với những điều đơn giản như thế này, nó dễ dàng để cuộn của riêng bạn. Dễ dàng hơn nhiều so với việc đi qua một tài liệu 50 trang để sử dụng thư viện của người khác, và sau đó bạn vẫn phải xử lý các lỗi và vấn đề của họ.
Pacerier 20/03/2015

Đúng, viết của riêng bạn bufferstreamlà tầm thường, đó là lý do tại sao tôi gọi nó là một phương pháp. Bạn vẫn muốn xử lý nó ở một nơi và không tích hợp logic bộ đệm với mã thông báo của bạn. Đối với Tuần tự hóa đối tượng, tôi rất nghi ngờ bạn sẽ nhận được thứ gì đó tốt hơn hàng ngàn giờ làm việc khác, ngay cả khi bạn thử, tôi thực sự khuyên bạn nên đánh giá giải pháp của mình trước các triển khai đã biết.
Dorus

2

Mặc dù bản thân là một người mới biết lập trình mạng, tôi muốn chia sẻ kinh nghiệm hạn chế của mình bằng cách thêm một vài điểm:

  • TCP có nghĩa là một chi phí chung - bạn phải đo các số liệu thống kê có liên quan
  • UDP là giải pháp thực tế cho các tình huống chơi trò chơi được nối mạng, nhưng tất cả các triển khai dựa trên nó đều có thuật toán phụ, CPU để giải thích cho các gói bị mất hoặc bị gửi không theo thứ tự

Liên quan đến các phép đo, các số liệu cần được xem xét là:

Như đã đề cập, nếu bạn phát hiện ra rằng bạn không bị giới hạn theo nghĩa và có thể sử dụng UDP, hãy thực hiện điều đó. Có một số triển khai dựa trên UDP ngoài kia, vì vậy bạn không phải phát minh lại bánh xe hoặc làm việc chống lại nhiều năm kinh nghiệm và kinh nghiệm đã được chứng minh. Những triển khai đáng nói đến là:

Kết luận: do việc triển khai UDP có thể vượt trội hơn (gấp 3 lần) một TCP, nên xem xét nó một cách hợp lý, một khi bạn đã xác định kịch bản của mình là thân thiện với UDP. Được cảnh báo! Việc triển khai ngăn xếp TCP đầy đủ trên UDP luôn là một ý tưởng tồi.


Tôi đã sử dụng UDP. Tôi chỉ chuyển sang TCP. Mất gói tin của UDP đơn giản là không thể chấp nhận được đối với dữ liệu quan trọng mà khách hàng cần. Tôi có thể gửi dữ liệu chuyển động qua UDP.
joehot200

Vì vậy, những điều tốt nhất bạn có thể làm là: thực sự, chỉ sử dụng TCP cho các hoạt động quan trọng HOẶC sử dụng triển khai giao thức phần mềm dựa trên UDP (với Enet đơn giản và UDT được kiểm tra tốt). Nhưng trước tiên, hãy đo lường sự mất mát và quyết định xem UDT có mang lại cho bạn lợi thế hay không.
teodron 20/03/2015
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.