从零开始打造一个 Swift 网络框架

▲点击上方“CocoaChina”关注即可免费学习 iOS 开发


说起网络框架,大家第一时间就会想到 AFNetworking、Alamofire 这些业内响当当的作品,有的老鸟也会适当伤感一下曾经用的 ASI 。这些框架都有一个共同点——功能都很复杂,很齐全,而我们往往只能用到很小很小的一个部分。


事实上,咱们做 App 的时候,绝大多数时候对网络的需求都是收发 GET/POST 请求。就这样来看,根据需求来造个属于自己的轮子,似乎也是个不错的选择。尤其是现在苹果提供的 NSURLSession 已经非常强大,基于原生的 SDK 来做一个自己的框架,其实是很容易的。


根据这个思想,我之前撸了一个简单的网络库 AaHTTP,在工作的项目里重度用了一段时间也没有遇到什么特别的问题。


现在我们就来一步步看看如何做一个属于自己的简单的网络框架。


发送请求的步骤分析


要发送一个请求,分为如下步骤:


  1. 如果携带的参数是 GET 类型,则将参数进行 URL encode(转化为 y1=x1&y2=x2的形式),追加到原始 url 的后面。如果参数是 POST 类型,则 URL 不变。

  2. 用最新的 URL 生成一个 NSMutableURLRequest 的对象

  3. 如果参数是 POST 的情况,设置 Content-Type 为 application/x-www-form-urlencoded,并将参数进行 URL encode,并添加到 body 中。

  4. 使用 NSURLSession 发送该请求


URL encode时,需要对特殊字符进行转义。


定义发送请求的接口


根据上面的步骤,我们不难一步到位的实现发送请求,新建一个 AaNet.swift (名字您随意),并声明我们的类方法:


class AaNet: NSObject {
class func request( method : String =  "GET" ,url : String ,form : Dictionary = [:],success : (data : NSData?)->Void,fail:(error : NSError?)->Void){
}
func buildParams(parameters: [String: AnyObject]) -> String {
return  ""
}
}


我们首先声明了两个函数,request 函数接受的参数依次是:


method: 请求类别
url: 目标地址
form: 参数表
success: 成功的回调, 类型为(data:NSData?) -> Void
fail: 失败的回调,类型为(error : NSError?) -> Void


第二个函数 buildParams,输入一个字典,返回一个字符串。很容易想到就是我们用来做 url encode 的函数。


建议大家写代码前,都先写出主要函数的声明和对应的参数、返回值的类型。这其实就是一种最基本的架构工作


实现发送请求


现在按照之前的分析,我们来实现请求发送的逻辑:


class func request( method : String =  "GET" ,fail:(error : NSError?)->Void){
var  innerUrl = url
if  method ==  "GET" {
innerUrl +=  "?"  + AaNet().buildParams(form)
}
let req = NSMutableURLRequest(URL: NSURL(string: innerUrl)!)
req.HTTPMethod = method
if  method ==  "POST"  {
req.addValue( "application/x-www-form-urlencoded" , forHTTPHeaderField:  "Content-Type" )
print( "POST PARAMS (form)" )
req.HTTPBody = AaNet().buildParams(form).dataUsingEncoding(NSUTF8StringEncoding)
}
let session = NSURLSession.sharedSession()
print(req.description)
let task = session.dataTaskWithRequest(req) { (data, response, error) -> Void  in
if  error != nil{
fail(error: error)
print(response)
} else {
if  (response as! NSHTTPURLResponse).statusCode  == 200{
success(data : data)
} else {
fail(error: error)
print(response)
}
}
}
task.resume()
}


整个流程很直观,虽然 GET 参数和 POST 参数处理的位置不同,但都是用我们的 url encode 函数 buildParams 来操作的。区别是 GET 请求的话,处理完后直接 append 到 url 后面,而 POST 需要用 UTF8 encode 一下,放在 request 的 body 里。


然后用 NSURLSession 的默认 session: sharedSession() 来发送请求,并在回调里判断 statusCode 以及 error 对象是否为 nil 来判断请求是否为空,来分别调用我们的 success 回调或 fail 回调。


实现 URL encode


现在我们来实现 buildParams,大体的步骤为:


encode:


1. 把输入字典转换为键值对的数组。[ (Key,Value) ]


2. 对于每一个 (key,value),执行:


2.1 对 key 进行转义,得到 key'


2.2 检查 value 的类型,如果是简单的值,则对其进行转义,得到 value'。并将 (key',value') 输出到结果数组中。


2.3 如果 value 是数组,则用当前的 key 和 value 中的每一个元素组成 tuple: [(key,subValue)],递归执行步骤2。


2.4 如果 value 是字典,也先把 value 对应的字段转化为键值对数组,但是 key 的形式为 key[subKey],前面是 key 是当前的 key,subKey 代表 value 对应的字典中的 key。得到键值对数组后,递归执行步骤2。


3. 步骤2执行完毕后,我们会得到一个一维的、并且 key 和 value 都被转义过的键值对数组 [ (key,value) ],然后我们将其转换为 key1=value1&key2=value2&...keyN=valueN 的形式返回。


仔细感受一下,步骤2是不是有一个 flat 的过程。


我们先实现转义:


func escape(string: String) -> String {
let legalURLCharactersToBeEscaped: CFStringRef =  ":&=;+!@#$()',*"
return  CFURLCreateStringByAddingPercentEscapes(nil, string, nil, legalURLCharactersToBeEscaped, CFStringBuiltInEncodings.UTF8.rawValue) as String
}


没啥技术含量,可直接抄去用。然后根据我们上面的分析,实现 URL encode:


func buildParams(parameters: [String: AnyObject]) -> String {
var  components: [(String, String)] = []
for  key  in  Array(parameters.keys).sort() {
let value: AnyObject! = parameters[key]
components += self.queryComponents(key, value)
}
return  (components.map{ "($0)=($1)" } as [String]).joinWithSeparator( "&" )
}
func queryComponents(key: String, _ value: AnyObject) -> [(String, String)] {
var  components: [(String, String)] = []
if  let dictionary = value as? [String: AnyObject] {
for  (nestedKey, value)  in  dictionary {
components += queryComponents( "(key)[(nestedKey)]" , value)
}
else  if  let array = value as? [AnyObject] {
for  value  in  array {
components += queryComponents( "(key)" , value)
}
else  {
components.appendContentsOf([(escape(key), escape( "(value)" ))])
}
return  components
}


我们用了一个辅助函数 queryComponent 来表达步骤2这个递归过程。


至此,我们就完成了请求的封装,这个部分完整的代码在这里


现在我们就可以用它来发送请求了,比如我们想通过 bing 网页词典来查询 joepardize 这个单词的意思:


AaNet.request( "GET" , url:  "http://cn.bing.com/dict/" , form: [ "q" : "jeopardize" ], success: { (data)  in
print(String(data: data!, encoding: NSUTF8StringEncoding))
}) { (error)  in
}


返回:(这里没有对结果进行 parse,这个不属于本文的内容


**Optional("//jeopardize - ****必应**** Dictionary//


更优雅的接口和适配器模式


显然,目前的接口并不友好,封装也很低级。对于移动应用的网络开发而言,还有几个基本的需求没有被覆盖:


  • 默认的主机名: 我们的 app 一般的后台就一个域名,如果我们每次发一个请求都要敲一遍域名那真的太蛋疼了。

  • 默认的参数列表: 很多参数是基本每个请求都要带的,比如 app 的版本,用户设备的语言等等。

  • 更加简短并让人一看就懂得函数调用。

  • 参数可缺省

  • 错误处理可缺省


要实现上述的需求,我们有两条路可以走:


  • 在 AaNet 内部加上对应的逻辑,然后对之前的 request 做各种函数重载来实现。

  • 做一个新的模块,实现上述功能,但底层的数据发送调用 AaNet,AaNet 代码不变。


凭直觉来看,似乎应该选择第二个方案,首先上面的需求可能是多变的,但 AaNet 目前完成的功能是基本不会变的(除非 HTTP 协议的标准改变),变化的和不变的应该分开。其次是我们在将来有可能遇到 AaNet 不能满足我们的需求,需要采用一些更加成熟的框架(e.g. AFNetworking 等)的时候,迁移的成本要最低的话,用一个中间层把我们的代码和 AaNet 隔开是个很不错的选择。


这个思想在设计模式中叫做适配器模式, 我们新开一个 AaHTTP (名字任意)类来处理上述的需求,在底层调用 AaNet 来实现请求的发送。 然后在代码里调用 AaHTTP 的方法来完成业务逻辑,这样,即便某一天我们要需要替换网络通信的框架,也只是需要在 AaHTTP 内部的实现上修改 AaNet 为其他实现即可,不需要修改其他代码。 这里的 AaHTTP 就是一种典型的适配器。


实现 AaHTTP


比起 AaNet, AaHTTP 的实现是很简单的,主要都是一些设计层面的东西。


方便区别 GET 和 POST, 用字符串肯定是不明智的,我们增加一个 enum:


enum RequestMethod{
case  Post
case  Get
}


成员变量什么的就不用一一列举了,大家可以直接查看该文件完整的源代码。 这里看一下对外暴露的4个方法


为了实现链式调用,每个方法返回的都是自身


func fetch(url : String) -> AaHTTP{
setDefaultParas()
curUrl =  "(hostName)(url)"
self.method = .Get
return  self
}
func post(url : String) -> AaHTTP{
setDefaultParas()
curUrl =  "(hostName)(url)"
self.method = .Post
return  self
}
func paras(p : [String:AnyObject]) -> AaHTTP{
_ = p.reduce( "" ) { (str, p) -> String  in
parameters[p.0] = p.1
return  ""
}
return  self
}
func go(success : String -> Void, failure : NSError?->Void){
var  smethod =  ""
if  method == .Get{
smethod =  "GET"
} else {
smethod =  "POST"
}
AaNet.request(smethod, url: curUrl, form: parameters, success: { (data) -> Void  in
print( "request successed in (self.curUrl)" )
let result = String(data: data!, encoding: NSUTF8StringEncoding)
success(result!)
}) { (error) -> Void  in
print( "request failed in (self.curUrl)" )
failure(error)
}
}


fetch 和 post 分别生成 GET 和 POST 请求,paras 方法设置参数,go 方法进行实际请求操作。


现在,我们可以这样来发送网络请求:


aht.shareInstance.fetch("http://yahoo.com").go({ (result) in print(result) }) { (error) in print(error) }


如果有参数的话:


aht.shareInstance.fetch("http://cn.bing.com/dict/").paras(["q":"jeopardize"]).go({ (result) in print(result) }) { (error) in print(error) }


通过该类内部的 hostname 属性,即可实现缺省的主机名。


结语


至此,我们就完成了一个最简单、但足以应付绝大多数网络请求的框架,或者也可以基于此走得更远,比如:


  • 尝试管理多个 NSURLSession

  • 尝试实现文件的下载与上传

  • 尝试集成常见的 restful api authentication 的功能,比如 BCE的鉴权机制






微信号:CocoaChinabbs


▲长按二维码“识别”关注即可免费学习 iOS 开发

月薪十万、出任CEO、赢娶白富美、走上人生巅峰不是梦

--------------------------------------

商务合作QQ:645047738

投稿邮箱:support@cocoachina.com

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。