Критические изменения в библиотеке вводы\вывода dart:io

Классы и интерфейсы стандартной библиотеки для ввода\вывода dart:io подверглись значительной переработки, и теперь соответствует новым интерфейсам и концепциям используемым в основных библиотеках dart:core и dart:async. Таким образом, практически полностью убраны реализации классов, использующие в работе регистрацию и вызов обратных callback-функций, и все асинхронные операции проводятся с помощью интерфейсов Stream и Future.

На данный момент, изменения представлены только в ветки для разработчиков "bleeding_edge", но в течение недели они будут совмещены с основной веткой.

К сожалению, данные изменения требуют переписывания существующего кода, в тех местах, где присутствуют обращения к классам библиотеки dart:io. К счастью, команда разработчиков, снабдила пользователей краткой справкой, по новым способам работы с dart:io.

Потоки в dart:io

Классы InputStream и OutputStream замещаются классами, реализующими новые интерфейсы IOSink и Stream<List<int>>. И соответственно вся логика использования потоков ввода-вывода перестраивается с учетом особенностей новых интерфейсов.

Чтение данных из потока - Stream вместо InputStream

При чтении данных из потока Stream<List<int>> теперь достаточно использовать только один метод Stream.listen, с помощью которого можно единовременно установить все функции для обработки событий - получение данных, завершение чтения и возникновение ошибок. В следующем примере, представлен код для старого и нового способов чтения данных из потока:

dart:io v1
InputStream stream = ...
stream.onData = () {
  var data = request.inputStream.read();
  /* обработка данных */
};
stream.onClosed = () {
  /* все данные получены */
};
stream.onError = (e) {
  /* ошибки при чтение данных */
};
dart:io v2
Stream<List<int>> stream = ...
stream.listen(
  (data) {
    /* обработка данных */
  },
  onDone: () {
    /* все данные получены */
  },
  onError: (e) {
    /* ошибки при чтение данных */
  });

Стоит отметить, что вместе с классом InputStream удален класс для обработки текстовых потоков StringInputStream. А для того, чтобы работать с текстовыми потокам представлены два новых класса для трансформации байтовых потоков - StringDecoder и LineTransformer. Класс StringDecoder преобразовывает поток типа List<int> в поток строк String, а класс LineTransformer преобразовывает поток строк String в новый поток строк, но такой, в котором, каждый элемент String - является отдельной текстовой-строкой, т.е. завершается силовом конца строки.

dart:io v1
InputStream stream = ...
StringInputStream stringStream = new StringInputStream(stream);
stringStream.onLine = () {
  String line = stringStream.readLine();
  /* обработка строки текста */
};
stringStream.onClosed = () {
  /* все данные получены */
};
stream.onError = (e) {
  /* ошибки при чтение данных */
};
dart:io v2
Stream<<int>> stream = ... stream .transform(new StringDecoder()) .transform(new LineTransformer()) .listen( (String line) { /* обработка строки текста */ }, onDone: () { /* все данные получены */ }, onError: (e) { /* ошибки при чтение данных */ });

Запись данных в поток - IOSink вместо OutputStream

Класс IOSink заменил OutputStream и следующий пример демонстрирует, как адаптировать старый код, для использования нового интерфейса:

dart:io v1
OutputStream stream = ...
// записываем массив байт - "Hello"
stream.write([72, 101, 108, 108, 111]);
// записываем строку
stream.writeString(", OutputStream!");
stream.close();
dart:io v2
IOSink sink = ...
// записываем массив байт - "Hello"
sink.add([72, 101, 108, 108, 111]);
// записываем строку
sink.addString(", IOSink!");
sink.close();

Кроме того, интерфейс IOSink позволяет напрямую получать данные из потока, с помощью метода IOSink.addStream.

HTTP

Основные изменения, касающиеся реализации HTTP-протокола для серверной и клиентской части библиотеки dart:io заключаются в следующем:

  • Новый интерфейс HttpServer ожидающий и обрабатывающий http-запросы, создается с помощью статического метода HttpServer.bind.
  • Интерфейс HttpServer обрабатывает поток объектов, типа HttpRequests.
  • Из класса HttpServer удален сеттер defaultRequestHandler и метод addRequestHandler.
  • Классы HttpRequest и HttpClientResponse реализуют интерфейс Stream<List<int>>.
  • Классы HttpClientRequest и HttpResponse реализуют интерфейс IOSink.

HTTP-сервер

Чтобы создать новый http-сервер для обработки запросов c определенного адреса и порта, теперь используется статический метод HttpServer.bind, который создает экземпляр HttpServer и асинхронно возвращает его с помощью объект типа Future. Следующий пример демонстрирует, как адаптировать старый код, для использования нового интерфейса:

dart:io v1
HttpServer server = new HttpServer();
server.defaultRequestHandler = …
server.addRequestHandler(…);
server.listen(“127.0.0.1”, 8080);
// Теперь HTTP-сервер подсоединен к адресу и порту, и ожидает входящие запросы.
dart:io v2
HttpServer.bind(“127.0.0.1”, 8080)
    .then((HttpServer server) {
      server.listen(
          (HttpRequest request) {
            // Обработка нового запроса.
          });
}

Методы HttpServer.defaultRequestHandler и HttpServer.addRequestHandler удалены из класса HttpServer и теперь вся обработка запросов происходит в callback-функции, задаваемой с помощью метода HttpServer.listen. В качестве параметра этой callback-функции передается объект класса HttpRequest, а параметры для ответа сервера, следует задавать через свойство request.response.

HTTP-клиент

Что касается клиентских http-запросов, то основной класс HttpClient сохранен, а удален класс HttpClientConnection. И теперь в качестве результата на новый запрос асинхронно возвращает объект типа Future. Для удобства использования метод завершения запроса к сервер - HttpClientRequest.close асинхронно возвращает ответ от сервера Future. Следующий пример демонстрирует, как адаптировать старый код, для использования нового интерфейса:

dart:io v1
HttpClient client = new HttpClient();
HttpClientConnection connection = client.get(...);
connection.onRequest = (HttpClientRequest request) {
  // Подготавливаем запрос
  request.outputStream.close();
}
connection.onResponse = (HttpClientResponse response) {
  // Обрабатываем ответ
}
dart:io v2
HttpClient client = new HttpClient();
client.get(...)
    .then((HttpClientRequest request) {
      // Подготавливаем запрос
      return request.close();
    })
    .then((HttpClientResponse response) {
      // Обрабатываем ответ
    });

Веб-сокеты

Интерфейсы для работы с веб-сокетами значительно упрощены, и теперь используется всего один класс WebSocket - для реализации и клиента и сервера. Класс WebSocket представляет поток собыйти.

Веб-сокет сервер

Со стороны сервера веб-сокеты обрабатываются с использованием специального трансформатора WebSocketTransformer. Данный трансформатор позволяет преобразовать поток типа HttpRequests в поток WebSockets. Следующий пример демонстрирует, как адаптировать старый код, для использования нового интерфейса:

dart:io v1
HttpServer server = new HttpServer();
server.listen(...);
WebSocketHandler handler = new WebSocketHandler();
handler.onOpen = (WebSocketConnection connection) {
  connection.onMessage = (Object message) {
    /* обрабатываем сообщение */
  };
  connection.onClosed = (status, reason) {
    /* закрытие соединения */
  };
};
server.defaultRequestHandler = handler.onRequest;
dart:io v2
HttpServer.bind(...).then((server) {
  server.transform(new WebSocketTransformer()).listen((WebSocket webSocket) {
    webSocket.listen((event) {
      if (event is MessageEvent) {
        /* обрабатываем сообщение */
      } else if (event is CloseEvent) {
        /* закрытие соединения */
      }
    });
  });

Веб-сокет клиент

С точке зрения клиентских соединений, использование класса WebSocket стало еще проще. Для соединения с сервером достаточно просто использовать статический метод WebSocket.connect, который асинхронно возвращает объект типа Future. В следующем примере, представлен код для старого и нового способов работы с клиентскими веб-сокетами:

dart:io v1
HttpClient client = new HttpClient();
HttpClientConnection conn = client.openUrl(“https://127.0.0.1:8080”);
WebSocketClientConnection wsconn = new WebSocketClientConnection(conn);
wsconn.onMessage = (message) {
  /* обрабатываем сообщение */
}
wsconn.onClosed = (status, reason) {
  /* закрытие соединения */
};
dart:io v2
WebSocket.connect("ws://127.0.0.1:8080")
    .then((WebSocket webSocket) {
      webSocket.listen((event) {
        if (event is MessageEvent) {
          /* обрабатываем сообщение */
        } else if (event is CloseEvent) {
          /* закрытие соединения */
        }
      });
    });

Процессы

Класс Process использует интерфейс Stream<List<int>> для обработки потоков стандартного вывода, и вывода ошибок (stdout и stderr), а так же интерфейс IOSink для потока ввода (stdin), а код завершения процесса теперь доступен через свойство Process.exitCode.

dart:io v1
Process process = ...
process.stdout.onData = ...
process.stdout.onDone = ...
process.onExit = (exitCode) { /* обработка кода завершения процесса */ }
dart:io v2
Process process = ...
process.stdout.listen(...);
process.exitCode.then((exitCode) { /* обработка кода завершения процесса */ });

Также сменились типы для стандартных потоков ввода-вывод уровня приложения - stdin теперь является объектом класса Stream<List<int>> , а stdout и stderr - IOSink.

Файлы и директории

Для чтения и записи файлов теперь тоже используются интерфейсы Stream<List<int>> и IOSink. Для того, чтобы прочитать данные из файла следует заменить метод File.openInputStream на новый File.openRead, который будет возвращать поток типа Stream<List<int>>. А для записи в файл, следует использовать новый метод File.openWrite, который возвращает поток типа IOSink.

Изменился и способ листинга директорий - метод Directory.list теперь возвращает поток объектов класса FileSystemEntity, который является родительским классом как для директорий Directory, так и для файлов File. Класс DirectoryLister, используемый ранее для обработки списка вложенных файлов и поддиректорий для данной директории - удален. В следующем примере, представлен код для старого и нового способов работы с листингом директорий:

dart:io v1
Directory dir = ...
DirectoryLister lister = dir.list();
lister.onDir = (Directory directory) { /* обработка директорий */ };
lister.onFile = (File file) { /* обработка файлов */ };
lister.onDone = (bool complete) { /* завершение листинга директории */ };
lister.onError = (error) { /* обработка ошибок */ };
dart:io v2
Directory dir = ...
dir.list().listen(
    (FileSystemEntity fse) {
      if (fse is Directory) {
        Directory directory = fse;
        /* обработка директорий */
      } else if (fse is File) {
        File file = fse;
        /* обработка файлов */
      }
    },
    onDone: () { /* завершение листинга директории */ },
    onError: (error) { /* обработка ошибок */ });

Сокеты

Поскольку классы для работы с сокетами Socket и SecureSocket поддерживают двухсторонний ввод-вывод, то теперь они оба реализуют одновременно интерфейсы Stream<List<int>> и IOSink. Поэтому полностью удалены все старые методы установки callback-функций для чтения и записи с помощью потоков InputStream и OutputStream.

Аналогично методам http-сервера, чтобы установить новый сокет для обработки запросов, используется статический метод Socket.bind, который создает экземпляр Socket и асинхронно возвращает его с помощью объект типа Future. В следующем примере, представлен код для старого и нового способов создания сокетов:

dart:io v1
Socket socket = new Socket(host, port);
socket.onConnect = () { /* соединение установлено. */ }
dart:io v2
Socket.connect(host, port).then((Socket socket) {
  /* соединение установлено. */
};

Классы ServerSocket и SecureServerSocket теперь используют статический метод bind для асинхронного подсоединения к сетевому интерфейсу:

dart:io v1
ServerSocket socket = new ServerSocket(host, port);
socket.onConnection = (Socket clientSocket) {
  /* Do something with the clientSocket. */
}
dart:io v2
ServerSocket.bind(host, port).then((ServerSocket socket) {
  socket.listen((Socket clientSocket) {
    /* Do something with the clientSocket. */
  })
});

Raw-сокеты

Для предоставления низкоуровневого доступа к сокетам представлен ряд новых классов - raw-сокеты, на которых строятся более высокоуровневые классы Socket и ServerSocket. Подробнее с ними можно ознакомится в документации к API.