作者大神Q原创,谢绝转载,谢谢,demo地址 NetworkArchitecture。
现在写文章越来越感觉有点累,因为很多时候本来只打算讲某个点,但是写着写着会涉及到的越多,所以不知不觉越写越多,甚至有的我也不了解,需要在写的同时去研究,不过好处是通过写文章来发现自己的不足及需要学习的地方。
就拿这篇文章来说,本来就只是打算写iOS网络优化之设置合理的请求并发数
,但是写着写着想可能看到的同学还需要知道一些网络层架构的知识,就又写了简单的网络层架构的文章,哎,写着写着天都黑了。
网络请求简单介绍
关于HTTP
首先,HTTP是超文本传输协议,是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,其主要特点有如下:
-
支持客户/服务器模式;
-
简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快;
-
灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记;
-
无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间;
-
无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP1.1
既然上面提到了HTTP是基于请求与响应的,且最主要的两个特点就是无连接和无状态,但需要说明的是,虽然是无连接的,但其底层也就是传输层大多却是基于 TCP面向连接的通信方式,因此,这里的无连接指的是:当server端和client端进行通讯的时候,client端向server端发起请 求,server端接收请求之后返回给client端一个响应,之后就会断开不再继续保持连接了;这样有一个好处就是对于只有一次访问的连接来说不仅节省 资源还很高效,但很明显,如果client端还想继续多次访问server端就需要重新建立连接也就是会多次进行TCP的“三次握手,四次挥手”的过程, 这样一来并没有节省资源而且还很低效,因此,使用keep-alive(又称持久连接、连接重用)可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,避免了建立或者重新建立连接的次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的TCP连接意味着更少的系统内核调用,socket的accept()和close()调用)。
HTTP 1.0 中keep-alive默认是关闭的,需要在HTTP头加入"Connection: Keep-Alive",才能启用Keep-Alive;HTTP 1.1中默认启用Keep-Alive,如果加入"Connection: close ",才关闭。目前大部分浏览器都是用HTTP 1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,所以是否能完成一个完整的Keep- Alive连接就看服务器设置情况。
设置合理的并发数
为何设置并发数?
Screen Shot 2018-06-30 at 7.09.52 PM.png在这里我们所说到情况是没有HTTP Pipeline(管道化)的情况,如上图如果我们建立了一个TCP连接,假设这个时候客户端有3个并发请求,那么必须request 1发出并且得到服务端的response后request 2才能发出,同理request 3。这样并发数多的时候排在后面的就要一直等。有些业务场景会出现多个Request集中产生的情况,此时我们需要设置一个合理的并发数。并发数如果太小,会导致“劣质”的请求block住“优质”的请求。如果并发数太大,带宽有限的场景下,会增加请求的整体延迟。
如何设置并发数
有的人可能会想既然如此我们可以通过设置TCP连接数来控制并发数,那么怎么设置呢?其实很简单通过 httpMaximumC运营商onnectionsPerHost
设置,目前比较知名的开源网络请求框架比如AFNetworking和Alamofire都没有设置默认连接数。据说2G网络下一次只能维持一个TCP,3G是2个,4G和wifi是不限,最好是6个。为什么是据说,因为第一我们考证过,第二我测试的结果好像不是这样,但很多大牛都是这么说的,哈哈,如果不对这个锅我可不背。
不过怎样,这种方式都是不合理的,为什么呢?
当设置如下代码:
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.HTTPMaximumConnectionsPerHost = 2
configuration.timeoutIntervalForRequest = 30
let manager = Alamofire.Manager(configuration: configuration)
如图所示,前8个请求是成功的,后面的都 timeout 了,其实看过上面那张图之后这很好理解了。那正确的设置请求并发数的姿势是怎么样的呢?一个解决方案是在自定义的异步 NSOperation 子类中包装您的请求,然后使用操作队列的 maxConcurrentOperationCount 控制并发请求数,而不是 HTTPMaximumConnectionsPerHost 参数。以下我们通过Alamofire举例,AFNetworking实际上是一样的。
gjXC8.png并发数设置实战
首先我们对创建一个ConcurrentOperation基类,这部分苹果官方文档也有Concurrency Programming Guide
public class ConcurrentOperation : Operation {
//Mark: - Types
enum State {
case Ready, Executing, Finished
func keyPath() -> String {
switch self {
case .Ready:
return "isReady"
case .Executing:
return "isExecuting"
case .Finished:
return "isFinished"
}
}
}
//Mark: - Properties
var state = State.Ready {
willSet {
willChangeValue(forKey: newValue.keyPath())
willChangeValue(forKey: state.keyPath())
}
didSet {
didChangeValue(forKey: oldValue.keyPath())
didChangeValue(forKey: state.keyPath())
}
}
override init() {
state = .Ready
super.init()
}
//Mark: - Operation
public override var isReady: Bool {
return super.isReady && state == .Ready
}
public override var isExecuting: Bool {
return state == .Executing
}
public override var isFinished: Bool {
return state == .Finished
}
public override var isAsynchronous: Bool {
return true
}
func completeOperation() {
state = .Finished
}
}
然后再讲请求包装在ConcurrentOperation子类中
class NetworkOperation : ConcurrentOperation {
//定义属性,当你实例化
//这个对象时,将提供,并将在请求最终启动时使用
//
//在这个例子中,我将跟踪(a)URL;和(b)当请求完成时关闭调用
let URLString: String
let networkOperationCompletionHandler: (_ responseObject: Any?, _ error: Error?) -> ()
//我们还将跟踪所产生的请求操作,以防我们需要稍后取消它
weak var request: Alamofire.Request?
//定义捕获发出请求时使用的所有属性的init方法
init(URLString: String, networkOperationCompletionHandler: @escaping (_ responseObject: Any?, _ error: Error?) -> ()) {
self.URLString = URLString
self.networkOperationCompletionHandler = networkOperationCompletionHandler
super.init()
}
//当操作实际开始时,这是将被调用的方法
override func main() {
request = Alamofire.SessionManager.cmInstance.request(URLString)
.responseJSON { response in
//个人而言,我只是在init中传递给我的完成处理程序
self.networkOperationCompletionHandler(response.result.value, response.result.error)
// now that I'm done, complete this operation
self.completeOperation()
}
}
//支持取消请求,万一我们需要它
override func cancel() {
request?.cancel()
super.cancel()
}
}
使用就很简单了
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
for _ in 0 ..< 50{
let operation = NetworkOperation(URLString: "你的url") { responseObject, error in
if responseObject == nil {
print("failed")
} else {
print("succed")
}
}
queue.addOperation(operation)
}
接下来你需要做的是通过监听网络环境通过maxConcurrentOperationCount
设置合理的并发数,比如4G和Wifi下设置成6,当网络环境较差时可以适当降低并发数,3G设置成2。
参考文章:
关于HTTP中的keep-alive
NSURLSession concurrent requests with Alamofire
网友评论