http introduce

HTTP协议介绍

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议。
HTTP是一个基于TCP/IP通信协议来传输数据(HTML文件等)。
HTTP是一个属于应用层的面向对象协议,由于其简捷的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断完善和扩展。目前在WWW中使用的是HTTP/1.1版,HTTP-NG(Next Generation of HTTP)的建议已经提出。
HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务器即web服务器发送所有请求。
http请求-响应模型.jpg


主要特点

  1. 简单快速:客户向服务器请求服务时,只需要传送请求方法和路径。请求常用的方法有GET,POST,HEAD。每种方法规定了客户与服务器联系的类型。
  2. 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content——type加以标记。
  3. 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户请求,收到客户端应答后,即断开连接。
  4. 无状态:HTTP协议是无状态的协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。

HTTP之URL

HTTP使用URI(Uniform Resource Identifiers)统一资源标识符来建立连接和传输数据。URL(Uniform Resource Locator)统一资源是一种具体的URI,包含了用于查找某个资源足够的信息。一般URL由以下部分组成 :
http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1#name
从上面URL可以看出,一个完整的URL包括以下部分:

  1. 协议:可以是http,也可以是ftp等其他协议
  2. 域名:该URL中域名为“www.aspxfans.com”
  3. 端口:非必须,如果省略,将采用默认端口
  4. 虚拟目录:从域名后第一个“/”到到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是必须部分。该URL中虚拟目录是“/news/”
  5. 文件名部分:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名
  6. 锚部分:从“#”开始到最后,都是锚部分。本例中的锚部分是“name”。锚部分也不是一个URL必须的部分
  7. 参数部分:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为“boardID=5&ID=24618&page=1”。参数可以允许有多个参数,参数与参数之间用“&”作为分隔符。

URI,URL,URN

URI是一种抽象的,高层次概念定义,而URL,URN则是具体的资源标识方式。URL和URN都是一种URI。URN(Uniform Resource Name)同一资源命名,唯一标识一个实体的标识符,但是不能给出实体的位置。系统可以先在本地寻找一个实体,在它试着在Web上找到该实体之前。它也允许Web位置改变,然而这个实体却还是能够被找到。标识持久性的Internet资源,URN可以提供一种机制,用于查找和检索特定命名空间的架构文件。尽管普通URL也可以提供类似的功能,但是在这方面,URN更加强大和容易管理,因为URN可以引用多个URL。
URN作为特定内容的唯一名称,与当前资源所在地无关,使用URN后,可以将资源四处迁移,不用担心迁移后无法访问。
URN在web中主要应用是下拉菜单的制作。使用URN时下拉菜单的易扩展性将会得到很大的提高。
P2P下载中使用磁力链接是URN的一种实现,它可以持久化标识一个BT资源,资源分布式存储在P2P网络中,无需中心服务器用户即可找到并下载它。


请求消息REQUEST

一个HTTP请求消息包括以下格式:请求行,请求头,空行,请求数据四部分。
http请求消息结构
http响应也包括四部分,状态行,响应头,空行,响应数据。


状态码

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:

1xx:指示信息–表示请求已接收,继续处理

2xx:成功–表示请求已被成功接收、理解、接受

3xx:重定向–要完成请求必须进行更进一步的操作

4xx:客户端错误–请求有语法错误或请求无法实现

5xx:服务器端错误–服务器未能实现合法的请求

常见状态码:

200 OK //客户端请求成功

400 Bad Request //客户端请求有语法错误,不能被服务器所理解

401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用

403 Forbidden //服务器收到请求,但是拒绝提供服务

404 Not Found //请求资源不存在,eg:输入了错误的URL

500 Internal Server Error //服务器发生不可预期的错误

503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常


HTTP服务端

关于http服务端,所知的可以使用容器,如tomcat,weblogic,windows系统自带的iis等。近年最火的springboot就是整合了tomcat。 也可以基于netty或者jetty手写http服务端。 关于服务端框架,最流行的架构为spring mvc架构,其他的有jersery、jfinal等。 关于http服务端,后面再详细研究


HTTP客户端

  1. 最原始的,使用JDK中的URLConnection进行http请求;
  2. 最流行的,使用apache的httpclient包;
  3. 移动端,可使用okhttp(pc端也可用)

使用URLConnection做Get请求

1
2
3
4
5
6
   public void urlDoGet() throws Exception{
URL url =new URL("http://httpbin.org/get");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
InputStream is = connection.getInputStream();
log.info("result: {}",IOUtils.toString(is,"UTF-8"));
}

小结:
new url->打开连接->获取流


使用URLConnection做POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void urlDoPost() throws Exception{
URL url =new URL("http://httpbin.org/post");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setConnectTimeout(3000);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "applicaton/json");
/**
* os只是写缓存,不会发送数据到服务器
*/
OutputStream os = connection.getOutputStream();
JSONObject json = new JSONObject();
json.put("name", "hello");
IOUtils.write(json.toJSONString(), os,"UTF-8");
os.flush();
os.close();
//getInputStream才发送数据
InputStream is = connection.getInputStream();
log.info("result: {}",IOUtils.toString(is,"UTF-8"));
}

小结:
new url-> 打开链接->设置属性->获取out流->写数据->获取in流->读取数据


使用httpClient

1
2
3
4
5
6
7
8
public void clientDoGet() throws Exception{
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://httpbin.org/get");
CloseableHttpResponse response = httpclient.execute(httpGet);
log.info("clientDoGet result:{}",EntityUtils.toString(response.getEntity()));
response.close();
httpclient.close();
}

小结:创建 client->创建get对象->execute


代理:请求不直接发往目的地址,而是通过代理服务器转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public void clientDoPost() throws Exception{
//代理
HttpHost proxy = new HttpHost("127.0.0.1", 8080, "http");
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(3000)
.setConnectionRequestTimeout(3000)
.setProxy(proxy).build();
HttpPost post = new HttpPost("http://httpbin.org/post");
post.setConfig(config);
CloseableHttpResponse response = httpclient.execute(post,context);
log.info("clientDoPost result:{}",EntityUtils.toString(response.getEntity()));
response.close();
httpclient.close();
}

小结:创建host->创建config,并设置属性->创建post->execute


配置SSL,连接池,cookie

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public void clientDoPost() throws Exception{
//代理
HttpHost proxy = new HttpHost("127.0.0.1", 8080, "http");
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(3000)
.setConnectionRequestTimeout(3000)
.setProxy(proxy).build();

//连接池
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100);

//ssl
SSLContext sslConext = SSLContextBuilder.create().loadTrustMaterial(new File("my.keystore"), "nopassword".toCharArray(),
new TrustSelfSignedStrategy()).build();
SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslConext, new String[] { "TLSv1" },null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());

//CredentialsProvider 认证,没用过
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope("httpbin.org", 80),
new UsernamePasswordCredentials("user", "passwd"));
//-------------------
CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm)
.evictExpiredConnections().evictIdleConnections(5L, TimeUnit.SECONDS)
.setSSLSocketFactory(sslFactory)
.setDefaultCredentialsProvider(credsProvider)
.build();

//cookie
CookieStore cookieStore = new BasicCookieStore();
HttpClientContext context = HttpClientContext.create();
context.setCookieStore(cookieStore);
HttpPost post = new HttpPost("http://httpbin.org/post");
post.setConfig(config);
CloseableHttpResponse response = httpclient.execute(post,context);
List<Cookie> cookies = cookieStore.getCookies();
for (int i = 0; i < cookies.size(); i++) {
log.info("Local cookie: {}" , cookies.get(i));
}
log.info("clientDoPost result:{}",EntityUtils.toString(response.getEntity()));
response.close();
httpclient.close();
}


httpclient源码包:org.apache.http.examples.client下有很多demo,其使用方式完全可以参考该包下的内容。

关于httpclient重试,可以参考kingszelda的博客,这里只引用最关键的一段:

1
2
3
4
5
6
 public static final String HTTP_REQ_SENT= "http.request_sent";

public boolean isRequestSent() {
final Boolean b = getAttribute(HTTP_REQ_SENT, Boolean.class);
return b != null && b.booleanValue();
}

如何判断一个请求是否发送成功,可看到如果当前的httpContext中的http.request_sent属性为true,则认为已经发送成功,否则认为还没有发送成功。那么一次正常的http请求http.request_sent属性是如何设置的?

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
protected HttpResponse doSendRequest(
final HttpRequest request,
final HttpClientConnection conn,
final HttpContext context) throws IOException, HttpException {
Args.notNull(request, "HTTP request");
Args.notNull(conn, "Client connection");
Args.notNull(context, "HTTP context");

HttpResponse response = null;

context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
     //首先在请求发送之前,将http.request_sent放入上下文context的属性中,值为false
context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.FALSE);
     //将request的Header放入连接中
conn.sendRequestHeader(request);
//如果是post/put这种有body的请求,需要先判断100-cotinue扩展协议是否支持
     //即发送包含body请求前,先判断服务端是否支持同样的协议如果不支持,则不发送了。除非特殊约定,默认双端是都不设置的。
if (request instanceof HttpEntityEnclosingRequest) {
boolean sendentity = true;
final ProtocolVersion ver =
request.getRequestLine().getProtocolVersion();
if (((HttpEntityEnclosingRequest) request).expectContinue() &&
!ver.lessEquals(HttpVersion.HTTP_1_0)) {
conn.flush();
if (conn.isResponseAvailable(this.waitForContinue)) {
response = conn.receiveResponseHeader();
if (canResponseHaveBody(request, response)) {
conn.receiveResponseEntity(response);
}
final int status = response.getStatusLine().getStatusCode();
if (status < 200) {
if (status != HttpStatus.SC_CONTINUE) {
throw new ProtocolException(
"Unexpected response: " + response.getStatusLine());
}
// discard 100-continue
response = null;
} else {
sendentity = false;
}
}
}
       //如果可以发送,则将body序列化后,写入当前流中
if (sendentity) {
conn.sendRequestEntity((HttpEntityEnclosingRequest) request);
}
}
     //刷新当前连接,发送数据
conn.flush();
     //将http.request_sent置为true
context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.TRUE);
return response;
}

上面是一个完成的http通信部分,步骤如下:

开始前将http.request_sent置为false
通过流flush数据到服务端
然后将http.request_sent置为true
显然,对于conn.flush()这一步是会发生异常的,这种情况下就认为没有发送成功。

对于我们场景应用中的get和post,可以总结为

  1. 只有发送IOException才会重试;
  2. InterruptedException,UnknownHostException,ConnectionException,SSLException四种异常不重试
  3. 默认重试3次

另外,常遇到的两种超市异常,Read Timeout和Connect Timeout继承InterruptedException,不会重试。那么哪些情况下会重试呢?会发生上述四种异常之外的哪些异常呢?可能出问题的一步在于HttpClientConnection.flush()的一步,跟进去可以得知其操作的对象是一SocketOutputStream,而这个类的flush是空实现,所以只需要看wirte方法即可。

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
private void socketWrite(byte b[], int off, int len) throws IOException {


if (len <= 0 || off < 0 || len > b.length - off) {
if (len == 0) {
return;
}
throw new ArrayIndexOutOfBoundsException("len == " + len
+ " off == " + off + " buffer length == " + b.length);
}

FileDescriptor fd = impl.acquireFD();
try {
socketWrite0(fd, b, off, len);
} catch (SocketException se) {
if (se instanceof sun.net.ConnectionResetException) {
impl.setConnectionResetPending();
se = new SocketException("Connection reset");
}
if (impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
} else {
throw se;
}
} finally {
impl.releaseFD();
}
}

可以看到,这个方法会抛出IOExecption,代码中对SocketException异常进行了加工。从之前的分析中可以得知,SocketException是不在可以忽略的范围内的。

所以从上面代码上就可以分析得出对于传输过程中socket被重置或者关闭的时候,httpclient会对post请求进行重试。

禁止重试:所以我们在构建httpClient实例的时候手动禁止掉即可。

1
2
3
4
5
6
7
8

/**
* Disables automatic request recovery and re-execution.
*/
public final HttpClientBuilder disableAutomaticRetries() {
automaticRetriesDisabled = true;
return this;
}


OKHttp的使用很有特色

1
2
3
4
5
6
public void okSyncGet() throws Exception{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("https://publicobject.com/helloworld.txt").build();
Response resp = client.newCall(request).execute();
log.info("okclient result: {}",resp.body().string());
}

小结:new client->new request->new call().execute()

okhttp还可使用异步的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void okAsyncGet() throws Exception{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("https://publicobject.com/helloworld.txt").build();
client.newCall(request).enqueue(new Callback(){

@Override
public void onFailure(Call arg0, IOException arg1) {
log.info("okAsyncGet onFailure");
}

@Override
public void onResponse(Call arg0, Response resp) throws IOException {
log.info("okAsyncGet onResponse");
Headers responseHeaders = resp.headers();
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
log.info("okAsyncGet result:{}",resp.body().string());
}

});
}

Post请求的使用方式

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public void okPost() throws Exception{
OkHttpClient client = new OkHttpClient();
Request req = new Request.Builder().url("https://api.github.com/markdown/raw")
.post(new RequestBody(){

@Override
public MediaType contentType() {
return MediaType.parse("text/x-markdown; charset=utf-8");
}

@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
}

}).build();
Response resp = client.newCall(req).execute();
log.info("poststream resp:{}",resp.body().string());
}

public void okPostString() throws Exception{
MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
OkHttpClient client = new OkHttpClient();
Request req = new Request.Builder().url("https://api.github.com/markdown/raw")
.post(RequestBody.create(mediaType, "hello")).build();
Response resp = client.newCall(req).execute();
log.info("poststream resp:{}",resp.body().string());

}


public void okPostForm() throws Exception{
OkHttpClient client = new OkHttpClient();
new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS).build();
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request req = new Request.Builder().url("https://api.github.com/markdown/raw")
.post(formBody).build();
Response resp = client.newCall(req).execute();
log.info("okPostForm resp:{}",resp.body().string());
}

小结:new client->new request()->new call().execute; 区别在于构造request时可以使用不同类型的requestbody填充。


参考资料

  1. 《JDK中的URLConnection参数详解》http://www.blogjava.net/supercrsky/articles/247449.html
  2. 《使用HttpURLConnection向服务器发送post和get请求》http://www.cnblogs.com/linjiqin/archive/2011/09/19/2181634.html
  3. 《关于HTTP协议,一篇就够了》https://www.cnblogs.com/ranyonsue/p/5984001.html
  4. httpclient源码包:org.apache.http.examples.client
  5. 《okhttp介绍》https://github.com/square/okhttp/wiki
  6. 《httpclient源码粗解》http://www.cnblogs.com/kingszelda/p/8886403.html