iOS 通俗易懂的HTTP网络

原文

去了饿厂面试后了解到了自己计算机基础的薄弱, 非科班出身薄弱也是自然的, 说实话, 我也并不是特别想要往底层深究, 因为越底层的东西越会抽象成服务输送给大众, 就好比自来水, 一般人都不会想要去了解自来水的底层逻辑吧, 但作为开发者, 我们还是得了解下基础的网络概念.

iOS 通俗易懂的HTTP网络

关于HTTP与HTML的发明有个很有趣的插曲, 那就是首个万维网服务器与浏览器是在一台NeXTStep计算机上编写的, 在1997年, Apple收购了NeXTStep Computer并将NeXTStep作为mac OS的基础后来成为了iOS的基础.

URL

每个Web资源被称为统一资源标识符(Uniform Resource Identifier, URI) 其中包括 URL 和  URN, 现在几乎所有的URI都是URL.

URL 通用格式

://: @: / ; ?#

几乎没有哪个URL中包含了所有这些组件, URL最重要的三个部分是 (scheme 方案), (host 主机), (path 路径)

比如说, 你想要获取URL https://www.apple.com/index.html 那么URL包含以下三个部分:

https 是URL方案(scheme). 方案告诉客户端如何访问资源.

www.apple.com 是服务器的位置, 告知客户端资源位于何处.

/index.html 是资源路径, 说明了请求的是服务器上哪个指定文件资源.

构建网络架构URL时遵循服务版本化和服务定位器原则.

报文

所有的HTTP报文都可以分为两类, 请求报文(request message) 和 响应报文 (response message) 我们可以通过Chrome模拟请求得到请求报文和响应报文, 我们来简单的看一下首部中的一些简单的概念.

响应首部

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE
Content-Type: application/json; charset=utf-8
Content-Length: 2180
Date: Mon, 31 Jul 2017 02:56:17 GMT
Connection: keep-alive

由上述响应首部, 我们可得知以下信息:

  1. 应用程序支持最高的HTTP版本号为1.1.

  2. 状态码200表示请求成功. 如为3XX表示重定向, 4XX表示客户端错误, 5XX表示服务器错误.

  3. 原因短语OK仅为显示, 并无实际含义.

  4. Content-Type就是MIME Type, 用以区分传输资源, 例子中主体部分是字符集为utf-8的json数据.

  5. Content-Length表示主体部分包含了2180字节的数据.

  6. Date表示了服务器产生响应的日期.

  7. Connection 连接类型为keep-alive.

  8. Access-Control-Allow-Origin 服务器域名为http://localhost:3000.

  9. Access-Control-Allow-Methods服务器实现的方法为GET,HEAD,PUT,POST,DELETE.

请求首部

  1. GET /api/J1/getJ1List HTTP/1.1

  2. Host: localhost:3001

  3. Connection: keep-alive

  4. Pragma: no-cache

  5. Cache-Control: no-cache

  6. Accept: application/json, text/plain, */*

  7. Origin: http://localhost:3000

  8. User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1

  9. Referer: http://localhost:3000/

  10. Accept-Encoding: gzip, deflate, br

  11. Accept-Language: zh-CN,zh;q=0.8

由上述请求首部, 我们可以得知以下信息:

  1. 使用了GET方法进行请求, 请求的路由为/api/J1/getJ1List, HTTP版本号为1.1

  2. Host 提供了接受请求的服务器的主机名和端口号localhost:3001.

  3. Connection 和响应首部信息对照.

  4. Pragma 随报文传送指示的方式, 并不专用于缓存.

  5. Cache-Control 用于随报文传送缓存指示.

  6. Accept 接受的任意媒体类型, 和响应首部的Content-Type信息对照

  7. Origin 当前访问域名, 与Access-Control-Allow-Origin信息对照

  8. User-Agent 将发起请求的应用程序名称告知服务器.

  9. Referer 提供了包含当前请求URI的文档的URL.

  10. Accept-Encoding 告诉服务器能够发送哪些编码方式.

  11. Accept-Language 告诉服务器能够发送哪些语言.

实体

具体来说, HTTP承载的实体需要满足以下条件.

  • 可以被正确识别(通过Content-Type首部说明媒体格式, Content-Language首部说明语言), 以便浏览器和其他客户端能正确处理内容.

  • 可以被正确地解包(通过Content-Length首部和Content-Encoding首部).

  • 是最新的(通过实体验证码和缓存过期控制).

  • 符合用户的需要(基于Accept系列的内容协商首部).

  • 在网络上可以快速有效地传输(通过范围请求, 差异编码以及其他数据压缩方法).

  • 完整到达, 未被篡改(通过传输编码首部和Content-MD5校验和首部).

HTTP / 1.1版定义了一下10个基本字体首部字段.

  • Content-Type 实体中所承载对象的类型.

  • Content-Length 所传送实体的长度或大小.

  • Content-Language 与所传送实体主体的长度或大小.

  • Content-Encoding 对象数据所做的任意变换 (比如, 压缩).

  • Content-Location 一个备用位置, 请求时可通过它获得对象.

  • Content-MD5 实体主体内容的校验和.

  • Last-Modified 所传输内容在服务器上创建或最后修改的日期时间.

  • Expires 实体数据将要失效的日期时间.

  • Allow 该资源所允许的各种请求方法, 例如, GET和HEAD.

  • ETag 这份文档特定实例的唯一验证码. ETag首部没有正式定义为实体首部, 但它对许多涉及实体的操作来说, 都是一个重要的首部.

  • Chahe-Control 指出应该如何缓存该文档, 和ETag首部类似, Chche-Control首部也没有正式定义为实体首部.

连接

世界上几乎所有的HTTP通信都是由TCP/IP承载的, HTTP要传送一条报文时, 会以流的形式将报文数据的内容通过一条打开的TCP链接按序传输, TCP收到数据流之后, 会将数据流砍成被称作段的小数据块, 并将段封装在IP分组中.

iOS URLSession

我们先来简单的看下iOS中如何使用HTTP网络, 使用系统的URLSession进行网络请求, 将请求方法设置为GET, 当然默认就是GET, 使用单例创建URLSession进行任务回调, URLSession是异步请求, dataTask默认是关闭状态, 需要手动开启dataTask.resume().

var request = URLRequest(url: URL(string: "http://localhost:3001/api/J1/getJ1List")!)
request.httpMethod = "GET"
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, err) in
    if err != nil {
        print(err.debugDescription)
    } else {
        let responseStr = String(data: data!, encoding: String.Encoding.utf8)
        print(responseStr!)
        print("mimeType: /(String(describing: response?.mimeType))")
    }
    if let response = response as? HTTPURLResponse {
        print("statusCode: /(response.statusCode)")
        for (tab, result) in response.allHeaderFields {
            print("/(tab.description) - /(result)")
        }
        if response.statusCode == 200 {
            print(response)
        }
    }
}
dataTask.resume()

结合上面的内容, 我们发送了一个GET请求到http://localhost:3001/api/J1/getJ1List, 现在我们就会分析URL了, 方案是http, 主机为localhost, 端口号为3001, 路径为/api/J1/getJ1List

{ URL: http://localhost:3001/api/J1/getJ1List } { status code: 200, headers {
    "Access-Control-Allow-Methods" = "GET,HEAD,PUT,POST,DELETE";
    "Access-Control-Allow-Origin" = "*";
    Connection = "keep-alive";
    "Content-Length" = 2180;
    "Content-Type" = "application/json; charset=utf-8";
    Date = "Mon, 31 Jul 2017 07:29:52 GMT";
} }

返回的响应报文与Chrome中显示相同, 在iOS9之后系统推荐使用URLSeesion, 使用起来非常的方便快捷. 当然URLSession的功能不止于此, 若想深究请看官方文档, 在网络可达性方面使用系统Reachability框架.

缓存

HTTP为我们提供了几个用来对已缓存对象进行再验证的工具吗但最常用的是If-Modified-Since和If-None-Match首部. 将这个首部添加到GET请求中去, 就可以告诉服务器, 只有在缓存了对象的副本之后, 又对其进行了修改的情况下, 才发送此对象.

iOS NSURLRequestCachePolicy

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,

    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

设置缓存策略会对应的添加请求首部Cache-Control等到URLRequest中.

缓存的处理步骤

  1. 接收: 缓存从网络中读取抵达的请求报文.

  2. 解析: 缓存对报文进行解析, 提取出URL和各种首部.

  3. 查询: 缓存查看是否有本地副本可用, 如果没有, 就获取一份副本 (并将其保存在本地).

  4. 新鲜度检测: 缓存查看已缓存副本是否足够新鲜, 如果不是, 就询问服务器是否有任何更新.

  5. 创建响应: 缓存会用新的首部和一缓存的主体来构建一条响应报文.

  6. 发送: 缓存通过网络将响应发回客户端.

  7. 日志: 缓存可选地创建一个日志文件条目来描述这个事务.

Cookie

可以笼统的将cookie分为两类: 会话cookie和持久cookie. 会话cookie是一种临时cookie, 它记录了用户访问站点时的设置和偏好. 用户退出浏览器时, 会话cookie就被删除了. 持久cookie的生存时间更长一些, 他们存储在硬盘上, 浏览器退出, 计算机重启时他们仍然存在, 通常会用持久cookie维护某个用户会周期性访问的站点的配置文件或登录名.

会话cookie和持久cookie之间唯一的区别就是它们的过期时间, 如果设置了Discard参数, 或者没有设置Expires或Max-Age参数来说明扩展的过期时间, 这个cookie就是一个会话cookie.

iOS HTTPCookie / HTTPCookieStorage

// 阻止应用保存`cookie`.
HTTPCookieStorage.shared.cookieAcceptPolicy = .never
// 从响应中获取`cookie`.
guard let url = URL(string: "http://localhost:3001/api/J1/getJ1List") else {
    return
}
let request = URLRequest(url: url)
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, err) in
    if err != nil {
        print(err.debugDescription)
    } else {
        let responseStr = String(data: data!, encoding: String.Encoding.utf8)
        print(responseStr!)
        print("mimeType: /(String(describing: response?.mimeType))")
    }
    if let response = response as? HTTPURLResponse {
        print("statusCode: /(response.statusCode)")
        1
        // get cookie from response
        let cookies = HTTPCookie.cookies(withResponseHeaderFields: response.allHeaderFields as! [String : String], for: url)
        1
        for cookie in cookies {
            print("Cookie: /(cookie)")
        }
        1
        for (tab, result) in response.allHeaderFields {
            print("/(tab.description) - /(result)")
        }
        if response.statusCode == 200 {
            print(response)
        }
    }
}
dataTask.resume()
// 删除cookie.
func deleteCookie(cookieName:String, url:URL) {
    let jar = HTTPCookieStorage.shared
    guard let storedcookies = jar.cookies(for: url) else {
        return
    }
    for cookie in storedcookies {
        jar.deleteCookie(cookie)
    }
}
// 创建cookie.
guard let url = URL(string: "http://localhost:3001/api/J1/getJ1List") else {
    return
}
let properties = [HTTPCookiePropertyKey.name : "FOO",
                  HTTPCookiePropertyKey.value : "This is foo",
                  HTTPCookiePropertyKey.path : "/",
                  HTTPCookiePropertyKey.originURL : "url"]
guard let cookie = HTTPCookie.init(properties: properties) else {
    return
}

var request = URLRequest(url: url)
var newCookies: [HTTPCookie] = [cookie]
var newHeaders = HTTPCookie.requestHeaderFields(with: newCookies)
request.allHTTPHeaderFields = newHeaders
1
let dataTask = session.dataTask(with: request) { (data, response, err) in {
    ...
    }
}
dataTask.resume()

cookie是可以禁止的, 而且可以通过日志分析或其他方式来实现大部分跟踪记录, 所以cookie自身并不是很大的安全隐患. 实际上, 可以通过提供一个标准的审查方法在远程数据库中保存个人信息, 并将匿名cookie作为键值, 来降低客户端到服务器的敏感数据传输频率.

认证

认证就是要给出一些身份信息, 当出示像护照或驾照那样有照片的身份证件时, 就给出了一些证据, 说明你就是你所声称的那个人, 在自动取款机上输入PIN码, 或在计算机系统的对话框中输入了密码时, 也是在证明你就是你所声称的那个人.

HTTP提供了一个原生的质询 / 响应(challenge / response)框架, 简化了对用户的认证过程.

iOS URLProtectionSpace

_ =  URLProtectionSpace(host: "localhost",
                        port: 3001, protocol: NSURLProtectionSpaceHTTP,
                        realm: "moblie",
                        authenticationMethod: NSURLAuthenticationMethodDefault)

最佳实践是使用URLProtectionSpace验证手机银行应用的用户与安全的银行服务器进行通信, 特别是在发出的请求会操纵后端数据时更是如此. URLProtectionSpace是要认证的服务器或域, 是多有进来的URLAuthenticationChallenges的一个属性.

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    
    let defaultSpace = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLProtectionSpaceHTTP, realm: "mobile", authenticationMethod: NSURLAuthenticationMethodDefault)
    
    let trustSpace = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLAuthenticationMethodDefault, realm: "mobile", authenticationMethod: NSURLAuthenticationMethodClientCertificate)
    
    let validSpaces = [defaultSpace, trustSpace]
    if !validSpaces.contains(challenge.protectionSpace) {
        let msg = "We're unable to establish a secure connection. Please check your network connection and try again"
        DispatchQueue.main.async {
            let alert = UIAlertController(title: "Unsecure Connection", message: msg, preferredStyle: .alert)
            alert .addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            self.present(alert, animated: true, completion: nil)
        }
        challenge.sender?.cancel(challenge)
    }
}

上述代码片段添加了额外的保护空间, 这位后端提供了一些灵活性. 当确定要支持的保护控件后, 请创建它们, 然后将它们添加到数组中以便与进来的认证挑战相比较. 实际上, 你应该定义有效的保护控件作为模型层的一部分, 这样就可以在所有网络中重用它们了, 如果认证挑战的保护控件与所有支持的空间不匹配, 那么你应该通知用户取消认证质询.

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
        
            if challenge.previousFailureCount == 0 {
                let creds = URLCredential(user: "Castie!", password: "******", persistence: URLCredential.Persistence.forSession)
                challenge.sender?.use(creds, for: challenge)
            } else {
                challenge.sender?.cancel(challenge)
                DispatchQueue.main.async {
                    let alert = UIAlertController(title: "Unsecure Connection", message: msg, preferredStyle: .alert)
                    alert .addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                    self.present(alert, animated: true, completion: nil)
                }
            }
        }
    }

在确定质询是针对HTTP Basic或另一种支持的质询类型后, 应该确保没有失败, 并使用用户输入的用户名与密码创建URLCredential对象. 如果质询失败, 那就警告用户并取消质询.

URLCredentialStorage.shared.set(creds, for: protectionSpace)

使用URLCredentialStorage可以处理证书数据的认证响应.

优化

iOS 用户都希望应用能够立刻响应每个请求, 移动产业都有这样一条原则, 即屏幕越小, 用户越没耐心. 提供让用户乐于使用的应用意味着要珍惜用户的时间, 就像珍惜你自己的时间一样. 通过压缩请求和响应来优化应用所使用的带宽, 通过管道化请求避免不必要的延迟, 甚至通过缓存响应来避免冗余的网络请求都会加速应用并改进用户体验.

压缩请求和响应

在默认情况下, URLSession 会为每个请求添加Accept-Encoding: gzip, deflate 告知服务器, 客户端可以接收使用gzip或DEFLATE压缩的负载, 不过服务器可以自己选择是否压缩响应. 这样, 通过响应负载压缩来提升性能的关键在于配置服务器以支持压缩.

如果想要禁用负载压缩, 应用可以通过清除自动设定的Accept-Encoding首部来实现.

var request = URLRequest(url: URL(string: "http://localhost:3000")!, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 20)
request.addValue("", forHTTPHeaderField: "Accept-Encoding")

HTTP管道

HTTP管道是重用现有TCP连接的一种方式, 它使得HTTP客户端能够在对第一个请求的响应返回前在相同的TCP Socket上发送第二个请求, 响应返回的顺序与请求发起的顺序保持一致.

var request = URLRequest(url: URL(string: "http://localhost:3000")!)
request.httpShouldUsePipelining = true

重定义缓存

除了上述提到的缓存策略, iOS还提供了重新定义默认的缓存, 并指定了更大的内存容量和持久化储存, 以便在应用重启后依然可以使用.

let cache = URLCache(memoryCapacity: 1024 * 1024, diskCapacity: 1024 * 1024 * 20, diskPath: "URLCache")
URLCache.shared = cache

最后

其实HTTP是一个很复杂的工程, 包括很多的首部定义及各种代理网关, DNS及均衡负载等知识, 不过这些一般都是由服务端进行完成, 不过好在现在BAAS这种平台的出现极大的便利了我们这些前端开发者, 但基础的知识还是需要了解的, 你说呢?

作者:Castie1

链接:http://www.jianshu.com/p/f1aadc9f5dc3

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

PS:如果您想和业内技术大牛交流的话,请加qq群(527933790)或者关注微信公众 号(AskHarries),谢谢!

转载请注明原文出处:Harries Blog™ » iOS 通俗易懂的HTTP网络

赞 (0)

分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址