Skip to content

Web 主机

快速启动

cangjie
import spire_web_hosting.*

main() {
    let builder = WebHost.createBuilder()

    let host = builder.build()

    host.run()

    return 0
}

总结

  1. 只需要三行代码即可启动 web主机web主机 是对通用主机的扩展,因此拥有通用主机的所有能力。
  2. 启动 web 主机,会创建一个 http服务器,监听 5000 端口来处理 http 请求,此时可以在浏览器上输入 http://localhost:5000

请求管道

请求管道 是一个由多个**中间件(Middleware)**按顺序组成的处理链,负责处理 HTTP 请求并生成响应。每个中间件可以:

  1. 处理请求(如验证、日志记录)
  2. 修改请求或响应(如添加 HTTP 头)
  3. 直接返回响应(如静态文件中间件找到匹配文件时终止管道)
  4. 调用下一个中间件(通过 next() 传递)

管道顺序由注册顺序决定,典型流程:
请求 → 中间件1 → 中间件2 → ... → 终结点(如 MVC)→ 响应

项目架构图

use 将多个请求委托链接在一起。 next 参数表示管道中的下一个委托。 可通过不调用 参数使管道短路。 通常可在 next 委托前后执行操作,如以下示例所示:

cangjie
import spire_web_hosting.*

main() {
    let builder = WebHost.createBuilder()

    let host = builder.build()

    host.use{ context, next =>
        println("middleware1: start")
        next()
        println("middleware1: endle")
    }

    host.use{ context, next =>
        println("middleware2: start")
        next()
        println("middleware2: endle")
    }

    host.use{ context, next =>
        println("middleware3: start")
        next()
        println("middleware3: endle")
    }

    host.run()

    return 0
}

总结

  1. WEB主机除了实现了通用主机之外,还在此基础上提供了请求管道
  2. 你可以在浏览器上输入http://localhost:5000查看输出结果
  3. 有了请求管道,我们就可以通过编写中间件来配置请求处理逻辑,并且中间件是可插拔的
  4. 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

自定义中间件

自定义中间件只需要实现IMiddleware接口,中间件支持依赖注入,但只能依赖单例服务

异常处理中间件

让我来来写一个异常处理中间件,如果是已知的异常,那么打印异常消息,否则提示服务器错误。

cangjie
import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_injection.*

main() {
    let builder = WebHost.createBuilder()

    //注册路由中间件需要的服务
    builder.services.addRouting()

    let host = builder.build()

    host.use(ExceptionHandlerMiddleware())

    //添加终结点和终结点中间件,路由中间件
    host.useEndpoints{ endpoints =>
        //添加终结点1
        endpoints.mapGet("hello1") { context =>
            if (!context.request.isHttps) {
                throw KnownException("必须使用https协议")
            }
        }

        //添加终结点2
        endpoints.mapGet("hello2") { context =>
            if (!context.request.isHttps) {
                throw Exception("必须使用https协议")
            }
        }
    }

    host.run()

    return 0
}

public class KnownException <: Exception {
    public init(message: String) {
        super(message)
    }
}

public class ExceptionHandlerMiddleware <: IMiddleware {
    public func invoke(context: HttpContext, next: () -> Unit) {
        try {
            next()
        } catch (known: KnownException) {
            context.response.write("{success: false, message: \"${known.message}}\"")
            context.response.status(StatusCodes.InternalServerError)
            context.response.addHeader(HeaderNames.ContentType, "application/json;chatset=utf-8")
        } catch (ex: Exception) {
            context.response.write("{success: false, message: \"服务器错误\"}")
            context.response.status(StatusCodes.InternalServerError)
            context.response.addHeader(HeaderNames.ContentType, "application/json;chatset=utf-8")
        }
    }
}

总结

  1. 异常处理中间件一般要放到最上面,这样才能保住拦截到其他的中间件产生的异常信息
  2. 这里我们使用了路由中间件,中间件一般控制执行流程

简易 OpenApi

下面我们实现一个比较完整的中间件,里面运用到了 中间件容器选项 等技术。并使用扩展语法,便于使用者直接调用。

cangjie
import std.collection.*
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()

    //注册OpenApi中间件需要的服务
    builder.services.addOpenApi()

    let host = builder.build()

    //启用openapi中间件
    if (host.environment.isDevelopment()) {
        host.useOpenApi()
    }

    //添加终结点和终结点中间件,路由中间件
    host.useEndpoints{ endpoints =>
        endpoints.mapGet("/hello") { context =>
            context.response.write("get")
        }

        endpoints.mapPost("/hello") { context =>
            context.response.write("post")
        }
    }

    host.run()

    return 0
}

//openapi选项
public class OpenApiOptions {
    public var path = "/openapi"
    public var title = "spire OpenApi"
}

//openapi服务
public interface IOpenApiService {
    func createDocument(): String
}

//openapi服务实现
public class OpenApiService <: IOpenApiService {

    public OpenApiService(let _dataSource: EndpointDataSource, let _options :IOptions<OpenApiOptions>) {

    }

    public func createDocument() {
        let sb = StringBuilder()
        sb.append("<html>")
        sb.append("<title>${_options.value.title}</title>")
        sb.append("<body>")
        for (pattern in _dataSource.endpoints |> filterMap{f => f as RouteEndpoint}) {
            let methods = pattern.metadata.getMetadata<IHttpMethodMetadata>()
                .flatMap{ f => String.join(f.httpMethods |> collectArray, delimiter: ",")}
            sb.append("<a href=${pattern.routePattern}>${methods ?? ""}:${pattern.routePattern}</a><br/>")
        }
        sb.append("</body>")
        sb.append("</html>")
        return sb.toString()
    }
}

//openapi中间件
public class OpenApiMiddleware <: IMiddleware {

    public OpenApiMiddleware(let _service :IOpenApiService, let _options: IOptions<OpenApiOptions>) {

    }

    public func invoke(context: HttpContext, next: () -> Unit) {
        if (_options.value.path == context.request.path.value) {
            let document = _service.createDocument()
            context.response.write(document)
        } else {
            next()
        }
    }
}

//扩展容器
extend ServiceCollection{
    public func addOpenApi() {
        addOpenApi{_ => }
    }

    public func addOpenApi(configureOptions: (OpenApiOptions)-> Unit) {
        this.configure(configureOptions)
        this.tryAddSingleton<IOpenApiService, OpenApiService>()
    }
}

//扩展请求管道
extend ApplicationBuilder{
    public func useOpenApi() {
        this.use<OpenApiMiddleware>()
    }
}

总结

  1. EndpointDataSource 服务是由路由中间件注册的。
  2. 一般我们只在开发环境才启用 OpenApi 中间件。

Http 服务器

web 主机在启动时会构建请求管道得到请求管道执行器,并启动 http 服务器来监听端口处理 http 请求。源码如下:

cangjie
class DefaultHttpRequestDistributor <: HttpRequestDistributor {
    private let _app: RequestDelegate
    private let _services: IServiceProvider

    init(app: RequestDelegate, services: IServiceProvider) {
        _app = app
        _services = services
    }

    public func register(_: String, _: HttpRequestHandler): Unit {

    }

    public func register(_: String, _: (HttpContextBase) -> Unit): Unit {

    }

    public func distribute(_: String) {
        let logger = _services.getOrThrow<ILoggerFactory>().createLogger("spire.hosting.lifetime")
        return FuncHandler { context =>
            try (requestScope = _services.createScope()) {
                let contextImpl = HttpContextImpl(context, requestScope.services)
                try {
                    setHttpContextAccessor(contextImpl)
                    _app(contextImpl)
                } catch (ex: Exception) {
                    contextImpl.response.write("Internal Server Error")
                    contextImpl.response.status(StatusCodes.InternalServerError)
                    contextImpl.response.addHeader(HeaderNames.ContentType, "text/plain; charset=utf-8")
                    logger.error(ex, ex.message)
                }
            }
        }
    }

    private func setHttpContextAccessor(context: HttpContextImpl): Unit {
        if (let Some(contextAccessor) <- context.services.getOrDefault<IHttpContextAccessor>()) {
            if (let internalContextAccessor: HttpContextAccessor <- contextAccessor) {
                internalContextAccessor.setup(context)
            }
        }
    }
}

总结

  1. Web主机 会去实现 HttpRequestDistributor 接口,将请求分发给 handler 去处理,handler 就是最终构建的请求管道。
  2. 每次监听到 http 请求都会创建一个作业域,我们称为 请求作用域,并将子容器保存到 HttpContext上。
  3. 同时会判断容器中是否注册了 IHttpContextAccessor,如果注册了 IHttpContextAccessor 则将 HttpContext 实列保存到 IHttpContextAccessor 上。通过解析 IHttpContextAccessor 服务,即可访问到当前 HttpContext 实列

测试请求生命周期

我们可以编写一个测试案例来演示上面的源码要表达的含义

cangjie

import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_injection.*

main() {

    let builder = WebHost.createBuilder()

    builder.services.addRouting()

    builder.services.addHttpContextAccessor() 

    builder.services.addScoped<Server, Server>() 

    let host = builder.build()

    host.useEndpoints{ endpoints =>
        // 添加终结点1
        endpoints.mapGet("hello") { context =>

            let server1 = context.services.getOrThrow<Server>()

            let server2 = context.services.getOrThrow<Server>()

            println("requestUrl:${server1.getRequestUrl()}")
            // Server的生命周期注册为Scoped,在请求管道中,同一次请求共享同一个作用域,因此他们是同一个实例
            println("server1 == server2:${refEq(server1, server2)}")
        }
    }

    host.run()

    return 0
}

public class Server {
    public Server(
        let _httpContextAccessor: IHttpContextAccessor) {

    }

    public func getRequestUrl() {
        if (let Some(context) <-_httpContextAccessor.context) {
            return context.request.getDisplayUrl()
        }
        throw UnsupportedException()
    }
}

内置服务

通用主机为我们内置了一些服务,具体如下:

接口用途
ILoggerFactory用于创建和配置 ILogger 实例,管理日志记录器的生命周期和日志提供程序(如 Console、File 等)。
IConfiguration提供对应用程序配置(如 appsettings.json、环境变量、命令行参数等)的键值对访问和读取功能。
IHostEnvironment提供当前宿主环境的信息(如 ApplicationNameEnvironmentNameContentRootPath 等)。
IWebHostEnvironment提供当前宿主环境的信息(如 webRootPath )。
IHttpContextAccessor通过依赖注入的方式访问当前 HttpContext (需要手动注册)。