HTTPTransport 5.1.1

HTTPTransport 5.1.1

Jeorge Taflanidi 维护。



  • Jeorge Taflanidi

HTTPTransport

描述

这个库是一个 Alamofire 包装器,允许同步 HTTP 请求。

基本上,您可以使用如下这种常规的流程控制

Alamofire.request(someRequest).response { reponse in
    if response ...
}

HTTPTransport 允许您使用这样的常规流程控制

let result = transport.send(someRequest)

if result ...

注意:库作者假设您知道如何构建移动应用程序,并将有关多线程和同步网络弊端的讨论留在了括号之后。

使用方法

安装:CocoaPods

pod 'HTTPTransport'

主要参与者

库的基本概念非常直接:您需要发送一个 请求 经过一个 传输 来接收一些 结果 — 这三个主要角色您将需要处理。

HTTP传输

除了执行实际的HTTP调用外,一个HTTP传输实例还包含对连接的非功能要求,如保持已建立的HTTP会话、实施的安全措施,以及默认的请求和响应处理堆栈,包括错误处理。

class HTTPTransport {
    let session:              Session
    let requestInterceptors:  [HTTPRequestInterceptor]
    let responseInterceptors: [HTTPResponseInterceptor]
}

了解关于 会话拦截器 的更多信息。

HTTPRequest

一个瑞士军刀般的多功能工具,能满足您构建HTTP请求的所有需求。

class HTTPRequest {
    let httpMethod:           HTTPMethod
    let endpoint:             String
    var headers:              [String: String]
    var parameters:           [HTTPRequestParameters]
    var requestInterceptors:  [HTTPRequestInterceptor]
    var responseInterceptors: [HTTPResponseInterceptor]
    let session:              Session?
    let timeout:              TimeInterval
}

基本上按您期望的方式工作。首先,它是一个HTTP请求信封字段的容器对象,包括一个 HTTPMethod、一个 endpoint(URL或其部分)、请求头和请求主体。

其次,每个HTTPRequest实例指定其自己的超时间隔、一个自定义的Session(如果需要),以及应用于此特定请求及其响应的两个拦截器集合。其中大多数选项都有默认值,因此不会太困扰您。

HTTPRequest类提供多种修改其内容的方式,包括一个智能构造函数,允许基于其他HTTPRequest实例来创建HTTPRequest实例,详见食谱中的基础依赖请求

class HTTPRequest {
    func with(header: String, value: String) -> Self
    func with(cookieName name: String, value: String) -> Self
    func with(cookie: HTTPCookie) -> Self
    func with(parameter: String, value: Any, encoding: HTTPRequestParameters.Encoding) -> Self
    func with(parameters: [String: Any], encoding: HTTPRequestParameters.Encoding) -> Self
    func with(parameters: HTTPRequestParameters) -> Self
    func with(parameters: [HTTPRequestParameters]) -> Self
    func with(interceptors: [HTTPRequestInterceptor]) -> Self
    func with(interceptors: [HTTPResponseInterceptor]) -> Self
}


let userSearchRequest =
    HTTPRequest(endpoint: "/user")
        .with(cookieName: "SESSION_ID", value: sessionId)
        .with(parameters: ["first_name": "John", "last_name": "Appleseed"], encoding: .url)

请求参数通过一个独立的容器类 HTTPRequestParameters 表示,允许每个HTTPRequest包含几组不同编码的参数。

有两组扩展自HTTPRequest的子类:DataUploadHTTPRequestFileUploadHTTPRequest。两者基本上都是不言自明的;它们用于上传Data和文件。

HTTP传输的结果

第三个主要角色,表示HTTP调用的结果。要么是.success(成功)要么是.failure(失败)。

enum Result {
    case success(response: HTTPResponse)
    case failure(error: NSError)
}

您需要知道的主要观点是,对于成功的HTTP调用来讲或失败的HTTP调用,定义依赖于您所应用验证技术的不同。

默认情况下,Alamofire的validate()方法会被调用(见HTTPTransport.useDefaultValidation属性),这意味着只有状态码为2xx的响应被认定为成功的,否则会被转换为错误。禁用useDefaultValidation会将所有服务器响应都视为成功,无论响应内容如何,而像网络断开等情况则会导致失败。

在低级别上,响应会受到设置的一组响应拦截器的影响,这些拦截器在Alamofire验证之前生效。这就是为什么你可能可以考虑将ClarifyErrorInterceptor加入到你的传输响应拦截器栈中,因为它会丰富结果中的NSError对象。

HTTPRequestParameters

本质上,这是一个字典,额外包含了这个字典将被如何编码的信息。

class HTTPRequestParameters {
    var parameters: [String: Any]
    let encoding:   Encoding

    subscript(parameterName: String) -> Any? { get set }

    enum Encoding {
        case json
        case url
        case propertyList
        case custom(encode: EncodeFunction)
    }
}

jsonpropertyList将被编码到体中,url参数进入查询字符串。

你的HTTPRequest可能包含多个HTTPRequestParameters集合

class HTTPRequest {
    var parameters: [HTTPRequestParameters]
}


let request =
    HTTPRequest(
        parameters: [
            HTTPRequestParameters(parameters: ["name": "John"], encoding: .json),
            HTTPRequestParameters(parameters: ["dob": "12/12/12"], encoding: .json),
            HTTPRequestParameters(parameters: ["age": 5], encoding: .url),
        ]
    )

这里的规则如下:

  • 具有相同编码的参数合并为一个字典;
  • 具有相同编码和相同键的参数会覆盖合并字典中的先前值;
  • 参数将在具有相同编码的base请求参数之后追加;
  • 参数会覆盖具有相同编码和相同键的base参数;
  • propertyList参数和json参数不会在同一个体中混合,它们会相互覆盖;最后一个是胜利者;
  • FileUploadHTTPRequest请求忽略json参数;propertyList参数在文件多部分之后附加;
  • DataUploadHTTPRequest请求同时忽略propertyListjson参数。

Session

会话对象包含Alamofire的SessionManager并提供了一种方便的配置连接安全的方法,这与Security对象相关。

class Session {
    let manager: SessionManager

    convenience init()
    init(security: Security)
}

Security对象允许通过证书指纹检查主机

class Security {
    class var noEvaluation: Security

    init(certificates: [Certificate])
}


struct Certificate {
    let host:        String
    let fingerprint: Fingerprint
    
    enum Fingerprint {
        case sha1(fingerprint: String)
        case sha256(fingerprint: String)
        case publicKey(fingerprint: String)
        case debug
        case disable
    }
}

通过字符串交集检查主机名。这意味着Certificate(host: "host.com", fingerprint:...)应用于诸如https://www.host.com/queryhttps://host.comhttps://api.host.com/v1等URL。

拦截器

HTTPRequestInterceptorHTTPResponseInterceptor 是受启发于 okhttp拦截器Django中间件 等的抽象中间件类。拦截器可以改变输入和输出,每个 HTTPTransport 实例都包含两个列表,分别对应用于每个请求的请求拦截器和随后应用于每个响应的响应拦截器。

换句话说,当您的应用通过 传输 发送一个 请求 时,后者会在实际发送之前将这个 请求 通过其请求拦截器列表。收到 响应 后,传输会将它通过响应拦截器列表传递,然后将其传输到您的 应用 中。

class HTTPRequestInterceptor {
    func intercept(request: URLRequest) -> URLRequest
}


class HTTPResponseInterceptor {
    func intercept(response: RawResponse) -> RawResponse

    struct RawResponse {
        let request:  URLRequest?
        let response: HTTPURLResponse?
        let data:     Data?
        let error:    Error?
    }
}

拦截器可能会也可能不会改变它们处理的数据。例如,您的一个请求拦截器可能会为每个请求添加一个 Authentication 标头。另一个请求拦截器可能会只将请求数据打印到控制台日志中。

您可以通过扩展上述提到的类来实现自己的拦截器。《HTTPTransport》库已经包含了一些基本的实用拦截器,例如:

  • LogRequestInterceptorLogResponseInterceptor — 允许您记录请求和响应;
  • AddCookieInterceptor — 将 cookieProvider 中的 cookies 添加到每个请求;
  • ReceivedCookieInterceptor — 将接收到的 cookies 存储到 cookieStorage
  • ClarifyErrorInterceptor — 将 API 错误 JSON 载荷(例如 {"code": 500, "message": "Database error"})转换为 NSError 实例,见下文

NSError

HTTPTransport 提供了对现有 NSError 类的扩展,增加了实时 HTTP 响应的一些额外部分(如果有)。

大多数这些功能只有在启用 ClarifyErrorInterceptor 时才会工作。

extension NSError {
    var url: String? // contains URL when HTTPRequest have failed to serialize into URLRequest

    var httpStatusCode:             HTTPStatusCode? // HTTP status
    
    var responseBodyData:           Data?           // received bytes
    var responseBodyString:         String?         // received bytes as UTF8 string
    var responseBodyJSON:           Any?            // received bytes as a JSON object
    var responseBodyJSONDictionary: [String: Any]?  // received JSON object casted to dictionary
    
    var responseBodyErrorCode:      String?         // parsed error code from received JSON
    var responseBodyErrorMessage:   String?         // parsed error message from received JSON
}

食谱

基本 GET 请求

// assuming all following code runs in a background thread

let request   = HTTPRequest(endpoint: "https://api.service.com")
let transport = HTTPTransport()

let result: HTTPTransport.Result = transport.send(request: request)

switch result {
    case .success(let httpResponse):
        print(httpResponse.httpStatus)
        do {
            if let json: [String: Any] = try httpResponse.getJSONDictionary() {
                print(json)
            }
        } catch {
            print("JSONSerialization error")
        }
    case .failure(let nsError):
        if let httpStatus: HTTPStatusCode = nsError.httpStatusCode {
            print(httpStatus)
        } else {
            print(nsError.localizedDescription)
        }
}

基本依赖请求

// assuming all following code runs in a background thread

let transport = HTTPTransport()

let baseRequest =
    HTTPRequest(endpoint: "https://api.service.com")
        .with(header: "User-Agent", value: "Application/iOS")

let authRequest = HTTPRequest(endpoint: "/session", base: baseRequest)
let authResult  = transport.send(request: authRequest)

if let sessionId: String = getSessionId(authResult) {
    let userSearchRequest =
        HTTPRequest(endpoint: "/user", base: baseRequest)
            .with(cookieName: "SESSION_ID", value: sessionId)
            .with(parameters: ["first_name": "John", "last_name": "Appleseed"], encoding: .url)
    
    let searchResult = transport.send(request: userSearchRequest)
    if let users: [User] = getUsers(searchResult) {
        showUsers(users)
    } else {
        showEmptyScreen()
    }
} else {
    showError()
}

日志记录

let transport = HTTPTransport(
    requestInterceptors: [
        LogRequestInterceptor(logLevel: LogRequestInterceptor.LogLevel.url),
    ],
    responseInterceptors: [
        LogResponseInterceptor(logLevel: LogResponseInterceptor.LogLevel.everything),
    ]
)

let result = transport.send(...)

发送和接收 cookie

let cookieStorage: CookieStoring & CookieProviding = getCookieStorage()

let transport = HTTPTransport(
    requestInterceptors: [
        AddCookieInterceptor(cookieProvider: cookieStorage),
    ],
    responseInterceptors: [
        ReceivedCookieInterceptor(cookieStorage: cookieStorage),
    ]
)

let result = transport.send(...)

带有 body 和 URL 参数的 POST 请求

let urlParameters = HTTPRequestParameters(
    parameters: ["first_name" : "John"],
    encoding: .url
)

let bodyParameters = HTTPRequestParameters(
    parameters: ["salary" : 100000],
    encoding: .json
)

let updateSalaryRequest = HTTPRequest(
    httpMethod: HTTPRequest.HTTPMethod.post,
    endpoint: "https://api.company.com/employees",
    parameters: [urlParameters, bodyParameters]
)

let result = transport.send(request: updateSalaryRequest)

使用 SHA1 数字指纹进行 SSL 硬件

let fingerprint =
    "ED D6 27 B8 8B 51 B0 24 B9 BF 90 4C D4 AB 9A AB E2 4B 93 00"
        .replacingOccurrences(of: " ", with: "")

let security = Security(
    certificates: [
        TrustPolicyManager.Certificate(host: "google.com", fingerprint: .sha1(fingerprint: fingerprint))
    ]
)

let transport = HTTPTransport(security: security)
let result = transport.send(request: HTTPRequest(endpoint: "https://google.com/ncr"))

演进

你可能已经注意到我们的库试图不暴露 Alamofire 接口。有一个简单的方法来消除这种递归依赖,并在 URLSession 框架之上建立独立逻辑。

这些深远计划需要我们目前无法承担的大量努力。尽管如此,这是我们最终渴望达成的重大目标。

因此,欢迎提交拉取请求,但在实际编码之前,请考虑先创建一个预问题。