理解HTTP
理解 HTTP
什么是 HTTP?
超文本传输协议(Hyper Text Transfer Protocol)。
HTTP 是做什么的?
规范了浏览器和服务器交互的数据格式。
HTTP 的特点?
- 简单快捷: 客户向服务器请求服务时, 只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。
- 灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
- 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
- 无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。 1.1版本之后支持可持续性连接。
HTTP 交互流程
- 客户端和服务器端建立连接
- 客户端发送请求数据到服务器端(HTTP 协议)
- 服务器端接收到请求后, 进行处理, 然后将处理结果响应客户端(HTTP协议)
- 关闭客户端和服务器端的连接(HTTP1.1 后不会立即关闭)
HTTP 协议之请求格式
请求格式的结构: 请求头:请求方式、 请求的地址和 HTTP 协议版本 请求行: 消息报头, 一般用来说明客户端要使用的一些附加信息 空行:位于请求行和请求数据之间, 空行是必须的。 请求数据: 非必须。
HTTP协议的请求方式
- 根据 HTTP 标准, HTTP 请求可以使用多种请求方法。
- HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD 方法。
- HTTP1.1 新增了五种请求方法: OPTIONS, PUT, DELETE, TRACE 和CONNECT 方法。
get 和 post 请求方式的区别:
- get 请求方式: 请求数据会以? 的形式隔开拼接在请求头中, 不安全, 没有请求实 体部分。HTTP 协议虽然没有规定请求数据的大小, 但是浏览器对 URL 的长度是有限制的, 所以 get 请求不能携带大量的数据。
- post 请求方式: 请求数据在请求实体中进行发送, 在 URL 中看不到具体的请求数据,安全。 适合数据量大的数据发送。
HTTP响应
响应格式的帧结构
响应行(状态行): HTTP 版本、 状态码、 状态消息 响应头: 消息报头, 客户端使用的附加信息 空行: 响应头和响应实体之间的, 必须的。 响应实体: 正文, 服务器返回给浏览器的信息
常见的状态码
- 200 OK //客户端请求成功
- 400 Bad Request //客户端请求有语法错误, 不能被服务器所理 解
- 401 Unauthorized //请求未经授权, 这个状态代码必须和 WWW-Authenticate 报头域一起使用
- 403 Forbidden //服务器收到请求, 但是拒绝提供服务 404 Not Found //请求资源不存在, eg: 输入了错误的 URL
- 500 Internal Server Error //服务器发生不可预期的错误
- 503 Server Unavailable //服务器当前不能处理客户端的请求, 一段时间后可能恢复正常。
下面基于socket编写一个简单的HTTP server。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
class SocketHandler implements Runnable {
final static String CRLF = "\r\n"; // 1
private Socket clientSocket;
public SocketHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
public void handleSocket(Socket clientSocket) throws IOException {
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream())
);
PrintWriter out = new PrintWriter(
new BufferedWriter( new OutputStreamWriter(clientSocket.getOutputStream())),
true
);
String requestHeader = "";
String s;
while ((s = in.readLine()) != null) {
s += CRLF; // 2 很重要,默认情况下in.readLine的结果中`\r\n`被去掉了
requestHeader = requestHeader + s;
if (s.equals(CRLF)){ // 3 此处HTTP请求头我们都得到了;如果从请求头中判断有请求正文,则还需要继续获取数据
break;
}
}
System.out.println("客户端请求头:");
System.out.println(requestHeader);
String responseBody = "客户端的请求头是:\n"+requestHeader;
String responseHeader = "HTTP/1.0 200 OK\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"Content-Length: "+responseBody.getBytes().length+"\r\n" +
"\r\n";
// 4 问题来了:1、浏览器如何探测编码 2、浏览器受到content-length后会按照什么方式判断?汉字的个数?字节数?
System.out.println("响应头:");
System.out.println(responseHeader);
out.write(responseHeader);
out.write(responseBody);
out.flush();
out.close();
in.close();
clientSocket.close();
}
@Override
public void run() {
try {
handleSocket(clientSocket);
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
public class MyHTTPServer {
public static void main(String[] args) throws Exception {
int port = 8000;
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("启动服务,绑定端口: " + port);
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(30); // 5
while (true) { // 6
Socket clientSocket = serverSocket.accept();
System.out.println("新的连接"
+ clientSocket.getInetAddress() + ":" + clientSocket.getPort());
try {
fixedThreadPool.execute(new SocketHandler(clientSocket));
} catch (Exception e) {
System.out.println(e);
}
}
}
}
这是一个实现 HTTP 1.0 的服务器,对于所有的 HTTP 请求,会把 HTTP 请求头响应回去。 这个程序说明了web服务器处理请求的基本流程,JSP、Servlet、Spring MVC 等只是在 这个基础上嫁了许多方法,以让我们更方面的编写 web 应用。web 服务器不仅可以基于多线程, 也可以基于多进程、Reactor模型等。
测试程序:
运行上面的程序。我们使用 curl 访问http://127.0.0.1
(也可以使用浏览器):
$ curl -i http://127.0.0.1:8000
HTTP/1.0 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 106
客户端的请求头是:
GET / HTTP/1.1
User-Agent: curl/7.35.0
Host: 127.0.0.1:8000
Accept: */*
Java 程序输出:
启动服务,绑定端口: 8000
新的连接/127.0.0.1:36463
新的连接/127.0.0.1:36463客户端请求头:
GET / HTTP/1.1
User-Agent: curl/7.35.0
Host: 127.0.0.1:8000
Accept: */*
响应头:
HTTP/1.0 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 106
程序解析:
// 1
:定义了 HTTP 头的换行符。
// 2
:in.readLine() 的结果默认不带换行符,这里把它加上。(这不是强制的,主要看你的程序逻辑需不需要,
这个程序的目标是把 HTTP 请求头响应回去)。
// 3
:此时s是一个空行,根据 HTTP 协议,整个请求头都得到了。
// 4
:Content-Length 的值是字节的数量。
// 5
:线程池。
// 6
:这个循环不停监听socket连接,使用 SocketHandler 处理连入的 socket,而这个处理是放在线程池中的。
HTTP 1.1:
HTTP 1.1 也是在这个思路的基础上实现的,即多个 HTTP 请求都在一个 TCP 连接中传输。对于 HTTP 1.1,如何区分出每个HTTP请求很重要,
比较简单的可以是用过Content-Length
判断一条请求是否结束。如果一个 HTTP 请求数据较多,往往采用 Chunked 方式,
可以参考 Chunked transfer encoding。