Skip to content

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主机提供了如下能力:

  1. 支持http协议
  2. 请求管道
  3. 内置大量的中间件

请求管道

请求管道 是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接口,我们演示一下如何实现一个静态文件中间件。

  1. spire_web_http模块下定义了http协议相关的接口和抽象类
  2. 中间件如果使用类的方式,支持依赖注入
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主机为我们提供了启动中间件和兜底中间件

启动中间件 它是请求管道中的第一个中间件,负责请求的开始和结束,具有如下特点

  1. 异常拦截
  2. 创建请求作用域和子容器,并注入到HttpContext
  3. 判定是否注册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提供当前宿主环境的信息(如 ApplicationNameEnvironmentNameContentRootPath 等)。
IWebHostEnvironment提供当前宿主环境的信息(如 webRootPath )。
IHttpContextAccessor通过依赖注入的方式访问当前 HttpContext (需要手动注册)。