简介
网络体系结构
网络体系结构把网络功能进行了层次拆分,不同的体系结构有不同的划分方式
- 物理层:物理层处理网络通信物理层面的事项,比如信号的传输和设备之间的物理链接。这一层包括了电缆,连接器和其他的硬件设备规范。
- 数据链路层:数据链路层提供在相同网络上不同设备之间无错误的传输,负责把数据拆成帧在物理层上传输。
- 网络层:网络层负责在不同网络上路由数据,它决定了网络传输中的最佳路径和拥塞控制。
- 传输层:提供了可靠的设备间端到端的通信,保证了数据无误的传输和正确的序列
- 会话层:会话层管理不同设备间通信会话,负责建立,维持和终止设备间的会话
- 表示层:表示层提供了设备间数据交换的标准格式,负责数据压缩,加密和解密
- 应用层:提供服务给终端用户,例如email,网络浏览和文件传输。也是在这一层,各个应用跟网络交互
网络请求过程
原生移动端主流的2种网络请求协议,分别是HTTP请求和WebSocket请求
HTTP
WebSocket
网络基础知识
URL
URL是(Uniform Resource Locator)的缩写,统一资源定位器。通常Domain Name也被称作Host
RESTful API
RESTful中通常使用JSON作为URL指向的资源,使用HTTP Method表示处理动作增删改查
- Resource
- Action
网络协议
传输层
TCP
(Transmission Control Protocol)是一个提供了可靠的,有序的,错误检查的在不同主机间的数据传输的面向链接的协议,需要使用三次握手和四次挥手来建立和断开链接,链接建立好之后才开始传输数据。TCP设计用来处理拥塞和网络错误,并且可以通过自动重新传输丢失的数据包来保证数据传输的可靠性。
三次握手
四次挥手
四要素
四要素对应一个链接
- 源IP
- 源端口
- 目的IP
- 目的端口
UDP
(User Datagram Protocol)是一个无链接的协议提供不可靠和无序的数据传输。在传输数据之前不需要建立链接,并且不提供任何数据重传和数据纠错功能。
应用层
HTTP
简介
超文本传输协议(Hyper-Text Transfer Protocol),超越普通文本,因为还包含了文字,图片,视频等,HTML就是常见的超文本
结构
HTTP的Request和Response结构主要由Status-Line, Header和Body或者说是Payload组成
Request
Response
Status Code
HTTP协议中使用status Code来标识response的状态
1.0
- 无状态(借助Cookie/Session机制做身份认证和状态记录)
- 链接依赖(每个请求响应使用一个独立的链接)
- 无长链接(每一次请求都需要发起新的链接)
- 有限的header信息
- 不支持缓存
- 不支持管道
1.1
- 长链接(保持连接,添加Connection: Keep-Alive字段,默认开启)
- 支持缓存
- 分块传输编码(添加Transfer-Encoding: chunked字段)
- 主机头(不同的网站可以托管在同一个服务器上,添加Host: :字段)
- 管道技术
队头阻塞
队头阻塞是指单个(慢)对象阻止其他/后续的对象前进
由于HTTP1.1是纯文本协议,只通过Header中的Content-Length来判断资源大小,不会进一步区分单个大块资源与其他资源。所以在一个链接上会一个一个完整的传输资源,如果前面的资源创建缓慢或者过大,会导致队头阻塞问题。这是HTTP协议导致的队头阻塞
由于前端H5场景下,需要加载的api, css, js, img等资源更多,所以做了如下优化。在移动端的场景下,主要还是api接口,数量也较少。
- 打开多个并行TCP链接
- 在多个域名上分片(sharding),如img.mysite.com, static.mysite.com等
- CDN
2.0
- 二进制分帧(减少消息的尺寸提升性能)
- 多路复用(允许在单个链接上同时发送多个请求响应)
- 服务端请求(允许服务端推送资源给客户端,而不用客户端主动请求)
- 头部压缩(减少Header的尺寸提升性能)
- 请求优先级(允许客户端分化请求的优先级)
HTTP1.1中导致队头阻塞是因为在资源块(resource chunks)之间没有分隔符和标识符,只能打开多个并行链接来解决。而在HTTP2.0中在单个TCP链接上解决了队头阻塞的问题,在资源块前面加上了帧(frames)。
Data frame包含了2个关键的元数据
- stream id
- length
简单描述的话,可能是这样拆分不同资源,混合传输,通过steam id进行组合,甚至可以加上优先级策略,决定传输的分配和顺序。
队头阻塞
不过HTTP/2还是无法解决TCP协议导致的队头阻塞。TCP基于自身的握手和数据确认机制,提供了可靠的链接。假设在传输1,2,3数据包,接收到1的数据包时,TCP交付数据。但是如果接收到3数据包,2数据包丢失时,3数据包将不会被交付,而是保存在接收缓冲区中(receive buffer)等待2数据的重传,之后按照顺序再交付这两个数据包。此时数据包2队头阻塞了数据包3。TCP相当于时顺序交付。
由于HTTP/2中单TCP链接承载了多种不同资源,所以TCP队头阻塞的情况会造成传输性能的降低。
3.0(QUIC)
- 从TCP修改成了UDP
- 减少了RTT
- 改进的拥塞控制
- 避免队头阻塞
- 连接迁移
队头阻塞真是实实在在推动了HTTP协议的发展,感谢队头阻塞。当然减少RTT也是推动网络协议发展的重要因素。
继续解决HTTP/2中TCP队头阻塞的问题,由于TCP协议的广泛使用,导致升级TCP协议可行性低,QUIC中使用了UDP协议来解决这个问题。与HTTP/2中的数据帧类似,QUIC中添加了流帧(stream frames)分别跟踪每个流的字节范围。QUIC在出现数据包丢失的时候,会判断流中的预期数据是否接收到,已经接收到预期数据就交付,反之等待丢失的数据包重传。
QUIC数据可能不再以与发送时完全相同的顺序交付,相当于在单个资源流中保留了顺序,但不再夸单个流(individual streams)进行排序。
Stream frame
- Stream id
- byte <start-end>
ALPN
应用层协议协商(Application-Layer Protocol Negotiation),是一个传输层安全协议(TLS)的扩展,ALPN使得应用层可以协商在安全连接层之上使用什么协议,比如是使用HTTP1.1还是使用HTTP2.0,避免了额外的往返通讯。
- NPN
NPN 是服务端发送所支持的 HTTP 协议列表,由客户端选择;NPN 的协商结果是在 Change Cipher Spec 之后加密发送给服务端
- ALPN
ALPN 是客户端发送所支持的 HTTP 协议列表,由服务端选择;ALPN 的协商结果是通过 Server Hello 明文发给客户端
HTTPS
HTTPS是在HTTP协议基础加上TLS加密的实现。
传输层安全协议(Transport Layer Sercurity)以及前身安全套接层(Secure Sockets Layer,SSL),是一种安全协议。
TLS协议采用主从式架构模型,用于在两个应用程序间透过网络创建起安全的连线,防止在交换资料时受到窃听及修改。
TLS协议的优势是与高层的应用层协议(如HTTP、FTP、Telnet等)无耦合。应用层协议能透明地运行在TLS协议之上,由TLS协议进行创建加密通道需要的协商和认证。应用层协议传送的数据在通过TLS协议时都会被加密,从而保证通信的私密性。
TLS 包含三个主要组件
- 加密:隐藏从第三方传输的数据
- 身份验证:确保交换信息的各方是他们声称的身份
- 完整性:验证数据是否并非伪造而来或未遭篡改过
握手过程
TLS1.2
2RTT
TLS1.3
1RTT
OCSP Stapling
OCSP(Online Certificate Status Protocol,在线证书状态协议)是由数字证书颁发机构CA(Certificate Authority)提供,客户端通过OCSP可实时验证证书的合法性和有效性。
启用OCSP Stapling功能后,OCSP信息查询的工作将由CDN服务器完成。CDN通过低频次查询,将查询结果缓存到服务器中(默认缓存时间60分钟)。当客户端向服务器发起TLS握手请求时,CDN服务器将证书的OCSP信息和证书一起发送到客户端,供用户验证,无需用户再向数字证书认证机构(CA)发送查询请求。极大地提高了TLS握手效率,节省了用户验证时间。
iOS
URL Loading System
NSURLSession
URL Loading System使用标准协议(例如https或你创建的自定义协议)提供对URL标识的资源的访问。加载是异步执行的,因此应用可以保持响应能力,并在数据或错误到达时处理它们。
你使用URLSession实例创建一个或多个URLSessionTask实例,这些URLSessionTask实例可以获取数据、下载文件或将数据和文件上传到服务器。要配置会话,请使用URLSessionConfiguration对象,该对象控制行为,例如如何使用缓存和cookie,或者是否允许在蜂窝网络上进行连接。
你可以重复使用一个会话来创建任务。例如,网络浏览器可能有分开的会话供常规浏览和私人浏览使用,而私人会话不会缓存其数据。
执行流程
代码示例
var receivedData: Data?
func startLoad() {
loadButton.isEnabled = false
let url = URL(string: "https://www.example.com/")!
receivedData = Data()
let task = session.dataTask(with: url)
task.resume()
}
// delegate methods
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode),
let mimeType = response.mimeType,
mimeType == "text/html" else {
completionHandler(.cancel)
return
}
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData?.append(data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
DispatchQueue.main.async {
self.loadButton.isEnabled = true
if let error = error {
handleClientError(error)
} else if let receivedData = self.receivedData,
let string = String(data: receivedData, encoding: .utf8) {
self.webView.loadHTMLString(string, baseURL: task.currentRequest?.url)
}
}
}
CFNetwok
CFNetwork是Core Services框架中的一个框架,它为网络协议提供了一个抽象库。 这些抽象使执行各种网络任务变得容易,例如:
- 使用BSD套接字
- 使用SSL或TLS创建加密的连接
- 解析DNS
- 使用HTTP,验证HTTP和HTTPS服务器
- 使用FTP服务器
- 发布,解决和浏览Bonjour服务
CFNetwork框架分两部分
基础API:
- CFSorcket API:CFSocket是BSD sockets的抽象。在很少开销的同时,CFSocket几乎实现了BSD sockets的所有功能,并且集成到RunLoop中。
- CFStream API:CFStream提供了数据读写方法,即读写流。使用它可以为内存、文件、网络(使用socket)的数据建立流。
CFNetwork API:
- CFFTP API:CFFTP是FTP协议的抽象,使用CFFTP使得与FTP服务器通信变得非常容易。通过使用CFFTP API,你可以创建FTP读取流(用于下载)和FTP写入流(用于上传)。
- CFHTTP API:CFHTTP是HTTP协议的抽象,使用CFHTTP发送和接收HTTP消息。
- CFHTTPAuthentication API:HTTP鉴权。
代码示例
//创建请求
CFStringRef url = CFSTR("https://http2.pro/api/v1");
CFURLRef myURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
CFStringRef requestMethod = CFSTR("GET");
CFHTTPMessageRef myRequest =
CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, myURL,
kCFHTTPVersion2_0);
// 设置body
//NSData *dataToPost = [@"apptoken=-1" dataUsingEncoding:NSUTF8StringEncoding];
//CFHTTPMessageSetBody(myRequest, (__bridge CFDataRef) dataToPost);
// 设置header
//CFHTTPMessageSetHeaderFieldValue(myRequest, CFSTR("Content-Type"), CFSTR("application/x-www-form-urlencoded; charset=utf-8"));
//创建流并开启
CFReadStreamRef requestStream = CFReadStreamCreateForHTTPRequest(NULL, myRequest);
CFReadStreamOpen(requestStream);
//接收响应
NSMutableData *responseBytes = [NSMutableDatadata];
CFIndex numBytesRead = 0;
do {
UInt8 buf[1024];
numBytesRead = CFReadStreamRead(requestStream, buf, sizeof(buf));
if (numBytesRead > 0) {
[responseBytes appendBytes:buf length:numBytesRead];
}
} while (numBytesRead > 0);
CFHTTPMessageRef response = (CFHTTPMessageRef) CFReadStreamCopyProperty(requestStream, kCFStreamPropertyHTTPResponseHeader);
CFHTTPMessageSetBody(response, (__bridgeCFDataRef)responseBytes);
CFReadStreamClose(requestStream);
CFRelease(requestStream);
CFAutorelease(response);
//转换为JSON
CFIndex statusCode;
statusCode = CFHTTPMessageGetResponseStatusCode(response);
CFDataRef responseDataRef = CFHTTPMessageCopyBody(response);
NSData *responseData = (__bridgeNSData *)responseDataRef;
NSMutableDictionary *jsonInfo = [NSJSONSerializationJSONObjectWithData:responseData options:NSJSONReadingAllowFragmentserror:nil];
NSLog(@"responseBody: %@", jsonInfo);
libcurl
libcurl提供了请求网络的简单接口,用C语言实现并被广泛使用,可以跨端使用同时支持iOS和Android平台。提供了下载文件,上传数据和通过HTTP,FTP,SMTP等协议链接远端服务器等功能
curl在macOS系统上是默认集成的,你可以通过命令来使用
curl www.baidu.com
在iOS上,libcurl有对应的framework,可以集成framework进行网络方面的开发
#import <curl/curl.h>
//初始化curl对象
CURL *curl = curl_easy_init();
//设置url
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
//设置回调
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
//执行
CURLcode res = curl_easy_perform(curl);
//处理
if (res != CURLE_OK) {
NSLog(@"Error: %s", curl_easy_strerror(res));
} else {
NSLog(@"Response: %s", response_data);
}
//清空
curl_easy_cleanup(curl);
数据处理
size_t write_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
size_t realsize = size * nmemb;
char *response = (char *)malloc(realsize + 1);
if (response == NULL) {
return 0;
}
memcpy(response, ptr, realsize);
response[realsize] = '\0';
char *old_data = (char *)userdata;
char *new_data = realloc(old_data, strlen(old_data) + realsize + 1);
if (new_data == NULL) {
free(response);
return 0;
}
strcat(new_data, response);
free(response);
userdata = new_data;
return realsize;
}
Cronet
Cronet是谷歌开发的网络库,提供了一个标准的iOS网络库。Cronet跟libcurl一样,是可以做成多端统一的实现
#import <Cronet/Cronet.h>
//创建NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com"]];
//创建engine
CronetEngine *engine = [Cronet getGlobalEngine];
//创建builder
CronetURLRequestBuilder *builder = [[CronetURLRequestBuilder alloc] initWithRequest:request];
//设置请求头
[builder addHeader:@"User-Agent" value:@"My App"];
//创建Cronet URL request
CronetURLRequest *cronetRequest = [builder build];
//发起请求并等待回应
NSError *error;
CronetURLResponse *response = [cronetRequest startWithError:&error];
//处理response
if (error) {
NSLog(@"Error: %@", error);
} else {
NSData *responseData = [response getBodyAsNSData];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
NSLog(@"Response: %@", responseString);
}
//清理
[cronetRequest cancel];
需要注意的是Cronet发起请求是异步过程,需要使用闭包和回调,而且通常在URL Loading System中注册和发起请求
WebKit
iOS中WKWebView作为WebKit的容器工具提供了用来加载和渲染web内容的高层级的API
//创建webview
let webView = WKWebView(frame: view.bounds)
view.addSubview(webView)
//创建request
let request = URLRequest(url: URL(string: "https://example.com")!)
//加载request
webView.load(request)
使用WKNavigationDelegate对应回调方法处理response
//处理response
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()") { (html: Any?, error: Error?) in
if let htmlString = html as? String {
print("Response: (htmlString)")
}
}
}
Websocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,iOS中可以依赖与Starscream库,这个库使用Swift对WebSocket端做了实现。
import Starscream
//创建socket对象
let socket = WebSocket(url: URL(string: "wss://example.com")!)
//设置代理
socket.delegate = self
//链接到webSocket
socket.connect()
实现WebSocketDelegate协议来处理WebSocket事件
func websocketDidConnect(socket: WebSocketClient) {
print("WebSocket connected")
socket.write(string: "Hello, server!")
}
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print("Received message: (text)")
}
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
if let error = error {
print("WebSocket disconnected with error: (error)")
} else {
print("WebSocket disconnected")
}
}
发消息
//发消息
socket.write(string: "Hello, server!")
断开链接
//断开链接
socket.disconnect()
Socket
操作系统通常会为应用程序提供一组应用程序接口(API),称为套接字接口(socket API)。应用程序可以通过套接字接口,来使用网络套接字交换数据。最早的套接字接口来自于4.2 BSD,因此现代常见的套接字接口大多源自Berkeley套接字(Berkeley sockets)标准。在套接字接口中,以IP地址及端口组成套接字地址(socket address)。远程的套接字地址,以及本地的套接字地址完成连线后,再加上使用的协议(protocol),这个五元组(five-element tuple),作为套接字对(socket pairs),之后就可以彼此交换资料。
例如,在同一台计算机上,TCP协议与UDP协议可以同时使用相同的port而互不干扰。 操作系统根据套接字地址,可以决定应该将资料送达特定的行程或线程。这就像是电话系统中,以电话号码加上分机号码,来决定通话对象一般。
socket是一种操作系统提供的进程间通信机制,实际做的事情就是对TCP/UDP等操作抽象成了几个工作。
套接字地址结构
struct sockaddr_in {
uint16_t sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct sockaddr {
uint16_t sa_family;
char sa_data[14];
}
- socket
客户端和服务器使用socket函数来创建一个套接字描述符(socket descriptor)。
#import <sys/types.h>
#import <sys/socket.h>
int socket(int domain, int type, int protocol);
- connect
客户端通过调用connect函数来建立和服务器的链接。
#import <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
- bind
剩下的套接字函数,bind、listen和accept,服务器用他们来和客户端建立连接。
#include <sys/socket>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- listen
客户端是发起连接请求的主动实体。服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。
#include <sys/socket>
int listen(int sockfd, int backlog);
- accept
服务器通过调用accept函数来等待来自客户端的连接请求。
accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后再addr中填写客户端你的套接字地址,并返回一个已链接描述符(connect descriptor),这个描述符可被用来利用Unix I/O函数与客户端通信。
#include <sys/socket>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
- getaddrinfo
getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串转化成套接字地址结构
- getnameinfo
getnameinfo函数和getaddrinfo函数时相反,将一个套接字地址结构转换成相应的主机和服务名字符串
优化
HTTP
数据源
- 控制源数据的大小
- 对于API请求,配合后端同学优化,瓶颈可能会在数据库和数据处理上
- 对于资源请求,使用OSS和CDN加速
- 对于埋点等数据上报请求,优化采样率控制,上报策略和压缩策略
链路优化
异地多活
异地多活的方案可以增加可用性,提升性能,提升扩展性,更好的灵活性,更好的灾备和恢复能力。当前的主流云厂商都给出了自己的方案,可以参考和部署。
DNS
IP直连
自研
HTTPDNS
IP直连使用了下发和择优机制,避开了传统DNS的过程,也避免了传统DNS统一被篡改和解析性能不佳的问题。在建立链接的时候直接使用了ip,但是在链接的过程中证书等还是需要对host进行验证,从而会带来一些问题
- Cookie
- 302重定向
- Webview业务场景
- SNI
TCP
建立链接是相对耗时的阶段,所以链接复用率是重要的性能指标,尽可能的减少链接的建立和耗时。
- 升级HTTP1.1到HTTP/2
- 升级HTTP/2到QUIC
- 域名收敛
TLS
- 升级TLS1.2到使用TLS1.3
- 开启OCSP Stapling
Request
- Task对象的缓存和调度
- 控制Request Size
Server
- 配合后端同学优化
Response
- 控制Response Size
引用
Optimizing Your App for Today’s Internet
Advances in Networking, Part 1