Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

  • Java核心基础

    • 多线程

    • Java常用类

    • 枚举类与注解

    • Java集合

    • 数据结构与算法

    • 泛型

    • IO流

    • 网络编程

      • 网络编程基础
      • InetAddress类
      • TCP编程
      • UDP编程
      • 发送Email
      • 接收Email
      • URL编程
      • HTTP编程
        • HTTP编程
        • 小结
      • 计算机网络常见面试题
    • 反射

    • 函数式编程

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java核心基础
  • 网络编程
SaulJWu
2021-01-10

HTTP编程

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

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

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

            GET / HTTP/1.1
            Host: www.sina.com.cn
            User-Agent: Mozilla/5 MSIE
            Accept: */*                ┌────────┐
┌─────────┐ Accept-Language: zh-CN,en  │░░░░░░░░│
│O ░░░░░░░│───────────────────────────>├────────┤
├─────────┤<───────────────────────────│░░░░░░░░│
│         │ HTTP/1.1 200 OK            ├────────┤
│         │ Content-Type: text/html    │░░░░░░░░│
└─────────┘ Content-Length: 133251     └────────┘
  Browser   <!DOCTYPE html>              Server
            <html><body>
            <h1>Hello</h1>
            ...
1
2
3
4
5
6
7
8
9
10
11
12
13
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判断客户端类型;
  • Accept:表示客户端能处理的HTTP响应格式,*/*表示任意格式,text/*表示任意文本,image/png表示PNG格式的图片;
  • Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。

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

POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

username=hello&password=123456
1
2
3
4
5
6

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

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

POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 38

{"username":"bob","password":"123456"}
1
2
3
4
5

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

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 133251

<!DOCTYPE html>
<html><body>
<h1>Hello</h1>
...
1
2
3
4
5
6
7
8

响应的第一行总是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请求一个图片,服务器响应图片请求后,会直接把二进制内容的图片发送给浏览器:

HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 18391

????JFIFHH??XExifMM?i&??X?...(二进制的JPEG图片)
1
2
3
4
5

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

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

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

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

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

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

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

# HTTP编程

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

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

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

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

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

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

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

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

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

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

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

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

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.*;

public class Main {
    // 全局HttpClient:
    static HttpClient httpClient = HttpClient.newBuilder().build();

    public static void main(String[] args) throws Exception {
        String url = "https://www.sina.com.cn/";
        HttpRequest request = HttpRequest.newBuilder(new URI(url))
            // 设置Header:
            .header("User-Agent", "Java HttpClient").header("Accept", "*/*")
            // 设置超时:
            .timeout(Duration.ofSeconds(5))
            // 设置版本:
            .version(Version.HTTP_2).build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        // HTTP允许重复的Header,因此一个Header可对应多个Value:
        Map<String, List<String>> headers = response.headers().map();
        for (String header : headers.keySet()) {
            System.out.println(header + ": " + headers.get(header).get(0));
        }
        System.out.println(response.body().substring(0, 1024) + "...");
    }
}
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

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

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

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

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

# 小结

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

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

帮我改善此页面 (opens new window)
#HTTP#响应代码#HttpClient
上次更新: 2021/02/08, 11:20:26
URL编程
计算机网络常见面试题

← URL编程 计算机网络常见面试题→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式