HTTP编程


什么是HTTP?HTTP就是目前使用最广泛的Web应用程序使用的基础协议,例如,浏览器访问网站,手机App访问后台服务器,都是通过HTTP协议实现的。

HTTP是HyperText Transfer Protocol的缩写,翻译为超文本传输协议,它是基于TCP协议之上的一种请求-响应协议。

我们来看一下浏览器请求访问某个网站时发送的HTTP请求-响应。当浏览器希望访问某个网站时,浏览器和网站服务器之间首先建立TCP连接,且服务器总是使用80端口和加密端口443,然后,浏览器向服务器发送一个HTTP请求,服务器收到后,返回一个HTTP响应,并且在响应中包含了HTML的网页内容,这样,浏览器解析HTML后就可以给用户显示网页了。一个完整的HTTP请求-响应如下:

  1. GET / HTTP/1.1
  2. Host: www.sina.com.cn
  3. User-Agent: Mozilla/5 MSIE
  4. Accept: */* ┌────────┐
  5. ┌─────────┐ Accept-Language: zh-CN,en │░░░░░░░░│
  6. │O ░░░░░░░│───────────────────────────>├────────┤
  7. ├─────────┤<───────────────────────────│░░░░░░░░│
  8. │ │ HTTP/1.1 200 OK ├────────┤
  9. │ │ Content-Type: text/html │░░░░░░░░│
  10. └─────────┘ Content-Length: 133251 └────────┘
  11. Browser <!DOCTYPE html> Server
  12. <html><body>
  13. <h1>Hello</h1>
  14. ...

HTTP请求的格式是固定的,它由HTTP Header和HTTP Body两部分构成。第一行总是请求方法 路径 HTTP版本,例如,GET / HTTP/1.1表示使用GET请求,路径是/,版本是HTTP/1.1

后续的每一行都是固定的Header: Value格式,我们称为HTTP Header,服务器依靠某些特定的Header来识别客户端请求,例如:

  • Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别请求是发给哪个网站的;
  • User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户端类型是IE还是Chrome,是Firefox还是一个Python爬虫;
  • Accept:表示客户端能处理的HTTP响应格式,*/*表示任意格式,text/*表示任意文本,image/png表示PNG格式的图片;
  • Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。

如果是GET请求,那么该HTTP请求只有HTTP Header,没有HTTP Body。如果是POST请求,那么该HTTP请求带有Body,以一个空行分隔。一个典型的带Body的HTTP请求如下:

  1. POST /login HTTP/1.1
  2. Host: www.example.com
  3. Content-Type: application/x-www-form-urlencoded
  4. Content-Length: 30
  5. username=hello&password=123456

POST请求通常要设置Content-Type表示Body的类型,Content-Length表示Body的长度,这样服务器就可以根据请求的Header和Body做出正确的响应。

此外,GET请求的参数必须附加在URL上,并以URLEncode方式编码,例如:http://www.example.com/?a=1&b=K%26R,参数分别是a=1b=K&R。因为URL的长度限制,GET请求的参数不能太多,而POST请求的参数就没有长度限制,因为POST请求的参数必须放到Body中。并且,POST请求的参数不一定是URL编码,可以按任意格式编码,只需要在Content-Type中正确设置即可。常见的发送JSON的POST请求如下:

  1. POST /login HTTP/1.1
  2. Content-Type: application/json
  3. Content-Length: 38
  4. {"username":"bob","password":"123456"}

HTTP响应也是由Header和Body两部分组成,一个典型的HTTP响应如下:

  1. HTTP/1.1 200 OK
  2. Content-Type: text/html
  3. Content-Length: 133251
  4. <!DOCTYPE html>
  5. <html><body>
  6. <h1>Hello</h1>
  7. ...

响应的第一行总是HTTP版本 响应代码 响应说明,例如,HTTP/1.1 200 OK表示版本是HTTP/1.1,响应代码是200,响应说明是OK。客户端只依赖响应代码判断HTTP响应是否成功。HTTP有固定的响应代码:

  • 1xx:表示一个提示性响应,例如101表示将切换协议,常见于WebSocket连接;
  • 2xx:表示一个成功的响应,例如200表示成功,206表示只发送了部分内容;
  • 3xx:表示一个重定向的响应,例如301表示永久重定向,303表示客户端应该按指定路径重新发送请求;
  • 4xx:表示一个因为客户端问题导致的错误响应,例如400表示因为Content-Type等各种原因导致的无效请求,404表示指定的路径不存在;
  • 5xx:表示一个因为服务器问题导致的错误响应,例如500表示服务器内部故障,503表示服务器暂时无法响应。

当浏览器收到第一个HTTP响应后,它解析HTML后,又会发送一系列HTTP请求,例如,GET /logo.jpg HTTP/1.1请求一个图片,服务器响应图片请求后,会直接把二进制内容的图片发送给浏览器:

  1. HTTP/1.1 200 OK
  2. Content-Type: image/jpeg
  3. Content-Length: 18391
  4. ????JFIFHH??XExifMM?i&??X?...(二进制的JPEG图片)

因此,服务器总是被动地接收客户端的一个HTTP请求,然后响应它。客户端则根据需要发送若干个HTTP请求。

对于最早期的HTTP/1.0协议,每次发送一个HTTP请求,客户端都需要先创建一个新的TCP连接,然后,收到服务器响应后,关闭这个TCP连接。由于建立TCP连接就比较耗时,因此,为了提高效率,HTTP/1.1协议允许在一个TCP连接中反复发送-响应,这样就能大大提高效率:

  1. ┌─────────┐
  2. ┌─────────┐ │░░░░░░░░░│
  3. O ░░░░░░░│ ├─────────┤
  4. ├─────────┤ │░░░░░░░░░│
  5. ├─────────┤
  6. │░░░░░░░░░│
  7. └─────────┘ └─────────┘
  8. request 1
  9. │─────────────────────>│
  10. response 1
  11. │<─────────────────────│
  12. request 2
  13. │─────────────────────>│
  14. response 2
  15. │<─────────────────────│
  16. request 3
  17. │─────────────────────>│
  18. response 3
  19. │<─────────────────────│

因为HTTP协议是一个请求-响应协议,客户端在发送了一个HTTP请求后,必须等待服务器响应后,才能发送下一个请求,这样一来,如果某个响应太慢,它就会堵住后面的请求。

所以,为了进一步提速,HTTP/2.0允许客户端在没有收到响应的时候,发送多个HTTP请求,服务器返回响应的时候,不一定按顺序返回,只要双方能识别出哪个响应对应哪个请求,就可以做到并行发送和接收:

  1. ┌─────────┐
  2. ┌─────────┐ │░░░░░░░░░│
  3. O ░░░░░░░│ ├─────────┤
  4. ├─────────┤ │░░░░░░░░░│
  5. ├─────────┤
  6. │░░░░░░░░░│
  7. └─────────┘ └─────────┘
  8. request 1
  9. │─────────────────────>│
  10. request 2
  11. │─────────────────────>│
  12. response 1
  13. │<─────────────────────│
  14. request 3
  15. │─────────────────────>│
  16. response 3
  17. │<─────────────────────│
  18. response 2
  19. │<─────────────────────│

可见,HTTP/2.0进一步提高了效率。

HTTP编程

既然HTTP涉及到客户端和服务器端,和TCP类似,我们也需要针对客户端编程和针对服务器端编程。

本节我们不讨论服务器端的HTTP编程,因为服务器端的HTTP编程本质上就是编写Web服务器,这是一个非常复杂的体系,也是JavaEE开发的核心内容,我们在后面的章节再仔细研究。

本节我们只讨论作为客户端的HTTP编程。

因为浏览器也是一种HTTP客户端,所以,客户端的HTTP编程,它的行为本质上和浏览器是一样的,即发送一个HTTP请求,接收服务器响应后,获得响应内容。只不过浏览器进一步把响应内容解析后渲染并展示给了用户,而我们使用Java进行HTTP客户端编程仅限于获得响应内容。

我们来看一下Java如何使用HTTP客户端编程。

Java标准库提供了基于HTTP的包,但是要注意,早期的JDK版本是通过HttpURLConnection访问HTTP,典型代码如下:

  1. URL url = new URL("http://www.example.com/path/to/target?a=1&b=2");
  2. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  3. conn.setRequestMethod("GET");
  4. conn.setUseCaches(false);
  5. conn.setConnectTimeout(5000); // 请求超时5秒
  6. // 设置HTTP头:
  7. conn.setRequestProperty("Accept", "*/*");
  8. conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)");
  9. // 连接并发送HTTP请求:
  10. conn.connect();
  11. // 判断HTTP响应是否200:
  12. if (conn.getResponseCode() != 200) {
  13. throw new RuntimeException("bad response");
  14. }
  15. // 获取所有响应Header:
  16. Map<String, List<String>> map = conn.getHeaderFields();
  17. for (String key : map.keySet()) {
  18. System.out.println(key + ": " + map.get(key));
  19. }
  20. // 获取响应内容:
  21. InputStream input = conn.getInputStream();
  22. ...

上述代码编写比较繁琐,并且需要手动处理InputStream,所以用起来很麻烦。

从Java 11开始,引入了新的HttpClient,它使用链式调用的API,能大大简化HTTP的处理。

我们来看一下如何使用新版的HttpClient。首先需要创建一个全局HttpClient实例,因为HttpClient内部使用线程池优化多个HTTP连接,可以复用:

  1. static HttpClient httpClient = HttpClient.newBuilder().build();

使用GET请求获取文本内容代码如下:

  1. import java.net.URI;
  2. import java.net.http.*;
  3. import java.net.http.HttpClient.Version;
  4. import java.time.Duration;
  5. import java.util.*;
  6. public class Main {
  7. // 全局HttpClient:
  8. static HttpClient httpClient = HttpClient.newBuilder().build();
  9. public static void main(String[] args) throws Exception {
  10. String url = "https://www.sina.com.cn/";
  11. HttpRequest request = HttpRequest.newBuilder(new URI(url))
  12. // 设置Header:
  13. .header("User-Agent", "Java HttpClient").header("Accept", "*/*")
  14. // 设置超时:
  15. .timeout(Duration.ofSeconds(5))
  16. // 设置版本:
  17. .version(Version.HTTP_2).build();
  18. HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
  19. // HTTP允许重复的Header,因此一个Header可对应多个Value:
  20. Map<String, List<String>> headers = response.headers().map();
  21. for (String header : headers.keySet()) {
  22. System.out.println(header + ": " + headers.get(header).get(0));
  23. }
  24. System.out.println(response.body().substring(0, 1024) + "...");
  25. }
  26. }

如果我们要获取图片这样的二进制内容,只需要把HttpResponse.BodyHandlers.ofString()换成HttpResponse.BodyHandlers.ofByteArray(),就可以获得一个HttpResponse<byte[]>对象。如果响应的内容很大,不希望一次性全部加载到内存,可以使用HttpResponse.BodyHandlers.ofInputStream()获取一个InputStream流。

要使用POST请求,我们要准备好发送的Body数据并正确设置Content-Type

  1. String url = "http://www.example.com/login";
  2. String body = "username=bob&password=123456";
  3. HttpRequest request = HttpRequest.newBuilder(new URI(url))
  4. // 设置Header:
  5. .header("Accept", "*/*")
  6. .header("Content-Type", "application/x-www-form-urlencoded")
  7. // 设置超时:
  8. .timeout(Duration.ofSeconds(5))
  9. // 设置版本:
  10. .version(Version.HTTP_2)
  11. // 使用POST并设置Body:
  12. .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
  13. HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
  14. String s = response.body();

可见发送POST数据也十分简单。

练习

HTTP编程 - 图1下载练习:使用HttpClient (推荐使用IDE练习插件快速下载)

小结

Java提供了HttpClient作为新的HTTP客户端编程接口用于取代老的HttpURLConnection接口;

HttpClient使用链式调用并通过内置的BodyPublishersBodyHandlers来更方便地处理数据。

读后有收获可以支付宝请作者喝咖啡:

HTTP编程 - 图2