编写HTTP客户端和服务器
— 焉知非鱼Write HTTP clients & servers
有什么意义呢?
- HTTP 协议允许客户端和服务器进行通信。
- dart:io 包有编写 HTTP 程序的类。
- 服务器监听主机和端口上的请求。
- 客户端使用 HTTP 方法请求发送请求。
- http_server 包提供了更高级别的构件。
前提条件: HTTP 服务器和客户端严重依赖 future 和流,本教程中没有解释这些内容。你可以从异步编程 codelab和流教程中了解它们。
HTTP(超文本传输协议)是一种通信协议,用于通过互联网将数据从一个程序发送到另一个程序。数据传输的一端是服务器,另一端是客户端。客户端通常是基于浏览器的(用户在浏览器中输入或在浏览器中运行的脚本),但也可能是一个独立的程序。
服务器与主机和端口绑定(它与一个IP地址和一个端口号建立专属连接)。然后服务器监听请求。由于 Dart 的异步性,服务器可以同时处理很多请求,具体如下。
- 服务器监听
- 客户端连接
- 服务器接受并接收请求(并继续监听)
- 服务器可以继续接受其他请求
- 服务器写入请求的响应或几个请求,可能是交错的请求
- 服务器最终结束(关闭)响应
在 Dart 中,dart:io 库包含了编写 HTTP 客户端和服务器所需的类和函数。此外,http_server 包包含了一些更高层次的类,使其更容易编写客户端和服务器。
重要:基于浏览器的程序不能使用 dart:io 库。
dart:io 库中的 API 只适用于独立的命令行程序。它们不能在浏览器中工作。要从基于浏览器的客户端发出 HTTP 请求,请参考 dart:html HttpRequest 类。
本教程提供了几个例子,说明编写 Dart HTTP 服务器和客户端是多么容易。从服务器的 hello world
开始,你将学习如何编写服务器的代码,从绑定和监听到响应请求。你还可以学习到客户端:提出不同类型的请求(GET 和 POST),编写基于浏览器和命令行的客户端。
获取源码 #
- 获取 Dart 教程的示例代码。
- 查看
httpserver
目录,其中包含本教程所需的源码。
运行 hello world 服务器 #
本节的示例文件:hello_world_server.dart。
让我们从一个小型的服务器开始,用字符串 Hello, world
来响应所有的请求。
在命令行中,运行 hello_world_server.dart
脚本:
$ cd httpserver
$ dart bin/hello_world_server.dart
listening on localhost, port 4040
在任何浏览器中,访问 localhost:4040。浏览器会显示 Hello, world!
。
在这种情况下,服务器是一个 Dart 程序,客户端是你使用的浏览器。然而,你可以用 Dart 编写客户端程序-无论是基于浏览器的客户端脚本,还是独立的程序。
快速浏览一下代码 #
在 hello world
服务器的代码中,一个 HTTP 服务器与主机和端口绑定,监听 HTTP 请求,并写入响应。需要注意的是,该程序导入了 dart:io 库,其中包含了服务器端程序和客户端程序的 HTTP 相关类(但不包含 Web 应用)。
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();
}
}
接下来的几节内容包括服务器端绑定、发出客户端 GET 请求、监听和响应。
将服务器绑定到主机和端口 #
本节示例:hello_world_server.dart。
main()
中的第一条语句使用 HttpServer.bind()
创建一个 HttpServer 对象,并将其绑定到主机和端口。
var server = await HttpServer.bind(
InternetAddress.loopbackIPv4,
4040,
);
该代码使用 await
异步调用 bind
方法。
主机名 #
bind()
的第一个参数是指定主机名。你可以用一个字符串来指定一个特定的主机名或IP地址,也可以用 InternetAddress 类提供的这些预定义的值来指定主机。
值 | 用例 |
---|---|
回环 IPv4 或 loopbackIPv6 | 服务器在 loopback 地址上监听客户端活动,该地址实际上是 localhost。使用IP协议的4或6版本。这些主要用于测试。我们建议您使用这些值而不是 localhost 或 127.0.0.1 。 |
任何 IPv4 或 anyIPv6 | 服务器监听任何 IP 地址上指定端口上的客户端活动。使用IP协议的4或6版本。 |
默认情况下,当使用V6互联网地址时,也会使用V4监听器。
端口 #
bind()
的第二个参数是指定端口的整数。端口唯一地标识主机上的服务。1024 以下的端口号为标准服务保留(0除外)。例如,FTP 数据传输通常在端口20上运行,每日报价在端口17上运行,HTTP 在端口80上运行。你的程序应该使用1024以上的端口号。如果端口已经在使用中,你的服务器的连接将被拒绝。
侦听请求 #
服务器使用 await for
开始监听 HTTP 请求。每收到一个请求,代码就会发送一个 “Hello, world!” 的响应。
await for (HttpRequest request in server) {
request.response.write('Hello, world!');
await request.response.close();
}
你将在监听和处理请求一节中了解更多关于 HttpRequest 对象包含的内容以及如何编写响应。但首先,让我们看看客户端产生请求的一种方式。
使用 HTML 表单发出 GET 请求 #
本节的示例文件:number_thinker.dart 和 make_a_guess.html。
本节介绍了一个命令行服务器,它可以随机选择一个0到9之间的数字。客户端是一个基本的 HTML 网页,make_a_guess.html
,你可以用它来猜数字。
试试吧
- 运行数字思考者服务器
在命令行,运行 number_thinker.dart
server。你应该看到类似下面的东西:
$ cd httpserver
$ dart bin/number_thinker.dart
I'm thinking of a number: 6
- 启动网络服务器
从应用程序的顶部目录运行 webdev serve
。
更多信息:webdev 文档
- 打开 HTML 页面
在浏览器中,进入 localhost:8080/make_a_guess.html。
- 做一个猜测
选择一个数字,然后按猜测按钮。
在客户端中没有涉及到 Dart 代码。客户端请求是通过浏览器向 Dart 服务器发出的,在 make_a_guess.html
中的 HTML 表单,它提供了一个自动制定和发送客户端 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>
下面是表单的工作原理:
- 表单的
action
属性被分配给发送请求的 URL - 表单的
method
属性定义了请求的类型,这里是GET
。其他常见的请求类型包括 POST、PUT 和 DELETE。 - 表单中任何有名称(
name
)的元素,比如<select>
元素,都会成为查询字符串中的一个参数。 - 当按下提交按钮(
<input type="submit"...>
)时,提交按钮会根据表单的内容制定请求并发送。
一个 RESTful GET 请求 #
REST(REpresentational State Transfer)是一套设计 Web 服务的原则。乖巧的 HTTP 客户端和服务器遵守为 GET 请求定义的 REST 原则。
一个 GET 请求:
- 只检索数据
- 不会改变服务器的状态
- 有长度限制
- 可以在请求的 URL 中发送查询字符串
在这个例子中,客户端发出了一个符合 REST 的 GET 请求。
监听和处理请求 #
本节的示例文件: number_thinker.dart 和 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);
}
}
当一个 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.');
}
一个 HttpRequest 对象有很多属性,提供了关于请求的信息。下表列出了一些有用的属性。
属性 | 信息 |
---|---|
method | ‘GET’, ‘POST’, ‘PUT’ 等方法中的一个。 |
uri | 一个 Uri 对象:scheme、host、port、query string 和其他关于请求资源的信息。 |
response | 一个 HttpResponse 对象:服务器将其响应写入其中。 |
headers | 一个 HttpHeaders 对象:请求的头信息,包括 ContentType、内容长度、日期等。 |
使用方法属性 #
下面的数想器例子中的代码使用 HttpRequest 的 method
属性来确定收到了什么样的请求。这个服务器只处理 GET 请求。
if (request.method == 'GET') {
handleGet(request);
} else {
request.response
..statusCode = HttpStatus.methodNotAllowed
..write('Unsupported request: ${request.method}.')
..close();
}
使用 uri 属性 #
在浏览器中输入一个 URL 会产生一个 GET 请求,它只是简单地从指定的资源中请求数据。它可以通过附加在 URI 上的查询字符串随请求发送少量数据。
void handleGet(HttpRequest request) {
final guess = request.uri.queryParameters['q'];
// ···
}
使用 HttpRequest 对象的 uri
属性来获取一个 Uri 对象,这个 Uri 对象包含了用户输入的 URL 的信息。Uri 对象的 queryParameters
属性是一个 Map,包含查询字符串的组件。通过名称来引用所需的参数。本例使用 q
来标识猜测的数字。
设置响应的状态码 #
服务器应该设置状态码来表示请求的成功或失败。前面看到数想家将状态码设置为 methodNotAllowed
来拒绝非 GET 请求。在后面的代码中,为了表示请求成功,响应完成,数想家服务器将 HttpResponse
状态码设置为 HttpStatus.ok
。
void handleGet(HttpRequest request) {
final guess = request.uri.queryParameters['q'];
final response = request.response;
response.statusCode = HttpStatus.ok;
// ···
}
HttpStatus.ok
和 HttpStatus.methodNotAllowed
是 HttpStatus 类中许多预定义状态码中的两个。另一个有用的预定义状态码是 HttpStatus.notFound
(经典的 404)。
除了状态码(statusCode
),HttpResponse 对象还有其他有用的属性:
属性 | 信息 |
---|---|
contentLength | 响应的长度,-1 表示事先不知道长度。 |
cookies | 要在客户端设置的 Cookies 列表。 |
encoding | 编写字符串时使用的编码,如 JSON 和 UTF-8。 |
headers | 响应头,是一个 HttpHeaders 对象。 |
将响应写到 HttpResponse 对象 #
每个 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();
// ···
}
}
从独立的客户端进行 POST 请求 #
本节的示例文件:basic_writer_server.dart 和 basic_writer_client.dart。
在 hello world
和 number thinker
的例子中,浏览器生成了简单的 GET 请求,对于更复杂的 GET 请求和其他类型的请求,如 POST、PUT 或 DELETE,你需要写一个客户端程序,其中有两种。
- 一个独立的客户端程序,它使用
dart:io
的 HttpClient 类。 - 基于浏览器的客户端,使用 dart:html 中的 API。本教程不涉及基于浏览器的客户端。要查看基于浏览器的客户端和相关服务器的代码,请参见 note_client.dart、note_server.dart 和 note_taker.html。
让我们看看一个独立的客户端,basic_writer_client.dart
和它的服务器 basic_writer_server.dart
。客户端发出一个 POST 请求,将 JSON 数据保存到服务器端的文件中。服务器接受请求并保存文件。
试试吧 #
在命令行上运行服务器和客户端。
- 首先,运行服务器:
cd httpserver
$ dart bin/basic_writer_server.dart
- 在一个新的终端中,运行客户端:
$ 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()
方法进行请求。发起一个请求涉及两个 Future。
post()
方法建立与服务器的网络连接并完成第一个 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);
}
/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 字符串格式。
一个 RESTful POST 请求 #
与 GET 请求类似,REST 为 POST 请求提供了指导方针。
一个 POST 请求:
- 创建一个资源(在这个例子中,一个文件)
- 使用一个 URI,其结构与文件和目录路径名相似;例如,URI 没有查询字符串。
- 以 JSON 或 XML 格式传输数据
- 没有状态,也不会改变服务器的状态。
- 无长度限制
这个例子中的客户端发出 REST 兼容的 POST 请求。
要想看到使 REST 兼容的 GET 请求的客户端代码,请看 number_guesser.dart。它是一个独立的客户端,用于数字思考者服务器,定期进行猜测,直到猜对为止。
在服务器中处理一个 POST 请求 #
本节的示例文件:basic_writer_server.dart 和 basic_writer_client.dart。
一个 HttpRequest 对象是一个字节列表流(Stream<List<int>
)。要获得客户端发送的数据,就要监听 HttpRequest 对象上的数据。
如果来自客户端的请求包含了大量的数据,数据可能会以多个分块的形式到达。你可以使用 Stream 中的 join()
方法来连接这些分块的字符串值。
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();
}
}
/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
。
关于 CORS 头的说明 #
如果你想为运行在不同源头(不同主机或端口)的客户端提供服务,你需要添加 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');
}
更多信息,请参考维基百科的跨源资源共享一文。
使用 http_server 包 #
本节的示例文件:mini_file_server.dart 和 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
文件的内容来响应所有请求。
试试吧 #
在命令行中运行服务器:
$ cd httpserver
$ dart bin/mini_file_server.dart
在浏览器中输入 localhost:4044。服务器会显示一个 HTML 文件。
这是迷你文件服务器的代码:
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();
}
}
这段代码确定文件是否存在,如果存在,则打开文件,并将文件内容管道化到HttpResponse对象。
第二个服务器,你可以在 basic_file_server.dart 中找到它的代码,使用 http_server 包。
试试吧 #
在命令行中运行服务器:
$ cd httpserver
$ dart bin/basic_file_server.dart
在浏览器中输入 localhost:4046。服务器显示与之前相同的 index.html 文件。
在这个服务器中,处理请求的代码要短得多,因为 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);
}
}
这里,请求的资源 index.html 是由 VirtualDirectory 类中的 serviceFile()
方法提供的。你不需要写代码来打开一个文件并将其内容用管道传送到请求中。
另一个文件服务器 static_file_server.dart
也使用 http_server 包。这个服务器可以服务于服务器目录或子目录中的任何文件。
运行 static_file_server.dart
,用 localhost:4048 这个 URL 进行测试。
下面是 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*/
}
/1/ 允许客户端请求服务器目录内的文件。
/2/ 一个匿名函数,处理对目录本身的请求,即 URL 不包含文件名。该函数将这些请求重定向到 index.html
。
/3/ serveFile
方法为一个文件提供服务,在这个例子中,它为目录请求服务index.html。
/4/ VirtualDirectory 类提供的 serviceRequest
方法处理指定文件的请求。
使用 bindSecure() 的 https 方法 #
本节的示例:hello_world_server_secure.dart。
你可能已经注意到,HttpServer 类定义了一个叫做 bindSecure()
的方法,它使用 HTTPS(Hyper Text Transfer Protocol with Secure Sockets Layer)提供安全连接。要使用 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();
}
}
/1/ 安全网络连接的可选设置在 SecurityContext 对象中指定,有一个默认的对象 SecurityContext.defaultContext,包括知名证书机构的可信根证书。
/2/ 一个包含从服务器证书到签名机关根证书链的文件,格式为 PEM。
/3/ 一个包含(加密的)服务器证书私钥的文件,PEM 格式。
/4/ 在服务器上,上下文参数是必需的,对客户端来说是可选的。如果省略它,则使用默认的内置可信根的上下文。
其他资源 #
请访问这些 API 文档,了解本教程中讨论的类和库的更多细节。
Dart 类 | 目的 |
---|---|
HttpServer | 一个 HTTP 服务器 |
HttpClient | 一个 HTTP 客户端 |
HttpRequest | 一个服务器端请求对象 |
HttpResponse | 一个服务器端响应对象 |
HttpClientRequest | 一个客户端请求对象 |
HttpClientResponse | 一个客户端响应对象 |
HttpHeaders | 请求头 |
HttpStatus | 响应的状态 |
InternetAddress | 一个互联网地址 |
SecurityContext | 包含安全连接的证书、密钥和信任信息。 |
http_server 包 | 一个具有较高级别的 HTTP 类的包 |
下一步该怎么做? #
- 如果你还没有尝试过服务器端的 codelab,可以尝试编写一个服务器应用程序。
- Servers with Dart 链接到编写独立 Dart 应用程序的资源,包括服务器。