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
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:
Zadania:
A.
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
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ł