Главная страница

Введение в сетевое программирование


Скачать 1.2 Mb.
НазваниеВведение в сетевое программирование
Дата08.10.2021
Размер1.2 Mb.
Формат файлаppt
Имя файлаlecture_07.ppt
ТипЗадача
#243459

Введение в сетевое программирование




Задача





Запрограммировать службу удаленных вычислений:

Разработка протокола Calculation 1.0





Запрос клиента (12*6):
    CALC * 12 6 \n

    Ответы сервера:

    ОК 72 \n (если все нормально)
    ERR \n (если запрос неправильный)
    \n – нужен, чтобы узнать где конец сообщения
    Следующий запрос клиент может посылать только после получения ответа на предыдущий!
    Вопрос: используем TCP или UDP ?

Сервер (TCP)





запускается заранее, до подключения клиентов сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345
выделяет память для очереди подключений
В цикле:
    устанавливает соединение с клиентом из очереди; если очередь пуста, то ждет подключения клиента принимает/передает данные закрывает соединение с клиентом, когда тот отключился

Клиент (TCP)





получает от ОС случайный номер порта для общения с сервером устанавливает соединение с сервером передает/принимает данные закрывает соединение с сервером

Сервер (UDP)





запускается заранее, до подключения клиентов сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345
В цикле:

Клиент (UDP)





получает от ОС случайный номер порта для общения с сервером передает/принимает данные

Интерфейс транспортного уровня. Сокеты.





Socket (гнездо) - структура данных, идентифицирующая сетевое соединение
Команды:
    SOCKET – создать новый (пустой) сокет
    BIND – сервер связывает свой локальный адрес (порт) с сокетом





LISTEN – сервер выделяет память под очередь подсоединений клиентов и устанавливает сокет в состояние «listening» (TCP)
ACCEPT – сервер ожидает подсоединения клиента или принимает первое подключение из очереди (TCP)
CONNECT–клиент запрашивает соединение (TCP)





SEND / SEND_TO – послать данные (TCP / UDP)
RECEIVE / RECEIVE_FROM – получить данные (TCP / UDP)
DISCONNECT – запросить разъединение (TCP)

Реализации


Linux: sys/socket.h
Windows (основана на коде BSD) : winsock2.h
Кроссплатформенные обертки на C, C++
Обертки в скипровых ЯП





TCP организует «виртуальное» потоковое соединение
UDP передает отдельные пакеты
На практике наивные программисты думают, что одному вызову send будет соответствовать один вызов recv




В случае использования TCP возможны ситуации:
    отправка несколькими send и получение одним recv
    отправка одним send и получение всех данных по частям несколькими recv (если размер отправляемых данных > MTU)

    При вызове recv, если размер параметра «буфер» меньше объема полученных данных, то

    в случае TCP следующие вызовы recv получат оставшиеся данные (часто удобно получать по 1 байту)
    в случае UDP – не поместившиеся в буфер данные удаляются








Переменные:
    SOCKET ListenSocket, ClientSocket;
    sockaddr_in ServerAddr;
    int err, maxlen = 512;
    string recvbuf(maxlen,’=’);





ListenSocket = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerAddr.sin_port=htons(12345);
bind(ListenSocket, &ServerAddr, sizeof(ServerAddr));





listen (ListenSocket, 50);
while (true) {
    ClientSocket = accept(ListenSocket, NULL, NULL);
    while (err > 0) {
      err = recv(ClientSocket, &recvbuf[0], maxlen, 0);
      if (err <= 0) break;
      string result = …… вычисляем результат ……………
      send(ClientSocket, &result[0], result.size(),0);
      }

      closesocket(ClientSocket); }

TCP-клиент на C++





Переменные:
    SOCKET ClientSocket;
    sockaddr_in ServerAddr;
    int err, maxlen = 512;
    string recvbuf(maxlen,’=‘);

TCP-клиент на C++





ConnectSocket = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerAddr.sin_port=htons(12345);
connect (ConnectSocket, (sockaddr *) &ServerAddr, sizeof(ServerAddr));

TCP-клиент на C++





for (int i = 0; i < 1000; i++) {
    string query = "CALC ………..\n";
    send (ConnectSocket, &query[0], query.size(),0);
    err = recv(ConnectSocket,&recvbuf[0],maxlen,0);
    if (err <= 0) break;
    cout << recvbuf.substr(0,err);
    }

    closesocket(ConnectSocket);

Ненадежный UDP - сервер





Переменные:
    SOCKET SendRecvSocket;
    sockaddr_in ServerAddr, ClientAddr;
    int err, maxlen = 512, ClientAddrSize=sizeof(ClientAddr);
    string recvbuf(maxlen,’=‘);

Ненадежный UDP - сервер





SendRecvSocket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerAddr.sin_port=htons(12345);
bind (SendRecvSocket, ServerAddr, sizeof(ServerAddr));
// listen – нельзя !!!

Ненадежный UDP - сервер





while (true) {
    // accept – нельзя !!!
    err = recvfrom (SendRecvSocket, recvbuf, maxlen, 0, (sockaddr *)&ClientAddr, &ClientAddrSize);
    if (err <= 0) continue;
    cout << recvbuf.substr(0,err) << “\n”;
    string result = …………………………………….
    sendto (SendRecvSocket, &result[0], result.size(), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr));

    }

Ненадежный UDP - клиент





Переменные:
    SOCKET SendRecvSocket;
    sockaddr_in ServerAddr;
    int err, maxlen = 512;
    string recvbuf(maxlen,’=‘);

Ненадежный UDP - клиент





SendRecvSocket = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP);
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerAddr.sin_port=htons(12345);

Ненадежный UDP - клиент





for (int i = 0; i < 1000; i++) {
    string query="CALC …………..\n";
    sendto (SendRecvSocket, &query[0], query.size(), 0, (sockaddr *)&ServerAddr, sizeof(ServerAddr));
    err = recvfrom (SendRecvSocket,&recvbuf[0],maxlen,0,0,0);
    if (err > 0) cout << recvbuf.substr(0,err);
    }

    closesocket(SendRecvSocket);

Вопросы для самоконтроля





Почему клиент не вызывает bind, разве ему не нужен номер порта?
Сервер ждет подключения клиентов в функции listen?
Если мы сделаем сервер, который общается одновременно с несколькими клиентами, как мы будем проверять от какого клиента получила данные функция recv?

Вопросы для самоконтроля





UDP-клиент не вызывает функцию connect, где же тогда ему присваивают номер порта?
На какой функции сервер ждет запроса клиента?
Что происходит если размер буфера приема маловат?

Upgrade


Что произойдет, если сообщения будут большими (длинная арифметика)?
Как изменить программы, чтобы клиент смог посылать конвейерные запросы?
А если одновременно подключатся много клиентов? Если это приведет к неудобствам, то как их исправить?
А вдруг какой-нибудь пакет потеряется?




TCP-сервер 2.0 на C++ (длинная арифметика)


Прием одного запроса от клиента разбивается на несколько recv:
string recv_string;
char lastSymb = 0;
while (lastSymb != 10) //10 = \n
{
    err = recv(ClientSocket, &recvbuf[0], maxlen, 0);
    lastSymb = recvbuf[err-1];
    recv_string += recvbuf.substr(0,err);

    }



TCP-клиент 2.0 на C++ (длинная арифметика)


Прием ответа сервера разбивается на несколько recv:
string recv_string;
char lastSymb = 0;
while (lastSymb != 10) //10 = \n
{
    err = recv(ConnectSocket, &recvbuf[0], maxlen,0);
    lastSymb = recvbuf[err-1];
    recv_string += recvbuf.substr(0,err);

    }



Надежный UDP – клиент


Отправляем одинаковые запросы, пока не получим ответ!
while (err == 0) {
    sendto(SendRecvSocket, &query[0], query.size(), 0, (sockaddr *)&ServerAddr, sizeof(ServerAddr));
    // проверяем, получен ли результат
    err = select(-1, &fds, 0, 0, &timeToWaitAnswer);
    if (err == 0) cout << «Пакет потерян. Попытаемся еще раз\n";
    }

    recvfrom(SendRecvSocket,&recvbuf[0],maxlen,0,0,0);




Вариант 1. Использование потоков
while (true)
{
int *clientSocket = new int[1];
*clientSocket = accept(listenSocket,NULL,NULL);
if (*clientSocket < 0) handleError("accept failed:");
pthread_t threadId;
pthread_create(&threadId,NULL, processClient,
(void*)clientSocket);
}




Функция processClient


static pthread_mutex_t cs_mutex =
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
void *processClient(void *dataPtr)
{
pthread_mutex_lock( &cs_mutex );
cout << ++concurrentClientCount << "\n";
pthread_mutex_unlock( &cs_mutex );
int clientSocket = ((int*)dataPtr)[0];
recv / send …

}





Вариант 2. Использование неблокирующих сокетов
listenSocket = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK,IPPROTO_TCP);
vector<pollfd> allSockets;
pollfd tmpPollfd; tmpPollfd.fd = listenSocket; tmpPollfd.events = POLLIN;
allSockets.push_back(tmpPollfd);
while (true) {
poll(&allSockets[0], allSockets.size(), -1);
for (int i = 0; i < allSockets.size(); ++i) { // цикл не нужен для epoll
if (allSockets[i].revents == 0) continue; //на этот сокет ничего не пришло
if (i == 0) { //произошло подключение нового клиента
int clientSocket = accept(listenSocket, NULL, NULL);
tmpPollfd.fd = clientSocket;
allSockets.push_back(tmpPollfd);
}
else { //произошел прием данных от клиента
int clientSocket = allSockets[i].fd;
recv / send …
} }





listenSocket = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
//создаем специальный файловый дескриптор для работы с epoll
int epfd = epoll_create(MAX_CLIENT_COUNT);
//добавляем впускающий сокет в общий массив для функции epoll
ev.data.fd = listenSocket; ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenSocket, &ev);
//резервируем ресурсы для максимального количества клиентов
events.reserve(MAX_CLIENT_COUNT);
//добавляем событие, соответствующее подключению нового клиента в общий массив событий epoll
events.push_back(ev);
listen(listenSocket, 1000)
while (true) {
//ждем подключения нового клиента или прихода запроса от какого-нибудь из уже подключенных клиентов
int readyToReadfdCount = epoll_wait(epfd, &events[0], MAX_CLIENT_COUNT,-1);
for (int index = 0; index < readyToReadfdCount; index++) {
//подключился новый клиент
if (events[index].data.fd== listenSocket) {
clientSocket = accept(listenSocket, (struct sockaddr *)&clientAddress, &addrlen);
//добавляем сокет подключившегося клиента в массив событий epoll
ev.data.fd = clientSocket;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientSocket, &ev);
events.push_back(ev);
}
else {





Вариант 3. Использование событийно ориентированного фреймворка (обертки над poll), например, Node.js
var net = require('net');
var server = net.createServer(processClient);
server.listen(28563, '0.0.0.0');
function processClient(clientSocket) {
//регистрация обработчика события прихода данных
clientSocket.on('data', function (data) {
… обработка data
clientSocket.write(answer);
});
}






написать администратору сайта