Autor Wątek: Odnajdywanie serwera, select, i pełne odbieranie/wysyłanie struktur  (Przeczytany 2595 razy)

Offline LizarD

  • Użytkownik

# Czerwiec 24, 2013, 16:28:21
Witam!

W jaki sposób wygląda odnajdywanie serwera w sieci lokalnej ? Jest podobno na to taki sposób że serwer co jakiś czas wysyła broadcasty ( pakiety na protokole UDP ) z informacją że istnieje, ale jak to wygląda w praktyce, pod jakie adresy wysyła ? Czy to nie jest tak że pod każde np w pętli wysyłane są od adresu 0.0.0.0 do 255.255.255.255 to by chyba zajechało serwer...

Drugie pytanie jest odnośnie korzystania z funkcji select, trochę nie rozumiem tego w praktyce, a więc:
Trzeba utworzyć dwie listy deskryptorów plików jedną pomocniczą a drugą główną:
fd_set master;
 fd_set read_fds;
Pytanie tylko po co ? Dalej czyszczę listy makrem FD_ZERO, w przypadku serwera dodaje gniazdo nasłuchujące do głównej listy, i zaraz przed użyciem select kopiuję główną liste do tej pomocniczej:
while(1)
{
       read_fds = master;
       if( select( fdmax + 1, & read_fds, NULL, NULL, NULL ) == - 1 ) {
            return false;
       }
}
fdmax jest numerem/rozmiarem ostatniego utworzonego deskryptora czyli SOCKET'u, Przy akceptowaniu nowego klienta należy sprawdzić czy nowy SOCKET nie jest "większy" od ostatniego:
newfd = accept(...);

if( newfd > fdmax ) {
       fdmax = newfd;
}
Ale dlaczego w funkcji select dodaje się jedynkę ?

I gdzie otrzymuję wynik funkcji select ? Włąśnie w pomocniczej liście ? W read_fds zostają tylko gniazda z których mogę coś odczytać ?

Trzecie sprawa jest związana z wysyłaniem i odbieraniem danych, żebym zawsze wiedział co i ile tego przychodzi mi od serwera to stosuję tzw nagłówki:
struct PacketHeader
{
          int packet_type;
          int size;
};
Zawsze będę znał rozmiar pierwszych danych, ale pytanie co jak te pierwsze dane nie zostaną do końca wysłane/odebrane ? Jak wysłać resztę ? W przypadku tekstu to nie byłoby problemu, ale strukturę wysyłam tak:
send(socket, (char *)&header, sizeof(struct PacketHeader), 0);I analogicznie odbieram, czy może w TCP nie zawracać sobie tym głowy ?
« Ostatnia zmiana: Czerwiec 24, 2013, 16:31:37 wysłana przez LizarD »

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline yarpen

  • Użytkownik

# Czerwiec 24, 2013, 16:42:25
W jaki sposób wygląda odnajdywanie serwera w sieci lokalnej ? Jest podobno na to taki sposób że serwer co jakiś czas wysyła broadcasty ( pakiety na protokole UDP ) z informacją że istnieje, ale jak to wygląda w praktyce, pod jakie adresy wysyła ?
Ja robie inaczej. Server jedynie slucha, to klienci kiedy szukaja serwerow broadcastuja zapytanie.

Offline Liosan

  • Redaktor

# Czerwiec 24, 2013, 16:49:34
Ale nawrzucałeś tematów do jednego wątku...

W jaki sposób wygląda odnajdywanie serwera w sieci lokalnej ? Jest podobno na to taki sposób że serwer co jakiś czas wysyła broadcasty ( pakiety na protokole UDP ) z informacją że istnieje, ale jak to wygląda w praktyce, pod jakie adresy wysyła ?
Broadcasty, jak sama nazwa wskazuje, nie są wysyłane pod konkretny adres tylko broadcastowane w danej podsieci :) Może serwer broadcastować, chociaż ja w grach widziałem raczej broadcasty robione przez klientów. Serwery raczej są pasywne i nie pytają w koło kto chce z nimi pogadać...

Drugie pytanie jest odnośnie korzystania z funkcji select, trochę nie rozumiem tego w praktyce, a więc:
Trzeba utworzyć dwie listy deskryptorów plików jedną pomocniczą a drugą główną:
(...)
Pytanie tylko po co ?
Nie trzeba, i na przyszłość proponuję poszukać bardziej klarownego tutoriala. Ale można. Master jest po to, żeby trzymać listę obsługiwanych portów, a read_fds jest obiektem "roboczym", który będzie potem wykorzystany żeby się dowiedzieć skąd można czytać. Zresztą sam się tego domyślasz. W każdym razie cytując manpage
Cytuj
On exit, the sets are modified in place to indicate which file descriptors actually changed status.

Wracając do kwestii trzeba-nie-trzeba - jeśli nasłuchujesz na wielu gniazdach to możesz sobie FD trzymać z boku na wektorze, ale pewnie tak jest wygodniej. Jeśli nasłuchujesz na jednym gnieździe (to ma sens bo dzięki temu nasłuchujesz nieblokująco), to możesz jego FD trzymać na zmiennej typu int. Co kto lubi, liczby jak liczby.

fdmax jest numerem/rozmiarem ostatniego utworzonego deskryptora czyli SOCKET'u,Ale dlaczego w funkcji select dodaje się jedynkę ?
Bo tako rzecze manpage:
Cytuj
nfds is the highest-numbered file descriptor in any of the three sets, plus 1.

I analogicznie odbieram, czy może w TCP nie zawracać sobie tym głowy ?
Nie pamiętam dokładnie co warstwa TCP zapewnia, ale w przypadku gier polecałbym nie zawracać sobie tym głowy, a jak napotkasz takie problemy to odsyłaj od nowa całą strukturę :)

Liosan

Offline Xion

  • Redaktor
    • xion.log

# Czerwiec 24, 2013, 17:04:02
Na wszystkie powyższe pytania odnośnie funkcji select() odpowiada dokumentacja...

Offline bies

  • Użytkownik

# Czerwiec 24, 2013, 17:32:25
Trzecie sprawa jest związana z wysyłaniem i odbieraniem danych, żebym zawsze wiedział co i ile tego przychodzi mi od serwera to stosuję tzw nagłówki:
struct PacketHeader
{
          int packet_type;
          int size;
};
Zawsze będę znał rozmiar pierwszych danych, ale pytanie co jak te pierwsze dane nie zostaną do końca wysłane/odebrane ? Jak wysłać resztę ? W przypadku tekstu to nie byłoby problemu, ale strukturę wysyłam tak:
send(socket, (char *)&header, sizeof(struct PacketHeader), 0);I analogicznie odbieram, czy może w TCP nie zawracać sobie tym głowy ?
Nie możesz nie zawracać sobie głowy. Najłatwiej skopiuj tą strukturę do char[] i stamtąd wysyłaj / odbieraj. Można też robić jakieś sztuczki z (char *)&struct+offset.

Offline magik6000

  • Użytkownik

# Czerwiec 24, 2013, 19:02:49
@up, może zignorować, a w razie fragmentacji/utraty aż 8 bajtowego pakietu spokojnie można rozłączyć się z takim userem.. noo chyba, że przewidujemy użytkowników korzystających z IPAC. Szansa na fragmentację tak małego jest nikła, między innymi dla tego, że sam nagłówek IP i TCP jest od nich większy, więc taki zabieg byłby kompletnie nieopłacalny.. co do straconych pakietów, jest XXI wiek i przez 4lata istnienia mojego routera licznik straconych pakietów pokazuje liczbę 5


// mała korekta techniczna -Xirdus
« Ostatnia zmiana: Czerwiec 24, 2013, 19:38:37 wysłana przez Xirdus »

Offline Rolek

  • Użytkownik

# Czerwiec 24, 2013, 19:47:37
@up TCP gwarantuje, że dane dotrą w całości i w kolejności takiej, w jakiej zostały wysłane (lub przy całkowitym zaniku komunikacji połączenie zostanie uznane za zerwane).
Mankamentem jest to, że przesyłane dane teoretycznie stanowią jednolity strumień; wyodrębnienie z tego strumienia, pojedynczych wiadomości należy do zadań protokołu warstwy aplikacji (o ile definiuje, że dane mają być przesyłane w taki sposób).
Pakiet TCP nie jest wysyłany od razu po wywołaniu send, tylko po zebraniu ilości, którą warto wysłać w jednym pakiecie (lub po minięciu timeouta). Przy częstym wysyłaniu może się zdarzyć, że będzie brakowało np. 4 bajtów do MTU ;) i wtedy połowa 8bajtowej struktury załapie się na koniec poprzedniego pakietu a reszta poleci następnym.
Więc odbieranie dane należy buforować aż do zebrania ilości, którą będzie można przetworzyć.

Offline LizarD

  • Użytkownik

# Czerwiec 24, 2013, 19:57:21
Nie możesz nie zawracać sobie głowy. Najłatwiej skopiuj tą strukturę do char[] i stamtąd wysyłaj / odbieraj. Można też robić jakieś sztuczki z (char *)&struct+offset.
recv zapisuje dane na początek bufora więc chyba powinienem zrobić tak:
...
char* bufor = new char[sizeof(PacketHeader)];
int offset = 0;
while( offset = recv(socket, bufor + offset, sizeof(PacketHeader) - offset, 0);
{
}

PacektHeader* header = (PacektHeader*) bufor;

@up TCP gwarantuje, że dane dotrą w całości i w kolejności takiej, w jakiej zostały wysłane (lub przy całkowitym zaniku komunikacji połączenie zostanie uznane za zerwane).
Mankamentem jest to, że przesyłane dane teoretycznie stanowią jednolity strumień; wyodrębnienie z tego strumienia, pojedynczych wiadomości należy do zadań protokołu warstwy aplikacji (o ile definiuje, że dane mają być przesyłane w taki sposób).
Pakiet TCP nie jest wysyłany od razu po wywołaniu send, tylko po zebraniu ilości, którą warto wysłać w jednym pakiecie (lub po minięciu timeouta). Przy częstym wysyłaniu może się zdarzyć, że będzie brakowało np. 4 bajtów do MTU ;) i wtedy połowa 8bajtowej struktury załapie się na koniec poprzedniego pakietu a reszta poleci następnym.
Więc odbieranie dane należy buforować aż do zebrania ilości, którą będzie można przetworzyć.
Czyli jaka jest minimalna danych które na na pewno zostaną wysłane ?

A co do broadcastowania, to z tego co wyczytałem to jest to dostępne tylko i wyłącznie przy protokole UDP więc do serwera muszę stworzyć kolejny "serwer" na UDP, i identycznie w kliencie ?

Offline Xion

  • Redaktor
    • xion.log

# Czerwiec 24, 2013, 19:59:50
Cytuj
Więc odbieranie dane należy buforować aż do zebrania ilości, którą będzie można przetworzyć.
Tak, potrzebujemy jeszcze większego bufferbloat. Po to są bufory systemowe, żeby przy takim formacie pakietów jak powyżej można było zrobić:
recv(sfd, &type, sizeof(int));
recv(sfd, &size, sizeof(int));
recv(sfd, &data, size);

Offline bies

  • Użytkownik

# Czerwiec 24, 2013, 21:46:39
recv zapisuje dane na początek bufora więc chyba powinienem zrobić tak:
...
char* bufor = new char[sizeof(PacketHeader)];
int offset = 0;
while( offset = recv(socket, bufor + offset, sizeof(PacketHeader) - offset, 0);
{
}

PacektHeader* header = (PacektHeader*) bufor;
Prawie, w tej pętli offset powinien się zwiększać o wynik recv().

Czyli jaka jest minimalna danych które na na pewno zostaną wysłane ?
Nie ma. W przypadku gniazdek blokujących oczywiście poczekasz aż wyśle Ci całość danych ale w przypadku nieblokujących możesz dostać błąd (EWOULDBLOCK) nawet na pierwszym bajcie. W realnym świecie to się oczywiście nie zdarza (czyt. prawdopodobieństwo jest przyzerowe). Z resztą w przypadku send() najłatwiej właśnie użyć blokujących gniazdek. Bufory systemowe w praktyce wyeliminują blokowanie.

Tak, potrzebujemy jeszcze większego bufferbloat. Po to są bufory systemowe, żeby przy takim formacie pakietów jak powyżej można było zrobić:
recv(sfd, &type, sizeof(int));
recv(sfd, &size, sizeof(int));
recv(sfd, &data, size);
Nie. Jak pisał Rolek możesz trafić na końcówkę MTU i pierwsze recv() odczyta np. 2 bajty z inta. I to w realnym świecie się może zdarzyć szczególnie jak występuje ciągła komunikacja na jednym gniazdku a nie tylko proste pytanie - odpowiedź - rozłączenie.

Offline Xion

  • Redaktor
    • xion.log

# Czerwiec 24, 2013, 23:39:24
Jeśli recv odczytać mniej bajtów niż podaliśmy, to rzecz jasna powtarzamy to w pętli. Sadziłem, że ta część jest oczywista :P

Offline LizarD

  • Użytkownik

# Czerwiec 27, 2013, 22:54:37
Czyli że ostatecznie jak jest z tym buforowaniem i nie pełnym wysłaniem, w przypadku gniazd blokujących ? Mam się o to martwić czy nie ? Skoro nie ma minimalnej ilości danych do pełnego wysłania.

Czy MS nie przygotował przypadkiem jakiejś własnej biblioteki ułatwiającej korzystanie z winsock ? Na tej stronie http://msdn.microsoft.com/en-us/library/windows/desktop/ms737593%28v=vs.85%29.aspx jest kompletny kod serwera, jest on troche inny niż w innych tutorialach, chodzi głównie o nawiązywanie połączenia zwykle wygląda to tak:
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    memset( (void*)&saddr, 0, sizeof(saddr) );
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(10000);
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);

A MS podaje coś takiego, oraz kożsyta z pliku ws2tcpip.h
ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    // Resolve the server address and port
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Create a SOCKET for connecting to server
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }