Contents

Write HTTP clients & servers

What's the point?

  • 必须具备使用期货和流进行异步编程的知识.
  • HTTP协议允许客户端和服务器进行通信.
  • dart:io包具有用于编写HTTP程序的类.
  • 服务器在主机和端口上侦听请求.
  • 客户端使用HTTP方法请求发送请求.
  • http_server软件包提供了更高级别的构建块.

HTTP(超文本传输​​协议)是一种通信协议,用于通过互联网将数据从一个程序发送到另一个程序. 数据传输的一端是服务器,而另一端是客户端. 客户端通常基于浏览器(用户在浏览器中键入或在浏览器中运行的脚本),但也可能是独立程序.

服务器绑定到主机和端口(它与IP地址和端口号建立独占连接). 然后服务器侦听请求. 由于Dart具有异步特性,因此服务器可以一次处理许多请求,如下所示:

  • 服务器监听
  • 客户端连接
  • 服务器接受并接收请求(并继续侦听)
  • 服务器可以继续接受其他请求
  • 服务器写入请求或几个可能交错的请求的响应
  • 服务器最终结束(关闭)响应.

在Dart中, dart:io库包含编写HTTP客户端和服务器所需的类和函数. 另外, http_server程序包包含一些更高级别的类,这些类使编写客户端和服务器更加容易.

本教程提供了几个示例,这些示例说明编写Dart HTTP服务器和客户端非常容易. 从服务器的问候世界开始,您将学习如何通过绑定和侦听响应请求为服务器编写代码. 您还将了解客户端:发出各种请求(GET和POST),编写基于浏览器的客户端和命令行客户端.

Get the source code

  • 获取Dart教程示例代码.
  • 查看httpserver目录,其中包含本教程所需的源.

Run the hello world server

本节的示例文件: hello_world_server.dart.

让我们从一个小型服务器开始,该服务器以字符串Hello, world!响应所有请求Hello, world!

在命令行中,运行hello_world_server.dart脚本:

$ cd httpserver
$ dart bin/hello_world_server.dart
listening on localhost, port 4040

In any browser, visit localhost:4040 . open_in_browser 在任何浏览器中,访问 localhost:4040 . 浏览器显示Hello, world!

The response from the hello world server.

在这种情况下,服务器是Dart程序,客户端是您使用的浏览器. 但是,您可以使用Dart编写客户端程序-基于浏览器的客户端脚本或独立程序.

A quick glance at the code

在hello世界服务器的代码中,HTTP服务器绑定到主机和端口,侦听HTTP请求,然后编写响应. 请注意,该程序将导入dart:io库,该库包含服务器端程序和客户端程序(但不包括Web应用程序)的HTTP相关类.

import 'dart:io';

Future main() async {
  var server = await HttpServer.bind(
    InternetAddress.loopbackIPv4,
    4040,
  );
  print('Listening on localhost:${server.port}');

  await for (HttpRequest request in server) {
    request.response.write('Hello, world!');
    await request.response.close();
  }
}
hello_world_server.dart

接下来的几节介绍服务器端绑定,发出客户端GET请求,侦听和响应.

Binding a server to a host and port

本节示例: hello_world_server.dart.

main()的第一条语句使用HttpServer.bind()创建HttpServer对象,并将其绑定到主机和端口.

var server = await HttpServer.bind(
  InternetAddress.loopbackIPv4,
  4040,
);
hello_world_server.dart

该代码使用await异步调用bind方法.

Hostname

bind()的第一个参数指定主机名. 您可以将特定的主机名或IP地址指定为字符串. 另外,您可以使用InternetAddress类提供的以下预定义值指定主机:

Value 用例
loopbackIPv4
or
loopbackIPv6
服务器在回送地址(实际上是localhost)上侦听客户端活动. 使用IP协议的版本4或6. 这些主要用于测试. 我们建议您使用这些值而不是localhost127.0.0.1 .
anyIPv4
or
anyIPv6
服务器在任何IP地址上的指定端口上侦听客户端活动. 使用IP协议的版本4或6.

默认情况下,使用V6互联网地址时,也会使用V4侦听器.

Port

bind()的第二个参数是指定端口的整数. 该端口唯一地标识主机计算机上的服务. 低于1024的端口号为标准服务保留(0除外). 例如,FTP数据传输通常在端口20上运行,当天行情在端口17上运行,HTTP在端口80上运行.您的程序应使用1024或更高版本的端口号. 如果端口已在使用中,则服务器连接将被拒绝.

Listening for requests

服务器开始使用await for侦听HTTP请求. 对于收到的每个请求,代码都会发送" Hello,world!"响应.

await for (HttpRequest request in server) {
  request.response.write('Hello, world!');
  await request.response.close();
}
hello_world_server.dart

侦听和处理请求部分中,您将学到更多有关HttpRequest对象包含的内容以及如何编写响应的信息. 但是首先,让我们看一下客户端生成请求的一种方式.

Using HTML forms to make GET requests

本节的示例文件: number_thinker.dart and make_a_guess.html.

本节提供了一个命令行服务器,该服务器随机选择一个0到9之间的数字.客户端是一个基本的HTML网页make_a_guess.html ,您可以用来猜测该数字.


尝试一下!

  1. 运行数字思想器服务器

    在命令行中,运行number_thinker.dart服务器. 您应该看到类似于以下内容:

    $ cd httpserver
    $ dart bin/number_thinker.dart
    I'm thinking of a number: 6
    
  2. 启动Web服务器

    从应用程序的顶部目录运行webdev serve .

    更多信息: webdev documentation

  3. 打开HTML页面

    在浏览器中,转到localhost:8080 / make_a_guess.html .

  4. 做一个猜想

    选择一个数字,然后按Guess按钮.

    The user makes a guess using a pull-down menu.


客户端中不包含Dart代码. 客户端请求是通过make_a_guess.html中的HTML表单从浏览器向Dart服务器发出的,该表单提供了自动制定和发送客户端HTTP请求的方式. 该表格包含下拉列表和按钮. 该表单还指定了URL(包括端口号)和请求类型( 请求方法 ). 它还可能包含构建查询字符串的元素.

这是make_a_guess.html HTML形式:

<form action="http://localhost:4041" method="GET">
  <select name="q">
    <option value="0">0</option>
    <option value="1">1</option>
    <option value="2">2</option>
    <!-- ··· -->
    <option value="9">9</option>
  </select>
  <input type="submit" value="Guess">
</form>
make_a_guess.html

表单的工作方式如下:

  • 为表单的action属性分配了将请求发送到的URL.
  • 表单的method属性定义了请求的类型,这里是GET . 其他常见的请求类型包括POST,PUT和DELETE.
  • 表单中任何具有name元素(例如<select>元素)都将成为查询字符串中的参数.
  • When pressed, the submit button (<input type="submit"...>) formulates the request based on the content of the form and sends it.

A RESTful GET request

REST(代表性状态转移)是用于设计Web服务的一组原则. 行为良好的HTTP客户端和服务器遵守为GET请求定义的REST原则.

GET请求:

  • 仅检索数据
  • 不会改变服务器的状态
  • 有长度限制
  • 可以在请求的URL中发送查询字符串

在此示例中,客户端发出符合REST的GET请求.

Listening for and handling requests

本节的示例文件: number_thinker.dart and make_a_guess.html.

Now that you’ve seen the browser-based client for this example, let’s take a look at the Dart code for the number thinker server, starting with main().

服务器再次绑定到主机和端口. 在这里,为每个收到的请求调用顶级handleRequest()方法. 因为HttpServer实现了Stream,所以可以使用await for来处理请求.

import 'dart:io';
import 'dart:math' show Random;

Random intGenerator = Random();
int myNumber = intGenerator.nextInt(10);

Future main() async {
  print("I'm thinking of a number: $myNumber");

  HttpServer server = await HttpServer.bind(
    InternetAddress.loopbackIPv4,
    4041,
  );
  await for (var request in server) {
    handleRequest(request);
  }
}
number_thinker.dart

GET请求到达时, handleRequest()方法将调用handleGet()来处理请求.

void handleRequest(HttpRequest request) {
  try {
    if (request.method == 'GET') {
      handleGet(request);
    } else {
      // ···
    }
  } catch (e) {
    print('Exception in handleRequest: $e');
  }
  print('Request handled.');
}
number_thinker.dart

HttpRequest对象具有许多提供有关请求的信息的属性. 下表列出了一些有用的属性:

Property Information
method 'GET''POST''PUT'等之一.
uri Uri对象:方案,主机,端口,查询字符串以及有关所请求资源的其他信息.
response HttpResponse对象:服务器在其中写入其响应的对象.
headers An HttpHeaders object: the headers for the request, including ContentType, content length, date, and so on.

Using the method property

编号思想器示例中的以下代码使用HttpRequest method属性来确定已接收到哪种请求. 该服务器仅处理GET请求.

if (request.method == 'GET') {
  handleGet(request);
} else {
  request.response
    ..statusCode = HttpStatus.methodNotAllowed
    ..write('Unsupported request: ${request.method}.')
    ..close();
}
number_thinker.dart

Using the uri property

在浏览器中键入URL会生成GET请求,该请求只是从指定资源中请求数据. 它可以通过附加到URI的查询字符串与请求一起发送最少的数据.

void handleGet(HttpRequest request) {
  final guess = request.uri.queryParameters['q'];
  // ···
}
number_thinker.dart

使用HttpRequest对象中的uri属性可获取Uri对象,该对象包含有关用户键入的URL的信息. Uri对象的queryParameters属性是一个包含查询字符串组成部分的Map. 通过名称引用所需的参数. 本示例使用q标识猜测的数字.

Setting the status code for the response

服务器应设置状态代码以指示请求成功或失败. 之前您看到数字思想者将状态代码设置为methodNotAllowed来拒绝非GET请求. 在代码的稍后部分,为了指示请求成功并且响应已完成,数字思想器服务器将HttpResponse状态代码设置为HttpStatus.ok .

void handleGet(HttpRequest request) {
  final guess = request.uri.queryParameters['q'];
  final response = request.response;
  response.statusCode = HttpStatus.ok;
  // ···
}
number_thinker.dart

HttpStatus.okHttpStatus.methodNotAllowedHttpStatus类中许多预定义状态代码中的两个. 另一个有用的预定义状态代码是HttpStatus.notFound (您的经典404).

除了statusCode ,HttpResponse对象还具有其他有用的属性:

Property Information
contentLength 响应的长度; -1表示长度未知.
cookies 在客户端中设置的Cookie列表.
encoding 编写字符串(例如JSON和UTF-8)时使用的Encoding .
headers 响应头,一个HttpHeaders对象.

Writing the response to the HttpResponse object

每个HttpRequest对象都有一个对应的HttpResponse对象. 服务器通过响应对象将数据发送回客户端.

使用HttpResponse写入方法之一( write()writeln()writeAll()writeCharCodes() )将响应数据写入HttpResponse对象. 或通过addStream将HttpResponse对象连接到流并写入该流. 响应完成后,关闭对象. 关闭HttpResponse对象会将数据发送回客户端.

void handleGet(HttpRequest request) {
  // ···
  if (guess == myNumber.toString()) {
    response
      ..writeln('true')
      ..writeln("I'm thinking of another number.")
      ..close();
    // ···
  }
}
number_thinker.dart

Making a POST request from a standalone client

本节的示例文件: basic_writer_server.dart and basic_writer_client.dart.

在问候世界和数字思想家的示例中,浏览器生成了简单的GET请求. 对于更复杂的GET请求和其他类型的请求,例如POST,PUT或DELETE,您需要编写一个客户端程序,其中有两种:

  • 一个独立的客户端程序,它使用dart:ioHttpClient类.

  • 基于浏览器的客户端,该客户端使用dart:html中的 API . 本教程不介绍基于浏览器的客户端. 要查看基于浏览器的客户端和相关服务器的代码,请参见note_client.dart, note_server.dartnote_taker.html.

    让我们看一个独立的客户端basic_writer_client.dart及其服务器basic_writer_server.dart . 客户端发出POST请求以将JSON数据保存到服务器端文件. 服务器接受该请求并保存文件.


尝试一下!

在命令行上运行服务器和客户端.

  1. First, run the server:

    $ cd httpserver
    $ dart bin/basic_writer_server.dart
    
  2. 在新的终端中,运行客户端:

    $ cd httpserver
    $ dart bin/basic_writer_client.dart
    Wrote data for Han Solo.
    

查看服务器写入到file.txt的JSON数据:

{"name":"Han Solo","job":"reluctant hero","BFF":"Chewbacca","ship":"Millennium Falcon","weakness":"smuggling debts"}

客户端创建一个HttpClient对象,并使用post()方法发出请求. 发出请求涉及两个期货:

  • post()方法建立与服务器的网络连接,并以第一个Future结束,该Future返回一个HttpClientRequest对象.

  • 客户端组成请求对象并关闭它. close()方法将请求发送到服务器,并返回第二个Future,它以HttpClientResponse对象完成.

import 'dart:io';
import 'dart:convert';

String _host = InternetAddress.loopbackIPv4.host;
String path = 'file.txt';

Map jsonData = {
  'name': 'Han Solo',
  'job': 'reluctant hero',
  'BFF': 'Chewbacca',
  'ship': 'Millennium Falcon',
  'weakness': 'smuggling debts'
};

Future main() async {
  HttpClientRequest request = await HttpClient().post(_host, 4049, path) /*1*/
    ..headers.contentType = ContentType.json /*2*/
    ..write(jsonEncode(jsonData)); /*3*/
  HttpClientResponse response = await request.close(); /*4*/
  await utf8.decoder.bind(response /*5*/).forEach(print);
}
basic_writer_client.dart
  1. post()方法需要主机,端口和所请求资源的路径. 除了post()HttpClient类还提供用于发出其他类型请求的函数,包括postUrl()get()open() .

  2. HttpClientRequest对象具有HttpHeaders对象,该对象包含请求标头. 对于某些标头,例如contentType ,HttpHeaders具有特定于该标头的属性. 对于其他标头,请使用set()方法将标头放入HttpHeaders对象.

  3. 客户端使用write()将数据写入请求对象. 此示例中的JSON编码与ContentType标头中指定的类型匹配.

  4. close()方法将请求发送到服务器,完成后将返回HttpClientResponse对象.

  5. 来自服务器的UTF-8响应被解码. 使用dart:convert库中定义的转换器将数据转换为常规Dart字符串格式.

A RESTful POST request

与GET请求类似,REST为POST请求提供准则.

POST请求:

  • 创建资源(在此示例中为文件)
  • 使用具有与文件和目录路径名相似的结构的URI; 例如,URI没有查询字符串
  • 传输数据为JSON或XML
  • 没有状态,并且不会更改服务器的状态
  • 没有长度限制

此示例中的客户端发出符合REST的POST请求.

要查看发出符合REST的GET请求的客户端代码,请查看number_guesser.dart. 它是数字思考器服务器的独立客户端,它会定期进行猜测,直到正确猜测为止.

Handling a POST request in a server

本节的示例文件: basic_writer_server.dart and basic_writer_client.dart.

HttpRequest对象是字节列表Stream<List<int>>Stream<List<int>> ). 要获取从客户端发送的数据,请侦听HttpRequest对象上的数据.

如果来自客户端的请求包含大量数据,则数据可能会分多个块到达. 您可以在Stream中使用join()方法来连接这些块的字符串值.

The flow of control in a server processing requests.

basic_writer_server.dart文件实现遵循此模式的服务器.

import 'dart:io';
import 'dart:convert';

String _host = InternetAddress.loopbackIPv4.host;

Future main() async {
  var server = await HttpServer.bind(_host, 4049);
  await for (var req in server) {
    ContentType contentType = req.headers.contentType;
    HttpResponse response = req.response;

    if (req.method == 'POST' &&
        contentType?.mimeType == 'application/json' /*1*/) {
      try {
        String content =
            await utf8.decoder.bind(req).join(); /*2*/
        var data = jsonDecode(content) as Map; /*3*/
        var fileName = req.uri.pathSegments.last; /*4*/
        await File(fileName)
            .writeAsString(content, mode: FileMode.write);
        req.response
          ..statusCode = HttpStatus.ok
          ..write('Wrote data for ${data['name']}.');
      } catch (e) {
        response
          ..statusCode = HttpStatus.internalServerError
          ..write('Exception during file I/O: $e.');
      }
    } else {
      response
        ..statusCode = HttpStatus.methodNotAllowed
        ..write('Unsupported request: ${req.method}.');
    }
    await response.close();
  }
}
basic_writer_server.dart
  1. 该请求具有HttpHeaders对象. 回想一下,客户端将contentType标头设置为JSON(application / json). 该服务器拒绝未经JSON编码的请求.

  2. POST请求对其可以发送的数据量没有限制,并且数据可以分多个块发送. 此外,JSON是UTF-8,并且UTF-8字符可以在多个字节上编码. join()方法将这些块放在一起.

  3. 客户端发送的数据为JSON格式. 服务器使用dart:convert库中提供的JSON编解码器对其进行解码.

  4. 请求的URL是localhost:4049 / file.txt . 代码req.uri.pathSegments.last从URI中提取文件名: file.txt .

A note about CORS headers

如果要为在不同来源(不同主机或端口)上运行的客户端提供服务,则需要添加CORS标头. 以下代码取自note_server.dart,允许来自任何源的POST和OPTIONS请求. 请谨慎使用CORS标头,因为它们可能会使您的网络面临安全风险.

void addCorsHeaders(HttpResponse response) {
  response.headers.add('Access-Control-Allow-Origin', '*');
  response.headers
      .add('Access-Control-Allow-Methods', 'POST, OPTIONS');
  response.headers.add('Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept');
}
note_server.dart

有关更多信息,请参阅Wikipedia的文章跨域资源共享 .

Using the http_server package

本节的示例文件: mini_file_server.dart and static_file_server.dart.

对于某些更高级别的构建块,我们建议您尝试使用http_server pub软件包,该软件包包含一组类,这些类与dart:io库中的HttpServer类一起使实现HTTP服务器更加容易.

在本节中,我们将仅使用api从dart:io编写的服务器与使用dart:io和http_server编写的具有相同功能的服务器进行比较.

您可以在mini_file_server.dart找到第一个服务器. 它通过从web目录返回index.html文件的内容来响应所有请求.

尝试一下!

  1. 在命令行上运行服务器:

    $ cd httpserver
    $ dart bin/mini_file_server.dart
    
  2. 在浏览器中输入localhost:4044 . 服务器显示一个HTML文件:

    The file served by mini_file_server.dart.

Here’s the code for mini file server:

import 'dart:io';

File targetFile = File('web/index.html');

Future main() async {
  var server;

  try {
    server = await HttpServer.bind(InternetAddress.loopbackIPv4, 4044);
  } catch (e) {
    print("Couldn't bind to port 4044: $e");
    exit(-1);
  }

  await for (HttpRequest req in server) {
    if (await targetFile.exists()) {
      print("Serving ${targetFile.path}.");
      req.response.headers.contentType = ContentType.html;
      try {
        await req.response.addStream(targetFile.openRead());
      } catch (e) {
        print("Couldn't read file: $e");
        exit(-1);
      }
    } else {
      print("Can't open ${targetFile.path}.");
      req.response.statusCode = HttpStatus.notFound;
    }
    await req.response.close();
  }
}
mini_file_server.dart

此代码确定文件是否存在,如果存在,则打开文件并将内容通过管道传递到HttpResponse对象.

您可以在basic_file_server.dart中找到其代码的第二台服务器使用http_server软件包.

尝试一下!

  1. 在命令行上运行服务器:

    $ cd httpserver
    $ dart bin/basic_file_server.dart
    
  2. 在浏览器中输入localhost:4046 . 服务器显示与上一个相同的index.html文件:

    The index.html file served by basic_file_server.dart.

在此服务器中,用于处理请求的代码要短得多,因为VirtualDirectory类会处理提供文件的详细信息.

import 'dart:io';
import 'package:http_server/http_server.dart';

File targetFile = File('web/index.html');

Future main() async {
  VirtualDirectory staticFiles = VirtualDirectory('.');

  var serverRequests =
      await HttpServer.bind(InternetAddress.loopbackIPv4, 4046);
  await for (var request in serverRequests) {
    staticFiles.serveFile(targetFile, request);
  }
}
basic_file_server.dart

在这里,请求的资源index.html由VirtualDirectory类中的serveFile()方法提供. 您无需编写代码即可打开文件并将其内容传递给请求.

另一个文件服务器static_file_server.dart也使用http_server包. 该服务器提供服务器目录或子目录中的任何文件.

运行static_file_server.dart ,并使用URL localhost:4048对其进行测试.

这是static_file_server.dart的代码.

import 'dart:io';
import 'package:http_server/http_server.dart';

Future main() async {
  var staticFiles = VirtualDirectory('web');
  staticFiles.allowDirectoryListing = true; /*1*/
  staticFiles.directoryHandler = (dir, request) /*2*/ {
    var indexUri = Uri.file(dir.path).resolve('index.html');
    staticFiles.serveFile(File(indexUri.toFilePath()), request); /*3*/
  };

  var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 4048);
  print('Listening on port 4048');
  await server.forEach(staticFiles.serveRequest); /*4*/
}
static_file_server.dart
  1. 允许客户端在服务器目录中请求文件.

  2. 匿名函数,用于处理对目录本身的请求,即URL不包含文件名. 该函数将这些请求重定向到index.html .

  3. serveFile方法提供文件. 在此示例中,它为目录请求提供index.html .

  4. VirtualDirectory类提供的serveRequest方法处理指定文件的请求.

Using https with bindSecure()

本节示例: hello_world_server_secure.dart.

您可能已经注意到HttpServer类定义了一个名为bindSecure()的方法,该方法使用HTTPS(带有安全套接字层的超文本传输​​协议bindSecure()提供安全连接. 要使用bindSecure()方法,您需要由证书颁发机构(CA)提供的证书. 有关证书的更多信息,请参阅什么是SSL和什么是证书?

仅出于说明目的,以下服务器hello_world_server_secure.dart使用Dart团队创建的用于测试的证书调用bindSecure() . 您必须为服务器提供自己的证书.

import 'dart:io';

String certificateChain = 'server_chain.pem';
String serverKey = 'server_key.pem';

Future main() async {
  var serverContext = SecurityContext(); /*1*/
  serverContext.useCertificateChain(certificateChain); /*2*/
  serverContext.usePrivateKey(serverKey, password: 'dartdart'); /*3*/

  var server = await HttpServer.bindSecure(
    'localhost',
    4047,
    serverContext, /*4*/
  );
  print('Listening on localhost:${server.port}');
  await for (HttpRequest request in server) {
    request.response.write('Hello, world!');
    await request.response.close();
  }
}
hello_world_server_secure.dart
  1. 在SecurityContext对象中指定安全网络连接的可选设置. 有一个默认对象SecurityContext.defaultContext,其中包含用于知名证书颁发机构的受信任的根证书.

  2. 一个文件,包含从服务器证书到签名授权机构根的证书链, 格式PEM.

  3. 包含(加密的)服务器证书私钥的文件, 格式PEM.

  4. 上下文参数在服务器上是必需的,对于客户端是可选的. 如果省略,则使用具有内置受信任根的默认上下文.

Other resources

请访问这些API文档,以获取有关本教程中讨论的类和库的更多详细信息.

飞镖类 Purpose
HttpServer HTTP服务器
HttpClient HTTP客户端
HttpRequest 服务器端请求对象
HttpResponse 服务器端响应对象
HttpClientRequest 客户端请求对象
HttpClientResponse 客户端响应对象
HttpHeaders 请求的标头
HttpStatus 回应状态
InternetAddress 互联网地址
SecurityContext 包含用于安全连接的证书,密钥和信任信息
http_server package 具有更高级别的HTTP类的软件包

What next?

by  ICOPY.SITE