Введение в сетевое программирование 1 Задача Запрограммировать службу удаленных вычислений: клиенты просят сервер вычислить выражение (для начала содержащее только одну арифметическую операцию +, -, *, / ) сервер возвращает результат 2 Разработка протокола Calculation 1.0 Запрос клиента (12*6): CALC * 12 6 \n Ответы сервера: ОК 72 \n (если все нормально) ERR \n (если запрос неправильный) \n – нужен, чтобы узнать где конец сообщения Вопрос: используем TCP или UDP ? 3 Сервер (TCP) 1) запускается заранее, до подключения клиентов 2) сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345 3) выделяет память для очереди подключений 4) В цикле: a) устанавливает соединение с клиентом из очереди; если очередь пуста, то ждет подключения клиента b) принимает/передает данные c) закрывает соединение с клиентом UDP 4 Клиент (TCP) 1) получает от ОС случайный номер порта для общения с сервером 2) устанавливает соединение с сервером 3) передает/принимает данные 4) закрывает соединение с сервером UDP 5 Сервер (UDP) 1) запускается заранее, до подключения клиентов 2) сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345 3) В цикле: ждет прихода сообщения обрабатывает данные передает результат TCP 6 Клиент (UDP) 1) получает от ОС случайный номер порта для общения с сервером 2) передает/принимает данные TCP 7 Интерфейс транспортного уровня. Сокеты. Socket (гнездо) - структура данных, идентифицирующая сетевое соединение Команды: SOCKET – создать новый (пустой) сокет BIND – сервер связывает свой локальный адрес (порт) с сокетом 8 Интерфейс транспортного уровня. Команды. LISTEN – сервер выделяет память под очередь подсоединений клиентов и устанавливает сокет в состояние «listening» (TCP) ACCEPT – сервер ожидает подсоединения клиента или принимает первое подключение из очереди (TCP) CONNECT–клиент запрашивает соединение (TCP) 9 Интерфейс транспортного уровня. Команды. SEND / SEND_TO – послать данные (TCP / UDP) RECEIVE / RECEIVE_FROM – получить данные (TCP / UDP) DISCONNECT – запросить разъединение (TCP) 10 Реализации Linux: sys/socket.h Windows (основана на коде BSD) : winsock2.h Кроссплатформенные обертки Обертки в скипровых ЯП 11 TCP-сервер на C++ (winsock2.h) Переменные: SOCKET ListenSocket, ClientSocket; sockaddr_in ServerAddr; int err, maxlen = 512; char* recvbuf = new char[maxlen+1]; char* result_string = new char[maxlen]; 12 TCP-сервер на C++ (winsock2.h) 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)); 13 TCP-сервер на C++ (winsock2.h) listen (ListenSocket, 50); while (true) {ClientSocket = accept(ListenSocket, NULL, NULL); err = recv (ClientSocket, recvbuf, maxlen, 0); if (err > 0) {recvbuf[err] = 0; printf("Received query: %s\n", (char* )recvbuf); // вычисляем результат int result = ……………………………………… 14 TCP-сервер на C++ (winsock2.h) snprintf(result_string, maxlen, "OK %d\n", result); send( ClientSocket, result_string, strlen(result_string), 0 ); printf("Sent answer: %s\n", result_string); } // end if closesocket(ClientSocket); } // end while 15 TCP-сервер 2.0 на C++ ClientSocket = accept(ListenSocket, 0, 0); string recv_string; char lastSymb = 0; while (lastSymb != 10) //10 = \n { } err = recv(ClientSocket, recvbuf, maxlen, 0); lastSymb = recvbuf[err-1]; recvbuf[err] = 0; //признак конца строки recv_string += recvbuf; 16 TCP-клиент на C++ Переменные: SOCKET ClientSocket; sockaddr_in ServerAddr; int err, maxlen = 512; char* recvbuf = new char[maxlen]; char* query = new char[maxlen]; 17 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); 18 TCP-клиент на C++ connect (ConnectSocket, (sockaddr *) &ServerAddr, sizeof(ServerAddr)); query = "CALC * 12 6\n"; send (ConnectSocket, query,strlen(query),0); printf("Sent: %s\n", query); 19 TCP-клиент на C++ err = recv(ConnectSocket,recvbuf,maxlen,0); if (err > 0) { recvbuf[err]=0; printf(“Result: %s\n", (char* )recvbuf); } closesocket(ConnectSocket); 20 Ненадежный UDP - сервер Переменные: SOCKET SendRecvSocket; sockaddr_in ServerAddr, ClientAddr; int err, maxlen = 512, ClientAddrSize=sizeof(ClientAddr); char* recvbuf = new char[maxlen+1]; char* result_string = new char[maxlen]; 21 Ненадежный 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); 22 Ненадежный UDP - сервер bind (SendRecvSocket, ServerAddr, sizeof(ServerAddr)); // listen – нельзя !!! while (true) { // accept – нельзя !!! err = recvfrom (SendRecvSocket, recvbuf, maxlen, 0, (sockaddr *)&ClientAddr, &ClientAddrSize); if (err > 0) {recvbuf[err]=0; printf("Received query: %s\n", (char* )recvbuf); // вычисляем результат int result = ……………………………………… 23 Ненадежный UDP - сервер snprintf (result_string,maxlen, "OK %d\n",result); sendto (SendRecvSocket, result_string, strlen(result_string), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr)); printf("Sent answer: %s\n", result_string); } // end if } // end while 24 Ненадежный UDP - клиент Переменные: SOCKET SendRecvSocket; sockaddr_in ServerAddr; int err, maxlen = 512; char* recvbuf = new char[maxlen+1]; char* query = new char[maxlen]; 25 Ненадежный 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); 26 Ненадежный UDP - клиент query="CALC * 12 6\n"; sendto (SendRecvSocket, query, strlen(query), 0, (sockaddr *)&ServerAddr, sizeof(ServerAddr)); printf("Sent: %s\n", query); 27 Ненадежный UDP - клиент err = recvfrom (SendRecvSocket,recvbuf,maxlen,0,0,0); if (err > 0) { recvbuf[err]=0; printf(“Result: %s\n", (char* )recvbuf); } closesocket(SendRecvSocket); 28 Вопросы Что произойдет, если сообщения будут большими (длинная арифметика или клиент будет посылать конвейерные запросы)? А если еще будет одновременно подключаться много клиентов? А вдруг какой-нибудь пакет потеряется? Как следует изменить алгоритмы? 29 TCP-клиент 2.0 на C++ string recv_string; char lastSymb = 0; while (lastSymb != 10) //10 = \n { err = recv(ConnectSocket, recvbuf, maxlen, 0); lastSymb = recvbuf[err-1]; recvbuf[err] = 0; recv_string += recvbuf; } 30 UDP – клиент 2.0 err = 0; while (err == 0) { sendto(SendRecvSocket, query, strlen(query), 0, (sockaddr *)&ServerAddr, sizeof(ServerAddr)); // проверяем, получен ли результат err = select(-1, &fds, 0, 0, &timeToWaitAnswer); if (err == 0) cout << "Packet was lost. Another attempt\n"; }//end while recvfrom(SendRecvSocket,recvbuf,maxlen,0,0,0); 31 Контрольные вопросы Почему клиент не вызывает bind, разве ему не нужен номер порта? Сервер ждет подключения клиентов в функции listen? Если мы сделаем сервер, который общается одновременно с несколькими клиентами, как мы будем проверять от какого клиента получила данные функция recv? 32 Контрольные вопросы UDP-клиент не вызывает функцию connect, где же тогда ему присваивают номер порта? На какой функции сервер ждет запроса клиента? Что происходит если размер буфера приема маловат? 33 Обработка запросов одновременно подключенных клиентов Вариант 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); } 34 Функция 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 … … } 35 Обработка запросов одновременно подключенных клиентов Вариант 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) { 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 … 36 } } Обработка запросов одновременно подключенных клиентов Вариант 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); }); } 37