无框架的服务器端Dart系列中的第1部分。
我真的很喜欢能够使用和我写Flutter应用时一样的语言来编写服务器代码。做到这一点的两个主要框架是 Aqueduct 和 Angel 。不幸的是, Angel正在被废弃 ,Aqueduct已经有一段时间没有发布稳定的版本了(截至2020年12月)。
既然A的都出来了,那就到了B计划的时候了。
框架虽好,但总有一点魔力。作为Dart核心库之一的dart:io库,已经包含了制作HTTP服务器所需的低级类和函数。所以本文将教你如何自己创建这样一个服务器。
如果成功的话,这可能会变成一系列的文章。或者如果Aqueduct或者其他Dart框架重新上线,那么我可能会给你指出这个方向。
让我们开始吧。完整的代码在文章的最后,如果你迷路的话。
设置
我想你已经 安装了Dart 。我正在使用Dart 2.12,这样我就可以习惯使用 不可空类型 进行编码。目前它自带Flutter的测试版。
flutter channel beta 复制代码
或者使用测试版的 Dart SDK 。
现在在命令行中创建一个新的Dart项目,名为my_server(或任何你喜欢的)。
dart create my_server 复制代码
用你喜欢的IDE打开那个文件夹。 VS Code 和 IntelliJ 都有一个Dart插件。如果你还没有安装,请安装它。
创建一个服务器
用下面的代码替换my_server.dart。
import 'dart:io'; Future<void> main() async { final server = await createServer(); print('Server started: ${server.address} port ${server.port}'); } Future<HttpServer> createServer() async { final address = InternetAddress.loopbackIPv4; const port = 4040; return await HttpServer.bind(address, port); } 复制代码
这将创建一个监听本地主机IP地址(127.0.0.1)、端口为4040的服务器。
你可以像这样从终端启动你的服务器。
dart run bin/my_server.dart 复制代码
你应该看到以下的打印输出。
Server started: InternetAddress('127.0.0.1', IPv4) port 4040 复制代码
恭喜你!你已经做了一个Dart服务器。你已经建立了一个Dart服务器。这很容易,不是吗?
你已经启动了服务器,但是它还没有真正做任何事情。按Control+C键强制关闭你的程序。
处理HTTP请求
HTTP请求包括GET、POST、PUT和DELETE等。如果你对它们不熟悉,那么你可以在 Becoming a backend developer - Part 1: 基础概念 阅读更多。
用下面的代码替换主函数。
Future<void> main() async { final server = await createServer(); print('Server started: ${server.address} port ${server.port}'); await handleRequests(server); } 复制代码
并在my_server.dart中添加handleRequests作为顶层函数。
Future<void> handleRequests(HttpServer server) async { await for (HttpRequest request in server) { request.response.write('Hello from a Dart server'); await request.response.close(); } } 复制代码
注释。
Stream<HttpRequest>
再次运行你的程序,然后在浏览器中打开以下地址。
你应该看到以下结果。
你的浏览器为此发出了一个GET请求。接下来你将看到如何路由不同类型的请求。
路由不同类型的请求
我们不要对每个请求都一视同仁,而是按类型进行路由。用下面的代码替换handleRequests方法。
Future<void> handleRequests(HttpServer server) async { await for (HttpRequest request in server) { switch (request.method) { case 'GET': handleGet(request); break; case 'POST': handlePost(request); break; default: handleDefault(request); } } } 复制代码
现在,对于每一个进来的请求,你都要把它路由到一个不同的方法。接下来的几节将探讨如何处理GET、POST和其他任何方法。
我们将首先实现handleGet,所以暂时注释掉handlePost和handleDefault。
case 'POST': // handlePost(request); break; default: // handleDefault(request); 复制代码
处理GET请求
在my_server.dart中添加以下顶层代码。
var myStringStorage = 'Hello from a Dart server'; void handleGet(HttpRequest request) { request.response ..write(myStringStorage) ..close(); } 复制代码
注释。
- myStringStorage全局变量在这里代表一个数据库。我们在这里从这个变量中读取数据,并将在下一节中向它写入数据。
- GET请求不应该改变服务器状态,所以我们只需要在响应中传回myStringStorage的值。
保存你的工作,重新启动服务器。然后在浏览器中再次打开以下地址。
你应该看到和之前一样的结果。
处理POST请求
POST请求的目的是向服务器添加新资源。
将handleRequests中的handlePost行取消标注,然后在my_server.dart中添加以下顶层函数。
Future<void> handlePost(HttpRequest request) async { myStringStorage = await utf8.decoder.bind(request).join(); request.response ..write('Got it. Thanks.') ..close(); } 复制代码
你还需要添加以下导入。
import 'dart:convert'; 复制代码
注释:
- handlePost主体中的第一行从请求中获取传入的数据块,将它们转换为UTF-8格式的字符串,并将它们连接成一个单一的字符串。
- 一旦有了这个字符串,这个方法就用它来更新全局变量myStringStorage的值。这象征着向数据库写入数据。
- 因为我们实际上只是更新一个现有的值,而不是创建一个新的值,所以定义REST API使用PUT而不是POST可能更有意义。但对于我们的例子来说,POST是可以的。
- 还没有任何安全性。如果你把这个服务器放在网上,世界上任何人都可以更新myStringStorage。在未来的文章中,我想谈谈认证和授权。现在你可以阅读 服务器端Dart的认证 。
保存你的工作并重新启动服务器。
你不能用浏览器进行POST请求,所以你需要另一种工具,比如 curl 或 Postman 。你可以通过阅读 这篇文章 来了解这些和其他的方法来进行POST请求。
我将使用Postman来进行POST请求。打开Postman,执行以下步骤。
- 选择POST作为请求类型。
- 在地址栏中写上 http://localhost:4040。
- 选择Body选项卡。
- 写上任何字符串,例如Hello。
- 点击发送按钮。
你应该会收到来自服务器的200 OK响应,并在响应的正文中写上Got it. Thanks.在响应的正文中。
在Postman中看到的来自服务器的响应
如果你运行另一个GET请求(无论是在你的浏览器或Postman),你应该看到myStringStorage的更新值。
很好!你成功地从客户端更新了服务器。
处理其他请求
客户端还可以发出很多其他的HTTP请求--比如PUT、PATCH、DELETE等等。你可以在你的路由器中添加更多的方法来处理它们中的任何一个。然而,在这里我们只想说,在我们的服务器上不允许有其他请求。 取消对handleDefault这一行的标注,然后添加下面的顶层方法。
void handleDefault(HttpRequest request) { request.response ..statusCode = HttpStatus.methodNotAllowed ..write('Unsupported request: ${request.method}.') ..close(); } 复制代码
注释:这次你将状态码设置为methodNotAllowed。
- 这次你把状态代码设置为 methodNotAllowed. 这相当于一个405的代码。
- 你在响应中写一个错误,然后把它发回给客户端。
保存你的工作并重新启动服务器。
在Postman中发送一个PUT请求来测试它。
Postman中看到的服务器错误响应
干得好 Good work. 你已经为建立自己的服务器开了个好头。你会在下面找到完整的代码。不过首先,看看接下来要采取的一些步骤。
继续
你可以到官方的 Dart服务器文档 中详细阅读本文所涉及的大部分概念。你也应该看看Dart团队维护的 http_server 和 shelf 包。
除非你只支持少量公共数据的简单GET请求,否则在你使用Dart作为一个真正的应用的后端之前,有几个主要的缺失部分需要解决。Dart是一个真正的应用程序的后端:
- 数据库。你需要能够从Dart与数据库进行通信,以存储和检索资源。
- 认证:你需要能够从Dart与数据库进行通信以存储和检索资源。你需要能够隐藏私人数据,只允许授权用户更新服务器上的资源。
- 部署:你需要能够隐藏私人数据,并且只允许授权用户更新服务器上的资源。在你的本地机器上运行服务器是很好的,但它最终需要从外部世界访问。
虽然在技术上并不是必须的,但以下主题也会是有用的知道。
- 测试: 如果你不测试你的服务器代码,你就不能确定做一个改变不会破坏它。
- 文件。你要返回给客户的可能不仅仅是数据库中的字符串。最终你也需要服务于文件。
- 并发性。如果你的服务器有一个以上的核心,你可能会使用它。为此你要在另一个隔离区上启动你的服务器。
- CI/CD:手动上传服务器代码的变化,过一段时间就会变得有点老。如果能建立一个系统,在服务器有变化时自动运行测试和更新,那就更好了。
没有承诺,但我想继续写关于这些主题的文章,这样你就可以学会如何自己做这些事情,而不需要依赖一个框架。但即使你真的使用了框架,知道事情是如何工作的,也会让你更有效率。
完整的代码
import 'dart:convert'; import 'dart:io'; Future<void> main() async { final server = await createServer(); print('Server started: ${server.address} port ${server.port}'); await handleRequests(server); } Future<HttpServer> createServer() async { final address = InternetAddress.loopbackIPv4; const port = 4040; return await HttpServer.bind(address, port); } Future<void> handleRequests(HttpServer server) async { await for (HttpRequest request in server) { switch (request.method) { case 'GET': handleGet(request); break; case 'POST': handlePost(request); break; default: handleDefault(request); } } } var myStringStorage = 'Hello from a Dart server'; void handleGet(HttpRequest request) { request.response ..write(myStringStorage) ..close(); } Future<void> handlePost(HttpRequest request) async { myStringStorage = await utf8.decoder.bind(request).join(); request.response ..write('Got it. Thanks.') ..close(); } void handleDefault(HttpRequest request) { request.response ..statusCode = HttpStatus.methodNotAllowed ..write('Unsupported request: ${request.method}.') ..close(); } 复制代码