Thursday, February 21, 2013

I/O Library Now Uses Streams

We have done a full re-design of the dart:io API (file, network, directories, etc) to use the classes and concepts in the core library and in dart:async. This means that there is almost no callback registration in dart:io any more. All async operations now use streams and futures.

(These changes are available in bleeding_edge as of today, and should show up in a weekly build soon.)




We think this is a great step forward in aligning all of the “dart:” API's and for you to have fewer concepts to learn when using Dart for different tasks.

However this is a breaking change which requires rewriting code using dart:io. The good news is that most of these changes should be mechanical. The following sections describe these changes in more details and give some examples on how to migrate code to the new API. For more information take a look on the API documentation for dart:io.

Streams in dart:io
The classes InputStream and OutputStream are gone and have been replaced with classes implementing IOSink and Stream<List<int>>.

When reading from a Stream<List<int>> just use the listen method which have arguments matching the callbacks previously used. This shows how to migrate code using an InputStream to the streams-based API:

dart:io v1 code
InputStream stream = ...
stream.onData = () {
 var data = request.inputStream.read();
 /* Process data. */
};
stream.onClosed = () {
 /* All data received. */
};
stream.onError = (e) {
 /* Error on input. */
};

dart:io v2 code
Stream<List<int>> stream = ...
stream.listen(
 (data) { /* Process data. */ },
 onDone: () { /* All data received. */ },
 onError: (e) { /* Error on input. */ });

As the InputStream class is now gone so is the StringInputStream for turning a stream of bytes into strings and/or lines. Two new stream transformers StringDecoder and LineTransformer have been added to address this. The StringDecoder transforms a stream of List<int> to a stream of String and the LineTransformer transforms a stream of String to a new stream of String where each string is a line.

dart:io v1 code
InputStream stream = ...
StringInputStream stringStream = new StringInputStream(stream);
stringStream.onLine = () {
 String line = stringStream.readLine();
 /* Do something with line. */
};
stringStream.onClosed = () {
 /* No more lines */
};
stream.onError = (e) {
 /* Error on input. */
};

dart:io v2 code
Stream<<int>> stream = ...
stream
 .transform(new StringDecoder())
 .transform(new LineTransformer())
 .listen((String line) { /* Do something with line. */ },
         onDone: () { /* No more lines */ },
         onError: (e) { /* Error on input. */ });

The IOSink replaces OutputStream and this shows how to migrate code using OutputStream to use an IOSink:

dart:io v1 code
OutputStream stream = …
stream.write([72, 101, 108, 108, 111]);  // Hello
stream.writeString(", world!");
stream.close();

dart:io v2 code
IOSink sink = …
sink.add([72, 101, 108, 108, 111]);  // Hello
sink.addString(", world!");
sink.close();

The IOSink also allows you to pipe directly from a stream.

HTTP
The main changes to the HTTP server and client part of dart:io are the following:

* A new HttpServer listening for requests is created using the static method bind.
* An HttpServer is a stream of HttpRequests.
* The defaultRequestHandler setter and addRequestHandler method on HttpServer are gone.
* The HttpRequest and HttpClientResponse objects implement Stream<List<int>>.
* The HttpClientRequest and HttpResponse objects implement IOSink.

To create a new listening HTTP server use the static method bind which returns a future.

dart:io v1 code
HttpServer server = new HttpServer();
server.defaultRequestHandler = …
server.addRequestHandler(…);
server.listen(“127.0.0.1”, 8080);
// Now the HTTP server is bound and listening for requests.

dart:io v2 code
HttpServer.bind(“127.0.0.1”, 8080)
   .then((HttpServer server) {
     server.listen(
         (HttpRequest request) {
           // Handle the request.
         });
}

The request routing through defaultRequestHandler and addRequestHandler is gone and any routing to specific request handling methods should be added to the listen handling. The HttpResponse is available as the response property on the HttpRequest object.

For client side HTTP the HttpClient class still exists. The HttpClientConnection class is gone, and instead the request initiation methods get, post, open, etc. now returns a future for the HttpClientRequest object. The HttpClientRequest object has a response property which is a future for the HttpClientResponse. As a convenience the HttpClientRequest.close method also returns the future for the response. This shows how to migrate HTTP client code:

dart:io v1 code
HttpClient client = new HttpClient();
HttpClientConnection connection = client.get(...);
connection.onRequest = (HttpClientRequest request) {
 // Prepare the request.
 request.outputStream.close();
}
connection.onResponse = (HttpClientResponse response) {
 // Process the response.
}

dart:io v2 code
HttpClient client = new HttpClient();
client.get(...)
   .then((HttpClientRequest request) {
     // Prepare the request.
     return request.close();
   })
   .then((HttpClientResponse response) {
     // Process the response.
   });

Web Sockets
The web socket interface has been simplified and now uses the same class for web socket connections on both the client and the server. The WebSocket class is a stream of events.

On the server the web socket handling is implemented as a stream transformer. This transformer can transform a stream of HttpRequests into a stream of WebSockets.

dart:io v1 code
HttpServer server = new HttpServer();
server.listen(...);
WebSocketHandler handler = new WebSocketHandler();
handler.onOpen = (WebSocketConnection connection) {
 connection.onMessage = (Object message) {
   /* Handle message. */
 };
 connection.onClosed = (status, reason) {
   /* Handle closed. */
 };
};
server.defaultRequestHandler = handler.onRequest;

dart:io v2 code
HttpServer.bind(...).then((server) {
 server.transform(new WebSocketTransformer()).listen((WebSocket webSocket) {
   webSocket.listen((event) {
     if (event is MessageEvent) {
       /* Handle message. */
     } else if (event is CloseEvent) {
       /* Handle closed. */
     }
   });
 });

On the client connecting a web socket has become much simpler. Just use the WebSocket static method connect which returns a future for the web socket connection as shown here:

dart:io v1 code
HttpClient client = new HttpClient();
HttpClientConnection conn = client.openUrl(“https://127.0.0.1:8080”);
WebSocketClientConnection wsconn = new WebSocketClientConnection(conn);
wsconn.onMessage = (message) {
 /* Handle message. */
}
wsconn.onClosed = (status, reason) {
 /* Handle closed. */
};

dart:io v2 code

WebSocket.connect("ws://echo.websocket.org")
   .then((WebSocket webSocket) {
     webSocket.listen((message) {
         /* Handle message. */
       },
onDone: () {
         /* Handle closed. */
       });
   });

Process
The Process class uses Stream<List<int>> for stdout and stderr and IOSink for stdin. The exit code for the process is now available through the exitCode future.

dart:io v1 code
Process process = ...
process.stdout.onData = ...
process.stdout.onDone = ...
process.onExit = (exitCode) { /* do something with exitCode. */ }

dart:io v2 code
Process process = ...
process.stdout.listen(...);
p.exitCode.then((exitCode) { /* do something with exitCode. */ });

Likewise the types of the top level properties stdin, stdout and stderr have been changed. stdio is a Stream<List<int>> and stdout and stderr are IOSinks.

File and directory
Reading and writing a file also uses Stream<List<int>> and IOSink. To read a file change the use of openInputStream to openRead which returns a stream of type Stream<List<int>>. Likewise change the use of openOutputStream to openWrite which returns an IOSink.

The Directory.list function now returns a stream of FileSystemEntity objects. The FileSystemEntity is a superclass of both File and Directory. The previous DirectoryLister where callbacks were set is now gone.

dart:io v1 code
Directory dir = ...
DirectoryLister lister = dir.list();
lister.onDir = (Directory directory) { /* Do something with directory. */ };
lister.onFile = (File file) { /* Do something with file. */ };
lister.onDone = (bool complete) { /* Listing ended.*/ };
lister.onError = (error) { /* Listing error.*/ };


dart:io v2 code
Directory dir = ...
dir.list().listen(
   (FileSystemEntity fse) {
     if (fse is Directory) {
       Directory directory = fse;
       /* Do something with directory. */
     } else if (fse is File) {
       File file = fse;
       /* Do something with file. */
     }
   },
   onDone: () { /* Listing ended.*/ },
   onError: (error) { /* Listing error.*/ });

Sockets
The classes Socket and SecureSocket both implement Stream<List<int>> and IOSink as they support bidirectional communication. This replaces all the reading and writing previously provided through a combination of callbacks, read and write methods, and InputStream and OutputStream objects backed by sockets.

Connecting a socket now uses a static method returning a future instead of a callback.

dart:io v1 code
Socket socket = new Socket(host, port);
socket.onConnect = () { /* Socket connected. */ }

dart:io v2 code
Socket.connect(host, port).then((Socket socket) {
 /* Socket connected. */
};

The classes ServerSocket and SecureServerSocket now uses a static method returning a future when binding to an interface. A listening server socket delivers the socket connections as a stream instead of through the onConnection callback.

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

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

Raw sockets
In order to provide a low level socket API we introduced raw sockets. Raw sockets give you access to low-level socket events without giving you data in your hand (think of this as the events you get from epoll on a socket file descriptor on Linux systems). Based on the low-level events you can read out data from the socket and decide when to write more data to the socket. The high-level Socket and ServerSocket classes are built on top of the RawSocket and RawServerSocket classes. Check out the API documentation on the Raw* classes.



(Photo Credit: Marooned cc)