Http 方面的知识平常也会遇到,但是一般了解的也不深,今天记录几个常遇到的知识点。
Http 报文
http 报文可以分为请求报文和响应报文,格式大同小异,主要分为三个部分:
- 起始行
- 首部
- 主体
请求报文格式:
1 | <method> <request-url> <version> |
响应报文格式:
1 | <version> <status> <reason-phrase> |
各个标签的解释:
1 | <method> 指请求方法,常用的主要是Get、 Post、Head 还有其他一些我们这里就不说了,有兴趣的可以自己查阅一下 |
method
状态码
常见的状态码主要有
200 OK 请求成功,实体包含请求的资源
301 Moved Permanent 请求的URL被移除了,通常会在Location首部中包含新的URL用于重定向。
304 Not Modified 条件请求进行再验证,资源未改变。
404 Not Found 资源不存在
206 Partial Content 成功执行一个部分请求。这个在用于断点续传时会涉及到。
header
在请求报文和响应报文中都可以携带一些信息,通过与其他部分配合,能够实现各种强大的功能。这些信息位于起始行之下与请求实体之间,以键值对的形式,称之为首部。每条首部以回车换行符结尾,最后一个首部额外多一个换行,与实体分隔开。
这里我们重点关注一下
Date
Cache-Control
Last-Modified
Etag
Expires
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match
实体
Http 缓存
当我们发起一个http请求后,服务器返回所请求的资源,这时我们可以将该资源的副本存储在本地,这样当再次对该url资源发起请求时,我们能快速的从本地存储设备中获取到该url资源,这就是所谓的缓存。缓存既可以节约不必要的网络带宽,又能迅速对http请求做出响应。
先摆出几个概念:
- 新鲜度检测
- 再验证
- 再验证命中
- 新鲜度检测
我们需要通过检测资源是否超过一定的时间,来判断缓存资源是否新鲜可用。那么这个一定的时间怎么决定呢?其实是由服务器通过在响应报文中增加Cache-Control:max-age
,或是Expire
这两个首部来实现的。值得注意的是 Cache-Control 是 http1.1 的协议规范,通常是接相对的时间,即多少秒以后,需要结合last-modified
这个首部计算出绝对时间。而 Expire 是 http1.0 的规范,后面接一个绝对时间。- 再验证
如果通过新鲜度检测发现需要请求服务器进行再验证,那么我们至少需要告诉服务器,我们已经缓存了一个什么样的资源了,然后服务器来判断这个缓存资源到底是不是与当前的资源一致。逻辑是这样没错。那怎么告诉服务器我当前已经有一个备用的缓存资源了呢?我们可以采用一种称之为条件请求
的方式实现再验证。- Http定义了5个首部用于条件请求:
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match
Https
简单的说 Http + 加密 + 认证 + 完整性保护 = Https
传统的 Http 协议是一种应用层的传输协议,Http 直接与 TCP 协议通信,其本身存在一些缺点:
- Http协议使用明文传输,容易遭到窃听。
- Http 对于通信双方都没有进行身份验证,通信双方无法确认对方是否是伪装的客户端或者服务端。
- Http 对于传输内容的完整性没有确认的办法,往往容易在传输的过程中遭到劫持篡改。
因此,在一些需要保证安全性的场景下,Http 无法抵御这些攻击。
Https 则可以听过增加的 SSL\TLS,支持对于通信内容的加密,以及通信双飞的身份验证。
Https 加密
近代密码学中加密的方式主要有两类:
- 对称秘钥加密
- 非对称秘钥加密
对称秘钥加密是指加密与解密过程使用同一把秘钥。这种方式的优点是处理速度快,但是如何安全的从一方将秘钥传递到通信的另一方是一个问题。
非对称秘钥加密是指加密与解密使用两把不同的秘钥。这两把秘钥,一把叫公开秘钥,可以随意对外公开。一把叫私有秘钥,只用于本身持有。得到公开秘钥的客户端可以使用公开秘钥对传输内容进行加密,而只有私有秘钥持有者本身可以对公开秘钥加密的内容进行解密。这种方式克服了秘钥交换的问题,但是相对于对称秘钥加密的方式,处理速度较慢。
SSL\TLS的加密方式则是结合了两种加密方式的优点。首先采用非对称秘钥加密,将一个对称秘钥使用公开秘钥加密后传输到对方。对方使用私有秘钥解密,得到传输的对称秘钥。之后双方再使用对称秘钥进行通信。这样即解决了对称秘钥加密的秘钥传输问题,又利用了对称秘钥的高效率来进行通信内容的加密与解密。
Https 的认证
SSL\TLS采用的混合加密的方式还是存在一个问题,即怎么样确保用于加密的公开秘钥确实是所期望的服务器所分发的呢?也许在收到公开秘钥时,这个公开秘钥已经被别人篡改了。因此,我们还需要对这个秘钥进行认证的能力,以确保我们通信的对方是我们所期望的对象。
目前的做法是使用由数字证书认证机构颁发的公开秘钥证书。服务器的运营人员可以向认证机构提出公开秘钥申请。认证机构在审核之后,会将公开秘钥与共钥证书绑定。服务器就可以将这个共钥证书下发给客户端,客户端在收到证书后,使用认证机构的公开秘钥进行验证。一旦验证成功,即可知道这个秘钥是可以信任的秘钥。
Https的通信流程:
- Client发起请求
- Server端响应请求,并在之后将证书发送至Client
- Client使用认证机构的共钥认证证书,并从证书中取出Server端共钥。
- Client使用共钥加密一个随机秘钥,并传到Server
- Server使用私钥解密出随机秘钥
- 通信双方使用随机秘钥最为对称秘钥进行加密解密。
TCP/IP 协议简介
IP
IP(Internet Protocol)协议提供了主机和主机间的通信。
为了完成不同主机的通信,我们需要某种方式来唯一标识一台主机,这个标识就是著名的 IP 地址。通过 IP 地址,IP 协议就能帮我们把一个数据包发送给对方。
TCP
上面说到,IP 协议提供了主机与主机间的通信。TCP 协议在 IP 协议提供的主机间通信功能的基础上,完成了两个主机上进程对进程的通信。
有了 IP,不同主机可以进行通信。但是计算机收到数据后,并不知道数据属于哪个进程(简单讲,进程就是一个正在运行的应用程序)。TCP的作用就在于,让我们知道这个数据属于哪个进程,从而完成进程间的通信。
为了标识数据属于哪个进程,我们给需要进行 TCP 通信的进程分配一个唯一的数字来标识它。这个数字就是我们常说的端口号。
TCP 的全称是 Transmission Control Protocol,大家对它说的最多的,大概是面向连接的特性了。之所以说它是有连接的,是说进行通信前,通信双方需要先经过一个三次握手的过程。三次握手完成后,连接便建立了。这时候我们才可以接受/发送数据。(与之对应的 UDP,不需要经过握手,就可以直接发送数据)。
三次握手过程:
- 首先,客户向服务端发送一个
SYN
,假设此时 sequence number 为x
。这个x
是由操作系统根据一定的规则生成的,不妨认为它是一个随机数。 - 服务端收到
SYN
后,会向客户端再发送一个SYN
,此时服务器的seq number = y
。与此同时,会ACK x+1
,告诉客户端“已经收到了SYN
,可以发送数据了”。 - 客户端收到服务器的
SYN
后,回复一个ACK y+1
,这个ACK
则是告诉服务器,SYN
已经收到,服务器可以发送数据了。
经过这 3 步,TCP 连接就建立了。这里需要注意的有三点:
- 连接是由客户端主动发起的
- 在第 3 步客户端向服务器回复
ACK
的时候,TCP 协议是允许我们携带数据的。之所以做不到,是 API 的限制导致的。 - TCP 协议还允许 “四次握手” 的发生,同样的,由于 API 的限制,这个极端的情况并不会发生。
Socket 基本用法
Socket 是 TCP 层的封装,通过 Socket,我们就能进程 TCP 通信。
在 Java 的 SDK 中,socket 的共有两个接口:用于监听客户连接的 ServerSocket 和用于通信的 Socket。使用 socket 的步骤如下:
- 创建 ServerSocket 并监听客户连接
- 创建 Socket 连接服务端
- 通过 Socket 获取输入输出流进行通信
Socket Demo:
1、创建 ServerSocket 并监听客户连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class EchoServer {
private final ServerSocket mServerSocket;
public EchoServer(int port) throws IOException {
// 1. 创建一个 ServerSocket 并监听端口 port
mServerSocket = new ServerSocket(port);
}
public void run() throws IOException {
// 2. 开始接受客户连接
Socket client = mServerSocket.accept();
handleClient(client);
}
private void handleClient(Socket socket) {
// 3. 使用 socket 进行通信 ...
}
public static void main(String[] argv) {
try {
EchoServer server = new EchoServer(9877);
server.run();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、使用 Socket 连接服务端
1 | public class EchoClient { |
3、通过 Socket 获取输入/输出流进行通信
首先,我们来实现服务端(服务端就是不停的输入数据,然后写会给客户端):
1 | public class EchoServer { |
接下来,是客户端的实现(客户端在读取用户输入的同时,又会读取服务器的响应,所以需要创建一个线程来读取服务器的响应):
1 | public class EchoClient { |
注意:
- 在上面代码中没有处理异常,实际中在发生异常时需要关闭 socket,并根据实际业务做一些错误处理工作。
- 在客户端,没有停止 readThread。实际中,可以通过关闭 socket 来让线程从阻塞中返回。
- demo 中服务端只处理了一个客户连接,如果需要同时处理多个客户端,可以创建线程来处理请求。