Введение в HTTP
Для начала разберемся, что из себя представляет HTTP.
Это текстовый протокол для обмена данными между браузером и веб-сервером.
Пример HTTP-запроса:
Первая строка передает метод запроса, идентификатор ресурса (URI) и версию HTTP-протокола. Затем перечисляются заголовки запроса, в которых браузер передает имя хоста, поддерживаемые кодировки, cookie и другие служебные параметры. После каждого заголовка ставится символ переноса строки .
У некоторых запросов есть тело. Когда отправляется форма методом POST, в теле запроса передаются значения полей этой формы.
Тело запроса отделяется от заголовков одной пустой строкой. Заголовок «Content-Type» говорит серверу, в каком формате закодировано тело запроса.
По умолчанию, в HTML-форме данные кодируются методом «application/x-www-form-urlencoded».
Иногда необходимо передать данные в другом формате. Например, при загрузке файлов на сервер, бинарные данные кодируются методом «multipart/form-data».
Сервер обрабатывает запрос клиента и возвращает ответ.
Пример ответа сервера:
В первой строке ответа передается версия протокола и статус ответа. Для успешных запросов обычно используется статус «200 OK». Если ресурс не найден на сервере, возвращается «404 Not Found».
Тело ответа так же, как и у запроса, отделяется от заголовков одной пустой строкой.
1.1 Создание socket’а
Создание socket’а осуществляется следующим системным вызовом
#include <sys/socket.h>
int socket (domain, type, protocol)
int domain;
int type;
int protocol;
Аргумент domain задает используемый для взаимодействия набор протоколов (вид коммуникационной области), для стека протоколов TCP/IP он должен иметь символьное значение AF_INET (определено в sys/socket.h).
Аргумент type задает режим взаимодействия:
- SOCK_STREAM — с установлением соединения;
- SOCK_DGRAM — без установления соединения.
Аргумент protocolзадает конкретный протокол транспортного уровня (из нескольких возможных в стеке протоколов). Если этот аргумент задан равным 0, то будет использован протокол «по умолчанию» (TCP для SOCK_STREAM и UDP для SOCK_DGRAM при использовании комплекта протоколов TCP/IP).
При удачном завершении своей работы данная функция возвращает дескриптор socket’а — целое неотрицательное число, однозначно его идентифицирующее. Дескриптор socket’а аналогичен дескриптору файла ОС UNIX.
При обнаружении ошибки в ходе своей работы функция возвращает число «-1».
Клиент сокета
Мы сохраним клиентскую программу сокета python как socket_client.py. Эта программа похожа на серверную, за исключением привязки.
Основное различие между серверной и клиентской программой состоит в том, что в серверной программе необходимо связать адрес хоста и адрес порта вместе.
Смотрите ниже пример кода клиента сокета:
import socket def client_program(): host = socket.gethostname() # as both code is running on same pc port = 5000 # socket server port number client_socket = socket.socket() # instantiate client_socket.connect((host, port)) # connect to the server message = input(" -> ") # take input while message.lower().strip() != 'bye': client_socket.send(message.encode()) # send message data = client_socket.recv(1024).decode() # receive response print('Received from server: ' + data) # show in terminal message = input(" -> ") # again take input client_socket.close() # close the connection if __name__ == '__main__': client_program()
Работа с WinSocket в Visual C++
Socket (гнездо, разъем) — абстрактное программное понятие, используемое для обозначения в прикладной программе конечной точки канала связи с коммуникационной средой, образованной вычислительной сетью. При использовании протоколов TCP/IP можно говорить, что socket является средством подключения прикладной программы к порту (см. выше) локального узла сети.
Socket-интерфейс представляет собой просто набор системных вызовов и/или библиотечных функций языка программирования СИ, разделенных на четыре группы:
Ниже рассматривается подмножество функций socket-интерфейса, достаточное для написания сетевых приложений, реализующих модель «клиент-сервер» в режиме с установлением соединения.
4.1. Системный вызов close
Для закрытия ранее созданного socket’а используется обычный системный вызов close, применяемый в ОС UNIX для закрытия ранее открытых файлов и имеющий следующий вид
int close (s) int s;
Аргумент s задает дескриптор ранее созданного socket’а.
Однако в режиме с установлением логического соединения (обеспечивающем, как правило, надежную доставку данных) внутрисистемные механизмы обмена будут пытаться передать/принять данные, оставшиеся в канале передачи на момент закрытия socket’а. На это может потребоваться значительный интервал времени, неприемлемый для некоторых приложений. В такой ситуации необходимо использовать описываемый далее системный вызов shutdown.
3.1. Посылка данных
Для посылки данных партнеру по сетевому взаимодействию используется системный вызов send, имеющий следующий вид
#include <sys/types.h> #include <sys/socket.h> int send (s, buf, len, flags) int s; char *buf; int len; int flags;
Аргумент s задает дескриптор socket’а, через который посылаются данные.
Аргумент buf указывает на область памяти, содержащую передаваемые данные.
Аргумент len задает длину (в байтах) передаваемых данных.
Аргумент flags модифицирует исполнение системного вызова send. При нулевом значении этого аргумента вызов send полностью аналогичен системному вызову write.
При успешном завершении send возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается «переполненным», то send переводит программу в состояние ожидания до момента его освобождения.
Количество открытых соединений
Итак, допустим мы хотим удержать максимально возможное число соединений. Сколько же это? Первым
параметром, в который мы уткнёмся — число одновременно открытых файлов на процесс операционной
системы.
ulimit -a | grep open
Следующая команда меняет это число в пределах сессии:
ulimit -n 1048576
Откуда берется константа — не знаю. В моей системе было так. Следует отметить, что для исполнения
нужны специальные права.
Далее следует обратить внимание на количество оперативной памяти и иметь ввиду, что каждое открытое
соединение откушает около 64Кб unswappable памяти. Таким образом, в моём случае:
python -c"print(`free | awk '/-\/+.*/ {print $4}'`/64)"
эта цифра была более 243422.0625. Значит будем пробовать открыть 200000 соединений. Не совсем всё
так просто. Существует целый ряд настроек в net.ipv4.tcp (/proc/sys/net/ipv4).
-
tcp_mem — векторная величина (минимум, нагрузка, максимум), характеризующая общие настройки
потребления памяти для протокола TCP. Измеряется в страницах (обычно страница — 4Кб). До минимума
операционная система ничего не делает, при среднем — старается ограничить использование памяти.
Максимум — максимальное число страниц, разрешённое для всех TCP сокетов. Так как мы замахиваемся
на 200000 соединений, нам надо бы минимум (200000*64)/4 = 3200000. Зачем заставлять нервничать
операционную систему.sudo sysctl -w net.ipv4.tcp_mem="3200000 3300000 3400000"
-
tcp_syncookies=0 — согласно рекомендациям разработчиков ядра
(http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt) этот режим лучше отключить на
сильной нагрузке -
net.ipv4.netfilter.ip_conntrack_max=1048576 — максимальное число соединений для работы механизма
connection tracking (используется, к примеру в iptables). -
net.ipv4.tcp_no_metrics_save=1 не сохранять результаты измерений TCP соединения в кеше при его
закрытии. В некоторых случаях помогает повысить производительность. -
net.ipv4.tcp_tw_reuse=1 разрешаем повторное использование TIME-WAIT сокетов в случаях, если
протокол считает это безопасным. -
net.core.somaxconn=200000 максимальное число открытых сокетов, ждущих соединения.
-
net.core.netdev_max_backlog=1000 максимальное количество пакетов в очереди на обработку, если
интерфейс получает пакеты быстрее, чем ядро может их обработать.
Итоговый скрипт подстройки ОС Linux
#!/bin/bash ulimit -n 1048576 MAX_SOCKS=`python -c"print(\`free | awk '/-\/+.*/ {print $4}'\`//64"` let MAX_SOCKS_MIDLE=$MAX_SOCKS+1000 let MAX_SOCKS_UP=$MAX_SOCKS+2000 sysctl -w net.ipv4.tcp_mem="$MAX_SOCKS $MAX_SOCKS_MIDLE $MAX_SOCKS_UP" sysctl -w net.ipv4.tcp_syncookies=0 sysctl -w net.ipv4.netfilter.ip_conntrack_max=1048576 sysctl -w net.ipv4.tcp_no_metrics_save=1 sysctl -w net.ipv4.somaxconn=$MAX_SOCKS sysctl -w net.ipv4.core.netdev_max_backlog=1000 sysctl -w net.ipv4.tcp_tw_recycle=0 sysctl -w net.ipv4.tcp_tw_reuse=0
SOCK_STREAM vs SOCK_DGRAM¶
См.также
- UDP
- TCP
Потоковый (SOCK_STREAM) | Дейтаграммный (SOCK_DGRAM) |
---|---|
Устанавливает соединение | Нет |
Гарантирует доставку данных | Нет в случае UDP |
Гарантирует порядок доставки пакетов | Нет в случае UDP |
Гарантирует целостность пакетов | Тоже |
Разбивает сообщение на пакеты | Нет |
Контролирует поток данных | Нет |
TCP гарантирует доставку пакетов, их очередность, автоматически разбивает
данные на пакеты и контролирует их передачу, в отличии от UDP.
Но при этом TCP работает медленнее за счет повторной передачи потерянных
пакетов и большему количеству выполняемых операций над пакетами. Поэтому
там где требуется гарантированная доставка (Веб-браузер, telnet, почтовый клиент) используется TCP, если же требуется передавать данные в реальном
времени (многопользовательские игры, видео, звук) используют UDP.
Отложенное подтверждение и алгоритм Нейгла.
Алгоритм Нейгла используется для предотвращения забивания сети мелкими пакетами и имеет очень
простую формулировку — запрещено посылать второй маленький пакет до тех пор, пока не придет
подтверждение на первый. Тем не менее данные отправляются при выполнении хотя бы одного из следующих
условий:
- можно послать полный сегмент размером MSS (максимальный размер сегмента)
- соединение простаивает, и можно опустошить буфер передачи
- алгоритм Нейгла отключен, и можно опустошить буфер передачи
- есть срочные данные для отправки
- есть маленьки сегмент, но его отправка уже задержана на достаточно длительное время (таймер
терпения persist timer на тайм-аут ретрансмиссии RTO) - окно приема, объявленное хостом на другом конце, открыто не менее чем на половину
- необходимо повторно передать сегмент
- требуется послать ACK на принятые данные
- нужно объявить об обновлении окна
Кроме того, существует отложенное подтверждение. При этом хост, получивший сегмент, старается
задержать отправку ACK, чтобы послать её вместе с данными.
Алгоритм Нейгла в купе с отложенным подтверждением в резонансе дают нежелательные задержки. Поэтому
часто его отключают. Отключение алгоритма Нейгла производится заданием опции TCP_NODELAY
const int on = 1; setsockopt( s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof( on ) );
Но более правильным было бы проектировать приложение таким образом, чтобы было как можно меньше
маленьких блоков. Лучше писать большие. Для этого можно объединять данные самостоятельно, а можно
пользоваться аналогом write, работающим с несколькими буферами:
#include <sys/uio.h> ssize_t writev( int fd, const struct iovec *iov, int cnt ); ssize_t readv( int fd, const struct iovec *iov, int cnt ); // Возвращают число переданных байт или -1 в случае ошибки struct iovec { char *iov_base; /* Адрес начала буфера*/ size_t iov_len; /* Длина буфера*/ }
В Winsock используется
Usage
Create a server or client object and provide to its constructor a callable object (for log printing)
having this signature :
void(const std::string&)
For now, you can disable log printing by choosing the flag ASocket::SettingsFlag::NO_FLAGS when creating
a socket or by providing a callable object that does nothing with the string message.
#include "TCPClient.h" #include "TCPServer.h" #include "TCPSSLServer.h" #include "TCPSSLClient.h" auto LogPrinter = [](const std::string& strLogMsg) { std::cout << strLogMsg << std::endl; } CTCPClient TCPClient(LogPrinter); // creates a TCP client CTCPServer TCPServer(LogPrinter, "12345"); // creates a TCP server to listen on port 12345 CTCPSSLClient SecureTCPClient(LogPrinter); // creates an SSL/TLS TCP client CTCPSSLServer SecureTCPSSLServer(LogPrinter, "4242"); // creates an SSL/TLS TCP server to listen on port 4242
Please note that the constructor of CTCPServer or the SSL/TLS version throws only an exception in the Windows
version when the address resolution fails, so you should use the try catch block in this particular context.
To listen for an incoming TCP connection :
ASocket::Socket ConnectedClient; // socket of the connected client, we can have a vector of them for example. /* blocking call, should return true if the accept is OK, ConnectedClient should also be a valid socket number */ m_pTCPServer->Listen(ConnectedClient);
A wait period (in milliseconds) can be set, to avoid waiting indefinitely for a client :
m_pTCPServer->Listen(ConnectedClient, 1000); // waits for 1 second. Will return true, if a client connected to the server
To connect to a particular server (e.g 127.0.0.1:669)
m_pTCPClient->Connect("127.0.0.1", "669"); // should return true if the connection succeeds
To send/receive data to/from a client :
const std::string strSendData = "Hello World !"; m_pTCPServer->Send(ConnectedClient, strSendData); /* or */ m_pTCPServer->Send(ConnectedClient, strSendData.c_str(), 13); /* or even an std::vector<char> as a second parameter */ char szRcvBuffer = {}; m_pTCPServer->Receive(ConnectedClient, szRcvBuffer, 13);
To send/receive data to/from a server :
const std::string strSendData = "Hello World !"; m_pTCPClient->Send(strSendData); /* or */ m_pTCPClient->Send(strSendData.c_str(), 13); /* or even an std::vector<char> */ char szRcvBuffer = {}; m_pTCPClient->Receive(szRcvBuffer, 13);
To disconnect from server or client side :
m_pTCPClient->Disconnect(); m_pTCPServer->Disconnect(ConnectedClient);
Before using SSL/TLS secured classes, compile both library and the test program with the preprocessor macro OPENSSL.
If you don’t want to compile secure classes, you can indicate that to CMake when generating a makefile or Visual Studio solutions, by setting SOCKET_CPP_BUILD_WITHOUT_SECURE_CLASSES=TRUE (under Windows, in CMake-GUI, add the entry, select «BOOL» and check «Value») :
cmake -DCMAKE_BUILD_TYPE=Release -DSOCKET_CPP_BUILD_WITHOUT_SECURE_CLASSES etc...
Almost all the operations look similar to the operations above for unencrypted communications, the differences are :
The client socket to provide to the Listen method of an CTCPSSLServer is of type ASecureSocket::SSLSocket.
ASecureSocket::SSLSocket ConnectedClient;
Before listenning for incoming SSL/TLS connections, you have to set the server’s certificate and private key paths via
the proper methods :
m_pSSLTCPServer->SetSSLCertFile(SSL_CERT_FILE); m_pSSLTCPServer->SetSSLKeyFile(SSL_KEY_FILE);
You can also set CA file if you want. Otherwise, for now, passphrase must be included in the private key file.
To create SSL test files, you can use this command :
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
IMPORTANT: In the SSL/TLS server, ASecureSocket::SSLSocket objects must be disconnected with SSL/TLS server’s
disconnect method to free used OpenSSL context and connection structures. Otherwise, you will have memory leaks.
5.1. Программа-сервер
Текст программы-сервера на языке программирования СИ выглядит следующим образом
1 #include <sys/types.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <netdb.h> 5 #include <memory.h> 6 #define SRV_PORT 1234 7 #define BUF_SIZE 64 8 #define TXT_QUEST "Who are you?\n" 9 main () { 10 int s, s_new; 11 int from_len; 12 char buf; 13 struct sockaddr_in sin, from_sin; 14 s = socket (AF_INET, SOCK_STREAM, 0); 15 memset ((char *)&sin, '\0', sizeof(sin)); 16 sin.sin_family = AF_INET; 17 sin.sin_addr.s_addr = INADDR_ANY; 18 sin.sin_port = SRV_PORT; 19 bind (s, (struct sockaddr *)&sin, sizeof(sin)); 20 listen (s, 3); 21 while (1) { 22 from_len = sizeof(from_sin); 23 s_new = accept (s, &from_sin, &from_len); 24 write (s_new, TXT_QUEST, sizeof(TXT_QUEST)); 25 from_len = read (s_new, buf, BUF_SIZE); 26 write (1, buf, from_len); 27 shutdown (s_new, 0); 28 close (s_new); 29 }; 30 }
Строки 1…5 описывают включаемые файлы, содержащие определения для всех необходимых структур данных и символических констант.
Строка 6 приписывает целочисленной константе 1234 символическое имя SRV_PORT. В дальнейшем эта константа будет использована в качестве номера порта сервера. Значение этой константы должно быть известно и программе-клиенту.
Строка 7 приписывает целочисленной константе 64 символическое имя BUF_SIZE. Эта константа будет определять размер буфера, используемого для размещения принимаемых от клиента данных.
Строка 8 приписывает последовательности символов, составляющих текст вопроса клиенту, символическое имя TXT_QUEST. Последним символом в последовательности является символ перехода на новую строку ‘\n’. Сделано это для упрощения вывода текста вопроса на стороне клиента.
В строке 14 создается (открывается) socket для организации режима взаимодействия с установлением логического соединения (SOCK_STREAM) в сети TCP/IP (AF_INET), при выборе протокола транспортного уровня используется протокол «по умолчанию» (0).
В строках 15…18 сначала обнуляется структура данных sin, а затем заполняются ее отдельные поля. Использование константы INADDR_ANY упрощает текст программы, избавляя от необходимости использовать функцию gethostbyname для получения адреса локального узла, на котором запускается сервер.
Строка 19 посредством системного вызова bind привязывает socket, задаваемый дескриптором s, к порту с номером SRV_PORT на локальном узле. Bind завершится успешно при условии, что в момент его выполнения на том же узле уже не функционирует программа, использующая этот номер порта.
Строка 20 посредством системного вызова listen организует очередь на три входящих к серверу запроса на соединение.
Строка 21 служит заголовком бесконечного цикла обслуживания запросов от клиентов.
На строке 23, содержащей системный вызов accept, выполнение программы приостанавливается на неопределенное время, если очередь запросов к серверу на установление связи оказывается пуста. При появлении такого запроса accept успешно завершается, возвращая в переменной s_new дескриптор socket’а для обмена информацией с клиентом.
В строке 24 сервер с помощью системного вызова write отправляет клиенту вопрос.
В строке 25 с помощью системного вызова read читается ответ клиента.
В строке 26 ответ направляется в стандартный вывод, имеющий дескриптор файла номер 1. Так как строка ответа содержит в себе символ перехода на новую строку, то текст ответа будет размещен на отдельной строке дисплея.
Строка 27 содержит системный вывод shutdown, обеспечивающий очистку системных буферов socket’а, содержащих данные для чтения («лишние» данные могут там оказаться в результате неверной работы клиента).
В строке 28 закрывается (удаляется) socket, использованный для обмена данными с очередным клиентом.
Примечание. Данная программа (как и большинство реальных программ-серверов) самостоятельно своей работы не завершает, находясь в бесконечном цикле обработки запросов клиентов. Ее выполнение может быть прервано только извне путем посылки ей сигналов (прерываний) завершения. Правильно разработанная программа-сервер должна обрабатывать такие сигналы, корректно завершая работу (закрывая, в частности, посредством close socket с дескриптором s).
accept()¶
См.также
- http://unixhelp.ed.ac.uk/CGI/man-cgi?accept+2
Используется для принятия запроса на установление соединения от удаленного хоста. Принимает следующие аргументы:
- sockfd — дескриптор слушающего сокета на принятие соединения.
- cliaddr — указатель на структуру sockaddr, для принятия информации об адресе клиента.
- addrlen — указатель на socklen_t, определяющее размер структуры, содержащей клиентский адрес и переданной в accept(). Когда accept() возвращает некоторое значение, socklen_t указывает сколько байт структуры cliaddr использовано в данный момент.
Примечание
Функция возвращает дескриптор сокета, связанный с принятым соединением, или −1 в случае возникновения ошибки.
Пример на Си
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
Пример на Python
Installation
You will need CMake to generate a makefile for the static library or to build the tests/code coverage program.
Also make sure you have Google Test installed and OpenSSL updated to the lastest version.
OPENSSL_CRYPTO_LIBRARY : /usr/local/openssl/lib/libcrypto.so (.a will cause a link problem…)
OPENSSL_INCLUDE_DIR : /usr/local/openssl/include
OPENSSL_SSL_LIBRARY : /usr/local/openssl/lib/libssl.so (or .a if you want)
The CMake script located in the tree will produce Makefiles for the creation of the static library and for the unit tests program.
To create a debug static library and a test binary, change directory to the one containing the first CMakeLists.txt and :
mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE:STRING=Debug make
To create a release static library, just change «Debug» by «Release».
The library will be found under «build//lib/libsocket.a» whereas the test program will be located in «build//bin/test_socket»
To directly run the unit test binary, you must indicate the path of the INI conf file (see the section below)
.//bin/test_socket /path_to_your_ini_file/conf.ini
Building under Windows via Visual Studio
Concerning Google Test, the library will be downloaded and built automatically from its github repository.
Under C:\OpenSSL-Win64\bin, you will find the DLLs : libcrypto-1_1-x64.dll and libssl-1_1-x64.dll that you will need, later, to place in the same directory as the binary file that used the socket-cpp library.
Open CMake (cmake-gui).
In «Where is the source code», put the socket-cpp path (e.g. C:/Users/Amine/Documents/Work/PROJECTS/GitHub/socket-cpp), where the main CMakeLists.txt file exist.
In «Where to build binaries», paste the directory where you want to build the project (e.g. C:/Users/Amine/Documents/Work/PROJECTS/GitHub/socket-cpp/build).
Click on «Configure».
If you want to indicate a configuration file to run unit tests with «CTest», before clicking on «Configure», click on «Add entry» and add this entry : TEST_INI_FILE : C:/Users/Amine/Documents/Work/PROJECTS/GitHub/socket-cpp/my_test_conf.ini (tour configuration file).
Then click on «Generate», you can choose a Visual Studio version if it is not done before (e.g. Visual Studio 15 2017 Win64)
Finally, click on «Open Project» to open the solution in Visual Studio.
In Visual Studio, you can change the build type (Debug -> Release). Build the solution (press F7). It must succeed without any errors. You can close Visual Studio.
The library will be found under C:\Users\Amine\Documents\Work\PROJECTS\GitHub\socket-cpp\build\lib\Release\socket.lib
After building a program using «socket.lib», do not forget to copy OpenSSL DLLs in the directory where the program binary is located.
For example, in the build directory (e.g. C:\Users\Amine\Documents\Work\PROJECTS\GitHub\socket_build), under «bin», directory, you may find «Debug», «Release» or both according to the build type used during the build in Visual Studio, and in it, the test program «test_socket.exe».
Before executing it, make sure to copy the OpenSSL DLLs in the same directory (e.g. copy C:\OpenSSL-Win64\bin\libcrypto-1_1-x64.dll and C:\OpenSSL-Win64\bin\libssl-1_1-x64.dll, do not change the name of the DLL !)
If you have provided a valid configuration file to CMake-gui (TEST_INI_FILE), you can run unit tests in the command prompt (cmd.exe — after building the project in VS) by changing directory to the one where CMake generated the build files and by running :
ctest -c "Release"
to test the «Debug» library, just change «Release» by «Debug» (but ensure that you have build it before). Do not forget to place the DLLs near «test_socket.exe» before launching «ctest», otherwise, tests will fail.
Сервер сокетов
Мы сохраним программу сервера сокетов, как socket_server.py. Чтобы использовать соединение, нам нужно импортировать модуль сокета.
Затем последовательно нам нужно выполнить некоторую задачу, чтобы установить соединение между сервером и клиентом.
Мы можем получить адрес хоста с помощью функции socket.gethostname(). Рекомендуется использовать адрес порта пользователя выше 1024, поскольку номер порта меньше 1024 зарезервирован для стандартного интернет-протокола.
Смотрите приведенный ниже пример кода сервера:
import socket def server_program(): # get the hostname host = socket.gethostname() port = 5000 # initiate port no above 1024 server_socket = socket.socket() # get instance # look closely. The bind() function takes tuple as argument server_socket.bind((host, port)) # bind host address and port together # configure how many client the server can listen simultaneously server_socket.listen(2) conn, address = server_socket.accept() # accept new connection print("Connection from: " + str(address)) while True: # receive data stream. it won't accept data packet greater than 1024 bytes data = conn.recv(1024).decode() if not data: # if data is not received break break print("from connected user: " + str(data)) data = input(' -> ') conn.send(data.encode()) # send data to the client conn.close() # close the connection if __name__ == '__main__': server_program()
Итак, наш сервер сокетов работает на порту 5000 и будет ждать запроса клиента. Если вы хотите, чтобы сервер не завершал работу при закрытии клиентского соединения, просто удалите условие if и оператор break. Цикл while используется для бесконечного запуска серверной программы и ожидания клиентского запроса.
Таймаут при вызове connect
alarm
void alarm_hndlr( int sig ) { return; } int main( int argc, char **argv ) { // ... signal( SIGALRM, alarm_hndlr ); alarm( 5 ); int rc = connect( s, (struct sockaddr * )&peer, sizeof( peer ) ); alarm( ); if( rc < ) { if( errno == EINTR ) error( 1, , "Timeout\n" ); // ... }
Способ простой, но имеет ряд проблем.
- Таймер, используемый в вызове alarm не должен больше нигде применяться.
- Перезапустить connect сразу не получится. Необходимо будет подождать, закрыть и заново открыть сокет.
- Некоторые системы могут возобновлять connect
Неблокирующие соединения
Неплохо об асинхронной работе с сокетами расписано тут:
http://www.wangafu.net/~nickm/libevent-book/01_intro.html
Суть в том, чтобы использовать неблокирующие сокеты и следить за ними с помощью системных вызовов.
Жалко только, что переносимое решение «из коробки» можно реализовать только с тупым и медленным select.
select
int main( int argc, char **argv ) { struct sockaddr_in peer; INIT() // set_address( argv1], argv2], &peer, "tcp" ); SOCKET s = socket( AAF_INET, SOCK_STREAM, ); if( !isvalidsock( s ) ) error( 1, errno, "Socket colling error" ); /* Добавляет флаг "не блокирующий" к флагам сокета (как дескриптора файла)*/ int flags = fcntl( s, F_GETFL, ) ); if( flags < ) error( 1, errno, "Error calling fcntl(F_GETFL)" ); if( fcntl( s, F_SETFL, flags | O_NONBLOCK ) < ) error( 1, errno, "Error calling fcntl(F_SETFL)" ); int rc = connect( s, (struct sockaddr * )&peer, sizeof( peer ) ); if( rc < && errno != EINPROGRESS ) error( 1, errno, "Error calling connect" ); if( rc == ) // вдруг уже не надо ждать { if( fcntl( s, F_SETFL, flags ) < ) error( 1, errno, "Error calling fcntl (flags recovery)"); client( s, &peer ); return ; } /* Если ждать надо, ждем с помощью select'а*/ fd_set rdevents; fd_set wrevents; fs_set exevents; FD_ZERO( &rdevents ); FD_SET( s, &rdevents ); wrevents = rdevents; exevents = rdevents; struct timeval tv; tv.tv_sec = 5; tv.tv_usec = ; rc = select( s + 1, &rdevents, &wrevents, &exevents, &tv ); if( rc < ) error( 1, errno, "Error calling select" ); else if( rc == ) error( 1, , "Connection timeout" ); else if( isconnected( s, &rdevents, &wrevents, &exevents ) ) { if( fcntl( s, F_SETFL, flags ) < ) error( 1, errno, "Error calling fcntl (flags recovery)" ) client( s, &peer ); } else error( 1, errno, "Error calling connect" ); return ; } /* В UNIX и WINDOWS разные методы уведомления об успешной попытке соединения, поэтому проверка вынесена в отдельную функцию.*/ // UNIX int isconnected( SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex) { int err; int len = sizeof( err ); errno = ; /*предполагаем, что ошибки нет*/ if( !FD_ISSET( s, rd ), !FD_ISSET( s, wr ) ) return ; if( getsockopt( s, SOL_SOCKET, SO_ERROR, &err, &len ) < ) return ; errno = err; /* если мы не соединились */ return err == ; } // Windows int isconnected( SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex) { WSASetLastError( ); if( !FD_ISSET( s, rd ) && !FD_ISSET( s, wr ) ) return ; if( !FD_ISSET( s, ex ) ) return ; return 1; }
Класс Socket
Класс Socket играет важную роль в сетевом программировании, обеспечивая функционирование как клиента, так и сервера. Главным образом, вызовы методов этого класса выполняют необходимые проверки, связанные с безопасностью, в том числе проверяют разрешения системы безопасности, после чего они переправляются к аналогам этих методов в Windows Sockets API.
Прежде чем обращаться к примеру использования класса Socket, рассмотрим некоторые важные свойства и методы этого класса:
Свойство или метод | Описание |
---|---|
AddressFamily | Дает семейство адресов сокета — значение из перечисления Socket.AddressFamily. |
Available | Возвращает объем доступных для чтения данных. |
Blocking | Дает или устанавливает значение, показывающее, находится ли сокет в блокирующем режиме. |
Connected | Возвращает значение, информирующее, соединен ли сокет с удаленным хостом. |
LocalEndPoint | Дает локальную конечную точку. |
ProtocolType | Дает тип протокола сокета. |
RemoteEndPoint | Дает удаленную конечную точку сокета. |
SocketType | Дает тип сокета. |
Accept() | Создает новый сокет для обработки входящего запроса на соединение. |
Bind() | Связывает сокет с локальной конечной точкой для ожидания входящих запросов на соединение. |
Close() | Заставляет сокет закрыться. |
Connect() | Устанавливает соединение с удаленным хостом. |
GetSocketOption() | Возвращает значение SocketOption. |
IOControl() | Устанавливает для сокета низкоуровневые режимы работы. Этот метод обеспечивает низкоуровневый доступ к лежащему в основе классу Socket. |
Listen() | Помещает сокет в режим прослушивания (ожидания). Этот метод предназначен только для серверных приложений. |
Receive() | Получает данные от соединенного сокета. |
Poll() | Определяет статус сокета. |
Select() | Проверяет статус одного или нескольких сокетов. |
Send() | Отправляет данные соединенному сокету. |
SetSocketOption() | Устанавливает опцию сокета. |
Shutdown() | Запрещает операции отправки и получения данных на сокете. |
Принципы сокетов¶
Каждый процесс может создать слушающий сокет (серверный сокет) и привязать его
к какому-нибудь порту операционной системы (в UNIX непривилегированные
процессы не могут использовать порты меньше 1024). Слушающий процесс обычно
находится в цикле ожидания, то есть просыпается при появлении нового
соединения. При этом сохраняется возможность проверить наличие соединений на
данный момент, установить тайм-аут для операции и т.д.
Каждый сокет имеет свой адрес. ОС семейства UNIX могут поддерживать много
типов адресов, но обязательными являются INET-адрес и UNIX-адрес. Если
привязать сокет к UNIX-адресу, то будет создан специальный файл (файл сокета)
по заданному пути, через который смогут сообщаться любые локальные процессы
путём чтения/записи из него (см. Доменный сокет Unix). Сокеты типа INET
доступны из сети и требуют выделения номера порта.
Функции обмена данными
В режиме с установлением логического соединения после удачного выполнения пары взаимосвязанных системных вызовов connect (в клиенте) и accept (в сервере) становится возможным обмен данными.
Этот обмен может быть реализован обычными системными вызовами read и write, используемыми для работы с файлами (при этом вместо дескрипторов файлов в них задаются дескрипторы socket’ов).
Кроме того могут быть дополнительно использованы системные вызовы send и recv, ориентированные специально на работу с socket’ами.
Примечание. Для обмена данными в режиме без установления логического соединения используются, как правило, системные вызовы sendtoи recvfrom. Sendto позволяет специфицировать вместе с передаваемыми данными (составляющими дейтаграмму) адрес их получателя. Recvfrom одновременно с доставкой данных получателю информирует его и об адресе отправителя.
Реализация коммуникации посредством TCP-сокетов в Java
Давайте посмотрим, как мы можем реализовать коммуникацию сокетов в Java. Мы сейчас напишем две Java-программы. Одной будет программа, запущенная на сервере, а другой — клиентская программа, которая будет взаимодействовать с сервером.
Реализация серверного сокета
В приведенной выше программе сервер открывает сокет с порта 50001 на серверной машине и ожидает клиента на . После подключения клиента создается экземпляр выходного потока. Это может быть использовано для отправки данных с сервера на подключенный клиент. Именно это и делает . После отправки данных соединение с клиентом завершается.
Теперь давайте создадим клиент для взаимодействия с серверным сокетом, созданным выше.
Реализация клиентского сокета
Показанная выше программа действует как клиент, создавая соединение с серверным сокетом. После подключения клиент получает отправленные сервером данные. Входной поток соединяется с буфером, используя для хранения полученных данных, так как мы не можем быть уверены, что данные будут использоваться сразу же после получения. Затем мы считываем данные из буфера и выводим их в консоль.
Запуск программ
Сначала запустите серверную Java-программу, а затем клиентскую Java-программу (потому что сервер уже должен работать для подключения клиента). Вы увидите в терминале, где работает клиентская программа. Вот что здесь произошло: серверная программа отправила данные клиенту по запросу, а клиентская программа вывела их на терминал.
В этой статье мы обсудили, что такое сокеты и Java-реализация связи TCP-сокетов.
Спасибо за чтение.
- Кто на свете всех сильнее - Java, Go и Rust в сравнении
- Java-Lombok: нужны ли геттеры и сеттеры?
- Топ - 9 фреймворков Java в 2020 году
Перевод статьи Pavindu Lakshan: “Fundamentals of TCP Socket Programming in Java”