Web 主机
快速启动
cangjie
import spire_web_hosting.*
main(args: Array<String>) {
let builder = WebHost.createBuilder(args)
let host = builder.build()
host.run()
return 0
}web主机具有和通用主机的全部能力,与之不同的是,web主机提供了如下能力:
- 支持http协议
- 请求管道
- 内置大量的中间件
请求管道
请求管道 是Web主机的核心能力,它是AOP编程思想的一种实践方式。
cangjie
import spire_web_hosting.*
main() {
let builder = WebHost.createBuilder()
let host = builder.build()
// 中间件1
host.use{ context, next =>
println("middleware1: start")
next()
println("middleware1: endle")
}
// 中间件2
host.use{ context, next =>
println("middleware2: start")
next()
println("middleware2: endle")
}
// 中间件3
host.use{ context, next =>
println("middleware3: start")
next()
println("middleware3: endle")
}
host.run()
return 0
}运行结果
bash
info: spire.hosting.lifetime
Now listening on: http://127.0.0.1:5000
info: spire.hosting.lifetime
Hosting environment: Development
info: spire.hosting.lifetime
Content root path: /app
info: spire.hosting.lifetime
Application started. Press Ctrl+C to shut down.
# 我们发起请求
curl http://127.0.0.1:5000
middleware1: start
middleware2: start
middleware3: start
middleware3: endle
middleware2: endle
middleware1: endle中间件
通过上一个案例,我们指定请求管道是由中间件串联而成的。接下来我们来掌握如何自定义中间件,以及中间件实现的技巧。
自定义中间件
定义中间件只需要实现IMiddleware接口,我们演示一下如何实现一个静态文件中间件。
- spire_web_http模块下定义了http协议相关的接口和抽象类
- 中间件如果使用类的方式,支持依赖注入
cangjie
import std.io.*
import std.fs.*
import spire_web_http.*
import spire_web_hosting.*
main() {
let builder = WebHost.createBuilder()
let host = builder.build()
host.useStaticFiles()
host.run()
return 0
}
/* 静态文件中间件 */
public class StaticFileMiddleware <: IMiddleware {
private let _env: IWebHostEnvironment
public init(env: IWebHostEnvironment) {
_env = env
}
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
let path = context.request.path.value
let file = "${_env.webRootPath}${path}"
if (exists(file) && path != "/") {
let file = FileInfo(file)
try (fs = File(file.path, OpenMode.Read)) {
let sr = StringReader(fs)
context.response.write(sr.readToEnd())
}
} else {
next(context)
}
}
}
/* 我们可以将中间件扩展到请求管道 */
extend ApplicationBuilder {
public func useStaticFiles() {
this.use<StaticFileMiddleware>()
}
}
- spire为我们提供了大量的中间件。
- 如果需要扩展中间件到请求管道,我们建议扩展到
ApplicationBuilder类上
数据传递
我们在HttpContext定义了features,用于在中间件之间传递数据,下面我们通过一个案例来演示如何定义和使用它。
cangjie
import spire_web_hosting.*
main(args: Array<String>) {
let builder = WebHost.createBuilder()
let host = builder.build()
// 中间件1
host.use { context, next =>
// 存入feature
context.features.set<IMiddlewareFeature>(MiddlewareFeature("middleware"))
next()
}
// 中间件2
host.use { context, next =>
// 获取上一个中间件定义的feature
if (let Some(f) <- context.features.get<IMiddlewareFeature>().flatMap {f => f.data}) {
f |> println
}
next()
}
host.run()
return 0
}
/*
我们推荐使用接口和实现分类的方式,并且接口名以Feature结尾
*/
public interface IMiddlewareFeature {
prop data: String
}
public class MiddlewareFeature <: IMiddlewareFeature {
private let _data: String
init(data: String) {
_data = data
}
public prop data: String {
get() {
_data
}
}
}特殊中间件
web主机为我们提供了启动中间件和兜底中间件。
启动中间件 它是请求管道中的第一个中间件,负责请求的开始和结束,具有如下特点
- 异常拦截
- 创建请求作用域和子容器,并注入到
HttpContext - 判定是否注册
HttpContextAccess,如果注册了那么将HttpContext注入
让我们编写一个案例来进行验证和演示:
cangjie
import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_logging.*
import spire_extensions_injection.*
main() {
let builder = WebHost.createBuilder()
builder.services.addRouting()
builder.services.addHttpContextAccessor()
builder.services.addScoped<RequestLifetimeService, RequestLifetimeService>()
let host = builder.build()
host.useEndpoints { endpoints =>
endpoints.mapGet("hello") { context =>
let service = context.services.getOrThrow<RequestLifetimeService>()
service.sayHello()
}
endpoints.mapGet("error") {
context => throw Exception("test exeception")
}
}
host.run()
return 0
}
public class RequestLifetimeService <: Resource {
private let _logger: ILogger
public RequestLifetimeService(let accessor: IHttpContextAccessor, let loggerFactory: ILoggerFactory) {
_logger = loggerFactory.createLogger<RequestLifetimeService>()
}
public func sayHello() {
if (let Some(context) <- accessor.context) {
context.response.write("hello")
} else {
throw UnsupportedException("not a web app")
}
}
public func isClosed() {
false
}
public func close() {
// 请求结束资源会被容器自动销毁
_logger.info("请求结束")
}
}我们分别发送者两个请求之后,查看控制台输出:
bash
info: demo.RequestLifetimeService
请求结束
error: spire.hosting.lifetime
An exception has occurred:
Exception: test exeception
at demo.main::lambda.0::lambda.1()(app\src\main.cj:21)兜底中间件
它是请求管道中的最后一个中间件,如果管道被穿透,将返回404,我们可以访问任意一个不存在的资源来进行验证。
配置服务器
我们可以通过ServerOptions来配置服务器选项
cangjie
import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_options.*
import spire_extensions_injection.*
main() {
let builder = WebHost.createBuilder()
builder.services.addRouting()
builder.services.configure<ServerOptions>{options =>
options.builder.maxRequestBodySize(1)
}
let host = builder.build()
// 我们可以测试发送body数据包
host.useEndpoints { endpoints =>
endpoints.mapPost("hello") { context =>
context.response.write("hello")
}
}
host.run()
return 0
}内置服务
通用主机为我们内置了一些服务,具体如下:
| 接口 | 用途 |
|---|---|
ILoggerFactory | 用于创建和配置 ILogger 实例,管理日志记录器的生命周期和日志提供程序(如 Console、File 等)。 |
IConfiguration | 提供对应用程序配置(如 appsettings.json、环境变量、命令行参数等)的键值对访问和读取功能。 |
IHostEnvironment | 提供当前宿主环境的信息(如 ApplicationName、EnvironmentName、ContentRootPath 等)。 |
IWebHostEnvironment | 提供当前宿主环境的信息(如 webRootPath )。 |
IHttpContextAccessor | 通过依赖注入的方式访问当前 HttpContext (需要手动注册)。 |