Сегодня представляем вашему вниманию гостевую статью, автор — Роман Власов. Выражаем автору благодарность за предоставление статьи для публикации у нас!
В данной статье мы рассмотрим использование интернет-сокетов для создания клиент-серверной программы, основная задача которой — мониторинг температуры процессора на большом количестве машин одновременно. Что-же, приступим!
Для начала сразу определимся с задачами. Нам нужна некая программа, запускаемая на машинах, данные о которых нам необходимо забирать (её мы назовём sender/отравитель ), и, как следствие, программа для сбора инфы со многих отправителей (monitor).
Почти все нужные для нас инструменты для создания и использования сетевых приложений находятся в модуле стандартной библиотеки std.socket.
Для начала продекларируем использование модуля с сокетами и ещё пары вспомогательных модулей:
import std.stdio; //эта строка комментариев не требует import std.socket; void main() { //... }
Далее начнем писать код для монитора.
В стандартной библиотеке языка D(далее Phobos) за сокеты отвечает абстрактный класс Socket, имеющий более узкоспециализированные дочерние классы. Нам же понадобится класс TcpSocket(), обеспечивающий передачу данных по протоколу TCP. После старта программы мы создаем новый сокет в реализации TcpSocket, после этого для его дальнейшей работы ему требуется привязка к определенному адресу и/или порту, чаще всего адресом является localhost, т. е. тот самый 127.0.0.1 . Привязка осуществляется вызовом метода bind с передачей ему экземпляра одной из реализаций а.к. Address. Будем использовать самый привычный формат IP адресов Ipv4. За парсинг и , собственно, хранение адреса такого формата отвечает класс InternetAddress(для IPv6 есть отдельный класс Internet6Address).
Конструктор класса InternetAddress может принимать как строку адреса, так и число ushort для порта. Если передается только порт, то в адрес автоматом ставится localhost:
void main() { auto listenerSocket = new TcpSocket(); //создаем TCP сокет listenerSocket.bind(new InternetAddress(2015)); //привязываем сокет к порту 2015 listenSocket.listen(0); //устанавливаем сокет в режим приёма входящих соединений. //... }
Далее опишем структуру сендера. Так как будет возможность подключения большого количества сендеров, будет удобно присвоить им имена:
struct Sender { string name; Socket sock; this(string name, Socket sock) { this.name = name; this.sock = sock; } }
Переходим к самому вкусному! :p
В Фобосе есть класс, так-называемый SocketSet. Как ясно из названия, он представляет собой список сокетов, и… в принципе всё! И для чего он нам? На этот вопрос нам ответит статичная функция Socket.select. Она пробегается по SocketSet’у и удаляет указатели на те сокеты, чьё состояние осталось прежним. Это упрощает построение асинхронных сетевых моделей, но мы будем использовать псевдоасинхронность.
for(;;) { sendersSet.add(listenerSocket); foreach(sender; senders) { sendersSet.add(sender.sock); } Socket.select(sendersSet, null, null); //обрабатываем сокеты, изенившие своё состояние sendersSet.reset(); }
Теперь нам необходимо обработать изменения состояния наших сокетов.
Первым делом будем обрабатывать сокет listenerSocket, ибо его изменение означает только одно – новое подключение. В следующем участке кода всё достаточно тривиально.
if(sendersSet.isSet(listenerSocket)) { Socket newSenderSocket = listenerSocket.accept(); if(senders.length < MaxConnection + 1) { string name; if(newSenderSocket.getString(name)) { writeln("New connection: ", name); senders ~= Sender(name, newSenderSocket); } } else writeln("Reached the maximum number of connections"); }
Далее, мы пробегаемся по всем клиентам, узнаём, остался ли он коллекции сокетов, и если это так, читаем принятые данные и выводим их на экран:
foreach(i, sender; senders) { if(sendersSet.isSet(sender.sock)) { string buffer; if(sender.sock.getString(buffer)) { writeln("by ", sender.name, ": "); writeln(buffer); continue; } sender.sock.close(); senders = senders.remove(i); i--; } }
Вы можете заметить функцию getString. Он читает данные у сокета, возвращает логическую переменную, информирующую об успехе операции, и присваивает внешней переменной принятые данные.
“Постойте, так это же метод! Как он может читать данные у сокета, если он изначально часть класса?” - Спросят программисты, не знакомые с Унифицированным Синтаксисом Вызова Функций (Uniform Function Call Syntax или UFCS). Для того, чтобы понять данную фишку, достаточно глянуть на объявление из функции из следующего участка кода и сравнить с её вызовом из кода выше:
bool getString(Socket sock, out string output) { char[1024] buf; auto dataLength = sock.receive(buf); if(dataLength != 0) { output = to!(string)(buf[0..dataLength]); return true; } else if(dataLength == Socket.ERROR) writeln("Connection error"); else { writeln("Connection closed"); } return false; }
UFCS позволяет строить конструкции подобного рода:
// заменить все, что похоже на числа типа real // на их округленный эквивалент stdin .byLine .map!(l => l.replaceAll!(c => c.hit.round) (reFloatingPoint)) .each!writeln;
С остальной частью функции getString ничего сложного. Единственный вопрос может вызвать атрибут параметра out в объявлении. Он сообщает компилятору, что данный параметр предназначен для вывода результата из функции и будет инициализирован значением по умолчанию при входе в неё. В языке C/C++ в этом ключе используются указатели, хоть это и менее безопасно:
bool getString(Socket* sock, string* buffer)…
Всё, сервер готов, но остался еще один штрих. Было бы полезно передавать программе параметр, отвечающий за порт прослушивания. Что-же, и на это найдется управа. В стандартной библиотеке есть модуль std.getopt. Он предназначен для получения и парсинга параметров, переданных приложению во время запуска. Парсинг параметров осуществляется вызовом функции getopt:
ushort listenedPort = 2015; auto result = getopt(args, "p", "port", &listenedPort); if(result.helpWanted) { defaultGetoptPrinter("Some info about the program.", result.options); } auto listenerSocket = new TcpSocket(); listenerSocket.bind(new InternetAddress(listenedPort));
В качестве параметров функции getopt передаются по порядку параметры, полученные во время запуска, два варианта одной записи и идентификатор опции. Т.е., функцию теперь можно вызывать как
./listener -p 2015
, так и
./listener –port=2015
А если будет передан –help, то программа автоматически выведет стандартный вывод помощи со строкой, обозначенной в функции defaultgetoptPrinter.
[accordion][panel intro="Полный код слушателя:"]
//исходный код приёмника данных о температуре ЦП import std.stdio : writeln; import std.socket; import std.conv; import std.algorithm : remove; import std.getopt; struct Sender { string name; Socket sock; this(string name, Socket sock) { this.name = name; this.sock = sock; } } bool getString(Socket sock, out string output) { char[1024] buf; auto dataLength = sock.receive(buf); if(dataLength != 0) { output = to!(string)(buf[0..dataLength]); return true; } else if(dataLength == Socket.ERROR) writeln("Connection error"); else { writeln("Connection closed"); } return false; } void main(string[] args) { ushort listenedPort = 2015; auto result = getopt(args, "p", "port", &listenedPort); if(result.helpWanted) { defaultGetoptPrinter("Some info about the program.", result.options); } auto listenerSocket = new TcpSocket(); listenerSocket.bind(new InternetAddress(listenedPort)); listenerSocket.listen(0); enum MaxConnection = 60; auto sendersSet = new SocketSet(MaxConnection + 1); Sender[] senders; for(;;) { sendersSet.add(listenerSocket); foreach(sender; senders) { sendersSet.add(sender.sock); } Socket.select(sendersSet, null, null); foreach(i, sender; senders) { if(sendersSet.isSet(sender.sock)) { string buffer; if(sender.sock.getString(buffer)){ writeln("by ", sender.name, ": "); writeln(buffer); continue; } sender.sock.close(); senders = senders.remove(i); i--; } } if(sendersSet.isSet(listenerSocket)) { Socket newSenderSocket = listenerSocket.accept(); string name; if(newSenderSocket.getString(name)) { writeln("New connection: ", name); Sender sender = Sender(name, newSenderSocket); senders ~= sender; } } sendersSet.reset(); } }
[/panel][/accordion]