Введение в сетевое программирование
Содержание
- 1 Введение в сетевое программирование
- 1.1 Интерфейс транспортного уровня
- 1.2 Сокеты
- 1.3 Команды
- 1.4 Постановка задачи
- 1.5 Разработка протокола Calculation 0.1
- 1.6 Алгоритм работы сервера (TCP)
- 1.7 Алгоритм работы клиента (TCP)
- 1.8 Алгоритм работы сервера (UDP)
- 1.9 Алгоритм работы клиента (UDP)
- 1.10 TCP-сервер (C++)
- 1.11 TCP-клиент (C++)
- 1.12 Ненадежный UDP-сервер (C++)
- 1.13 Ненадежный UDP-клиент (C++)
- 1.14 TCP-сервер (C#, .NET)
- 1.15 TCP-клиент (C#, .NET)
Введение в сетевое программирование
Интерфейс транспортного уровня
Сокеты
Socket (гнездо) – это структура данных, идентифицирующая сетевое соединение.
Зачем нужны эти сокеты? Сервер (программа) может одновременно поддерживать несколько TCP-соединений с другими компьютерами, используя один и тот же стандартный номер порта. Как это реализовать? Можно возложить эту задачу на программиста. Пусть он выбирает из буфера приема сетевого уровня пакеты, смотрит от кого они отправлены и отвечает соответствующим образом. Но можно сделать все это удобнее.
С каждым соединением должен быть связан свой поток, в который можно писать информацию и из которого можно ее считывать. Каждому потоку соответствует свой IP-адрес удаленного компьютера и свой порт удаленного компьютера. Будем назвать структуру данных, соответствующую каждому такому потоку, сокетом (розеткой). Таким образом, сервер можно сравнить с разветвителем с кучей розеток, к которым подключены клиенты. Если сделать так, то вместо того, чтобы разбираться в куче разносортных пакетов из буфера приема сетевого уровня, сервер будет читать из потоков, каждый из которых соответствует своему клиенту. Данные от клиентов не будут сваливаться в кучу, а будут распределяться по потокам-сокетам. Ответственность за такое распределение ложится не на программиста, а на драйвер транспортного уровня операционной системы. Сокеты были разработаны в университете в Berkley. Стали стандартом, вместо OSI-шного TLI (Transport Layer Interface).
С 1978 года начинает свою историю BSD UNIX , созданный в университете Беркли. Автором BSD был Билл Джой. В начале 1980-х компания AT&T, которой принадлежали Bell Labs, осознала ценность UNIX и начала создание коммерческой версии UNIX . Важной причиной раскола UNIX стала реализация в 1980 г. стека протоколов TCP/IP. До этого межмашинное взаимодействие в UNIX пребывало в зачаточном состоянии — наиболее существенным способом связи был UUCP (средство копирования файлов из одной UNIX-системы в другую, изначально работавшее по телефонным сетям с помощью модемов). Эти две операционные системы реализовали 2 различных интерфейса программирования сетевых приложений: Berkley sockets (TCP/IP) и интерфейс транспортного уровня TLI (OSI ISO) (англ. Transport Layer Interface). Интерфейс Berkley sockets был разработан в университете Беркли и использовал стек протоколов TCP/IP, разработанный там же. TLI был создан AT&T в соответствии с определением транспортного уровня модели OSI. Первоначально в ней не было реализации TCP/IP или других сетевых протоколов, но подобные реализации предоставлялись сторонними фирмами. Это, как и другие соображения (по большей части, рыночные), вызвало окончательное размежевание между двумя ветвями UNIX — BSD (университета Беркли) и System V (коммерческая версия от AT&T). Впоследствии, многие компании, лицензировав System V у AT&T, разработали собственные коммерческие разновидности UNIX, такие, как AIX, HP-UX, IRIX, Solaris.
Команды
SOCKET – создать новый (пустой) сокет.
BIND – сервер связывает свой локальный адрес (порт) с сокетом.
LISTEN – сервер выделяет память под очередь подсоединений клиентов (TCP).
ACCEPT – сервер ожидает подсоединения клиента или принимает первое подключение из очереди (TCP). Чтобы заблокировать ожидание входящих соединений, сервер выполняет примитив ACCEPT. Получив запрос соединения, транспортный модуль ОС создает новый сокет с теми же свойствами, что и у исходного сокета, и возвращает описатель файла для него. После этого сервер может разветвить процесс или поток, чтобы обработать соединение для нового сокета и параллельно ожидать следующего соединения для оригинального сокета.
CONNECT–клиент запрашивает соединение (TCP).
SEND/SEND_TO – послать данные (TCP/UDP).
RECEIVE/RECEIVE_FROM – получить данные (TCP/UDP).
DISCONNECT – запросить разъединение (TCP).
Постановка задачи
Для ознакомления с сетевым программированием разберем пример. Пусть требуется запрограммировать службу удаленных вычислений. Клиенты просят сервер вычислить выражение (для начала содержащее только одну арифметическую операцию +, -, *, / ), сервер возвращает результат.
Разработка протокола Calculation 0.1
Назовем наш протокол Calculation 0.1. Для начала определим формат запроса и ответа. Пусть, например, запрос клиента должен начинаться со слова CALC, далее через пробел оперция и потом два числа – аргументы. Тогда запрос клиента на вычисление произведения 12*6 будет выглядеть так:
CALC * 12 6 ENTER
Ответы сервера будет начинаться со слова OK, если запрос был корректен и далее через пробел число – результат вычислений. Если же запрос был неправелен, ответом будет одно слово ERR.
ОК 72 ENTER (если все нормально) ERR ENTER (если запрос неправильный)
ENTER нужен, чтобы узнать где конец строки.
После определения правил работы протокола, можно перейти к реализации. И тут встает вопрос: используем TCP или UDP? Мы разберем оба случая. И, для начала, разберем алгоритм взаимодействия сервера с клиентом, а потом попытаемся его реализовать программно на языке C++.
Алгоритм работы сервера (TCP)
- Запускается заранее, до подключения клиентов.
- Сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345.
- Выделяет память для очереди подключений.
- В цикле:
- устанавливает соединение с клиентом из очереди; если очередь пуста, то ждет подключения клиента;
- принимает/передает данные;
- закрывает соединение с клиентом.
Алгоритм работы клиента (TCP)
- Получает от ОС случайный номер порта для общения с сервером.
- Устанавливает соединение с сервером.
- Передает/принимает данные.
- Закрывает соединение с сервером.
Алгоритм работы сервера (UDP)
- Запускается заранее, до подключения клиентов.
- сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345.
- В цикле:
- ждет прихода сообщения;
- обрабатывает данные;
- передает результат.
Основное отличие UDP от TCP — не нужно возиться с соединениями.
Алгоритм работы клиента (UDP)
- Получает от ОС случайный номер порта для общения с сервером.
- Передает/принимает данные.
TCP-сервер (C++)
Будем использовать winsock2.h
SOCKET ListenSocket, ClientSocket;
sockaddr_in ServerAddr;
int err, maxlen = 512;
char* recvbuf = new char[maxlen];
char* result_string = new char[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);
// сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт 12345
bind ( ListenSocket, ServerAddr, sizeof(ServerAddr));
// выделяет память для очереди подключений
listen (ListenSocket, 50);
while (true)
{
// устанавливает соединение с клиентом из очереди
ClientSocket = аccept(ListenSocket, NULL, NULL);
// принимает данные
err = recv (ClientSocket, recvbuf, maxlen, 0);
// если очередь пуста, то ждет подключения клиента
if (err > 0)
{
recvbuf[err]=0;
printf("Received query: %s\n", (char* )recvbuf);
// вычисляем результат
int result = ... ;
_snprintf_s (result_string, maxlen, maxlen, "OK %d\n", result);
// передает данные
send( ClientSocket, result_string, strlen(result_string), 0 );
printf("Sent answer: %s\n", result_string);
}
// закрывает соединение с клиентом
closesocket(ClientSocket);
}
TCP-клиент (C++)
SOCKET ClientSocket;
sockaddr_in ServerAddr;
int err, maxlen = 512;
char* recvbuf = new char[maxlen];
char* query = new char[maxlen];
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-клиенту порт присваивается в функции connect
_snprintf_s (query,maxlen,maxlen,"CALC * 12 6\n");
// передает данные
send (ConnectSocket, query,strlen(query),0);
printf("Sent: %s\n", query);
// принимает данные
err = recv(ConnectSocket,recvbuf,maxlen,0);
if (err > 0)
{
recvbuf[err]=0;
printf(“Result: %s\n", (char* )recvbuf);
}
// закрывает соединение с сервером
closesocket(ConnectSocket);
Ненадежный UDP-сервер (C++)
SOCKET SendRecvSocket;
sockaddr_in ServerAddr, ClientAddr;
int err, maxlen = 512;
ClientAddrSize = sizeof(ClientAddr);
char* recvbuf = new char[maxlen];
char* result_string = new char[maxlen];
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);
// сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт 12345
bind ( ListenSocket, 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 = ... ;
_snprintf_s (result_string,maxlen,maxlen,
"OK %d\n",result);
// передает данные
sendto (SendRecvSocket, result_string, strlen(result_string), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr));
printf("Sent answer: %s\n", result_string);
}
}
Ненадежный UDP-клиент (C++)
SOCKET SendRecvSocket;
sockaddr_in ServerAddr;
int err, maxlen = 512;
char* recvbuf = new char[maxlen];
char* query = new char[maxlen];
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);
_snprintf_s (query,maxlen,maxlen,"CALC * 12 6\n");
// автоматически получает от ОС случайный номер порта для общения с сервером при отправке первой датаграммы
// передает данные
sendto (SendRecvSocket, query, strlen(query), 0, (sockaddr *)&ServerAddr, sizeof(ServerAddr));
printf("Sent: %s\n", query);
// принимает данные
err = recvfrom (SendRecvSocket,recvbuf,maxlen,0,0,0);
if (err > 0)
{
recvbuf[err]=0;
printf(“Result: %s\n", (char* )recvbuf);
}
closesocket(SendRecvSocket);
TCP-сервер (C#, .NET)
Int32 port = 12345;
IPAddress localAddr=IPAddress.Parse("127.0.0.1");
server = new TcpListener(localAddr, port);
server.Start();
while (true)
{
TcpClient client = server.AcceptTcpClient();
NetworkStream stream = client.GetStream();
stream.Read(query, 0, query.Length)
stream.Write(result, 0, result.Length);
client.Close();
}
TCP-клиент (C#, .NET)
Int32 ServerPort = 12345;
string ServerName=“localhost”
TcpClient client = new TcpClient(ServerName, ServerPort );
NetworkStream stream = client.GetStream();
stream.Write(query, 0, query.Length)
stream.Read(result, 0, result.Length);
stream.Close();
client.Close();