Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Brian 'Beej' Hall <beej@piratehaven.org>
Tłumaczenie: Bartosz Zapałowski <bartek@klepisko.eu.org>
Biblioteki C++

Trochę zaawansowane techniki

[lekcja] Rozdział 6. Zaawansowana technika obsługi połączeń blokujących, opartych o protokół TCP.
Poniższe wcale nie są zaawansowane, ale już wychodzą poza poziom podstawowy, który już przeszliśmy. Właściwie, jeśli doszedłeś tutaj, masz już całe podstawy programowania sieciowego w Uniksach! Gratulacje!

A więc wkraczamy w nowy, odważny świat niezwykłych rzeczy, których chcesz się nauczyć.

Blokowanie

Blokowanie. Już o tym gdzieś słyszałeś -- ale co to do licha jest? W skrócie, "blokowanie" w technicznym żargonie oznacza "usypianie". Zapewne już to zauważyłeś, że jak uruchamiasz listener, ten po prostu siedzi i czeka aż nadejdzie pakiet. Wszystko co się stało, to wywołanie recvfrom(), a że nie było żadnych danych, to recvfrom() się zablokował (usnął), dopóki nie nadejdą dane.

Wiele funkcji się blokuje.accept() się blokuje. Wszystkie funkcje recv() się blokują. Robią to bo mogą. Gdy utworzysz deskryptor gniazda funkcją socket(), jądro pozwala mu się blokować. Jeśli nie chcesz, by gniazdo się blokowało, musisz wywołać fcntl():
C/C++
#include <unistd.h>
#include <fcntl.h>
//...
sockfd = socket( AF_INET, SOCK_STREAM, 0 );
fcntl( sockfd, F_SETFL, O_NONBLOCK );
//...
Ustawiając gniazdo jako nieblokujące, możesz efektywnie wyciągnąć informacje o gnieździe. Jeśli próbujesz czytaj z nieblokującego gniazda i w tym czasie nie ma danych, gniazdo nie ma prawa się zablokować--zwróci ono -1, a errno zostanie ustawione na EWOULDBLOCK.

Mówiąc ogólnie, taki sposób wyciągania informacji o stanie jest złym pomysłem. Jeśli zostawisz swój program w pętli sprawdzającej w ten sposób czy nie nadeszły dane, zajmniejsz cały czas procesora. Bardziej eleganckie rozwiącanie sprawdzania czy są dane oczekujące do odczytania jest opisane w sekcji o select().

select() -- Zsynchronizowane wielokrotne operacje I/O

Ta funkcja jest w pewnym stopniu dziwna, ale za to bardzo użyteczna. Weźmy za przykład następującą sytuację: jesteś serwerem i chcesz nasłuchiwać na przychodzące połączenia jak również odczytywać dane z połączeń, które już masz.

Nie ma problemu, pewnie powiesz, wystarczy accept() i kilka wywołań recv(). Nie tak szybko, cwaniaczku! Co jeśli się zablokujesz na wywołaniu accept()? Jak zamierzasz w tym samym czasie odczytywać dane? "Użyj nieblokujących gniazd!" Nie ma mowy! Nie chcesz być świnią dla procesora. Co w takim razie?

select() pozwala ci monitorować wiele gniazd w tym samym czasie. Powie ci, które są gotowe do odczytu, które są gotowe do zapisu, oraz które wywołały wyjątki, jeśli naprawdę musisz to wiedzieć.

Bez dalszego mieszania, pokażę ci sposób wywołania select():
C/C++
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select( int numfds, fd_set * readfds, fd_set * writefds,
fd_set * exceptfds, struct timeval * timeout );
Funkcja monitoruje zestawy deksryptorów plików; dokładniej readfds, writefds, i exceptfds. Jeśli chcesz zobaczyć czy możesz czytać ze standardowego wejścia i jakiegoś deskryptora gniazda, sockfd, po prostu dodaj deksryptory plików 0 i sockfd do zestawu readfds. Parametr numfds powinien być ustawiony na wartość o jeden większą od największego deskryptora pliku. W tym przykładzie, powinieneś ustawić to na sockfd+1, ponieważ jest to na pewno większe od standardowego wejścia (0).

Gdy select() powróci, readfds będzie zmodyfikowane tak, by odpowiadało tym deksryptorom plików, które są gotowe do czytania. Możesz je po kolei sprawdzić makrem FD_ISSET(), poniżej.

Przed dalszym zagłębianiem się, powiem jak obsługiwać te zestawy. Każdy zestaw jest typu fd_set. Poniższe makra operują na tym typie:
  • FD_ZERO( fd_set * set )
     -- czyści zestaw
  • FD_SET( int fd, fd_set * set )
     -- dodaje fd do zestawu
  • FD_CLR( int fd, fd_set * set )
     -- usuwa fd z zestawut
  • FD_ISSET( int fd, fd_set * set )
     -- sprawsza, czy fd jest w zestawie
W końcu, co to jest to dziwne struct timeval? Czasami nie chcesz czekać w nieskończoność aż ktoś ci wyśle trochę danych. Może co 96 sekund chcesz wyświetlić "Ciągle działam..." na terminalu nawet jeśli nic się nie stało. Ta struktura czasowa pozwala ci sprecyzować czas przeterminowania. Jeśli czas jest przekroczony a select() nadal nie wykrył żadnego gotowego deskryptora pliku, powróci, tak byś mógł dalej przetwarzać dane.

struct timeval ma następujące pola:
C/C++
struct timeval {
    int tv_sec; // sekundy
    int tv_usec; // mikrosekundy
};
Po prostu ustaw tv_sec na ilośc sekund, jaką mamy odczekać, a v_usec na ilość mikrosekund, jaką mamy odczekać. Tak, napisałem mikrosekund, a nie milisekund. Jest 1000 mikrosekund w jednej milisekundzie, i 1000 milisekund w jednej sekundzie. Stąd jest 1000000 mikrosekund w sekundzie. Dlaczego więc nazywa się to ""usec"? "u" ma wyglądać jak grecka litera Îź (Mu), której używamy jako "mikro". Również gdy funkcja powraca, timeout może być uaktualnione tak, by pokazać ile jeszcze zostało czasu. To zależy od rodzaju Uniksa, którego używasz.

Faaajoowo! Mamy zegar z dokładnością do mikrosekund! W zasadzie, nie licz na to. Standardowy Uniksowy kwant czasu wynosi około 100 milisekund, więc możesz czekać trochę tak długo, niezależnie od tego jak mała dałeś wartość w struct timeval.

Inne pola zainsteresowania: jeśli ustawisz pola w swojej strukturze struct timeval na 0, select() powróci natychmiast, efektywnie pobierając informacje o wszystkich deskryptorach plików w twoim zestawie. Jeśli ustawisz parametr timeout na NULL, funkcja nigdy nie powróci z powodu przeterminowania, a więc będzie czekała, dopóki nie odkryje, że przynajmniej jeden deskryptor pliku jest gotowy. Jeśli nie zależy ci na czekaniu na dany zestaw, może go po prostu wstawić ja NULL w wyołaniu select().

Poniższy kawałek kodu czeka 2,5 sekundy na cokolwiek, żeby się pojawiło na standardowym wejściu:
C/C++
/*
    ** select.c -- pokaz użycia select()
    */

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define STDIN 0  // deskryptor pliku dla standardowego wejścia

int main( void )
{
    struct timeval tv;
    fd_set readfds;
   
    tv.tv_sec = 2;
    tv.tv_usec = 500000;
   
    FD_ZERO( & readfds );
    FD_SET( STDIN, & readfds );
   
    // nie martw się o writefds i exceptfds:
    select( STDIN + 1, & readfds, NULL, NULL, & tv );
   
    if( FD_ISSET( STDIN, & readfds ) )
         printf( "A key was pressed!\n" );
    else
         printf( "Timed out.\n" );
   
    return 0;
}
Jeśli używasz terminala buforującego linię, klawisz, który powinieneś wciśnąć to RETURN, albo zostanie przekroczony dozwolony czas.

Niektórzy z was mogą myśleć, że jest to świetny sposób na czekania na dane na gniazdach datagramowych -- i macie rację: mogą. Niektóre z Uniksów mogą korzystać z select w ten sposób, inne nie. Powienieś zobaczyć co mówi na ten temat strona podręcznika systemowego, jeśli chcesz z tego korzystać.

Niektóre Uniksy uaktualniają czas w twoim struct timeval by pokazać ile czasu jeszcze zostało przed przeterminowaniem. Ale inne nie. Nie bazuj na tym założeniu, jeśli chcesz być przenośny (użyj gettimeofday() jeśli chcesz śledzić mijający czas. Wiem, jest to pokręcone, ale tak już jest).

Co się dzieje, gdy gniazdo w stanie odczytu zamknie połączenie? W takim przypadku select() powraca z tym deskryptorem gniazda zaznaczonym jako "gotowe do odczytu". Gdy już wywołasz recv() na nim, recv() zwróci 0. W taki sposób wiesz, że klient zamknął połączenie.

Jeszcze jedna uwaga o select(): jeśli masz gniazdo, które nasłuchuje na połączenia, możesz sprawdzić, czy są nowe połączenia, dodając deskryptor tego gniazda do zestawu readfds.

I to, moi przyjaciele, było krótkie omówienie wszechmocnego select().

Ale, zgodnie z żądaniami, tu jest dogłębny przykład. Niestety, różnica pomiędzy powyższymi krótkimi, prostymi przykładami a tym tutaj jest znaczna. Ale mimo to przejrzyj ten przykład i przeczytaj opis, który po nim następuje.

Ten program działa jak prosty, wieloużytkownikowy serwer chat. Uruchom go w jednym oknie, potem zatelnetetuj się na niego ("telnet hostname 9034") kilkakrotnie z różnych okien. Gdy coś napiszesz w jednej sesji telneta, ten tekst powinien się pojawić w każdej innej sesji.
C/C++
/*
    ** selectserver.c -- fajoski, wieloosobowy serwer chat
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 9034   // port, na którym nasłuchujemy

int main( void )
{
    fd_set master; // główna lista deskryptorów plików
    fd_set read_fds; // pomocnicza lista deskryptorów dla select()
    struct sockaddr_in myaddr; // adres serwera
    struct sockaddr_in remoteaddr; // adres klienta
    int fdmax; // maksymalny numer deskryptora pliku
    int listener; // deskryptor gniazda nasłuchującego
    int newfd; // nowozaakceptowany deskryptor gniazda
    char buf[ 256 ]; // bufor na dane pochodzące od klienta
    int nbytes;
    int yes = 1; // dla setsockopt() SO_REUSEADDR, patrz niżej
    int addrlen;
    int i, j;
   
    FD_ZERO( & master ); // wyczyść główny i pomocniczy zestaw
    FD_ZERO( & read_fds );
   
    // utwórz gniazdo nasłuchująceo
    if(( listener = socket( AF_INET, SOCK_STREAM, 0 ) ) == - 1 ) {
        perror( "socket" );
        exit( 1 );
    }
   
    // zgub wkurzający komunikat błędu "address already in use"
    if( setsockopt( listener, SOL_SOCKET, SO_REUSEADDR, & yes,
    sizeof( int ) ) == - 1 ) {
        perror( "setsockopt" );
        exit( 1 );
    }
   
    // bind
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = INADDR_ANY;
    myaddr.sin_port = htons( PORT );
    memset( &( myaddr.sin_zero ), '\0', 8 );
    if( bind( listener,( struct sockaddr * ) & myaddr, sizeof( myaddr ) ) == - 1 ) {
        perror( "bind" );
        exit( 1 );
    }
   
    // listen
    if( listen( listener, 10 ) == - 1 ) {
        perror( "listen" );
        exit( 1 );
    }
   
    // dodaj gniazdo nasłuchujące do głównego zestawu
    FD_SET( listener, & master );
   
    // śledź najwyższy numer deskryptora pliku
    fdmax = listener; // póki co, ten jest największy
   
    // pętla główna
    for(;; ) {
        read_fds = master; // copy it
        if( select( fdmax + 1, & read_fds, NULL, NULL, NULL ) == - 1 ) {
            perror( "select" );
            exit( 1 );
        }
       
        // przejdź przez obecne połączenia szukając danych do odczytania
        for( i = 0; i <= fdmax; i++ ) {
            if( FD_ISSET( i, & read_fds ) ) { // mamy jednego!!
                if( i == listener ) {
                    // obsłuż nowe połączenie
                    addrlen = sizeof( remoteaddr );
                    if(( newfd = accept( listener,( struct sockaddr * ) & remoteaddr,
                    & addrlen ) ) == - 1 ) {
                        perror( "accept" );
                    } else {
                        FD_SET( newfd, & master ); // dodaj do głównego zestawu
                        if( newfd > fdmax ) { // śledź maksymalny
                            fdmax = newfd;
                        }
                        printf( "selectserver: new connection from %s on "
                        "socket %d\n", inet_ntoa( remoteaddr.sin_addr ), newfd );
                    }
                } else {
                    // obsłuż dane od klienta
                    if(( nbytes = recv( i, buf, sizeof( buf ), 0 ) ) <= 0 ) {
                        // błąd lub połączenie zostało zerwane
                        if( nbytes == 0 ) {
                            // połączenie zerwera
                            printf( "selectserver: socket %d hung up\n", i );
                        } else {
                            perror( "recv" );
                        }
                        close( i ); // papa!
                        FD_CLR( i, & master ); // usuń z głównego zestawu
                    } else {
                        // mamy trochę danych od klienta
                        for( j = 0; j <= fdmax; j++ ) {
                            // wyślij do wszystkich!
                            if( FD_ISSET( j, & master ) ) {
                                // oprócz nas i gniazda nasłuchującego
                                if( j != listener && j != i ) {
                                    if( send( j, buf, nbytes, 0 ) == - 1 ) {
                                        perror( "send" );
                                    }
                                }
                            }
                        }
                    }
                } // jakie to BRZYDKIE!!
            }
        }
    }
   
    return 0;
}
Zauważ, że użyłem dwóch zestawów w kodzie: master i read_fds. Pierwszy, master, przechowuje wszystkie deskryptory gniazda, które są aktulanie połączone, jak również deskryptor gniazda nasłuchującego na połączenia.

Powodem użycia zestawu master jest to, że select() zmienia podany zestaw tak, by znajdowały się w nim tylko te gniazda, które są gotowe do czytania. Ponieważ muszę mieć pełną listę połączeń niezależnie od wywołania select(), muszą ją przechowywać w bezpiecznym miejscu. W ostatniej chwili kopiuję zawartość master do read_fds, a dopiero potem wywołuję select().

Ale czy nie oznacza to, że za każdym nowym połączeniem muszę je dodać do zestawu master? Dokładnie! I za każdym razem, gdy połączenie zostanie zamknięte muszę usunąć je z zestawu master? Tak, dokładnie.

Zauważ, że sprawdzam, kiedy gniazdo listener jest gotowe do odczytu. Kiedy jest, oznacza to, że mam nowe, oczekujące połączenie, i wtedy je akceptuję (accept()) i dodaję je do zestawu master. Podobnie, gdy połączenie z klientem jest gotowe do odczytu, a recv() zwraca 0 wiem, że klient zamknął połączenie, a ja muszę usunąć je z zestawu master.

Jeśli natomiast recv() zwróci wartość niezerową, wiem, że pewna ilość danych została odebrana. Więc biorę ją i przechodzę przez całą listę master i wysyłam te dane do reszty połączonych klientów.

I to, moi przyjaciele, jest mniej niż prosty przegląd tajemniczej funckji select().

Radzenie sobie z niepełnym wysyłaniem

Pamiętasz, jak w sekcji o send(), powyżej, powiedziałem, że send() może nie wysłać wszystkich danych, które mu podałeś? Na przykład, chcesz, że wysłał 512 bajtów, ale on zwraca 412. Co się stało z pozostałymi 100 bajtami?

Są one ciągle w twoim małym buforze i czekają na wysłanie. Z pwodów niezależnych od ciebie, jądro zdecydowało nie wysyłać wszystkich danych za jednym razem, i teraz, przyjacielu, wszystko zależy od ciebie czy wyśwlesz resztę danych.

Mógłbyś napisać funckcję jak ta poniżej, która zrobi to za ciebie:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

int sendall( int s, char * buf, int * len )
{
    int total = 0; // ile bajtów już wysłaliśmy
    int bytesleft = * len; // ile jeszcze zostało do wysłania
    int n;
   
    while( total < * len ) {
        n = send( s, buf + total, bytesleft, 0 );
        if( n == - 1 ) { break; }
        total += n;
        bytesleft -= n;
    }
   
    * len = total; // zwróć ilość faktycznie wysłanych bajtów
   
    return n ==- 1 ?- 1: 0; // zwróć -1 w przypadku błędu, 0, gdy się powiedzie
}
W tym przykładzie s jest gniazdem, do którego chcesz wysłać dane. buf jest buforem zawierającym dane, a len jest wskaźnikiem do zmiennej typu int, zawierającej ilość bajtów, jaką zajmują dane w buforze.

Funkcja zwraca -1 w przypadku błędu (a errno jest ciągle ustawione z wywołania send()). Także ilość faktycznie wysłanych danych jest zwracana w zmiennej len. Będzie to taka sama liczba, jaką podałeś przy wowoływaniu funkcji, chyba że wystąpi błąd. sendall() zrobi co może, sąpiąc i dysząc, by wysłać dane, ale jeśli wystąpi błąd, funkcja natychmiast powraca.

Dla zachowania całości, oto przykładowe użycie tej funkcji:
C/C++
char buf[ 10 ] = "Beej!";
int len;

len = strlen( buf );
if( sendall( s, buf, & len ) == - 1 ) {
    perror( "sendall" );
    printf( "We only sent %d bytes because of the error!\n", len );
}
Co się dzieje na drugim, odbierającym dane, końcu gdy część pakietu dochodzi? Jeśli pakiety są zmiennej długości, skąd odbiorca wie, gdzie się kończy jeden pakiet, a zaczyna drugi? Yes, real-world scenarios are a royal pain in the donkeys (od tłumacza: niech to ktoś przetłumaczy!!). Prawdopodobnie będziesz musiał owinąć te dane w odpowiednie nagłówki (pamiętasz to z sekcji o enkapsulacji danych na samiusieńkim początku?). Przeczytaj, jeśli chcesz znać szczegóły!!

Enkapsulacja danych

Co to właściwie znaczy enkapsulować dane? W najprostszym przypadku oznacza to, że dodajesz nagłówek do nich z jakąś informacją identyfikującą dane lub długość pakietu, lub obie te informacje.

Jak powinien wyglądać twój nagłówek? W zasadzie są to binarne dane, które posiadają wszystko to, co powinno się tam znaleść byś mógł dokończyć projekt.

Nieźle. A jakie niejasne.

No dobra. Weźmy za przykład taką sytuację: masz wieloużytkownikowy program chat, który użycia SOCK_STREAM. Gdy użytkownik coś napisze (powie), dwa kawałki informacji muszą być przesłane do serwera: to co było powiedziane oraz kto to powiedział.

Dotąd wszystko pasuje? "Co za problem?" pytasz.

Problem jest taki, że wiadomości mogą być różnych długości. Jedna osoba o nazwie "tom" może powiedzieć "Hi", a inna zwana "Benjamin" może powiedzieć "Hey guys what is up?".

A więc wysyłasz (send()) cały ten towar do klientów tak jak on przychodzi. Strumień wyjściowy będzie wyglądał tak:
t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?
I tak dalej. Jakim cudem klient będzie wiedział, gdzie się jedna wiadomość zaczyna, a inna kończy? Mógłbyś, gdybyś chciał, sprawić by wszystkie wiadomości były tego samego rozmiaru i wtedy dopiero wywołujesz sendall(), który zakodowaliśmy powyżej. Ale to marnuje przepustowość! Nie chcemy wysyłać 1024 bajtów tylko po to, by "tom" mógł powiedzieć "Hi".

Tak więc owijamy dane w mały nagłówek i strukturę pakietu. Zarówno klient jak i serwer wiedzą jak zapakować i rozpakować (czasami jest to określane jako "marshal" i "unmarshal") te dane. Nie patrz teraz, ale zaczynamy definiować protokół, który określi jak klient rozmawia z serwerem!

W tym przypadku załóżmy, że nazwa użytkownika jest stałej długości i składa się z ośmiu znaków, dopełnionych znakiem '\0'. Następnie przyjmijmy, że dane są zmiennej długości, składające się maksymalnie ze 128 znaków. Spójrzmy teraz na przykładową strukturę pakietu, której możemy użyć w tej sytuacji:
  • len (1 bajt, bez znaku) -- całkowita długość pakietu, licząc ośmiobajtową nazwę użytkownika i dane wiadomości.
  • name (8 bajtów) -- nazwa użytkownika, dopełniona znakiem NULL, jeśli potrzeba.
  • chatdata (n-bajtów) -- same dane, nie więcej niż 128 bajtów. Długość pakietu powinna być obliczana jako długość tych danych plus 8 (długość pola nazwy użytkownika, powyżej).
Dlaczego wybrałem 8-bajtowe i 128-bajtowe limity dla powyższych pól? Wziąłem je z powietrza, zakładając, że będą odpowiednio długie. Możliwe, że 8 bajtów jest zbyt dużą restrykcją dla twoich potrzeb, ale możesz mieć 30-bajtowe nazwy użytkownika, jeśli chcesz. Wszystko zależy od ciebie.

Używając powyższe definicji pakietu, pierwszy pakiet składałby się z następujących informacji (w heksach i ASCII):
0A     74 6F 6D 00 00 00 00 00      48 69
  (długość)  T  o  m    (dopełnienie)     H  i

A drugi byłby podobny:
14     42 65 6E 6A 61 6D 69 6E      48 65 79 20 67 75 79 73 20 77 ...
  (długość)  B  e  n  j  a  m  i  n       H  e  y     g  u  y  s     w  ...
(Długość jest przechowywana w Network Byte Order, oczywiście. W tym przypadku jest to tylko jeden bajt, więc nie robi to różnicy, ale mówiąc ogólnie będziesz chciał przechowywać wszystkie binarne liczby w Network Byte Order w twoich pakietach.)

Gdy wysyłasz te dane, powinieneś być bezpieczny i używać komendy podobnej do <sendall(), powyżej, dzięki czemu wiesz, że wszystkie dane zostały wysłane, nawet jeśli potrzeba wykonać wiele wywołań do send(), by wszystkie dane wysłać.

Podobnie, gdy odbierasz dane, musisz włożyć w to trochę więcej pracy. By czuć się bezpiecznym, powinieneś założyć, że możesz odebrać tylko część pakietu (może to być "00 14 42 65 6E" od Bejnamina, ale to wszystko co otrzymamy w tym wywołaniu recv()). Musimy wywoływać recv() i wywoływać, dopóki cały pakiet nie jest odebrany.

Ale jak? Znamy ilość bajtów, które musimy odebrać, by pakiet był kompletny, ponieważ ta informacja jest umieszczona na początku pakietu. Znamy również maksymalny rozmiar pakietu wynoszący 1+8+128, czyli 137 bajtów (ponieważ tak zdefiniowaliśmy pakiet).

Możesz zadeklarować wystarczająco duży ciąg dla dwóch pakietów. Jest to twój roboczy ciąg, w którym będzie rozpakowywał pakiety, podczas gdy one nadchodzą.

Za każdym razem, gdy odbierasz (recv()) dane, musisz je umieścić w buforze roboczym i sprawdzić czy pakiet jest kompletny. Dokładniej, czy ilość bajtów w buforze jest większa niż lub równa długości podanej w nagłówku (+1, ponieważ długość w nagłówku nie wlicza siebie). Jeśli ilość bajtów w buforze jest mniejsza niż 1, pakiet nie jest kompletny, oczywiście. Musisz zrobić specjalny warunek dla tego, ponieważ pierwszy bajt jest śmieciem i nie możesz na nim polegać, by określić długość pakietu.

Gdy już pakiet jest kompletny, możesz z nim zrobić co tylko zechcesz. Użyj go, i usuń z twojego bufora roboczego.

Czy powyższe nie skacze ci jeszcze po głowie? Bo jest też druga możliwość: możesz odebrać za jednym pakietem kawałek kolejnego w jednym wywołaniu recv(). Stąd, masz w buforze roboczym jeden cały pakiet i część pakietu następnego. Potworna sprawa (ale to właśnie dlatego zrobiłeś swój bufor roboczy odpowiednio duży, by przechować dwa pakiety -- w przypadku gdyby się to zdarzyło!).

Ponieważ znasz długość pierwszego pakietu z nagłówna i śledziłeś ilość bajtów w buforze roboczym, możesz odjąć i obliczyć ile bajtów bufora roboczego należy do drugiego, niekompletnego pakietu. Gdy uporasz się z pierwszym, możesz wyczyścić bufor roboczy i przenieść część drugiego pakietu na początek bufora, dzięki czemu będzie on gotowy na kolejne wywołania recv().

(Niektórzy czytelnicy zauważą, że przenoszenie części drugiego pakietu na poczatek bufora roboczego zabiera czas, a program może być tak napisany, że nie będzie tego wymagał przez zaimplementowanie bufora kołowego. Niestety dla innych, dyskusja o buforach kołowych wychodzi poza ramy tego arytkułu. Jeśli nadal jesteś tego ciekaw, dobierz się do książki o strukturach danych.)

Nigdy nie powiedział, że będzie łatwo. No dobra, powiedziałem. I jest: musisz po prostu potrenować i wkrótce będzie to dla ciebie naturalne. Przysięgam na moc Excalibura!
Poprzedni dokument Następny dokument
Tło Klient-Serwer Więcej informacji