1 Komunikacja między procesami

Należy zapoznać się z poniższym tekstem i rozwiązać zadania A - D.

Zajmiemy się dokładniej komunikacją za pomocą tzw. gniazd. Sama idea gniazd została opracowana na Uniwersytecie Kalifornijskim w Berkley w celu zapewnienia możliwości komunikacyjnych w systemie Unix. Opracowany tam interfejs programowania nosi nazwę "Berkley socket interface" i jest stopniowo z mniejszymi lub większymi zmianami przejmowany przez kolejne systemy operacyjne. Największa zaleta jaką daje korzystanie z gniazd to w miarę prosty i przejrzysty interfejs niezależny od warstwy komunikacyjnej (oczywiście stosunkowo najmniej wygodnie korzysta się z interfejsu gniazd w czystym C, w Javie czy C# jest jeszcze prościej).

Skoro gniazda są tylko interfejsem programowania służącym do oprogramowania transmisji sieciowych, to zajmijmy się przez chwilę samymi protokołami i ich charakterystykami.

Bez wchodzenia w szczegóły, możemy podzielić protokoły na

Inna ważna linia dzieli protokoły na Każdy protokół może charakteryzować się posiadaniem lub brakiem następujących cech

2 Podstawy biblioteki Winsock

Gniazda zaadaptowano do systemu Windows pod nazwą Winsock. Za pomocą tej biblioteki można tworzyć aplikacje korzystające z wielu protokołów sieciowych. Dzięki prostemu interfejsowi, z perspektywy programisty korzystanie z różnych protokołów wygląda niemal identycznie.

Przy nawiązywaniu połączeń jedna strona jest "serwerem", który oczekuje połączenia, druga strona jest "klientem", który nawiązuje połączenie z "serwerem".

Poniżej zaprezentowano kod prostego serwera i klienta, którzy wymieniają się wiadomościami. Wykorzystano protokół TCP/IP.

Serwer w pętli oczekuje na klientów i po nawiązaniu połączenia tworzy nowy wątek, który zajmuje się odbieraniem komunikatów i odsyłaniem ich z powrotem (to tylko przykład!). Klient wymaga oczywiście parametru, którym jest nazwa serwera. Klient nawiązuje połączenie i wysyła do serwera komunikat, po czym czeka na jego echo.

Kod programu serwera:

// prosty moduł serwera
// komunikacji za pomocą Winsock
// użycie: server.exe

#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>

#define DEFAULT_PORT 5000
#define DEFAULT_BUFFER 4096

// tylko Visual C++
#pragma comment(lib, "ws2_32.lib")

// funkcja: wątek do komunikacji z klientem
DWORD WINAPI ClientThread(LPVOID lpParam)
{
    SOCKET sock = (SOCKET)lpParam;
    char szBuf[DEFAULT_BUFFER];
    int ret,
        nLeft,
        idx;

// serwer będzie oczekiwał na informacje od klienta
while(1)
{
    // najpierw odbierz dane 
    ret = recv(sock, szBuf, DEFAULT_BUFFER, 0);
    if (ret == 0)
        break; 
    else if (ret == SOCKET_ERROR)
    {
        printf("błąd funkcji recv(): %d\n", WSAGetLastError());
        break; 
    } 

    szBuf[ret] = '\0';
    printf("RECV: '%s'\n", szBuf);

    // następnie odeślij te dane, poporcjuj jeśli trzeba
    // (niestety send() może nie wysłać wszystkiego)
    nLeft = ret;
    idx = 0;
    while(nLeft > 0)
    {
        ret = send(sock, &szBuf[idx], nLeft, 0);
        if (ret == 0)
            break;
        else if (ret == SOCKET_ERROR)
        {
            printf("błąd funkcji send(): %d\n", WSAGetLastError());
            break; 
        } 
        nLeft -= ret;
        idx += ret;
    }
} 
return 0; 
}


int main(int argc, char *argv[])
{
    WSADATA wsd;
    SOCKET sListen,
    sClient;
    int iAddrSize;
    HANDLE hThread;
    DWORD dwThreadID;
    struct sockaddr_in local, client; 
    struct hostent *host = NULL;

// inicjuj Winsock 2.2 
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
{
    printf("Błąd ładowania Winsock 2.2!\n");
    return 1;
}

// twórz gniazdo do nasłuchu połączeń klientów
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (sListen == SOCKET_ERROR) 
{
    printf("Błąd funkcji socket(): %d\n", WSAGetLastError());
    return 1;
}

// wybierz interfejs (warstwę komunikacyjną)
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(DEFAULT_PORT); 
if (bind(sListen, (struct sockaddr *)&local, sizeof(local)) == SOCKET_ERROR)
{
    printf("Błąd funkcji bind(): %d\n", WSAGetLastError());
    return 1;
}


// nasłuch
host = gethostbyname("localhost");
if (host == NULL)
{
    printf("Nie udało się wydobyć nazwy serwera\n");
    return 1;
}

listen(sListen, 8);
printf("Serwer nasłuchuje.\n");
printf("Adres: %s, port: %d\n", host->h_name, DEFAULT_PORT);

// akceptuj nadchodzące połączenia
while (1)
{
    iAddrSize = sizeof(client);
    sClient = accept(sListen, (struct sockaddr *)&client, &iAddrSize);
    if (sClient == INVALID_SOCKET)
    {
        printf("Błąd funkcji accept(): %d\n", WSAGetLastError());
        return 1;
    }
    printf("Zaakceptowano połączenie: serwer %s, port %d\n", 
        inet_ntoa(client.sin_addr), ntohs(client.sin_port));
    hThread = CreateThread(NULL, 0, ClientThread,
        (LPVOID)sClient, 0, &dwThreadID);
    if (hThread == NULL)
    {
        printf("Błąd funkcji CreateThread(): %d\n", WSAGetLastError());
        return 1;
    } 
    CloseHandle(hThread); 
} 
closesocket(sListen);

WSACleanup();
return 0;
}
Kod programu klienta:
// prosty moduł klienta
// komunikacji za pomocą Winsock
// użycie: klient.exe -s:IP

#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>

#define DEFAULT_COUNT 5
#define DEFAULT_PORT 5000
#define DEFAULT_BUFFER 4096
#define DEFAULT_MESSAGE "Wiadomość testowa, Systemy Operacyjne 2002"

// tylko Visual C++
#pragma comment(lib, "ws2_32.lib")

char szServer[128], szMessage[1024];

// funkcja sposob_uzycia
void sposob_uzycia()
{
    printf("Klient.exe -s:IP\n");
    ExitProcess(1);
}

void WalidacjaLiniiPolecen(int argc, char **argv)
{
    int i;

    if (argc < 2)
    {
        sposob_uzycia();
    }

    for (i=1; i<argc; i++)
    {
        if (argv[i][0] == '-')
        {
        switch (tolower(argv[i][1]))
        {
        case 's':
            if (strlen(argv[i]) > 3)
            strcpy(szServer, &argv[i][3]);
            break;
        default:
            sposob_uzycia();
            break;
        }
        }
    }
}

int main(int argc, char *argv[])
{
    WSADATA wsd;
    SOCKET sClient;
    char szBuffer[DEFAULT_BUFFER];
    int ret, i;
    struct sockaddr_in server;
    struct hostent *host = NULL;

// linia poleceń

WalidacjaLiniiPolecen(argc, argv);

// inicjuj Winsock 2.2 
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
{
    printf("Błąd ładowania Winsock 2.2!\n");
    return 1;
}

strcpy(szMessage, DEFAULT_MESSAGE);

// twórz gniazdo do nasłuchu połączeń klientów
sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (sClient == INVALID_SOCKET) 
{
    printf("Błąd funkcji socket(): %d\n", WSAGetLastError());
    return 1;
}

// wybierz interfejs 
server.sin_addr.s_addr = inet_addr(szServer);
server.sin_family = AF_INET;
server.sin_port = htons(DEFAULT_PORT); 

// jeśli adres nie był w postaci xxx.yyy.zzz.ttt
// to spróbuj go wydobyć z postaci słownej

if (server.sin_addr.s_addr == INADDR_NONE)
{
    host = gethostbyname(szServer);
    if (host == NULL)
    {
        printf("Nie udało się wydobyć nazwy serwera: %s\n", szServer);
        return 1;
    }
    CopyMemory(&server.sin_addr, host->h_addr_list[0], host->h_length);
}

if (connect(sClient, (struct sockaddr *)&server, sizeof(server)) == SOCKET_ERROR)
{
    printf("Błąd funkcji connect(): %d\n", WSAGetLastError());
    return 1;
}

// wysyłaj i odbieraj dane

for (i=0; i<DEFAULT_COUNT; i++)
{
    ret = send(sClient, szMessage, strlen(szMessage), 0);
    if (ret == 0)
        break;
    else if (ret == SOCKET_ERROR)
    {
        printf("Błąd funkcji send(): %d\n", WSAGetLastError());
        return 1;
    }
    printf("Wysłano %d bajtów\n", ret);

    ret = recv(sClient, szBuffer, DEFAULT_BUFFER, 0);
    if (ret == 0)
        break;
    else if (ret == SOCKET_ERROR)
    {
        printf("Błąd funkcji recv(): %d\n", WSAGetLastError());
        return 1;
    }
    szBuffer[ret] = '\0';
    printf("RECV [%d bajtów]: '%s'\n", ret, szBuffer);
}
closesocket(sClient); 

WSACleanup();
return 0;
}
Komentarz:
Struktura obu programów jest dość podobna. Oba tworzą odpowiednie gniazda, przy czym: Oba programy wymieniają się krótkimi informacjami za pomocą funkcji send() i recv(). Serwer nasłuchuje na zadanym porcie, zaś po nawiązaniu połączenia z klientem tworzy nowe gniazdo i nowy wątek, po czym komunikuje się przez nowo utworzone gniazdo w nowym wątku. Dzięki temu serwer może obsługiwać wielu klientów jednocześnie, bez względu na to jak długo trwałoby obsługiwanie pojedyńczego klienta.

Zadania:

A.

B.

C. Użyć mechanizmu gniazd do przesyłania plików binarnych. Zaprogramować odpowiednie programy serwera i klienta. Serwer nasłuchuje na zadanym porcie, po nawiązaniu połączenia z klientem wysyła mu

Klient odtwarza plik wedle informacji serwera.

D. Przeczytać specyfikację jakiegoś "logicznego" protokołu sieciowego (FTP, HTTP, TELNET). Serwer takiego protokołu oczekuje od klientów komunikatów sformułowanych według odpowiedniej składni. Napisać prostego klienta sieciowego, który będzie potrafił