代码示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

本系列文章中,我们将尝试使用 NSURLSession 技术构建一个自己的网络请求库。

NSURLSession 简介

NSURLSession 是 iOS7 引入的新网络请求接口,在 WWDC2013 中有详细介绍,下面是描述其结构的一页 slides:

当应用在前台时,NSURLSession 跟 NSURLConnection 没有什么区别,但是在程序切换到后台之后 Background Session 就会更加灵活。

尝试 NSURLSession

准备工作

新建一个名为 BuildYourHTTPRequestLibrary 的单页面应用,在页面上居中放置一个按钮,名为 Request:

拖动绑定 Touch Up Inside 事件:

使用 NSURLSession

在 mainButtonBeTapped 函数内填充以下代码:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!)
  4. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  5. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  6. println(string)
  7. })
  8. task.resume()
  9. }

使用成功!

感受异步

异步

改写 mainButtonBeTapped 函数的代码:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!)
  4. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  5. println("just wait for 5 seconds!")
  6. sleep(5)
  7. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  8. println(string)
  9. })
  10. task.resume()
  11. }

再次尝试,两次打印之间间隔了五秒,主线程未阻塞,证明 NSURLSession 为异步执行。

阻塞

尝试多次点击,我们能够看到每五秒执行一次,直到全部执行完毕。

NSURLSession 采用的是 “异步阻塞” 模型,即所有请求在发出后都进入 2# 线程执行,在 2# 线程内部按照阻塞队列模式执行。

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

本章中,我们将一起尝试使用一个类来封装我们之前的代码,并尝试加入动态增加 HTTP 参数(params)的功能,之后封装出一个强大的接口。

基本封装

基础准备

新建一个 Swift 空文件,命名为 Network.swift,在里面写一个 Network 类,之后写一个静态方法 request():

  1. class Network{
  2. static func request() {
  3. let session = NSURLSession.sharedSession()
  4. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!)
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  6. println("just wait for 5 seconds!")
  7. sleep(5)
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  9. println(string)
  10. })
  11. task.resume()
  12. }
  13. }

修改 ViewController 中的按钮函数:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request()
  3. }

运行项目,点击按钮,效果和之前一致。

自定义 HTTP method 和 URL

修改 request() 方法,将 HTTP 方法和 URL 传进去:

  1. static func request(method: String, url: String) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSMutableURLRequest(URL: NSURL(string: url)!)
  4. request.HTTPMethod = method
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  6. println("just wait for 5 seconds!")
  7. sleep(5)
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  9. println(string)
  10. })
  11. task.resume()
  12. }

修改前面的函数调用:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request("GET", url: "http://baidu.com")
  3. }

运行项目,点击按钮,效果和之前一致。

使用闭包处理请求结果

函数是 Swift 中的一等公民,闭包可以作为函数参数和返回值,十分强大。下面我们就用闭包来处理网络请求的返回值。修改 request() 方法,传递进去一个闭包:

  1. static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSMutableURLRequest(URL: NSURL(string: url)!)
  4. request.HTTPMethod = method
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  6. callback(data: data, response: response , error: error)
  7. })
  8. task.resume()
  9. }

在前面函数调用处使用闭包进行结果处理:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request("GET", url: "http://baidu.com") { (data, response, error) -> Void in
  3. println("just wait for 5 seconds!")
  4. sleep(5)
  5. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  6. println(string)
  7. }
  8. }

运行项目,点击按钮,效果和之前一致。

动态增加 Params

GET 方法

GET 方法下,params 在经过 url encode 之后直接附在 URL 末尾发送给服务器。修改 request() 方法,传递进去一个 params 的字典:

  1. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. ... ...
  3. }

为了处理 params,我们从 Alamofire 偷来他的 params 处理函数。如果是 GET 方法,那就把处理过的 params 增加到 URL 后面。Network 类的完整代码如下:

  1. class Network{
  2. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  3. let session = NSURLSession.sharedSession()
  4. var newURL = url
  5. if method == "GET" {
  6. newURL += "?" + Network().buildParams(params)
  7. }
  8. let request = NSMutableURLRequest(URL: NSURL(string: newURL)!)
  9. request.HTTPMethod = method
  10. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  11. callback(data: data, response: response , error: error)
  12. })
  13. task.resume()
  14. }
  15. // 从 Alamofire 偷了三个函数
  16. func buildParams(parameters: [String: AnyObject]) -> String {
  17. var components: [(String, String)] = []
  18. for key in sorted(Array(parameters.keys), [(String, String)] {
  19. var components: [(String, String)] = []
  20. if let dictionary = value as? [String: AnyObject] {
  21. for (nestedKey, value) in dictionary {
  22. components += queryComponents("\(key)[\(nestedKey)]", value)
  23. }
  24. } else if let array = value as? [AnyObject] {
  25. for value in array {
  26. components += queryComponents("\(key)", value)
  27. }
  28. } else {
  29. components.extend([(escape(key), escape("\(value)"))])
  30. }
  31. return components
  32. }
  33. func escape(string: String) -> String {
  34. let legalURLCharactersToBeEscaped: CFStringRef = ":&=;+!@#$()',*"
  35. return CFURLCreateStringByAddingPercentEscapes(nil, string, nil, legalURLCharactersToBeEscaped, CFStringBuiltInEncodings.UTF8.rawValue) as String
  36. }
  37. }

修改前面的函数调用:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request("GET", url: "http://pitayaswift.sinaapp.com/pitaya.php", params: ["get": "Network"]) { (data, response, error) -> Void in
  3. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  4. println(string)
  5. }
  6. }

http://pitayaswift.sinaapp.com/pitaya.php 是我部署的用于测试的服务端代码,会直接返回 ?get=ooxx 中的 ooxx。运行项目,点击按钮,查看效果:

POST 方法

POST 方法下有几个协议可供选择,此处没有文件上传,我们采用较简单的 application/x-www-form-urlencoded 方式发送请求。request() 方法增加一些代码:

  1. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let session = NSURLSession.sharedSession()
  3. var newURL = url
  4. if method == "GET" {
  5. newURL += "?" + Network().buildParams(params)
  6. }
  7. let request = NSMutableURLRequest(URL: NSURL(string: newURL)!)
  8. request.HTTPMethod = method
  9. if method == "POST" {
  10. request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
  11. request.HTTPBody = Network().buildParams(params).dataUsingEncoding(NSUTF8StringEncoding)
  12. }
  13. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  14. callback(data: data, response: response , error: error)
  15. })
  16. task.resume()
  17. }

修改前面的函数调用:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request("POST", url: "http://pitayaswift.sinaapp.com/pitaya.php", params: ["post": "Network"]) { (data, response, error) -> Void in
  3. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  4. println(string)
  5. }
  6. }

使用 POST 方式发送请求,同样服务端会返回 key 为 post 的 value 的值。运行项目,点击按钮,结果和前面 GET 方法的结果一致。

至此,接口封装完成!

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

本文中,我们将一起降低之前代码的耦合度,并使用适配器模式实现一层独立于底层结构的网络 API,造一个真正的网络请求“库”。

降低耦合度

如何降低耦合度

现在的清汤挂面式的代码虽然便于理解,但是功能单一,代码杂乱。我们一起来分析 NSURLSession 的使用过程:

构造 NSURLRequest

确定 URL

确定 HTTP 方法(GET、POST 等)

添加特定的 HTTP 头

填充 HTTP Body

驱动 session.dataTaskWithRequest 方法,开始请求

具体实施

在 Network 下另外新建一个 NetworkManager 类,将 URL、params、files 等设为成员变量,让他们在构造函数中初始化:

  1. class NetworkManager {
  2. let method: String!
  3. let params: Dictionary let callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void
  4. let session = NSURLSession.sharedSession()
  5. let url: String!
  6. var request: NSMutableURLRequest!
  7. var task: NSURLSessionTask!
  8. init(url: String, method: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  9. self.url = url
  10. self.request = NSMutableURLRequest(URL: NSURL(string: url)!)
  11. self.method = method
  12. self.params = params
  13. self.callback = callback
  14. }
  15. }

之后,将上面分析的

1. 确定 URL

2. 确定 HTTP 方法(GET、POST 等)

3. 添加特定的 HTTP 头

4. 填充 HTTP Body

前三步封装到一个 function 中,最后一步封装到一个 function 中,然后把驱动 session.dataTaskWithRequest 的代码封装到一个 function 中:

  1. func buildRequest() {
  2. if self.method == "GET" && self.params.count > 0 {
  3. self.request = NSMutableURLRequest(URL: NSURL(string: url + "?" + buildParams(self.params))!)
  4. }
  5. request.HTTPMethod = self.method
  6. if self.params.count > 0 {
  7. request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
  8. }
  9. }
  10. func buildBody() {
  11. if self.params.count > 0 && self.method != "GET" {
  12. request.HTTPBody = buildParams(self.params).nsdata
  13. }
  14. }
  15. func fireTask() {
  16. task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  17. self.callback(data: data, response: response, error: error)
  18. })
  19. task.resume()
  20. }

之后使用一个统一的方法来驱动上面三个 function,完成请求:

  1. func fire() {
  2. buildRequest()
  3. buildBody()
  4. fireTask()
  5. }

同时,不要忘了那三个 parse params 的从 Alamofire 偷来的函数哦,也要放到这个类里面。至此,降低耦合的工作基本完成,接下来我们开始封装“网络API”。

使用适配器模式封装“网络API”

理解适配器模式

适配器模式是设计模式中的一种,很容易理解:我的 APP 需要一个获取某一个 URL 返回的字符串的功能,我现在选择的是 Alamofire,但是正在发展的 Pitaya 看起来不错,我以后想替换成 Pitaya,所以我封装了一层我自己的网络接口,用来屏蔽底层细节,到时候只需要修改这个类,不需要再深入项目中改那么多接口调用了。

适配器模式听起来高大上,其实这是我们在日常编码中非常常用的设计模式。

Do it!

修改 Network 类的代码为:

  1. class Network{
  2. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  3. let manager = NetworkManager(url: url, method: method, params: params, callback: callback)
  4. manager.fire()
  5. }
  6. }

搞定!

封装多级接口

不带 params 的接口:

  1. static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let manager = NetworkManager(url: url, method: method, callback: callback)
  3. manager.fire()
  4. }

两个 get 接口(带与不带 params):

  1. static func get(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let manager = NetworkManager(url: url, method: "GET", callback: callback)
  3. manager.fire()
  4. }
  5. static func get(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  6. let manager = NetworkManager(url: url, method: "GET", params: params, callback: callback)
  7. manager.fire()
  8. }

两个 post 接口(带与不带 params):

  1. static func post(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let manager = NetworkManager(url: url, method: "POST", callback: callback)
  3. manager.fire()
  4. }
  5. static func post(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  6. let manager = NetworkManager(url: url, method: "POST", params: params, callback: callback)
  7. manager.fire()
  8. }

测试接口

修改 ViewController 中的调用代码,测试多级 API:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. let url = "http://pitayaswift.sinaapp.com/pitaya.php"
  3. Network.post(url, callback: { (data, response, error) -> Void in
  4. println("POST 1 请求成功")
  5. })
  6. Network.post(url, params: ["post": "POST Network"], callback: { (data, response, error) -> Void in
  7. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
  8. println("POST 2 请求成功 " + string)
  9. })
  10. Network.get(url, callback: { (data, response, error) -> Void in
  11. println("GET 1 请求成功")
  12. })
  13. Network.get(url, params: ["get": "POST Network"], callback: { (data, response, error) -> Void in
  14. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
  15. println("GET 2 请求成功 " + string)
  16. })
  17. Network.request("GET", url: url, params: ["get": "Request Network"]) { (data, response, error) -> Void in
  18. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
  19. println("Request 请求成功 " + string)
  20. }
  21. }

运行项目,点击按钮,查看效果:

多级 API 封装成功!

自己动手写一个iOS 网络请求库的三部曲[转]的更多相关文章

  1. 【转载】一步一步搭建自己的iOS网络请求库

    一步一步搭建自己的iOS网络请求库(一) 大家好,我是LastDay,很久没有写博客了,这周会分享一个的HTTP请求库的编写经验. 简单的介绍 介绍一下,NSURLSession是iOS7中新的网络接 ...

  2. 造轮子 | 怎样设计一个面向协议的 iOS 网络请求库

    近期开源了一个面向协议设计的网络请求库 MBNetwork,基于 Alamofire 和 ObjectMapper 实现,目的是简化业务层的网络请求操作. 须要干些啥 对于大部分 App 而言,业务层 ...

  3. LXNetwork – 基于AF3.0封装的iOS网络请求库

    本框架实现思路与YTKNetwork和RTNetworking类似,相当于一个简单版,把每一个网络请求封装成对象.使用LXNetwork,你的每一个请求都需要继承LXBaseRequest类,通过覆盖 ...

  4. Retrofit网络请求库应用02——json解析

    PS:上一篇写了Retrofit网络请求库的简单使用,仅仅是获取百度的源码,来证明连接成功,这篇讲解如何解析JSON数据,该框架不再是我们之前自己写的那样用JsonArray等来解析,这些东西,我们都 ...

  5. 浅论Android网络请求库——android-async-http

    在iOS开发中有大名鼎鼎的ASIHttpRequest库,用来处理网络请求操作,今天要介绍的是一个在Android上同样强大的网络请求库android-async-http,目前非常火的应用Insta ...

  6. swift中第三方网络请求库Alamofire的安装与使用

    swift中第三方网络请求库Alamofire的安装与使用 Alamofire是swift中一个比较流行的网络请求库:https://github.com/Alamofire/Alamofire.下面 ...

  7. [转]Android各大网络请求库的比较及实战

    自己学习android也有一段时间了,在实际开发中,频繁的接触网络请求,而网络请求的方式很多,最常见的那么几个也就那么几个.本篇文章对常见的网络请求库进行一个总结. HttpUrlConnection ...

  8. Android之网络请求库

    自己学习android也有一段时间了,在实际开发中,频繁的接触网络请求,而网络请求的方式很多,最常见的那么几个也就那么几个.本篇文章对常见的网络请求库进行一个总结. HttpUrlConnection ...

  9. Android进阶笔记02:Android 网络请求库的比较及实战(二)

    一.Volley        既然在android2.2之后不建议使用HttpClient,那么有没有一个库是android2.2及以下版本使用HttpClient,而android2.3及以上版本 ...

随机推荐

  1. git在myelispse中的安装

    1.git在myelispse中的安装 http://blog.csdn.net/chinaonlyqiu/article/details/8830050

  2. Trailing return types

    Trailing return types是C++11关于函数声明的语言特性之一,旨在解决模版编程遇到的语法相关的问题,先看一个简单例子,感受一下什么是trailing return types: C ...

  3. OA请假流程 -- 编码

    OA请假流程 -- 编码 凡是内容不会发生变化的,都要写在xml配置文件中.需要定义如下内容: <process>标签 id英文命名 和 name 中文命名,然后只要与该流程相关的资源均以 ...

  4. HDU 5925 Coconuts 【离散化+BFS】 (2016CCPC东北地区大学生程序设计竞赛)

    Coconuts Time Limit: 9000/4500 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Su ...

  5. 游戏开发设计模式之子类沙盒模式(unity3d 示例实现)

    积累提供所有操作(的实现)来定义子类的行为用一个最简单的例子来讲解这个模式玩家操纵的英雄也就是这个游戏的主角会有许多技能,我们想定义许多不同的技能,来让玩家使用.首 先我们定义一个skillBase类 ...

  6. Difference Between Primes

    Problem Description All you know Goldbach conjecture.That is to say, Every even integer greater than ...

  7. lost connection to mysql server reading initial communication packet

  8. nyoj 95 众数问题【水】

    众数问题 时间限制:3000 ms  |  内存限制:65535 KB 难度:3   描述 所谓众数,就是对于给定的含有N个元素的多重集合,每个元素在S中出现次数最多的成为该元素的重数, 多重集合S重 ...

  9. 323. Number of Connected Components in an Undirected Graph

    算连接的..那就是union find了 public class Solution { public int countComponents(int n, int[][] edges) { if(e ...

  10. codeforces 710C

    C. Magic Odd Square time limit per test 1 second memory limit per test 256 megabytes input standard ...