Socket
Socket 又称套接字,实现了TCP/IP协议,应用程序可以通过 Socket 向网络发出请求或者应答网络请求。在Java中,Socket和ServerSocket类位于java.net包下。ServerSocket用于服务端,Socket则是建立网络连接时使用的,连接成功时,应用程序两端都会产生一个Scoket实例,操作这个实例,完成所需的会话,对于同一网络连接来说,套接字是平等的,无任何差别并不会因为在服务器端或者客户端而产生不同的级别,所以不管是Socket还是 ServerSocket它们的真正工作都是通过Socket类及其子类完成的。
TCP协议
HTTP通信都是由TCP/IP承载。TCP 是以太网协议(最底层的以太网协议Ethernet规定了电子信号如何组成数据包packet,解决了子网内部的点对点通信)和 IP 协议的上层协议,同时也是应用层协议的下层协议。而IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包,这就要依靠 TCP 协议。简而言之,TCP 协议的作用是保证数据通信的完整性和可靠性,防止丢包。
以太网数据包(packet)的大小是固定的,最初是1518字节,后面增加到1522字节甚至更大。其中 1500 字节是负载(payload),22字节是头信息(head)。IP 数据包在以太网数据包的负载里面,它也有自己的头信息,最少需要20字节,所以 IP 数据包的负载最多为1480字节。
如上所示IP 数据包在以太网数据包里面,而TCP 数据包则在 IP 数据包的负载里面且它的头信息最少也需要20字节,因此 TCP 数据包的最大负载是 1480 - 20 = 1460 字节。再加上 IP 和 TCP 协议往往有额外的头信息,所以 TCP 负载实际为1400字节左右。因此,一条1500字节的信息需要两个 TCP 数据包。
TCP的标志控制
以一个包的大小为1400字节为例,那么一次性发送大量数据,就必须分成多个包。(发送一个 10MB 的文件,就需要发送7100多个TCP包)因此为了方便接收的一方能按照顺序还原或者在发生丢包的时候明确丢了哪个包,在发送的时候,TCP 协议会为每个包进行编号(sequence number,简称 SEQ),通常第一个包的编号是一个随机数,而第二个包则会根据指定算法在第一个包的基础上计算得出第二个包的SEQ,假定第一个包的负载长度是100字节,那么可以推算出下一个包的编号应该是101,这样子每个数据包SEQ都可以得到两个编号——自身的编号以及下一个包的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件。
在收到 TCP 数据包以后,组装还原工作都是由操作系统内核去完成的。应用程序不会也不能直接处理 TCP 数据包。所以对于应用程序来说,不用关心数据通信的细节,除非线路异常,收到的总是完整的数据。而应用程序只需要按照“约定的格式”进行解析,TCP本身 并没有提供任何机制去表示原始文件的大小,这由应用层的协议来规定。比如我们常用的HTTP 协议就有一个头信息Content-Length,表示信息体的大小。对于操作系统来说只做一件事——就是持续地接收 TCP 数据包,将它们按照顺序组装好,一个包都不少(操作系统不会去处理 TCP 数据包里面的数据)一旦组装好 TCP 数据包,就把它们转交给应用程序,TCP 数据包里面有一个端口(port)参数就是用来指定转交给监听该端口的应用程序。以浏览器为例,浏览器收到组装好的原始数据,就会根据 HTTP 协议的Content-Length字段正确读出一段段的数据,然后解析并进行处理,一次 HTTP 通信会包含多个 TCP 通信。
理论上服务器发送数据包越快越好,最好一次性全发出去。但是发得太快或者带宽小、路由器过热、缓存溢出等其他客观因素影响,就有可能丢包。最理想的状态是,在线路允许的情况下达到最高速率。但是如何得知对方线路的理想速率是多少呢?答案就是慢慢试。TCP 协议为了做到效率与可靠性的统一,设计了一个慢启动(slow start)机制。开始的时候,发送得较慢,然后根据丢包的情况进行调整(如果不丢包,就加快发送速度;如果丢包,就降低发送速度)。在Linux 内核里面设定了(常量TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送10个数据包,然后停下来,等待接收方的确认,再继续发送。默认情况下,接收方每收到两个 TCP 数据包,就要发送一个确认消息ACK(全称acknowledgement),其中每个ACK 包含两个信息:
期待要收到下一个数据包的SEQ编号 接收方的接收窗口的剩余容量 发送方接到ACK消息,就得知了这两个信息,再加上自己已经发出的数据包的最新编号,就会推测出接收方大概的接收速度,从而降低或增加发送速率。这被称为"发送窗口",这个窗口的大小是可变的。
由于 TCP 通信是双向的,所以双方都需要发送 ACK。因为两方的窗口大小,很可能是不一样的。而且 ACK 只是很简单的几个字段,通常与数据合并在一个数据包里面发送。
TCP数据是通过名为IP数据报的小数据块来分发的,所以HTTP就是"HTTP over TCP over IP"这个协议栈的顶层了,而HTTPS则是HTTP的安全版本,即在HTTP和TCP之间插入了一个密码加密层(TLS或SSL)。
TCP连接一般是通过四个值来辨识的: <源IP地址 源端口号 目的IP地址 目的端口号> 这四个值一起可以唯一定义一条连接,两条不同的TCP连接不能拥有四个完全相同的地址组件值,但是不同连接的部分组件可以相同。
TCP连接过程是状态的转换,促使发生状态转换的是用户调用:OPEN,SEND,RECEⅣE,CLOSE,ABORT和STATUS。传送过来的数据段,特别那些包括以下标记的数据段SYN,ACK,RST和FIN。还有超时,上面所说的都会使TCP状态发生变化。
TCP 的三次握手
所谓的“三次握手”(three times handshake或者three-way handshake)是对每次发送的数据量是怎样跟踪进行协商使数据段的发送和接收同步,根据所接收到的数据量而确定的数据确认数及数据发送、接收完毕后何时撤消联系,并建立虚连接。为了提供可靠的传输,TCP在发送新的数据之前,会以特定的顺序将数据包编号Seq,并在接到这些包传送到目标机之后的确认消息Ack才开始进行数据传输。因此TCP总是用来发送大批量的数据,当应用程序在收到数据后要做出确认时也要用到TCP。简而言之,发起网络请求时,会首先建立起底层的TCP连接,在客户端与服务器建立TCP连接的时候会进行三次握手;而在断开TCP连接的时候需要进行四次挥手,简记连三断四。最初两端的TCP进程都处于CLOSED关闭状态,而S端程序启动并自动创建TCB 开始监听,随时等待响应C端的请求。
1、第一次握手
C端的TCP进程主动打开连接创建TCB,向C端将标志位SYN置为1,随机产生一个值Seq=x([SYN] Seq=x,即头部的同步位SYN=1,初始序号Seq=x),并将该数据包发送给S端请求连接(不过SYN=1的报文段不能携带数据,但要消耗掉一个SEQ)进入SYN_SENT(syn package has been sent)状态,完成第一次握手,等待S端确认。
2、 第二次握手
S端收到请求报文段(由标志位SYN=1得知是C端请求建立连接)同意之后被动打开连接,马上向C端发送确认包([SYN,ACK] Ack=x+1,Seq=y)S端进入到SYN_RCVD状态(syn package has been received) ,完成第二次握手。
3、第三次握手
C端接到S端的确认包(会首先检查是否是回应自己第一次握手发的请求包,即会检查Ack是否等于x+1,如果确认操作系统则为改TCP连接分配缓存和变量)之后再向S端发送确认包([ACK] Ack=y+1,Seq=x+1)(ACK报文段可以携带数据,不携带数据则不消耗Seq),S端也会检查(检查是否是回应自己第二次握手发的确认包,即会检查Ack是否等于y+1,如果确认操作系统则为改TCP连接分配缓存和变量),完成第三次握手,TCP连接已经建立,C端进入ESTABLISHED状态,最后S端收到C端的确认包后也进入ESTABLISHED状态,双方开始再次利用TCP包进行数据传输。
Wireshark观察网络包
显示过滤
一种是显示过滤器,就是主界面上那个,用来在捕获的记录中找到所需要的记录
一种是捕获过滤器,用来过滤捕获的封包,以免捕获太多的记录。 在Capture -> Capture Filters 中设置
过滤表达式
表达式规则
- 协议过滤
比如TCP,只显示TCP协议。
- IP 过滤
比如 ip.src ==192.168.1.102 显示源地址为192.168.1.102,
ip.dst==192.168.1.102, 目标地址为192.168.1.102
- 端口过滤
tcp.port ==80, 端口为80的
tcp.srcport == 80, 只显示TCP协议的源端口为80的。
- Http模式过滤
http.request.method=="GET", 只显示HTTP GET方法的。
- 逻辑运算符为 AND/ OR
常用的过滤表达式
封包详细信息
Frame: 物理层的数据帧概况
Ethernet II: 数据链路层以太网帧头部信息
Internet Protocol Version 4: 互联网层IP包头部信息
Transmission Control Protocol: 传输层T的数据段头部信息,此处是TCP
Hypertext Transfer Protocol: 应用层的信息,此处是HTTP协议
wireshark捕获到的TCP包中的每个字段:
实例分析TCP三次握手过程
打开wireshark, 打开浏览器输入:chendongqi.me/
在wireshark中输入http.request.method=="GET"过滤, 然后选中GET / HTTP/1.1的那条记录,右键然后点击"Follow TCP Stream",
这样做的目的是为了得到与浏览器打开网站相关的数据包,将得到如下图:
图中可以看到wireshark截获到了三次握手的三个数据包。第四个包才是HTTP的, 这说明HTTP的确是使用TCP建立连接的。
第一次握手数据包
客户端发送一个TCP,标志位为SYN,序列号为0, 代表客户端请求建立连接。 如下图
第二次握手的数据包
服务器发回确认包, 标志位为 SYN,ACK. 将确认序号(Acknowledgement Number)设置为客户的I S N加1以.即0+1=1, 如下图
第三次握手的数据包
客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1, 如下图:
C、S端初始关闭状态CLOSED,接着S端监听状态LISTEN,TCP三次握手流程可以简化为:
发送SYN包到服务器并进入SYN_SENT状态,等待确认 服务器收到并确认,再发送一个SYN包(即SYN+ACK包),进入SYN_RECV状态; 客户端收到服务器的SYN+ACK包并向服务器发送确认包,此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
TCP 的数据传输
为什么要进行三次握手?为什么C端还要向S端发送一次确认包呢?
第一次由C端发起的握手,如果S端接到请求,则说明C端的发送能力没有问题。但是仅仅是知道C端的发送能力,还需要确认C端的接收能力,因而S端会向C端发送第二次握手,此时C端就确认了两件事,S端发送和接收能力都没有问题以及自身的发送能力,但是呢S端并不知道C端的发送能力,所以需要第三次握手,而且三次握手机制,还可以防止已失效的连接请求报文段突然又传送到了S端,因而产生错误。假如在C端发出连接请求,但因连接请求报文暂时丢失而导致S端未收到确认,于是C端再重传一次连接请求,第二次S端接收到并进行了确认,建立了连接进行数据传输,完毕后,就释放了连接。而在这种情况下C端向S端发出了两个连接请求报文段,其中第一个暂时丢失,第二个到达了S端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达S端,此时S端会误认为C端又发出一次新的连接请求,于是就向C端发出确认报文段,而此时C端已经不需要与S端通信了,S端会一直等待C端发送数据,因此不采用三次握手的话,只要S端发出确认,就建立新的连接了,有可能导致资源浪费。
Server端易受到SYN攻击?
S端的资源分配是在第二次握手时分配的,而客户端的资源是在完成第三次握手时才分配的,所以服务器容易受到SYN洪泛攻击,SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向S端不断地发送SYN包,S端则回复确认包,并等待Client确认,由于源地址不存在,因此S端需要不断重发直至超时,而这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。可以通过降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某IP的重复SYN则丢弃后续请求来防范SYN攻击。
TCP“四”次挥手
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接,收到一个 FIN只意味着这一方向上没有数据流动,而一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。HTTP是短连接的协议,所以每次通信完成时,TCP都会释放连接,在释放TCP连接的时候会发送四次挥手。
举个例子假设S端发起中断连接请求(即发送FIN报文),C端接到FIN报文后,就知道:“S端没有数据要发给自己了”,但是如果C端还有数据未处理完成就不必马上关闭socket,还可以继续发送数据,但你先发送ACK告知S端"你的请求我收到了,但是我还没准备好,请继续你等我的消息"。此时S端就进入FIN_WAIT状态,继续等待C端的FIN报文。当C端确定数据已处理完毕不会再向S端发送数据时才向S端发送FIN报文,告知S端"好了,我这边数据发完了,准备好关闭连接了"。但是C端不确定S端是否已经收到了,所以会在收到S端的ACK之后才会进入CLOSED状态,而S端则继续等待了2MSL后依然没有收到回复,则证明C端已正常关闭,于是S端也就关闭了,TCP连接释放。
1、TCP的第一次挥手
首先是主动断开连接方A会向被动断开连接方B发送连接释放包([FIN,ACK] Seq=j Ack=k),并停止再向B发送数据,A进入FIN_WAIT_1(完成等待1)状态,等待B的确认…
2、TCP的第二次挥手
B方接到连接释放包之后,立马响应并向A方发送确认包([ACK] Seq=m Ack=j+1),B端进入CLOSE_WAIT(关闭等待)状态,而A收到B的确认后,进入FIN_WAIT_2(完成等待2)状态,等待B发出的连接释放报文段,此时的TCP处于半关闭状态(即单向关闭)…
3、TCP的第三次挥手
在B方确认自己不再向A方发送任何数据之后,马上向A方法发送连接释放包([FIN,ACK] Seq=z ,Ack=j+1)告知A自己也不再向A发送任何数据了,B进入LAST_ACK(最后确认)状态,等待A的确认…
4、TCP的第四次挥手
A收到B的连接释放报文段后立即响应发出确认包([ACK] Seq=j+1,Ack=z+1),A进入TIME_WAIT(时间等待)状态,此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,A才进入CLOSED状态。
最长报文寿命MSL(maximium segment lifetime),由官方RFC协议规定的2个MSL就是2分钟。
注意:四次挥手也并不总是四次挥手,中间的两个动作有时候是可以合并一起进行的,这个时候就成了三次挥手,主动关闭方就会从fin_wait_1状态直接进入到time_wait状态,跳过了fin_wait_2状态。而且前面所述的算法无论是分包还是发包都是由底层操作系统的协议栈去实现的,对于目前绝大部分的很多所谓的自定义协议都是在TCP/UDP的基础上再封装而已。
TCP的“四”次挥手可以简化为(以S端主动断开连接为例):C、S端初始ESTABLISHED状态——>S端主动断开向C端发送FIN包,S端进入FIN_WAIT_1状态——>C端马上响应发送ACK包进入CLOSE_WAIT状态——>S端接到C端的响应ACK包进入FIN_WAIT_2状态——>C端确认不再发送任何数据到S端,向S端发送FIN包,进入LAST_ACK状态——>C端接到S端的响应进入CLOSE状态——>S端等待2MLS之后进入CLOSE状态。
为什么A在TIME-WAIT状态必须等待2MSL的时间?
它是主动关闭的一方在回复完对方的挥手后进入的一个长期状态,这个状态标准的持续时间是4分钟,4分钟后才会进入到closed状态,释放套接字资源。一方面可以保证重传最后一个ack报文,确保对方可以收到。因为如果对方没有收到ack的话,会重传fin报文,处于time_wait状态的套接字会立即向对方重发ack报文;另一方面在这段时间内,该链接在对话期间于网际路由上产生的残留报文(因为路径过于崎岖,数据报文走的时间太长,重传的报文都收到了,原始报文还在路上)传过来时,都会被立即丢弃掉。4分钟的时间足以使得这些残留报文彻底消逝,防止“已失效的连接请求报文段”出现在本连接中可能会干扰新的链接。
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手,首先主动断开连接A端向被动断开连接B端发送FIN包开始第一次挥手,以A端为服务器,B端为客户端为例,客户端接到这个FIN包之后就明确知道了服务端不再向自己发送数据了,客户端马上发送确认包告知服务器已收到了执行第二次挥手,但是可能由于此时客户端的数据还未处理完成,在第二次挥手之后还需要经过一些清理等步骤,因此只有在确定不需要向服务端发送任何数据之后才会向服务端发送FIN包进行第三次挥手,而服务器接到FIN包就之后就知道客户端再也不会像自己发送任何数据了,就把响应包发送给客户端完成第四次挥手。
HTTP 请求解析流程概述
首先经过DNS 协议映射到对应的IP地址 再经过TCP协议分割,并按照序列号可靠发出 借助IP协议,,通过路由器得到对应的接收IP地址 然后经过TCP协议接收分割后得到的报文段并重组 最终经过HTTP协议,返回服务器处理报文后的资源
媒体资源类型MIME
由于因特网上有很多种不同类型的数据、资源,HTTP为了区分正确处理不同的传输对象,因此需要标记传输对象的类型。
HTTP请求命令
HTTP报文
HTTP是通过报文来传输数据的,因而HTTP请求从一定程度上来说是不安全的,而HTTP报文本质上是一行行的简单文本格式的字符串构成的格式化文本,每一行都以两个字符——\r回车符(ASCII码13)和\n换行符(ASCII码 10)来组成的行终止标记序列CRLF。
通常按照HTTP报文的类别又可以分为两种类型:请求报文和响应报文,不过两种报文基本机构大同小异都包含三大部分:起始行、首部、主体。
使用原生Socket 完成HTTP简单通信
此处我们使用的是HTTP协议进行通信,所以得根据HTTP协议中约定的URL的基本语法。形如: schemal " : //" “internet adress” [:port] “/resource_file_name” 形式来构造HttpURL,这也是HTTP请求头的数据来源。
public class SocketTest {
public static void main(String[] args) throws MalformedURLException {
// //高德地图获取天气api 响应体使用Content-Length
// final HttpUrl url = new HttpUrl("http://restapi.amap.com/v3/weather/weatherInfo?city=上海&key=ccbf1d251595efa936df0ba784346902");
// //快递100查询 api 响应体使用分块编码的api
// //final HttpUrl url=new HttpUrl("http://www.kuaidi100.com/query?type=shunfeng&postid=8989");
// System.out.println("host:" + url.getHost());
// System.out.println("protocol:" + url.getProtocol());
// System.out.println("port:" + url.getPort());
// System.out.println("path:" + url.getPath());
HttpUrl http = HttpUrl.builder()
.protocol("http")
.host("restapi.amap.com")
.port(80)
.path("/v3/weather/weatherInfo?city=上海&key=ccbf1d251595efa936df0ba784346902")
.build();
StringBuffer buffer = HttpUrlMaker.createRequestPacket(http);
Socket socket = new Socket();
InputStream inputStream = null;
OutputStream outputStream = null;
AtomicBoolean isDone = new AtomicBoolean(false);
try {
//通过端口号与指定Host的主机建立了连接
socket.connect(new InetSocketAddress(http.getHost(), http.getPort()), 5000);
//建立了Sockect 连接之后就可以通过对应的方法获取输入输出流,其中输入流是用于读取服务器的响应数据;而输出流则是客服端发送数据给服务器的
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
System.out.println("开始发送报文... \n" + buffer);
HttpUrlMaker.sendRequest(buffer, outputStream);
new Thread(new AnalyzeHttpRunnable(inputStream, outputStream, isDone)).start();
while (true) {
Thread.sleep(1000 * 10);
if (isDone.get()) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("Done!");
}
static class AnalyzeHttpRunnable implements Runnable {
private InputStream inputStream;
private OutputStream outputStream;
private AtomicBoolean isDone;
public AnalyzeHttpRunnable(InputStream inputStream, OutputStream outputStream, AtomicBoolean isDone) {
this.inputStream = inputStream;
this.outputStream = outputStream;
this.isDone = isDone;
}
@Override
public void run() {
HttpCodec httpCodec = new HttpCodec();
try {
//解析响应行
String responseLine = httpCodec.readLine(inputStream);
System.out.println("响应行:" + responseLine);
System.out.println("响应头:");
//解析响应头
Map<String, String> headers = httpCodec.readHeaders(inputStream);
for (Map.Entry<String, String> entry : headers.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
//解析Content-Length响应体
if (headers.containsKey("Content-Length")) {
int length = Integer.valueOf(headers.get("Content-Length"));
byte[] bytes = httpCodec.readBytes(inputStream, length);
System.out.println("\n响应体:" + new String(bytes));
} else {
//分块编码
String response = httpCodec.readChunked(inputStream);
System.out.println("分块响应体:" + response);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream!=null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream!=null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
isDone.compareAndSet(false, true);
}
}
}
}
public class HttpUrlMaker {
/**
* 创建Http请求报文
* @param url
* @return
*/
public static StringBuffer createRequestPacket(HttpUrl url) {
StringBuffer buffer=new StringBuffer();
//构造请求行
buffer.append(HttpConst.GET);
buffer.append(url.getPath());
buffer.append(" ");
buffer.append("HTTP/1.1");
buffer.append(HttpConst.CRLF);
//请求头
buffer.append(HttpConst.HOST);
buffer.append(url.getHost());
buffer.append(HttpConst.CRLF);
//请求体,这个请求可以为空所以。。。
buffer.append(HttpConst.CRLF);
return buffer;
}
public static void sendRequest(StringBuffer buffer, OutputStream outputStream) throws IOException {
outputStream.write(buffer.toString().getBytes());
outputStream.flush();
}
}
/**
* Http报文解析类
* Created by zpw on 2019/5/13.
*/
public class HttpCodec {
ByteBuffer byteBuffer;
public HttpCodec() {
//申请足够大的内存记录读取的数据 (一行)
this.byteBuffer = ByteBuffer.allocate(10 * 1024);
}
/**
* @param inputStream
* @return
* @throws IOException
*/
public String readLine(InputStream inputStream) throws IOException {
try {
byte b;
boolean isMaybeEofLine = false;
//标记
byteBuffer.clear();
byteBuffer.mark();
while ((b = (byte) inputStream.read()) != -1) {
byteBuffer.put(b);
// 读取到/r则记录,判断下一个字节是否为/n
if (b == HttpConst.CR) {
isMaybeEofLine = true;
} else if (isMaybeEofLine) {
//上一个字节是/r 并且本次读取到/n
if (b == HttpConst.LF) {
//获得目前读取的所有字节
byte[] lineBytes = new byte[byteBuffer.position()];
//返回标记位置
byteBuffer.reset();
byteBuffer.get(lineBytes);
//清空所有index并重新标记
byteBuffer.clear();
byteBuffer.mark();
return new String(lineBytes);
}
isMaybeEofLine = false;
}
}
} catch (IOException e) {
e.printStackTrace();
}
throw new IOException("Response Read Line.");
}
/**
* 用于解析头部
*
* @param inputStream
* @return
* @throws IOException
*/
public Map<String, String> readHeaders(InputStream inputStream) throws IOException {
Map<String, String> headers = new HashMap<>();
while (true) {
String line = readLine(inputStream);
//读取到空行 则下面的为body
if (isEmptyLine(line)) {
break;
}
int index = line.indexOf(":");
if (index > 0) {
String name = line.substring(0, index);
// ": "移动两位到 总长度减去两个("\r\n")
String value = line.substring(index + 2, line.length() - 2);
headers.put(name, value);
}
}
return headers;
}
/**
* @param inputStream
* @param length
* @return
* @throws IOException
*/
public byte[] readBytes(InputStream inputStream, int length) throws IOException {
byte[] bytes = new byte[length];
int readNum = 0;
while (true) {
readNum += inputStream.read(bytes, readNum, length - readNum);
//读取完毕
if (readNum == length) {
break;
}
}
return bytes;
}
public String readChunked(InputStream inputStream) throws IOException {
int len = -1;
boolean isEmptyData = false;
StringBuffer buffer = new StringBuffer();
while (true) {
if (len < 0) {
String line = readLine(inputStream);
//减掉\r\n
line.substring(0, line.length() - 2);
//Chunked 编码最后一段数据为0 \r\n\r\n
len = Integer.valueOf(line, 16);
isEmptyData = len == 0;
} else {
//块的长度不包括\r\n 所以加2 将\r\n读走
byte[] bytes = readBytes(inputStream, len + 2);
buffer.append(new String(bytes));
len = -1;
if (isEmptyData) {
return buffer.toString();
}
}
}
}
public boolean isEmptyLine(String line) {
return line == null || line.equals("\r\n");
}
}
public class HttpUrl {
private String protocol;
private String host;
private String path;
private int port;
//建造者方式初始化
private HttpUrl(String protocol, String host, String path, int port) {
this.protocol = protocol;
this.host = host;
this.path = path;
this.port = port;
}
public static HttpUrlBuilder builder() {
return new HttpUrlBuilder();
}
public static class HttpUrlBuilder {
private String protocol;
private String host;
private String path;
private int port;
private HttpUrlBuilder() {
}
public HttpUrlBuilder protocol(String protocol) {
this.protocol = protocol;
return this;
}
public HttpUrlBuilder host(String host) {
this.host = host;
return this;
}
public HttpUrlBuilder path(String path) {
this.path=path;
return this;
}
public HttpUrlBuilder port(int port) {
this.port=port;
return this;
}
public HttpUrl build() {
return new HttpUrl(protocol, host, path, port);
}
}
/**
* scheme://host:port/path?query#fragment
*
* @param path 传入完整的请求 比如http://restapi.amap.com/v3/weather/weatherInfo?city=上海&key=ccbf1d251595efa936df0ba784346902
* @throws MalformedURLException
*/
public HttpUrl(String path) throws MalformedURLException {
URL url = new URL(path);
this.host = url.getHost();
this.path = url.getFile();
this.path = (this.path == null || this.path.length() == 0) ? "/" : this.path;
this.protocol = url.getProtocol();
this.port = url.getPort();
port = port == -1 ? url.getDefaultPort() : port;
}
public String getProtocol() {
return protocol;
}
public String getHost() {
return host;
}
public String getPath() {
return path;
}
public int getPort() {
return port;
}
}
public class HttpConst {
//回车和换行
public static String CRLF = "\r\n";
//"/r"
public static int CR = 13;
public static int LF = 10;
public static String GET="GET ";
public static String POST="POST ";
public static String HOST="Host: ";
}
HTTPS和SSL握手
HTTPS=HTTP+SSL,SSL是HTTPS的安全保障,HTTPS是在HTTP的基础上加上了SSL安全加密层,把原来的明文数据进行加密之后再进行传输,如下图:
为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL\TSL协议,SSL\TSL依靠CA证书来验证服务器的身份,并为Client和Server此次通信进行加密:
SSL\TSL 概述
安全套接层SSL(Secure Sockets Layer )或传输层安全TLS(Transport Layer Security)是为网络通信提供安全及数据完整性的一种安全协议,用于在传输层对此次网络连接采用对称算法或非对称算法进行加密,保证信息安全,SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持,SSL协议可分为两层:
-
SSL记录协议(SSL Record Protocol)——它建立在可靠的传输协议(比如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。
-
SSL握手协议(SSL Handshake Protocol)——它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
采用HTTPS进行的网络通信大致流程:Client在向服务器发送数据前,先使用与服务器协商好的加密算法进行加密处理,然后才向服务器传输,Server端在接到数据之后,也会先进行解密再作响应,则如下图所示:
SSL协议的工作流程
服务器认证阶段
- 客户端向服务器发送一个开始信息“Hello”以便开始一个新的会话连接;
- 服务器根据客户的信息确定是否需要生成新的主密钥,如需要则服务器在响应客户的“Hello”信息时将包含生成主密钥所需的信息;
- 客户根据收到的服务器响应信息,产生一个主密钥,并用服务器的公开密钥加密后传给服务器;
- 服务器恢复该主密钥,并返回给客户一个用主密钥认证的信息,以此让客户认证服务器。
用户认证阶段
在此之前,服务器已经通过了客户认证,这一阶段主要完成对客户的认证。经认证的服务器发送一个提问给客户,客户则返回(数字)签名后的提问和其公开密钥,从而向服务器提供认证。
握手流程
SSL 协议既用到了公钥加密技术又用到了对称加密技术,对称加密技术虽然比公钥加密技术的速度快,可是公钥加密技术提供了更好的身份认证技术。SSL 的握手协议非常有效的让客户和服务器之间完成相互之间的身份认证,其主要过程如下:
-
客户端向服务器传送客户端SSL 协议的版本号,支持的加密算法种类,产生的随机数
-
服务器向客户端传送SSL 协议的版本号、选择的加密算法,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。
-
客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性(包括证书是否过期,发行服务器证书的CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配)。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。
-
客户端随机产生一个用于加密通讯的“对称密码”并用服务器的公钥(服务器的公钥从第二步中的服务器的证书中获得)对其加密再将加密后的“预主密码”传给服务器。
-
如果服务器要求客户端的进行身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的“预主密码”一起传给服务器。
-
如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性包括(客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥能否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表CRL中)检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。
-
服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通讯的加解密通讯。同时在SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。
-
客户端向服务器端发出信息,指明后面的数据通讯将使用的第七步中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
-
服务器向客户端发出信息,指明后面的数据通讯将使用的第七步中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
-
SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。
/**
* 仅信任本机预置的所有CA证书
*
* @param url
* @return
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws KeyManagementException
* @throws IOException
*/
public static Socket createHttpsConnection(HttpUrl url) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
//获取SSLContext 对象,将来用于获取创建支持SSL 协议的Socket对象
SSLContext sslContext = SSLContext.getInstance("TLS");
//服务器要使用HTTPS 必须要有CA证书
//数字证书,相当于是服务器的"身份证"一样,客户端进行通信之前需要首先确认是否信任这个"身份证"对应的服务器,后缀通常是.crt 或者.cer
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//传入null,表示信任本机(此处我是在Java 本地单元测试则代表PC 机本身,如果在手机中则代表手机中的所有CA证书)系统下预置所有的CA证书(权威CA 机构发布的证书)
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
//通过信任证书管理器对SSLContext对象进行初始化配置,之后才能用于获取SSLSocketFactory
sslContext.init(null, trustManagers, null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
//创建SSLSocket 默认端口443
Socket socket = socketFactory.createSocket();
socket.connect(new InetSocketAddress(url.getHost(), url.getPort()));
return socket;
}
/**
* 信任所有的CA证书,未校验证书的合法性
*
* @param url
* @return
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
* @throws IOException
*/
public static Socket createHttpsConnection2(HttpUrl url) throws NoSuchAlgorithmException, KeyManagementException, IOException {
//获取SSLContext 对象,将来用于获取创建支持SSL 协议的Socket对象
SSLContext sslContext = SSLContext.getInstance("TLS");
//这会信任所有的证书,包括自定义的还有预置,未校验证书的合法性,容易受到中间人攻击
sslContext.init(null, new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}, null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
//创建SSLSocket 默认端口443
Socket socket = socketFactory.createSocket();
socket.connect(new InetSocketAddress(url.getHost(), url.getPort()));
return socket;
}
/**
* 仅信任设置的自定义证书
*
* @param url
* @return
* @throws KeyStoreException
* @throws CertificateException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
public static Socket createHttpsConnection3(HttpUrl url) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, KeyManagementException {
//使用Java提供的KeyStore 证书库api 对自定义证书进行设置
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
//通过输入流生成对应的CA证书
Certificate certificate = certificateFactory.generateCertificate(new FileInputStream("c:\\\\xxx\\\\Xxx\"+\"\\\\crazy.cer"));
keyStore.load(null);
//第一个参数加密的参数
keyStore.setCertificateEntry("cmo", certificate);
//数字证书,相当于是服务器的"身份证"一样,客户端进行通信之前需要首先确认是否信任这个"身份证"对应的服务器,后缀通常是.crt 或者.cer
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//传入的自定义证书,在进行通信时会自动地进行校验
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
//获取SSLContext 对象,将来用于获取创建支持SSL 协议的Socket对象
SSLContext sslContext = SSLContext.getInstance("TLS");
//通过信任证书管理器对SSLContext对象进行初始化配置,之后才能用于获取SSLSocketFactory
sslContext.init(null, trustManagers, null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
////创建SSLSocket 默认端口443
Socket socket = socketFactory.createSocket();
socket.connect(new InetSocketAddress(url.getHost(), url.getPort()));
return socket;
}
/**
* 不仅使用自定义的证书进行Https 通信,还支持系统的预置证书
*
* @param url
* @return
* @throws KeyStoreException
* @throws CertificateException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
public static Socket createHttpsConnection4(HttpUrl url) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, KeyManagementException {
//使用Java提供的KeyStore 证书库api 对自定义证书进行设置
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
//通过输入流生成对应的CA证书
Certificate certificate = certificateFactory.generateCertificate(new FileInputStream("c:\\xxx\\Xxx" + "\\crazy.cer"));
keyStore.load(null);
keyStore.setCertificateEntry("cmo", certificate);
//自定义证书认证流程得到信任管理器
X509TrustManager trustManager = new MyX509TrustManager((X509Certificate) certificate);
//获取SSLContext 对象,将来用于获取创建支持SSL 协议的Socket对象
SSLContext sslContext = SSLContext.getInstance("TLS");
//通过信任证书管理器对SSLContext对象进行初始化配置,之后才能用于获取SSLSocketFactory
//TODO init方法第一个参数是自己的证书(对应服务端中的第二个参数),第二个是信任的服务器证书(对应服务端中的第一个参数),双向认证很简单,只需要在init的时候传入客户端自己的证书,然后服务端再需要把客户端的参数作为第二个参数传入而把服务端自己的参数作为第一个参数传入即可
sslContext.init(null, new TrustManager[]{trustManager}, null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
////创建SSLSocket 默认端口443
Socket socket = socketFactory.createSocket();
socket.connect(new InetSocketAddress(url.getHost(), url.getPort()));
return socket;
}
public static class MyX509TrustManager implements X509TrustManager {
X509Certificate mCertificate;
public MyX509TrustManager(X509Certificate certificate) {
this.mCertificate = certificate;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
//先去校验系统预置的CA证书
TrustManagerFactory factory = TrustManagerFactory.getInstance("X.509");
factory.init((KeyStore) null);
X509TrustManager trustManager = (X509TrustManager) factory.getTrustManagers()[0];
//若没有报异常则说明访问的服务器拥有的预置的CA机构证书
trustManager.checkServerTrusted(chain, authType);
} catch (Exception e) {
try {
//只校验了证书是否与服务器的公钥一致
chain[0].verify(mCertificate.getPublicKey());
System.out.println("校验成功");
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
} catch (SignatureException e1) {
e1.printStackTrace();
} catch (NoSuchProviderException e1) {
e1.printStackTrace();
} catch (InvalidKeyException e1) {
e1.printStackTrace();
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}