Http 网络基础
![](https://img.haomeiwen.com/i1895495/839e6427666ff256.png)
![](https://img.haomeiwen.com/i1895495/501b9db7f7be9850.png)
TCP
![](https://img.haomeiwen.com/i1895495/bbc65814dbcd8a19.png)
- Source Port和Destination Port:分别占用16位,表示源端口号和目的端口号;用于区别主机中的不同进程,IP地址用来区分不同主机的,源端口号和目的端口号配合上IP首部中的源IP地址就能确定为一个TCP连接
- Sequenece Number:用来标识从TCP发送端向TCP接收端的数据字节流,他表示在这个报文中的第一个数据字节流在数据流中的序号;主要用来解决网络乱序的问题。
- Acknowledgment Number:32位确认序号包发送确认的一端所期望收到的下一个序号,因此,确认需要应该是上次已成功收到数据字节序号+1,不过只有当标志位中的ACK标志(下面介绍)为1时该确认序列号的字段才有效。主要用来解决不丢包的问题。
- Offset:给出首部中32bit字的数目,需要这个值是因为任选字段的长度是可变的。这个字段占4bit(最多能表示15个32bit的字,即4*15=60个字节的首部长度),因此TCP最多有60个字节的首部。然而,没有任选字段,正常的长度是20字节。
- TCP Flags:TCP首部中有6个比特,它们总的多个可同时被设置为1,主要是用于操控TCP的状态机,依次为URG,ACK,PSH,RST,SYN,FIN。
- Window:窗口大小,也就是有名的滑动窗口,用来进行流量控制;这是一个复杂的问题
TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接:位码即tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)
TCP三次握手、四次挥手
![](https://img.haomeiwen.com/i1895495/5ff46bc580ff8e39.png)
HTTPS
HTTPS(Hypertext Transfer Protocol over Secure Socket Layer/基于安全套接字层的超文本传输协议,或者也可以说HTTP OVER SSL)是网景公司开发的web协议。SSL的版本最高为3.0,后来的版本被称为TLS,现在所用的协议版本一般都是TLS,但是由于SSL出现的时间比较早,所以现在一般指的SSL一般也就是TLS,本文中后面都统一使用SSL来代替TLS。HTTPS是以安全为目标的HTT通道,简单讲就是HTTP的安全版,所以你可以理解HTTPS=HTTP+SSL。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容是SSL。
数字摘要
数字摘要是采用单项Hash函数将需要加密的明文"摘要"成一串固定长度(128位)的密文,这一串密文又称为数字指纹,它有固定的长度,而且不同的明文摘要成密文,其结果总是不同的,而同样的明文摘要必定一定。“数字摘要”是https能确保数据完整性和防篡改的根本原因。常用的摘要主要有MD5、SHA1、SHA256等。
数字签名
数字签名技术就是对"非对称密钥加解密"和"数字摘要"两项技术的应用,它将摘要信息用发送者的私钥加密,与原文一起传送给接受者。接受者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原因产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输的过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。
数字签名过程:
明文——>hash运算——>摘要——>私钥加密——>数字签名
作用:
- 能确定消息确实由发送方签名并发出来的,因为别人假冒不了发送方的签名。
- 数字签名能确定消息的完整性。
数字证书
对于请求方来说,它怎么能确定它所得到的公钥一定是从目标主机哪里发送的,
而且没有被篡改过呢?亦或者请求的目标主机本身就是从事窃取用户信息的不正当行为呢?这时候,我们需要一个权威的值得信赖的第三方机构(一般是由政府机构审核并授权的机构)来统一对外发送主机机构的公钥,只要请求方这种机构获取公钥,就避免了上述问题
- 数字证书的颁发过程
用户首先产生自己的密钥对,并将公共密钥及部分个人身份信息传送给认证中心。认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来,然后,认证中心将发送给用户一个数字证书,该证书内包含用户的人信息和他的公钥信息,同时还附有认证中心的签名信息(根证书私钥)签名。用户就可以使用自己的数字证书进行相关的各种活动。数字证书由独立的证书发行机构发布,数字证书各不相同,每种证书可提供不同级别的可信度。 - 证书包含哪些内容
1、证书颁发机构的名称
2、证书本身的数字签名
3、证书持有者的公钥
4、证书签名用到的Hash算法 - 验证证书的有效性
浏览器默认都是会内置CA跟证书,其中根证书包含了CA的公钥
1、防伪造证书1:如果证书颁发机构是伪造的,浏览器不认识,直接认为是危险证书
2、造证书2:证书颁发机构是的确存在的,于是根据CA名,找到对应内置的CA根证书、CA的公钥。用CA的公钥,对伪造的证书的摘要进行解密,发现解密不了,认为是危险证书
3、防篡改:对于篡改的证书,使用CA的公钥对数字签名进行解密得到摘要A,然后再根据签名的Hash算法计算出证书的摘要B,对比A与B,若相等则正常,若不相等则是被篡改过的。
4、防过期失效验证:正式课在其过期前辈吊销,通常情况是该证书的私钥已经失密。较新的浏览器如果Chrome、Firefox、Opera和Internet Explored 都实现了在线证书的状态协议(OCSP)以排除这种情况:浏览器将网站提供的证书序列号通过OCSP发送给证书颁发机构,后者会告诉浏览器证书是否还是有效的。
有关数字签名、数字证书的图解,可以参照这篇文章
![](https://img.haomeiwen.com/i1895495/666a8d6df2065201.png)
![](https://img.haomeiwen.com/i1895495/dab4063c63fcb018.png)
HTTPS 与代理
我们知道从HTTPS的整个原理可以知道,客户端和服务器进行通信的成果,客户端是能拿到数据的,代理也一定能拿到,包括公共密钥,证书,算法等,但代理无法获取服务器的私钥,所以无法获取5/6/7/8的会话密钥,也就无法得到数据传输的明文,所以默认的情况下,charles是无法抓https的。那如何让charles转包并获取明文?也就是让charles获取私钥,获取服务器的是不可能的,那只能在通信过程中使用charles自己的证书,并在通信的过程中主动为请求的域名发放证书,流程如下
![](https://img.haomeiwen.com/i1895495/8e345d9c5045fbcc.png)
SPDY
SPDY可以说是综合了HTTPS和HTTP两者有点于一体的传输协议,主要解决:
- 降低延迟::针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个TCP连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
- 请求优先级:多路复用带来的一个新的问题是,在连接共享的基础上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到相应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样保证用户第一时间看到网页的内容。
- header压缩:HTTP1.x的header很多时候都是重复多余的。选择和是的压缩算法可以减少包的大小和数量。
- 基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
-
服务端推送(server push),采用SPDY的网页,例如一个网页有一个style.css请求,客户端在收到style.css数据的同时,服务端会将style.js文件推送给客户端,当客户端再次尝试获取style.js时就可以直接从缓存中获取到,不用再次发送请求了。
SPDY结构图
HTTP2.0
在HTTP/1.x中,如果客户端想发起多个并行请求必须建立多个TCP连接,这无疑增大了网络开销。另外HTTP/1.x不会压缩请求和响应头,导致了不必要的网络流量,HTTP/1.x不支持资源优先级导致底层TCP连接利用率低下。而这些问题都是HTTP/2要着力解决的。HTTP2.0可以说是SPDY的升级版(其实也是基于SPDY设计的),但是HTTP2.0跟SPDY仍有不同的地方,主要有以下两点:
- HTTP2.0支持明文HTTP传输,而SPDY只使用HTTPS
- HTTP2.0消息头的压缩算法采用HPACK,而非SPDY采用的DEFLATE
HTTP2 新特性
- 新的二进制格式(Binary Format):HTTP 1.x的解析是基于文本。基于文本洗衣的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制分帧格式,实现方便且健壮。
- 多路复用(multiPlexing):即连接共享,即每一个request都是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个requst,每个连接的request可以随机的混杂在一起,接受方可以根据request的id将request再归属到各自不同的服务端请求里面,后面有一张多路复用原理图
- 请求优先级:把HTTP消息分解为很多独立的帧后,就可以通过优化这些帧的交错和传输顺序,进一步提供性能。为了做到这一点,每个流都有一个带有31比特的的优先值。
- header压缩:HTTP1.x的header带有大量的信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一个header field表,既避免了重复的header传输,又减少了需要传输的大小。
- 服务端推送(server push):同SPDY一样,HTTP2.0也具有server push功能。
HTTP2.0性能增强的核心,全在于新增的二进制分帧层,它定义了如何封装HTTP消息并在客户端与服务器之间传输
![](https://img.haomeiwen.com/i1895495/9a745c832b7dc765.png)
HTTP2引入的新概念:
- 数据流:基于TCP连接之上的逻辑双向字节流,对应一个请求及响应。客户端每发起一个请求就建立一个数据流,后续该请求及其响应的所有数据都通过该数据流传输。
- 消息:一个请求或者响应对应的一系列数据帧
- 帧:HTTP/2的最小数据切片单位,每个帧包含帧首部,至少也会标示出当前帧所属的流。
逻辑关系:
- 所有通信都在一个TCP连接上完成,此连接可以承载任意数量的双向数据流。
- 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。
- 每条消息都是一条逻辑HTTP消息(例如请求或者响应),包含一个或多个帧。
- 帧是最小的通信单位,承载着特定类型的数据,例如HTTP标头、消息负载等等。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
- 每个HTTP消息分分解为多个独立的帧后可以交错发送,从而在宏观上实现了多个请求或者响应并行传输的效果。这类似于多进程环境下的时间分片机制。
HTTP2.0把HTTP协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。相应地,很多流可以并行地在同一个TCP连接上交换消息。
在HTTP/1.1中,如果客户端想发送多个平行的请求以及改进性能,必须使用多个TCP连接。HTTP2.0的二进制分帧层突破了限制;客户端和服务器可以把HTTP消息分解为互不依赖的帧,然后乱序发送,最后再把另一端把它们重新组合起来。
![](https://img.haomeiwen.com/i1895495/79bd26670aad1404.png)
![](https://img.haomeiwen.com/i1895495/d3648adccb016ce9.png)
隧道
Web隧道(Web tunnel)是HTTP的另一种用法,可以通过HTTP应用程序访问非HTTP协议的应用程序,Web隧道允许用户允许用户通过HTTP连接发送非HTTP流量,这样就可以在HTTP上捎带其他协议数据了。使用Web隧道最常见的原因就是要在HTTP连接中嵌入非HTTP流量。这样这类流量就可以穿过只允许Web流量通过的防火墙了。
建立隧道
Web隧道是用HTTP的CONNECT方法建立起来的。CONNECT方法请求隧道网管创建一条到达任一目的服务器和端口的TCP连接,并对客户端和服务器职期间的后续数据进行盲转发。
![](https://img.haomeiwen.com/i1895495/ffce30ecc7cc82d4.png)
- (a)是客户端相互发送了额一条CONNECT请求给隧道网关。客户端的CONNECT方法请求隧道网关打开一条TCP连接(在这里,打开的是到主机orders.joes-hardware.com的标准SSL端口443的连接);
- (b)和(c)中创建了TCP连接,一旦建立了TCP连接,网管就会发送一条HTTP200 Connection Established响应来通知客户端,此时,隧道就建立起来了。客户端通过HTTP隧道发送的所有数据都会被直接转发给输出TCP连接,服务器发送的所有数据都会通过HTTP隧道转发给客户端。
SSL隧道
最初开发Web隧道是为了通过防火墙来传输加密的SSL流量。很多组织都会将所有流量通过分组过滤路由器和代理服务器以隧道方式传输,以提升安全性。但有些协议,比如加密SSL,其信息是加密的的,无法通过传统的代理服务器转发。隧道会通过一条HTTP连接来传输SSL流量,以穿过端口80的HTTP防火墙。
![](https://img.haomeiwen.com/i1895495/496335eeaa3dfd03.png)
为了让SSL流量经现存的代理防火墙进行传输,HTTP中添加了一项隧道特性,在此特性中,可以将原始的加密数据放在HTTP报文中,通过普通的HTTP信道传送。
![](https://img.haomeiwen.com/i1895495/5af47fa4bbf45a6a.png)
前言
平常开发一般都是用第三方库,很少自己解析Http报文。这里用Socket模拟Http报文解析。
Http请求报文一般格式
Line | Content |
---|---|
1 | 请求方法+空格+URL+HTTP版本+回车+换行 |
2 | 头部字段key:val(多对) + 回车 + 换行 |
3 | 回车 + 换行 |
4 | 请求数据(POST/PUT该部分可选,GET、DELETE无该部分) |
示例,Http GET请求http://image.so.com/j?q=mobile&sn=0&pn=50 地址
GET /j?q=mobile&sn=0&pn=50 HTTP/1.1
Host: image.so.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
如图,第一行为起始行,表明了以GET方式请求,子路径/j?q=mobile&sn=0&pn=50(浏览器解析) ;HTTP版本1.1。下个的每一行都是一个首部字段。浏览器发送HTTP报文的时候,会从URL中解析出域名,Host 这个Header就是域名地址。其他Header写法类似。*** 需要注意的是Content-Type: multipart/form-data 时,有一个分隔符参数boundary ***
HTTP响应报文一般格式
Line | Content |
---|---|
1 | HTTP-Version + Status-Code + Reason-Phrase |
2 | 头部字段key:val(多对) + 回车 + 换行 |
3 | 回车 + 换行 |
4 | 响应报文内容 |
常见状态码:
- 200 OK:客户端请求成功
- 400 Bad Request:客户端请求有语法错误,不能被服务端理解。
- 401 Unauthorized:请求未经授权,这个状态码必须和WWW-Authenticate报头一起使用。
- 403 Forbidden:服务器手动请求,但是拒绝提供服务
- 404 Not Found:请求资源不存在
- 500 Internal Server Error: 服务器发生不可预期的错误。
- 503 Server Unavailable: 服务器当前不能处理客户端的请求。
HTTP 首部字段分类:
首部类型 | 作用 |
---|---|
通用首部 | 既可以出现爱请求报文中,页可以出现在响应报文中 |
请求部首 | 提供更多有关请求的信息 |
响应部首 | 提供更多有关响应的信息 |
实体部首 | 描述主题的长度和内容,或者资源自身 |
扩展部首 | HTTP 规范中没有定义的新部首 |
请求部首通知服务器关于客户端请求的信息,典型的有:
- Content-Type:请求数据的格式
- Content-Length:消息长度
- Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。
- User-Agent: 发出请求的浏览器类型,可以自行设置。
- Accept:客户端可识别的内容类型列表
- Accept-Encoding:客户端可识别的数据编码
- Connection:允许客户端和服务器指定与请求/响应连接有关的形象,例如,设置为Keep-Alive表示保持连接。
- Transfer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式。
模拟HTTP报文解析
SimpleHttpServer类,是一个后台线程,并持有一个ServerSocket实例,用于服务端接收HTTP请求。
public class SimpleHttpServer extends Thread {
public static final int HTTP_PORT = 8080;
ServerSocket mSocket = null;
public SimpleHttpServer() {
try {
mSocket = new ServerSocket(HTTP_PORT);
} catch (IOException e) {
e.printStackTrace();
}
if (mSocket == null) {
throw new RuntimeException("服务器Socket 初始化失败");
}
}
@Override
public void run() {
try {
while (true) {
System.out.println("等待连接中");
new DeliverThread(mSocket.accept()).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在构造函数中实例化了一个ServerSocket实例。run方法中不停接收客户端的请求。mSocket.accept()方法以阻塞的形式,等待客户端请求。一旦客户端发送请求,就实例化一个DeliverThread队请求进行处理;同时mSocket又等待另一个请求到来。
DeliverThread类,后台线程,持有一个Socket实例,对客户端请求做处理。
public class DeliverThread extends Thread {
Socket mClientSocket;
BufferedReader mInputStream;
PrintStream mOutputStream;
// 请求方法,GET POST等
String httpMethod;
// 子路径
String subPath;
// 分隔符
String boundary;
// 请求参数
Map<String,String> mParams = new HashMap<>();
Map<String,String> mHeaders = new HashMap<>();
// 是否已经解析完Header
boolean isParsedHeader = false;
public DeliverThread(Socket socket){
mClientSocket = socket;
}
@Override
public void run() {
try {
mInputStream = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));
mOutputStream = new PrintStream(mClientSocket.getOutputStream());
parseRequest();
handleResponse();
} catch (IOException e) {
e.printStackTrace();
}finally {
IoUtils.closeQuietly(mInputStream);
IoUtils.closeQuietly(mOutputStream);
IoUtils.closeSocket(mClientSocket);
}
}
private void parseRequest(){
String line;
try {
int lineNum = 0;
while ((line = mInputStream.readLine()) != null){
if(lineNum ==0){
parseRequestLine(line);
}
if(isEnd(line))
break;
if(lineNum != 0 && !isParsedHeader)
parseHeaders(line);
if(isParsedHeader)
parseRequestParams(line);
lineNum++;
}
}catch (IOException e){
e.printStackTrace();
}
}
// 是否是结束行
private boolean isEnd(String line) {
return line.equals("--" + boundary + "--");
}
private void parseRequestLine(String lineOne){
String[] tmpStrArray = lineOne.split(" ");
httpMethod = tmpStrArray[0];
subPath = tmpStrArray[1];
System.out.println("请求方式: "+httpMethod);
System.out.println("子路径: "+subPath);
System.out.println("HTTP版本: "+tmpStrArray[2]);
}
private void parseHeaders(String headerLine){
if(headerLine.equals("")){
isParsedHeader = true;
System.out.println("~~~~~~~~header解析完成");
return;
}else if (headerLine.contains("boundary")){
boundary = parseSecondField(headerLine);
System.out.println("分隔符: "+ boundary);
}else {
parseHeaderParam(headerLine);
}
}
private String parseSecondField(String line){
String[] headerArray = line.split(";");
parseHeaderParam(headerArray[0]);
if(headerArray.length>1){
return headerArray[1].split("=")[1];
}
return "";
}
private void parseHeaderParam(String headerLine){
String[] keyvalue = headerLine.split(":");
mHeaders.put(keyvalue[0].trim(),keyvalue[1].trim());
System.out.println("header参数名: "+keyvalue[0].trim() + " ,参数值: "+keyvalue[1].trim());
}
private void parseRequestParams(String paramLine) throws IOException {
if(paramLine.equals("--"+boundary)){
String contentDisposition = mInputStream.readLine();
String paramName = parseSecondField(contentDisposition);
mInputStream.readLine();
String paramValue = mInputStream.readLine();
mParams.put(paramName,paramValue);
System.out.println("param参数名: "+paramName + " , 参数值: "+paramValue);
}
}
private void handleResponse(){
sleep();
mOutputStream.println("HTTP/1.1 200 OK");
mOutputStream.println("Content-Type: application/json");
mOutputStream.println();
mOutputStream.println("{\"code\":\"success\"}");
}
private void sleep(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里主要的方法有两个:parseRequest 和 handleResponse。
HttpPost类,模拟客户端发起请求:
public class HttpPost {
public String url;
private Map<String,String> mParamsMap = new HashMap<>();
Socket mSocket;
public HttpPost(String url){
this.url = url;
}
public void addParam(String key,String value){
mParamsMap.put(key,value);
}
public void execute(){
try {
mSocket = new Socket(this.url, SimpleHttpServer.HTTP_PORT);
PrintStream outputStream = new PrintStream(mSocket.getOutputStream());
BufferedReader inputStream = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
final String boundary = "my_boundary_test";
writeHeader(boundary,outputStream);
writeParams(boundary,outputStream);
waitResponse(inputStream);
} catch (IOException e) {
e.printStackTrace();
}finally {
IoUtils.closeSocket(mSocket);
}
}
private void writeHeader(String boundary,PrintStream outputStream){
outputStream.println("POST /api/login/ HTTP/1.1");
outputStream.println("content-length:111");
outputStream.println("Host:"+this.url+":"+ SimpleHttpServer.HTTP_PORT);
outputStream.println("Content-Type:multipart/form-data; boundary="+boundary);
outputStream.println("User-Agent:android");
outputStream.println();
}
private void writeParams(String boundary,PrintStream outputStream){
Iterator<String> paramsKeySet = mParamsMap.keySet().iterator();
while (paramsKeySet.hasNext()){
String paramName = paramsKeySet.next();
outputStream.println("--"+boundary);
outputStream.println("Content-Dispositin: form-data; name="+paramName);
outputStream.println();
outputStream.println(mParamsMap.get(paramName));
}
outputStream.println("--"+boundary+"--");
}
private void waitResponse(BufferedReader inputStream) throws IOException {
System.out.println("请求结果:");
String responseLine = inputStream.readLine();
while (responseLine == null || !responseLine.contains("HTTP")){
responseLine = inputStream.readLine();
}
while ((responseLine = inputStream.readLine()) != null){
System.out.println(responseLine);
}
}
}
另附Utils类:
public class IoUtils {
public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void closeSocket(Socket closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
先运行类ServerDemo,模拟服务端等待请求发起:
public class ServerDemo {
public static void main(String[] arqs){
new SimpleHttpServer().start();
}
}
在运行类ClientDemo,模拟客户端发起请求:
public class ClientDemo {
public static void main(String[] arqs){
HttpPost httpPost = new HttpPost("127.0.0.1");
httpPost.addParam("username","zj");
httpPost.addParam("pwd","123456");
httpPost.execute();
}
}
运行效果:
![](https://img.haomeiwen.com/i1895495/9cff8092f2b77fb4.png)
![](https://img.haomeiwen.com/i1895495/f3f2a7a42886167f.png)
如果从浏览器访问127.0.0.1:8080
![](https://img.haomeiwen.com/i1895495/ea0e811b5ffe18e4.png)
浏览器访问127.0.0.1:8080/test
![](https://img.haomeiwen.com/i1895495/dd882ee3ddd3ffb4.png)