macOS 开发之实现 HTTP 的 GET 和 POST 请求

在 macOS 开发过程中会经常用到外部服务,通常是通过 HTTP 的 GET 和 POST 请求调用它们的 API 获得目标数据,本篇文章就说一下如何在 macOS 开发中实现 GET 和 POST 请求。

平台

因为 swift 语言在发展期,新版本可能会有写变化,但是使用方法应该不会大变,为了加以注意,这里列出写本文时作者测试代码的平台情况。

  • macOS 10.14.3
  • xcode 10.1
  • swift 4.2.1

网络请求

本文要讲的是使用 swift 的 urlSession 发送 GET 和 POST 请求。先讲一下一般的使用方法,为了更清楚一点,这里先直接粘上代码。

GET 请求

先上代码:

let session = URLSession(configuration: .default)
let url = "http://127.0.0.1/api/"
var request = URLRequest(url: URL(string: url)!)
let task = session.dataTask(with: request) {(data, response, error) in
    do {
        let r = try JSONSerialization.jsonObject(with: data!, options: []) as! NSDictionary
        print(r)
    } catch {
        print("无法连接到服务器")
        return
    }
}
task.resume()
  1. 创建一个网络会话
  2. 创建网络请求体,默认就是 GET 请求,所以直接使用 url 创建请求即可
  3. 利用会话创建一个数据任务,在任务创建中实现相应处理,代码中是将收到的响应数据转换为字典的处理:
    • data 是请求成功后收到的响应数据
    • response 是响应体内容
    • error 是网络请求中的错误信息
  4. 最后执行创建的数据任务

其中 JSONSerialization.jsonObject 是解析 json 字符串为字典数据的方法。

URLSession 具有单例 shared ,所以这里可以使用单例简化操作:

let url = URL(string: "http://127.0.0.1/api/")
URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data else {
        print(error?.localizedDescription ?? "Unkown Error!")
        return
    }
    print(String(data: data, encoding: .utf8))
}.resume()

下面的所有请求均可以使用 URLSession 的单例 shared 实现,下面就不一一赘述!

POST 请求

基本步骤类似于 GET 请求,只是在请求体上的配置有所不同,基本使用代码如下:

let session = URLSession(configuration: .default)
let url = "http://127.0.0.1/api/"
var request = URLRequest(url: URL(string: url)!)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let postData = ["username":"xxx","password":"123456"]
let postString = postData.compactMap({ (key, value) -> String in
    return "\(key)=\(value)"
}).joined(separator: "&")
request.httpBody = postString.data(using: .utf8)
let task = session.dataTask(with: request) {(data, response, error) in
    do {
        let r = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
        print(r)
    } catch {
        print("无法连接到服务器")
        return
    }
}
task.resume()

注意:

  1. 网络请求中使用 setValue 进行配置请求头信息
  2. 手动配置请求方法为 POST
  3. 上面的 POST 请求数据使用的类型是 application/x-www-form-urlencoded,所以要对请求数据进行特殊字符加 % 进行编码。

问题

在使用上面的操作过程中,因为每个人不同的需求可能会遇到不同的问题,这里列出可能会遇到的问题。

无法连接到网络(沙盒阻止)

在使用上述方法发送请求时,遇到了错误:

dns A server with the specified hostname could not be found

造成此问题的原因是受沙盒限制导致,两种解决方法:

  1. 点击项目左侧导航栏中的项目名称,右侧顶部标签选择 Capabilities 找到第一项 NetWork 勾选 Incoming Connections (Server)Outgoing Connections (Client),主要是 Outgoing Connections (Client)
  2. 关闭 App SandBox

建议使用第一种方法。

HTTP 屏蔽问题

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app’s Info.plist file.

这个问题是安全机制导致的,如果请求的 API 链接是 HTTP 的而不是 HTTPS 的可能会出现这个问题,解决方法是,在 Info.plist 中添加 App Transport Security Settings,在其下再添加一个 Allow Arbitrary Loads 值设置为 Yes 即可,如下图:

url 编码问题

进行请求操作中严格按照了上面的步骤操作,可是收到的是错误响应,最后查明原来是 url 编码问题,十里之前在开发一款图片 OCR 的 app 的时候,需要在 POST 数据中包含图片 BASE64 数据。但其中包含了特殊字符,这些字符需要进行 % 转码,要符合 RFC 3986 标准,这里给出十里的解决方法,是对 String 进行扩展两个属性,字符串这两个属性分别可以完成 url 编码和解码:

extension String {
    //将原始的url编码为合法的url
    var urlEncoded: String? {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+,;="
        var allowedCharacterSet = CharacterSet.urlQueryAllowed
        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
        return addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)
    }
    
    //将编码后的url转换回原始的url
    var urlDecoded: String? {
        return removingPercentEncoding
    }
}

上面的扩展中使用了现有的 addingPercentEncoding 方法,但是现有的字符集不能完全去除特殊字符,所以作者这里使用了自己创建的字符集 allowedCharacterSet

界面刷新问题

通过请求获取了数据以后,通常需要更新一下界面中的显示结果,此时就会出现问题!上面的接口采用异步的方式,就是为了防止请求时间过长导致界面卡死,请求是在新的线程进行,但是界面是在主线程处理,所以一旦在请求完直接处理界面的话,就会导致 App 崩溃,所以这里建议使用委托以及在委托线程中修改UI,方法如下(这里以简单的 GET 请求为例,POST 请求类似操作):

  • 封装一个请求的方法,其中一个参数是请求完成后处理事务的回调函数:
func sendGetRequest(url: String, completionHandler: @escaping ((Data?,URLResponse?,Error?)->Void)) {
    let session = URLSession(configuration: .default)
    let task = session.dataTask(with: URL(string: url)!, completionHandler: completionHandler)
    task.resume()
}
  • 定义刚才所说的回调函数,包含参数 data, response 和 error,这个回调可以根据不同 API 功能定义多个,以相应功能命名,比如是获取天气:
func getWeatherSuccess(data: Data?, response: URLResponse?, error: Error?) -> Void {
    DispatchQueue.main.async {
        // 这里完成 UI 相关操作,调用 UI 对象时要加上 self
        // 比如: self.weatherLabel = "..."
    }
} 
  • 最后在要请求数据的地方调用第一步定义的通用方法即可,只需指定不同的处理回调就可以完成多 API 的不同请求:
sendGetRequest(url: "http://127.0.0.1/api", getWeatherSuccess(data:response:error:))

天气实例

下面以一个获取天气的实例,讲解一下 HTTP 的 GET 请求方式,POST 方式按照上面讲的方式基本差不多少,无非是加了请求数据,这里就先不讲 POST 请求实例了。

我们要做的实例很简单,窗口中有一个 push button 和 一个 label,当点击 button 的时候就会触发请求获取天气数据,并将温度显示在标签上,不作复杂的处理讲明白方法即可。

天气 API

这里使用免费天气API,天气JSON API,不限次数获取十五天的天气预报提供的天气API,免费使用,可免费使用,不过免费版本数据固定 8 个小时更新一次,这倒无所谓了,我们只是用来学习实现 GET 请求而已。

GET 请求格式

示例请求链接:http://t.weather.sojson.com/api/weather/city/101030100 。

不用再带任何参数,请求是restfull风格,可以看到链接最后有一串数字,这串数字是城市编码 city_code ,一个 9 位数字,上面的编码代表的是 天津市 。地址是 http://t.weather.sojson.com/api/weather/city/ 和 city_code 的拼接。

城市编码

城市编码可以在 _city.json 中查询,打开文件查找即可,这里我们查询 济南,可以查找到:

{
  "_id": 281,
  "id": 282,
  "pid": 21,
  "city_code": "101120101",
  "city_name": "济南"
}

所以最终 GET 请求链接是:http://t.weather.sojson.com/api/weather/city/101120101

新建 Cocoa APP 工程

为了直接明了这里直接把工程新建和控件添加的过程以视频展示:

添加 GET 请求方法

打开文件 ViewController.swift ,在 ViewController 中添加如下方法:

class func getRequest(url: String, completionHandler: @escaping ((Data?, URLResponse?, Error?)->Void)) {
    let session = URLSession.init(configuration: .default)
    let task = session.dataTask(with: URL(string: url)!, completionHandler: completionHandler)
    task.resume()
}

请求成功后处理回调

定义一个请求成功后的回调函数,同样放到 ViewController 中:

func parseTemperatureForToday(data: Data?, response: URLResponse?, error: Error?) -> Void {
    DispatchQueue.main.async {
        do {
            let r = try JSONSerialization.jsonObject(with: data!, options: [JSONSerialization.ReadingOptions.mutableContainers]) as! NSDictionary
            let todayData = r.value(forKey: "data") as! NSDictionary
            let tempValue = todayData.value(forKey: "wendu") as! String
            self.temperatureLabel.stringValue = tempValue + " °C"
        } catch {
            print("无法连接到服务器")
            return
        }
    }
}

按钮点击处理

按钮点击就发送请求,在 getTemperature 方法中添加请求方法的调用即可完成实例:

@IBAction func getTemperature(_ sender: Any) {
    // 获取济南天气的 GET 请求链接
    let url = "http://t.weather.sojson.com/api/weather/city/101120101"
    ViewController.getRequest(url: url, completionHandler: self.parseTemperatureForToday(data:response:error:))
}

运行示例

编译运行,可能会遇到本文前面提到的HTTP 请求屏蔽无法连接网络(沙盒阻止) 的问题,按照上面的解决方法排出问题即可,最终运行效果如下图:

Demo 下载: GetRequestDemo

参考