Мета роботи: вивчити роботу з потоковими сокетами в режимі опиту. Створити сервер котрий буде відповідати на запити клієнтів.

4.1 Теоретичні відомості

Socket API був вперше реалізований в операційній системі UNIX. Зараз цей програмний інтерфейс доступний практично в будь-якій операційній системі. Хоча всі реалізації чимось відрізняються один від одного, основний набір функцій у них збігається. Спочатку сокети використовувалися в програмах на C/C++, але в даний час вони є майже в всіх нових мовах програмування (Perl, С#, Java та ін.).

Сокети надають дуже потужний і гнучкий механізм взаємодії між процесами (IPC). Вони можуть використовуватися для організації взаємодії програм на одному комп'ютері, по локальній мережі або через Інтернет, що дозволяє вам створювати розподілені додатки різної складності. Крім того, з їх допомогою можна організувати взаємодію з програмами, що працюють під управлінням інших операційних систем.

Сокети підтримують багато стандартних мережевих протоколів (конкретний їх список залежить від реалізації) і надають уніфікований інтерфейс для роботи з ними. Найбільш часто сокети використовуються для роботи в IP-мережах.

Сокети, незалежно від виду, поділяються на три типи: потокові, сирі і дейтаграмні. Потокові сокети працюють з установкою з'єднання, забезпечуючи надійну ідентифікацію обох сторін і гарантують цілісність і успішність доставки даних, спираючись на протокол TCP. Дейтаграмні сокети працюють без встановлення з'єднання і не забезпечують ні ідентифікації відправника, ні контролю успішності доставки даних, зате вони швидше потокових, спираючись на протокол UDP. Сирі сокети, вони надають можливість ручного формування TCP \ IP-пакетів.

Також існує 2 види сокетів:

- синхронні – затримують управління на час виконання операції;

- асинхронні – повертають управління, але продовжують виконувати роботу в фоні та після закінчення повідомляють про це.

4.1.1 Робота з сокетами

В даному випадку розглянемо роботу з синхронними потоковими сокетами (TCP).

Реалізація сервера:

1) підготувати бібліотеку до використання;

2) створити об’єкт типу Socket;

3) зв’язати цей об’єкт з локальною адресою/портом;

4) перейти в режим очікування;

5) витягнути із черги запит на з’єднання;

6) прийняти/передати дані;

7) закрити сокети, звільнити ресурси.

Реалізація клієнта:

1) підготувати бібліотеку до використання;

2) створити об’єкт типу Socket;

3) з’єднатися з сервером;

4) прийняти/передати дані;

5) закрити сокети, звільнити ресурси.

4.1.2 Реалізація сервера

Крок перший. «Підготовка бібліотеки»

Для роботи з бібліотекою Winsock 2.x потрібно підключити директиву #include <winsock2.h> та підготувати саму бібліотеку. Для цього використаємо функцію WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData).

- wVersionRequestedпараметр типу WORD, приймає значення версії сокетів (старший байт слова – номер версії, молодший – номер під версії);

- lpWSAData – структура типу WSADATA, в котру при успішній ініціалізації буде занесена інформація про виробника бібліотеки.

Приклад:

WORD sockVersion;

WSADATA wsaData;

sockVersion = MAKEWORD(2,2);

WSAStartup(sockVersion, &wsaData);

В UNIX подібних системах потрібно підключити такі директиви:

#include <sys/types.h>

#include <sys/socket.h>

Крок другий. «Створення об’єкту типу Socket»

Створимо об’єкт типу socket. Використовуємо функцію SOCKET s (int af, int type, int protocol).

- аf - сімейство протоколів (Зазвичай використовують AF_INET, тобто Internet протоколи.);

- type – тип сокету – спосіб передачі даних через мережу. В даному курсі ми будемо використовувати: SOCK_STREAM з встановленням з’єднання, використовується в основному з TCP та SOCK_DFRAM без встановлення з’єднання, використовуються з UDP;

- protocol – протокол для передачі даних.

Функція повертає дескриптор сокету, інакше INVALID_SOCKET.

Приклад:

SOCKET s = socket (AF_INET, SOCK_STREAM, 0);

// 0 – параметр за про мовчанням, означає TCP.

Крок третій «З’єднання сокета з локальною адресою»

Клієнт при підключені повинен вказати адресу/порт сервера. Отже на стороні сервера ми повинні вказати адресу/порт, а потім поєднати їх з сокетом. Для цього потрібно використати функцію int bind(SOCKET s, const struct sockaddr FAR* name, int namelen).

s – дескриптор сокету з котрим ми поєднаємо адресу/порт;

name - структура sockaddr, котра містить адресу/порт;

namelen – довжина структури sockaddr.

Якщо все добре, то функція повертає «0».

Структура sockaddr має вигляд:

struct sockaddr { unsigned short sa_family; // сімейство адрес, AF_xxx char sa_data[14];// 14 байтів для // зберігання адреси}; Так як працювати з sockaddr не зручно, то можна використовувати альтернативну структуру sockaddr_in:struct sockaddr_in { short int sin_family; // сімейство адресів unsigned short int sin_port; // номер порта struct in_addr sin_addr; // IP-адреса unsigned char sin_zero[8]; // "доповнення" до // розміру структури // sockaddr};

Приклад:

sockaddr_in sin;

sin.sin_family = AF_INET;

sin.sin_port = htons(8888);

sin.sin_addr.s_addr = INADDR_ANY;

result = bind(s, (LPSOCKADDR)&sin, sizeof(sin));

Крок четвертий «Режим очікування»

Коли ми створили сокет, пов’язали його з певною адресою/портом, далі ми повинні перейти в режим очікування, тобто очікування клієнтів на підключення. Для цього використовуємо функцію int listen(SOCKET s, int bac_klog).

s – дескриптор сокету;

bac_klog – максимальний розмір черги на підключення (кількість з’єднань, котрі сервер може одночасно оброблювати).

Приклад:

result = listen(s, 5);

 

Крок п’ятий «Витягти клієнта з черги»

В режимі очікування ми чекаємо поки клієнти не з’явиться в черзі на підключення (запит на з’єднання). Як тільки в черзі з’являється запит на підключення, витягнути звідти його можна за допомогою функції SOCKET accept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen)..

s- дескриптор сокету;

addr- структура sockaddr. При підключені клієнта в неї записується його адреса/порт. Якщо нас не цікавить адреса клієнта, можна передати NULL;

addrlen – довжина структури sockaddr;

Функція створює новий об’єкт типу Socket, з котрим ми потім будемо працювати.

Приклад:

SOCKET client;

result = accept(s, NULL, NULL);

Крок шостий «Прийом/передача даних»

Нам потрібно реалізувати обмін інформацією між сервером та клієнтом.

Для прийому повідомлень використовують функцію int send(SOCKET s, const char FAR * buf, int len,int flags).

s - дескриптор сокету клієнта;

buf – буфер в котрий ми отримаємо повідомлення;

len – розмір буфера;

flags – прапорці, (Якщо не використовуємо то «0»).

Повертає кількість прийнятих байт.

Функція send повертає управління відразу після виконання, незалежно від того чи отримала інша сторона інформацію.

Для відправлення повідомлень використовують функцію int recv (SOCKET s, char FAR* buf, int len, int flags).

s - дескриптор сокету клієнта;

buf – буфер з повідомленням;

len – розмір буфера;

flags – прапорці, (Якщо не використовуємо то «0»).

Функція recv повертає управління тільки після того як отримала інформацію.

Приклад:

SOCKET client;

result = accept(s, NULL, NULL);

 

char recv_buf[40];

char send_buf[40] = "ANSWER";

 

result = recv(client, recv_buf, 40, 0);

send(client, send_buf, 40, 0);

 

Крок сьомий «Закрити сокет, звільнити ресурси»

Після роботи с клієнтом потрібно закрити з’єднання. Також не забуваємо після закінчення роботи сервера закривати основний сокет, робити де ініціалізацію бібліотеки, звільнення ресурсів.

Приклад

closesocket(client);

closesocket(s);

WSACleanup();

 

В UNIX подібних системах використовують:

#include <unistd.h>int close(int fd);

4.1.3 Реалізація клієнта

Крок перший. «Підготовка бібліотеки»

Для роботи з бібліотекою Winsock 2.x потрібно підключити директиву #include <winsock2.h> та підготувати саму бібліотеку. Для цього використаємо функцію WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData).

- wVersionRequestedпараметр типу WORD, приймає значення версії сокетів (старший байт слова – номер версії, молодший – номер під версії);

- lpWSAData – структура типу WSADATA, в котру при успішній ініціалізації буде занесена інформація про виробника бібліотеки.

Приклад:

WORD sockVersion;

WSADATA wsaData;

sockVersion = MAKEWORD(2,2);

WSAStartup(sockVersion, &wsaData);

 

В UNIX подібних системах потрібно підключити такі директиви:

#include <sys/types.h>

#include <sys/socket.h>

Крок другий. «Створення об’єкту типу Socket»

Створимо об’єкт типу socket. Використовуємо функцію SOCKET s (int af, int type, int protocol).

- аf - сімейство протоколів (Зазвичай використовують AF_INET, тобто Internet протоколи.);

- type – тип сокету – спосіб передачі даних через мережу. В даному курсі ми будемо використовувати: SOCK_STREAM з встановленням з’єднання, використовується в основному з TCP та SOCK_DFRAM без встановлення з’єднання, використовуються з UDP;

- protocol – протокол для передачі даних;

Функція повертає дескриптор сокету, інакше INVALID_SOCKET.

Приклад:

SOCKET s = socket (AF_INET, SOCK_STREAM, 0);

// 0 – параметр за про мовчанням, означає TCP.

Крок третій «З’єднання з сервером»

На стороні клієнта нам потрібно з’єднатися з сервером вказавши адресу/порт сервера, для цього використовуємо функцію int connect(SOCKET s, const struct sockaddr FAR* name, int namelen).

s – дескриптор сокету;

name – структура sockaddr, котра містить в собі адресу і порт віддаленого вузла, з котрим ми будемо з’єднуватися;

namelen – розмір структури sockaddr.

Приклад:

hostEntry = gethostbyname("127.0.0.1");

SOCKADDR_IN serverInfo;

serverInfo.sin_family = AF_INET;

serverInfo.sin_addr = *((LPIN_ADDR)*hostEntry->h_addr_list);

serverInfo.sin_port = htons(8888);

 

result=connect(s,(LPSOCKADDR)&serverInfo, sizeof(serverInfo));

Крок четвертий «Прийом/передача даних»

Нам потрібно реалізувати обмін інформацією між сервером та клієнтом.

Для прийому повідомлень використовують функцію int send(SOCKET s, const char FAR * buf, int len,int flags).

s - дескриптор сокету клієнта;

buf – буфер в котрий ми отримаємо повідомлення;

len – розмір буфера;

flags – прапорці.

Вона повертає кількість прийнятих байт.

Функція send повертає управління відразу після виконання, незалежно від того чи отримала інша сторона інформацію.

Для відправлення повідомлень використовують функцію int recv (SOCKET s, char FAR* buf, int len, int flags).

s - дескриптор сокету клієнта;

buf – буфер з повідомленням;

len – розмір буфера;

flags – прапорці.

Функція recv повертає управління тільки після того як отримала інформацію.

Приклад:

SOCKET client;

result = accept(s, NULL, NULL);

 

char recv_buf[40];

char send_buf[40] = "ANSWER";

 

result = recv(client, recv_buf, 40, 0);

send(client, send_buf, 40, 0);

Крок п’ятий «Закрити сокет, звільнити ресурси»

Після роботи с клієнтом потрібно закрити з’єднання. Також не забуваємо після закінчення роботи сервера закривати основний сокет, робити де ініціалізацію бібліотеки, звільнення ресурсів.

Приклад

closesocket(client);

closesocket(s);

WSACleanup();

В UNIX подібних системах використовують:

#include <unistd.h>int close(int fd);

4.1.4 Приклад роботи з WinSock

 

 

Реалізація сервера:

#include "stdafx.h"

#include <winsock2.h>

 

#pragma comment(lib, "wsock32.lib")

 

#define SERVER_SOCKET_ERROR 1

#define SOCKET_OK 0

 

int _tmain(int argc, _TCHAR* argv[])

{

int result;

WORD sockVersion;

WSADATA wsaData;

sockVersion = MAKEWORD(2,2);

WSAStartup(sockVersion, &wsaData);

sockaddr_in sin;

sin.sin_family = AF_INET;

sin.sin_port = htons(8888);

sin.sin_addr.s_addr = INADDR_ANY;

SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

if(s == INVALID_SOCKET)

{

printf ("%s", "ERROR (don't create serrver)\n");

WSACleanup();

return SERVER_SOCKET_ERROR;

}

else

{

printf ("%s", " >>> Create socket \n");

}

 

result = bind(s, (LPSOCKADDR)&sin, sizeof(sin));

if(result == SOCKET_ERROR)

{

printf ("%s", "ERROR (don't associates a

local address with a socket)");

WSACleanup();

return SERVER_SOCKET_ERROR;

}

else

{

printf ("%s", " >>> Associates a local

addres with a socket\n");

}

 

result = listen(s, 5);

if(result == SOCKET_ERROR)

{

printf ("%s", " ERROR (don't listen a socket )\n");

WSACleanup();

return SERVER_SOCKET_ERROR;

}

else

{

printf ("%s", " >>> get socket to listen \n");

}

while (1)

{

SOCKET client;

client = accept(s, NULL, NULL);

if(client == INVALID_SOCKET)

{

printf ("%s", " >>> ERROR (don't

accept a new socket)\n");

WSACleanup();

return SERVER_SOCKET_ERROR;

}

else

{

printf ("%s", " >>> new socket

client ACCEPT (client found)\n");

}

 

char recv_buf[40];

char send_buf[40] = "ANSWERRRRRRR";

result = recv(client, recv_buf, 40, 0);

recv_buf[result]=0;

send(client, send_buf, 40, 0);

 

printf ("%s", recv_buf );

printf ("%s", "\n" );

 

if(result == SOCKET_ERROR)

{

int val = WSAGetLastError();

if(val == WSAENOTCONN)

{

printf ("%s", " ERROR (socket not

connected) ?? \n");

}

else if(val == WSAESHUTDOWN )

{

printf ("%s", " ERROR (socket has

been shut down!) ?? \n");

}

printf ("%s", " ERROR (FAILD RECV) \n");

return SERVER_SOCKET_ERROR;

}

closesocket(client);

printf ("%s", " >>> Close client socket \n");

 

} // END WHILE

 

closesocket(s);

printf ("%s", " >>> Close main socket \n");

WSACleanup();

return SOCKET_OK;

}

 

Реалізація клієнта:

#include "stdafx.h"

#include <winsock.h>

#pragma comment(lib, "wsock32.lib")

 

#define CS_ERROR 1

#define CS_OK 0

 

int _tmain(int argc, _TCHAR* argv[])

{

WORD version;

WSADATA wsaData;

int result;

version = MAKEWORD(2,2);

WSAStartup(version,(LPWSADATA)&wsaData);

LPHOSTENT hostEntry;

hostEntry = gethostbyname("127.0.0.1");

 

if(!hostEntry)

{

printf ("%s", " >>> ERROR (hostEntry NULL)\n");

WSACleanup();

return CS_ERROR;

}

 

SOCKET theSocket = socket(AF_INET, SOCK_STREAM, 0);

if(theSocket == SOCKET_ERROR)

{

printf ("%s", " ERROR (don't create socket)\n");

return CS_ERROR;

}

else

{

printf ("%s", " >>> Create socket \n");

}

 

sockaddr_in serverInfo;

serverInfo.sin_family = AF_INET;

serverInfo.sin_addr = *((LPIN_ADDR)*hostEntry->h_addr_list);

serverInfo.sin_port = htons(8888);

 

result=connect(theSocket,(LPSOCKADDR)&serverInfo,

sizeof(serverInfo));

if(result==SOCKET_ERROR)

{

printf ("%s", " ERROR (don't connect to Server)\n");

return CS_ERROR;

}

else

{

printf ("%s", " >>> Connect to Server\n");

}

 

char send_buf[40] = "";

char recv_buf[40] = "";

scanf ("%s", send_buf);

 

result = send(theSocket, send_buf, strlen(send_buf), 0);

if(result == SOCKET_ERROR)

{

printf ("%s", " ERROR (Failed send () )\n");

return CS_ERROR;

}

 

recv(theSocket, recv_buf, 40, 0);

printf ("%s", recv_buf);

printf ("%s", "\n");

 

closesocket(theSocket);

printf ("%s", " >>> Close socket\n");

WSACleanup();

return CS_OK;

}

4.1.5 Приклад роботи для Unix подібних систем

Ехо-Клієнт:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

 

char message[] = "Hello there!\n";

char buf[sizeof(message)];

 

int main()

{

int sock;

struct sockaddr_in addr;

 

sock = socket(AF_INET, SOCK_STREAM, 0);

if(sock < 0)

{

perror("socket");

exit(1);

}

 

addr.sin_family = AF_INET;

addr.sin_port = htons(3425);

addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)

{

perror("connect");

exit(2);

}

send(sock, message, sizeof(message), 0);

recv(sock, buf, sizeof(message), 0);

printf(buf);

close(sock);

return 0;

}

 

Ехо-Сервер:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

 

int main()

{

int sock, listener;

struct sockaddr_in addr;

char buf[1024];

int bytes_read;

 

listener = socket(AF_INET, SOCK_STREAM, 0);

if(listener < 0)

{

perror("socket");

exit(1);

}

 

addr.sin_family = AF_INET;

addr.sin_port = htons(3425);

addr.sin_addr.s_addr = htonl(INADDR_ANY);

if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)

{

perror("bind");

exit(2);

}

listen(listener, 1);

while(1)

{

sock = accept(listener, NULL, NULL);

if(sock < 0)

{

perror("accept");

exit(3);

}

while(1)

{

bytes_read = recv(sock, buf, 1024, 0);

if(bytes_read <= 0) break;

send(sock, buf, bytes_read, 0);

}

close(sock);

}

 

return 0;

}

4.2 Завдання

Написати сервер котрий одночасно буде відповідати на запити від клієнтів.

Для студентів котрі хочуть отримати відмінну оцінку: клієнт повинен надсилати щосекунди повідомлення с поточною датою. На стороні сервера реалізувати перевірку, якщо прийнята дата парна (секунди), у відповідь відсилати дату що прислав клієнт та повідомлення «Число парне», інакше «Число не парне»;

4.3 Питання

1. Що таке сокет? Які існують типи сокетів?

2. Яка функція використовується при створені сокету? Охарактеризуйте параметри котрі передаються у функцію?

3. Навіщо вказувати на стороні сервера адресу/порт?

4. Поясніть, чому при адресації сокетів використовуються різні структури перетворення типів?

5. Яка функція використовується для читання даних із сокету? Охарактеризуйте параметри котрі передаються у функцію?

6. Які функції обов’язково повинні бути присутніми при створення сервера/клієнта? Охарактеризуйте відповідь?