Jak duży powinien być mój bufor recv podczas wywoływania recv w bibliotece gniazd

131

Mam kilka pytań dotyczących biblioteki gniazd w C. Oto fragment kodu, do którego będę się odnosić w moich pytaniach.

char recv_buffer[3000];
recv(socket, recv_buffer, 3000, 0);
  1. Jak zdecydować, jak duży ma być plik recv_buffer? Używam 3000, ale to arbitralne.
  2. co się stanie, jeśli recv()otrzyma pakiet większy niż mój bufor?
  3. Skąd mam wiedzieć, czy otrzymałem całą wiadomość bez ponownego wywoływania recv i czy czekam w nieskończoność, gdy nie ma nic do odebrania?
  4. Czy jest sposób, aby bufor nie miał określonej ilości miejsca, tak żebym mógł dodawać do niego dane bez obawy, że zabraknie miejsca? może za pomocą strcatkonkatenacji najnowszej recv()odpowiedzi do bufora?

Wiem, że to wiele pytań w jednym, ale byłbym bardzo wdzięczny za wszelkie odpowiedzi.

adhanlon
źródło

Odpowiedzi:

232

Odpowiedzi na te pytania różnią się w zależności od tego, czy korzystasz z gniazda strumieniowego ( SOCK_STREAM), czy gniazda datagramowego ( SOCK_DGRAM) - w ramach protokołu TCP / IP pierwsze odpowiada TCP, a drugie UDP.

Skąd wiesz, do jakiej wielkości należy przekazać bufor recv()?

  • SOCK_STREAM: To naprawdę nie ma większego znaczenia. Jeśli twój protokół jest protokołem transakcyjnym / interaktywnym, po prostu wybierz rozmiar, który może pomieścić największą pojedynczą wiadomość / polecenie, jakiego można by się spodziewać (3000 jest prawdopodobnie w porządku). Jeśli twój protokół przesyła dane masowo, większe bufory mogą być bardziej wydajne - dobra zasada jest taka sama, jak rozmiar bufora odbioru jądra gniazda (często około 256kB).

  • SOCK_DGRAM: Użyj wystarczająco dużego bufora, aby pomieścić największy pakiet, jaki kiedykolwiek wysłał Twój protokół na poziomie aplikacji. Jeśli używasz UDP, generalnie protokół na poziomie aplikacji nie powinien wysyłać pakietów większych niż około 1400 bajtów, ponieważ z pewnością będą musiały zostać pofragmentowane i ponownie złożone.

Co się stanie, jeśli recvpakiet będzie większy niż bufor?

  • SOCK_STREAM: Pytanie tak naprawdę nie ma sensu, ponieważ gniazda strumieniowe nie mają pojęcia pakietów - są one po prostu ciągłym strumieniem bajtów. Jeśli dostępnych jest więcej bajtów do odczytania, niż ma miejsce w buforze, zostaną one umieszczone w kolejce przez system operacyjny i dostępne do następnego wywołania recv.

  • SOCK_DGRAM: Nadmiarowe bajty są odrzucane.

Skąd mam wiedzieć, czy otrzymałem całą wiadomość?

  • SOCK_STREAM: Musisz zbudować jakiś sposób określania końca wiadomości w protokole na poziomie aplikacji. Zwykle jest to albo przedrostek długości (rozpoczynający każdą wiadomość od jej długości) lub ogranicznik końca wiadomości (który może być na przykład znakiem nowej linii w protokole tekstowym). Trzecią, rzadziej używaną opcją jest ustalenie stałego rozmiaru każdej wiadomości. Możliwe są również kombinacje tych opcji - na przykład nagłówek o stałym rozmiarze, który zawiera wartość długości.

  • SOCK_DGRAM: Pojedyncze recvwywołanie zawsze zwraca pojedynczy datagram.

Czy istnieje sposób, aby bufor nie miał ustalonej ilości miejsca, aby móc dodawać do niego dane bez obawy, że zabraknie miejsca?

Nie. Możesz jednak spróbować zmienić rozmiar bufora za pomocą realloc()(jeśli pierwotnie przydzielono go za pomocą malloc()lub calloc()).

kawiarnia
źródło
1
Na końcu wiadomości w protokole, którego używam, znajduje się znak „/ r / n / r / n”. I mam pętlę do while, w środku wywołuję recv, umieszczam wiadomość na początku recv_buffer. a moja instrukcja while wygląda następująco while (((! (strstr (recv_buffer, "\ r \ n \ r \ n")); Moje pytanie brzmi: czy jest możliwe, aby jeden recv uzyskał "\ r \ n" i next recv get "\ r \ n", aby mój warunek while nigdy się nie
spełnił
3
Tak to jest. Możesz rozwiązać ten problem, zapętlając się, jeśli nie masz pełnej wiadomości, i upychając bajty z następnej recvdo bufora po częściowej wiadomości. Nie powinieneś używać strstr()na surowym buforze wypełnionym przez recv()- nie ma gwarancji, że zawiera on terminator nul, więc może to spowodować strstr()awarię.
kawiarnia
3
W przypadku UDP nie ma nic złego w wysyłaniu pakietów UDP powyżej 1400 bajtów. Fragmentacja jest całkowicie legalna i stanowi fundamentalną część protokołu IP (nawet w IPv6, ale zawsze początkowy nadawca musi przeprowadzić fragmentację). W przypadku UDP zawsze oszczędzasz, jeśli używasz bufora o rozmiarze 64 KB, ponieważ żaden pakiet IP (v4 lub v6) nie może mieć rozmiaru powyżej 64 KB (nawet po pofragmentowaniu), a to nawet obejmuje nagłówki IIRC, więc dane zawsze będą na pewno poniżej 64 KB.
Mecki,
1
@caf Czy musisz opróżniać bufor przy każdym wywołaniu recv ()? Widziałem pętlę kodu, zbieranie danych i ponowne zapętlanie, co powinno zebrać więcej danych. Ale jeśli bufor kiedykolwiek się zapełni, czy nie musisz go opróżniać, aby uniknąć naruszenia pamięci z powodu zapisywania ilości pamięci przydzielonej dla bufora?
Alex_Nabu
1
@Alex_Nabu: Nie musisz go opróżniać, o ile pozostało w nim trochę miejsca i nie mówisz, recv()aby zapisać więcej bajtów niż pozostało miejsca.
kawiarnia
16

W przypadku protokołów przesyłania strumieniowego, takich jak TCP, możesz prawie ustawić bufor na dowolny rozmiar. To powiedziawszy, zalecane są wspólne wartości, które są potęgami 2, takie jak 4096 lub 8192.

Jeśli jest więcej danych niż twój bufor, zostanie po prostu zapisany w jądrze do następnego wywołania recv.

Tak, możesz dalej zwiększać swój bufor. Możesz zrobić recv do środka bufora, zaczynając od offsetu idx, zrobiłbyś:

recv(socket, recv_buffer + idx, recv_buffer_size - idx, 0);
R Samuel Klatchko
źródło
6
Siła dwójki może być bardziej wydajna na wiele sposobów i jest zdecydowanie zalecana.
Yann Ramin
3
Rozwijając @theatrus, godną uwagi wydajnością jest to, że operator modulo można zastąpić bitowym i maską (np. x% 1024 == x & 1023), a dzielenie liczb całkowitych można zastąpić operacją przesunięcia w prawo (np. x / 1024 = = x / 2 ^ 10 == x >> 10)
vicatcu
15

Jeśli masz SOCK_STREAMgniazdo, recvpobiera po prostu „do pierwszych 3000 bajtów” ze strumienia. Nie ma jasnych wskazówek, jak duży jest bufor: jedyny moment, w którym wiesz, jak duży jest strumień, to koniec ;-).

Jeśli masz SOCK_DGRAMgniazdo, a datagram jest większy niż bufor, recvwypełnia bufor pierwszą częścią datagramu, zwraca -1 i ustawia errno na EMSGSIZE. Niestety, jeśli protokołem jest UDP, oznacza to utratę reszty datagramu - po części dlatego UDP nazywa się protokołem zawodnym (wiem, że istnieją niezawodne protokoły datagramowe, ale nie są one zbyt popularne - nie mogłem nazwij jeden z rodziny TCP / IP, pomimo tego, że dobrze go znam ;-).

Aby dynamicznie powiększyć bufor, przydziel go początkowo za pomocą malloci używaj reallocw razie potrzeby. Ale to nie pomoże ci ze recvźródła UDP, niestety.

Alex Martelli
źródło
7
Ponieważ UDP zawsze zwraca co najwyżej jeden pakiet UDP (nawet jeśli w buforze gniazda znajduje się wiele pakietów) i żaden pakiet UDP nie może przekraczać 64 KB (pakiet IP może mieć maksymalnie 64 KB, nawet po pofragmentowaniu), użycie bufora 64 KB jest całkowicie bezpieczny i gwarantuje, że nigdy nie stracisz żadnych danych podczas recv na gnieździe UDP.
Mecki
8

W przypadku SOCK_STREAMgniazda rozmiar bufora tak naprawdę nie ma znaczenia, ponieważ pobierasz tylko część oczekujących bajtów i możesz pobrać więcej w następnym wywołaniu. Po prostu wybierz dowolny rozmiar bufora, na jaki Cię stać.

W przypadku SOCK_DGRAMgniazda otrzymasz pasującą część wiadomości oczekującej, a reszta zostanie odrzucona. Możesz uzyskać rozmiar oczekującego datagramu za pomocą następującego ioctl:

#include <sys/ioctl.h>
int size;
ioctl(sockfd, FIONREAD, &size);

Alternatywnie można użyć MSG_PEEKi MSG_TRUNCflagi recv()wezwanie do uzyskania wielkości oczekujących datagramów.

ssize_t size = recv(sockfd, buf, len, MSG_PEEK | MSG_TRUNC);

Musisz MSG_PEEKzajrzeć (nie odebrać) oczekującej wiadomości - recv zwraca rzeczywisty, nie obcięty rozmiar; i nie musisz MSG_TRUNCprzepełniać obecnego bufora.

Wtedy możesz tylko malloc(size)prawdziwy bufor i recv()datagram.

smokku
źródło
MSG_PEEK | MSG_TRUNC nie ma sensu.
Markiz Lorne
3
Chcesz, aby MSG_PEEK zerkał (nie odbierał) oczekującego komunikatu, aby uzyskać jego rozmiar (recv zwraca rzeczywisty, nie obcięty rozmiar) i potrzebujesz MSG_TRUNC, aby nie przepełniał bieżącego buforu. Po uzyskaniu rozmiaru przydzielasz właściwy bufor i odbierasz (nie podglądasz, nie obcinasz) oczekującą wiadomość.
smokku
@Alex Martelli mówi, że 64KB to maksymalny rozmiar pakietu UDP, więc jeśli malloc()dla bufora 64KB MSG_TRUNCjest to niepotrzebne?
mLstudent33,
2
Protokół IP obsługuje fragmentację, więc datagram może być większy niż pojedynczy pakiet - będzie pofragmentowany i przesyłany w wielu pakietach. Także SOCK_DGRAMto nie tylko UDP.
smokku
1

Nie ma absolutnej odpowiedzi na twoje pytanie, ponieważ technologia zawsze musi być związana z konkretną implementacją. Zakładam, że komunikujesz się w protokole UDP, ponieważ rozmiar bufora przychodzącego nie powoduje problemów w komunikacji TCP.

Zgodnie z RFC 768 , rozmiar pakietu (łącznie z nagłówkiem) dla UDP może wynosić od 8 do 65 515 bajtów. Tak więc rozmiar odporny na awarie dla bufora przychodzącego wynosi 65 507 bajtów (~ 64 KB)

Jednak nie wszystkie duże pakiety mogą być prawidłowo trasowane przez urządzenia sieciowe. Więcej informacji można znaleźć w istniejącej dyskusji:

Jaki jest optymalny rozmiar pakietu UDP dla maksymalnej przepustowości?
Jaki jest największy rozmiar bezpiecznego pakietu UDP w Internecie

YeenFei
źródło
-4

16kb ma rację; jeśli używasz gigabitowej sieci Ethernet, każdy pakiet może mieć rozmiar 9 kb.

Andrew McGregor
źródło
3
Gniazda TCP są strumieniami, co oznacza, że ​​recv może zwracać dane zgromadzone z wielu pakietów, więc rozmiar pakietu jest całkowicie nieistotny dla TCP. W przypadku UDP, każde wywołanie recv zwraca co najwyżej jeden pakiet UDP, tutaj rozmiar pakietu jest istotny, ale poprawny rozmiar pakietu to około 64 KB, ponieważ pakiet UDP może (i często będzie) fragmentowany, jeśli jest to wymagane. Jednak żaden pakiet IP nie może przekraczać 64 KB, nawet przy fragmentacji, więc recv na gnieździe UDP może zwrócić co najwyżej 64 KB (a to, co nie zostanie zwrócone, jest odrzucane dla bieżącego pakietu!)
Mecki