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 world服务器的代码中,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

You’ll learn more about what the HttpRequest object contains and how to write the response in the section Listening for and handling requests. But first, let’s look at one way a client generates a request.

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 make_a_guess.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,用于将请求发送到该URL.
  • 表单的method属性定义了请求的类型,这里是GET . 其他常见的请求类型包括POST,PUT和DELETE.
  • 表单中任何具有name元素(例如<select>元素)都将成为查询字符串中的参数.
  • 当按下时,提交按钮( <input type="submit"...> )根据表单的内容制定请求并将其发送.

A RESTful GET request

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

A GET request:

  • 仅检索数据
  • 不会改变服务器的状态
  • 有长度限制
  • can send query strings in the URL of the request

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

Listening for and handling requests

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

现在,您已经看到了此示例的基于浏览器的客户端,让我们看一下数字思考器服务器的Dart代码,从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 One of 'GET', 'POST', 'PUT', and so on.
uri Uri对象:方案,主机,端口,查询字符串以及有关所请求资源的其他信息.
response HttpResponse对象:服务器在其中写入其响应的对象.
headers HttpHeaders对象:请求的标头,包括ContentType,内容长度,日期等.

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. 首先,运行服务器:

    $ 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服务器更加容易.

在本部分中,我们将仅使用dart:io的API编写的服务器与使用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.

这是迷你文件服务器的代码:

import 'dart:io';

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

Future main() async {
  Stream<HttpRequest> 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

Here, the requested resource, index.html, is served by the serveFile() method in the VirtualDirectory class. You don’t need to write code to open a file and pipe its contents to the request.

另一个文件服务器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