美文网首页iOS常用
iOS原生网络请求复盘及封装

iOS原生网络请求复盘及封装

作者: 正直的瓜子脸 | 来源:发表于2021-01-27 18:46 被阅读0次

    在平时的项目开发中,对于网络请求,也许你基本都是用AFNetworking或Alamofire,你使用他们提供的现成的封装好的方法,然后就用的如此心安理得,以至于你再也无法理解这些请求是怎么实现的,这对于弄清楚网络请求的原理是很不利的。第三方网络请求库再怎么好用,你用的再多,也无法让你真正熟悉网络库。因为不熟悉原理,也就无法自己定制最契合项目的网络请求类。
    例如: 请求一条视频资源或是图片资源时,要求添加防盗链才能访问,你却不知道往哪里加,加到哪个字段。
    例如: POST请求的参数格式,有默认格式和json格式,如果你不了解一下,就知道到参数传入一个字典,怎么拼接的却不知道


    下面我们以一个简单的登录请求来梳理一下发起一个网络请求都有哪些步骤

    请求地址: "http://192.168.1.53:8080/user/login"
    请求参数: ["id": 666, "name": "xiaoming"]

    1. 创建request对象,拼接好参数,并放到正确的位置
    let urlstr = "http://192.168.1.53:8080/user/login"
    var request: URLRequest
    

    GET请求:

    //GET请求的参数直接拼接到url后即可
    let finalstr = urlstr + "?id=666&name=xiaoming"
    let url = URL(string: finalstr)!
    request = URLRequest(url: url)
    

    POST请求:

    let url = URL(string: urlstr)!
    request = URLRequest(url: url)
    //POST请求的参数单独放到请求体内
    if paramStyle == .defualt {
        //1) 正规携带参数的POST请求(这个可以参考Alomofire封装)
        let str = "id=666&name=xiaoming"
        let data = str.data(using: .utf8)
        request.httpBody = data
    }else {
        //2)以json形式携带参数的POST请求
        let param: [String: Any] = ["id": 666, "name": "xiaoming"]
        let data = try? JSONSerialization.data(withJSONObject: param, options: .prettyPrinted)
        request.httpBody = data
    }
    
    2. 设置request一些配置
    //请求方式(GET、POST等)
    request.httpMethod = httpMethod.rawValue
    //缓存策略
    request.cachePolicy = .useProtocolCachePolicy
    //请求超时时间(秒)
    request.timeoutInterval = 30
    //设置请求头,或添加一个请求头字段
    var headers: [String: String] = [:]
    headers["Content-Type"] = "application/json"
    headers["custom"] = "123"
    request.allHTTPHeaderFields = headers    //[String : String]?
    //或写成
    //request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    //request.setValue("123", forHTTPHeaderField: "custom")
    

    或使用URLSessionConfiguration来设置

    //2. 设置request一些配置
    let config = URLSessionConfiguration.default
    //缓存策略
    config.requestCachePolicy = .useProtocolCachePolicy
    //请求超时时间(秒)
    config.timeoutIntervalForRequest = 30
    //设置请求头
    var headers: [String: String] = [:]
    headers["Content-Type"] = "application/json";
    config.httpAdditionalHeaders = headers    //[String : String]?
    //主机的最大连接数
    config.httpMaximumConnectionsPerHost = 10
    
    3. 开始任务
    //3. 开始任务
    let session = URLSession.shared
    let dataTask = session.dataTask(with: request) { (data, urlResponse, error) in
        print("请求成功,data: \(data), response: \(urlResponse)")
    }
    //使用resume方法启动任务。开始或继续
    dataTask.resume()
    

    //3. 开始任务
    let session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    let dataTask = session.dataTask(with: request) { (data, urlResponse, error) in
        print("data: \(data), response: \(urlResponse)")
    }
    //使用resume方法启动任务
    dataTask.resume()
    

    附: HTTPMethod和ParamStyle的申明如下:

    ///HTTP请求方式
    enum HTTPMethod: String {
        case GET = "GET"
        case POST = "POST"
        case PUT = "PUT"
        case HEAD = "HEAD"
        case DELETE = "DELETE"
        case CONNECT  = "CONNECT"
        case TRACE = "TRACE"
        case OPTIONS = "OPTIONS"
    }
    
    ///Post请求时的参数格式
    enum ParamStyle {
        ///参数以默认方式放到body里
        case defualt
        ///参数以json形式放到body里
        case json
    }
    

    发起一个网络请求起始挺繁琐的,所以我们需要对网络请求做一个良好的封装

    iOS一个网络请求大概需要如下模块:
    URLSessionConfiguration, URLSessionURLRequestURLSessionTask

    1. 封装一个网络请求单例
    class NetworkingManager: NSObject {
        
        ///返回原始数据
        typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
        ///返回指定类型的数据(经过解析之后返回指定类型的模型对象)
        typealias ModelCompletionHandler<T> = (_ model: T?, _ error: Error?) -> Void
        
        ///单例
        static let shared = NetworkingManager()
        ///请求的host地址
        let host = "http://192.168.1.53:8080"
        
        ///session的一些配置
        var sessionConfig: URLSessionConfiguration
        ///会话
        var session: URLSession
        
        
        
        
        override private init() {
            sessionConfig = URLSessionConfiguration.default
            //缓存策略
            sessionConfig.requestCachePolicy = .useProtocolCachePolicy
            //请求超时时间(秒)
            sessionConfig.timeoutIntervalForRequest = 30
            //设置请求头
            var headers: [String: String] = [:]
            headers["Content-Type"] = "application/json"
            sessionConfig.httpAdditionalHeaders = headers
            
            session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
            
            super.init()
        }
        
        
        
        
        
        ///返回遵循RFC 3986的查询字符串键或值的百分比转义字符串。
        private func escape(string: String) -> 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)")
    
            let escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
            return escaped
        }
        
        
        ///把传入的key和value组合成一个元组数组返回
        private func queryComponents(key: String, value: Any) -> [(String ,String)] {
            var element: (String, String)
            if let bool = value as? Bool {
                let boolStr = bool ? "true" : "false"
                //let str = bool ? "1" : "0"
                element = (escape(string: key), escape(string: boolStr))
            }
            else if let str = value as? String {
                element = (escape(string: key), escape(string: str))
            }else {
                element = (escape(string: key), escape(string: "\(value)"))
            }
            return [element]
        }
        
        
        /*
         把传入的字典格式化为字符串
         输入: ["id": 666, "name": "xiaoming"]
         输出: "id=666&name=xiaoming"
         */
        private func queryParamString(params: [String: Any]?) -> String? {
            guard let params = params else { return nil }
            var components: [(String, String)] = []
            //把字典变换为远组数组,方便格式化
            for key in params.keys.sorted(by: <) {
                let value = params[key]!
                components += queryComponents(key: key, value: value)
            }
            let str = components.map({ "\($0)=\($1)" }).joined(separator: "&")
            return str
        }
        
        
        
        
        /// 创建一个request对象
        /// - Parameters:
        ///   - httpMethod: 请求方式
        ///   - paramStyle: 参数格式
        ///   - urlStr: 请求地址 host+path
        ///   - params: 参数
        private func request(httpMethod: HTTPMethod, paramStyle: ParamStyle, urlStr: String, params: [String: Any]?) -> URLRequest {
            let url = URL(string: urlStr)!
            var request: URLRequest = URLRequest(url: url)
            //1. 创建request对象,拼接好参数,并放到正确的位置
            switch httpMethod {
            case .GET:
                //GET请求的参数直接拼接到url后即可
                if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) {
                    //一开始就包含在url中的参数
                    let paramStrOriginal = urlComponents.percentEncodedQuery.map({ $0 + "&" }) ?? ""
                    /*或写成
                    var paramStrOriginal = ""
                    if let s = urlComponents.percentEncodedQuery, !s.isEmpty {
                        let paramStrOriginal = s + "&"
                    }*/
                    
                    //请求时传入的参数
                    let paramStr = queryParamString(params: params) ?? "" //"id=666&name=xiaoming"
                    
                    let percentEncodedQuery = paramStrOriginal + paramStr
                    urlComponents.percentEncodedQuery = percentEncodedQuery
                    
                    //拼接了参数后的最终url
                    request.url = urlComponents.url
                }
            case .POST:
                //POST请求的参数单独放到请求体内
                if paramStyle == .defualt {
                    //1) 正规携带参数的POST请求
                    let paramStr = queryParamString(params: params) //"id=666&name=xiaoming"
                    let data = paramStr?.data(using: .utf8, allowLossyConversion: false)
                    request.httpBody = data
                }else {
                    //2)以json形式携带参数的POST请求
                    var data: Data?
                    if let params = params {
                        data = try? JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
                    }
                    request.httpBody = data
                }
            default:
                break
            }
            return request
        }
        
        
        ///发起一个网络请求
        @discardableResult
        func http(httpMethod: HTTPMethod, paramStyle: ParamStyle = .defualt, path: String, params: [String: Any]?, completionHandler: @escaping CompletionHandler) -> URLSessionDataTask {
            //请求地址: host + path
            let urlStr = host + path  //"http://192.168.1.53:8080/user/login"
            //创建一个request
            let request = self.request(httpMethod: httpMethod, paramStyle: paramStyle, urlStr: urlStr, params: params)
            //开始任务
            let dataTask = session.dataTask(with: request) { (data, urlResponse, error) in
                print("data: \(String(describing: data)), response: \(String(describing: urlResponse))")
                completionHandler(data, urlResponse, error)
            }
            //使用resume方法启动任务。开始或继续
            dataTask.resume()
            return dataTask
        }
        
        
        ///取消所有网络请求
        func cancelAllTask() {
            session.getAllTasks { (tasks) in
                tasks.forEach { (task) in
                    /*某些特殊请求不执行取消
                    let url = task.currentRequest?.url
                    if url?.absoluteString.contains("/user/login") == false {
                        task.cancel()
                    }*/
                    task.cancel()
                }
            }
        }
        
        
    }
    
    2. 调用封装好的请求,执行登录操作
    func login(id: Int, name: String) {
        let loginPath = "/user/login"
        let params: [String: Any] = ["id": 666, "name": "xiaoming"]
            
        NetworkingManager.shared.http(httpMethod: .POST, paramStyle: .defualt, path: loginPath, params: params) { (data, urlResponse, error) in
          //登录成功一般都会返回用户信息,这里把response解析成User对象返回就行了,有错误就处理错误,没错误就走登录成功的逻辑就行。这里就不赘述数据解析和业务逻辑的东西了。
        }
    }
    

    至此,一个网络请求的封装和调用的完整流程以基本OK。


    研究了一下Alomofire的源码,它的封装思路大体如下:

    1. 定义一个SessionManager对象,里面包含了session及sessionDelegate,以及一些默认配置。SessionManager还提供创建request的方法。此方法的功能有:
      1)、 将参数格式化(会处理一些不合规的字符),拼接到url上或是放到body里,然后返回一个URLRequest。
      2)、 SessionManager再把这个URLRequest包装成自定义的DataRequest(继承于自定义的Request),这里会默认调用.resume方法,并把结果在Request中保存下来,等调用解析回调(如.responseJSON)的时候,再以特定的解析方式解析好response,然后回调出去。

    2. 定义一个DataRequest,并对response提供了不同的解析方法(.responseJSON,.responseString,.responseData)

    相关文章

      网友评论

        本文标题:iOS原生网络请求复盘及封装

        本文链接:https://www.haomeiwen.com/subject/cwxizktx.html