add io_streamed_response (#388)

Closes #387

Adds `IOStreamedResponse` which is now the type returned by `IOClient`.
It adds a `detachSocket` method forwarding to the inner client.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 747ea8d..50fa3f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 ## 0.12.1-dev
 
+* Add `IOStreamedResponse` which includes the ability to detach the socket.
+  When sending a request with an `IOClient` the response will be an 
+  `IOStreamedResponse`.
 * Remove dependency on `package:async`.
 
 ## 0.12.0+4
diff --git a/lib/io_client.dart b/lib/io_client.dart
index 170acfb..4c92b94 100644
--- a/lib/io_client.dart
+++ b/lib/io_client.dart
@@ -3,3 +3,4 @@
 // BSD-style license that can be found in the LICENSE file.
 
 export 'src/io_client.dart' show IOClient;
+export 'src/io_streamed_response.dart' show IOStreamedResponse;
diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart
index f60aafe..85821e9 100644
--- a/lib/src/io_client.dart
+++ b/lib/src/io_client.dart
@@ -8,7 +8,7 @@
 import 'base_client.dart';
 import 'base_request.dart';
 import 'exception.dart';
-import 'streamed_response.dart';
+import 'io_streamed_response.dart';
 
 /// Create an [IOClient].
 ///
@@ -24,7 +24,7 @@
 
   /// Sends an HTTP request and asynchronously returns the response.
   @override
-  Future<StreamedResponse> send(BaseRequest request) async {
+  Future<IOStreamedResponse> send(BaseRequest request) async {
     var stream = request.finalize();
 
     try {
@@ -44,7 +44,7 @@
         headers[key] = values.join(',');
       });
 
-      return StreamedResponse(
+      return IOStreamedResponse(
           response.handleError(
               (HttpException error) =>
                   throw ClientException(error.message, error.uri),
@@ -56,7 +56,8 @@
           headers: headers,
           isRedirect: response.isRedirect,
           persistentConnection: response.persistentConnection,
-          reasonPhrase: response.reasonPhrase);
+          reasonPhrase: response.reasonPhrase,
+          inner: response);
     } on HttpException catch (error) {
       throw ClientException(error.message, error.uri);
     }
diff --git a/lib/src/io_streamed_response.dart b/lib/src/io_streamed_response.dart
new file mode 100644
index 0000000..fc69837
--- /dev/null
+++ b/lib/src/io_streamed_response.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'base_request.dart';
+import 'streamed_response.dart';
+
+/// An HTTP response where the response body is received asynchronously after
+/// the headers have been received.
+class IOStreamedResponse extends StreamedResponse {
+  final HttpClientResponse _inner;
+
+  /// Creates a new streaming response.
+  ///
+  /// [stream] should be a single-subscription stream.
+  IOStreamedResponse(Stream<List<int>> stream, int statusCode,
+      {int contentLength,
+      BaseRequest request,
+      Map<String, String> headers = const {},
+      bool isRedirect = false,
+      bool persistentConnection = true,
+      String reasonPhrase,
+      HttpClientResponse inner})
+      : _inner = inner,
+        super(stream, statusCode,
+            contentLength: contentLength,
+            request: request,
+            headers: headers,
+            isRedirect: isRedirect,
+            persistentConnection: persistentConnection,
+            reasonPhrase: reasonPhrase);
+
+  /// Detaches the underlying socket from the HTTP server.
+  Future<Socket> detachSocket() async => _inner.detachSocket();
+}
diff --git a/test/io/client_test.dart b/test/io/client_test.dart
index 25bbd6b..e5bd66f 100644
--- a/test/io/client_test.dart
+++ b/test/io/client_test.dart
@@ -8,7 +8,7 @@
 import 'dart:io';
 
 import 'package:http/http.dart' as http;
-import 'package:http/src/io_client.dart' as http_io;
+import 'package:http/io_client.dart' as http_io;
 import 'package:test/test.dart';
 
 import 'utils.dart';
@@ -121,4 +121,15 @@
     var contentType = (headers['content-type'] as List).single;
     expect(contentType, startsWith('multipart/form-data; boundary='));
   });
+
+  test('detachSocket returns a socket from an IOStreamedResponse', () async {
+    var ioClient = HttpClient();
+    var client = http_io.IOClient(ioClient);
+    var request = http.Request('GET', serverUrl);
+
+    var response = await client.send(request);
+    var socket = await response.detachSocket();
+
+    expect(socket, isNotNull);
+  });
 }