因为项目中很多地方都与Http协议有关,零散的了解了一下Http协议,但是没有系统的学习过。
今天根据网上其他同学的整理,加上我的一些经验,我也整理了一份。当做学习记录吧。
一、什么是HTTP协议
HTTP,全名HyperText Transfer Protocol。因特网上应用最为广泛的一种网络协议。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。现在对与HTTP协议的应用已经很多了,浏览器上网,下载文件,服务器间通信等。
二、HTTP协议格式
如下图,一次完整的HTTP交易过程,是由HTTP请求和HTTP响应组成的。下面介绍下HTTP的报文格式。
HTTP请求消息:在HTTP请求中,第一行必须是一个请求行(request line),用来说明请求类型、要访问的资源以及使用的HTTP版本。紧接着是一个报头(header)小节,用来说明服务器要使用的附加信息。在首部之后是一个空行,再此之后可以添加任意的其他数据,称之为主体(body)。
如下图所示,为访问百度首页的请求:
1) 第一行为请求行,分为3个部分。
[GET]为请求类型,请求类型有很多种,GET、POST、PUT、DELETE等,用的最多就是GET和POST了。
[http://www.baidu.com/]为请求网址,网址后面可以跟请求参数,http://www.baidu.com/s?ie=utf-8&f=8
[HTTP/1.1]为协议版本
2) 第二行到最后一行为报头。每一个报头域都是由名字+“:”+空格+值组成,消息报头域的名字是大小写无关的。HTTP协议定义了很多的报头,图片中看到的Accept,Host,Cookie只是其中的一部分。【关于HTTP headers的简要介绍,请查看:Quick reference to HTTP headers】
3) 接下来应该是body部分,但是下图中没有,因为GET请求是没有body部分的,如果是POST请求就可以看到了。说到这里,网上有很多讲GET和POST区别的,在我看来,有没有body部分,才是二者最大的区别。虽然没有body,但是报头下方依然有一个空行。
下面的图是POST请求的例子,很清楚可以看到body部分。
HTTP响应消息:响应消息和请求消息很类似,第一行为状态行,接下来是报头,报头后为一行空行,下面是返回内容,同样称之为body。
下图为上面请求百度首页的响应报文。
1)第一行为状态行,分为是三个部分,第一部分协议版本,第二部分状态码,第三部分状态码描述。
2)接下来的报头和body,都与HTTP请求消息类似。
三、HTTP请求过程
HTTP支持两种建立连接的方式:非持久连接和持久连接(HTTP1.1默认的连接方式为持久连接)。
非持久连接:
让我们查看一下非持久连接情况下从服务器到客户传送一个Web页面的步骤。假设该贝面由1个基本HTML文件和10个JPEG图像构成,而且所有这些对象都存放在同一台服务器主机中。再假设该基本HTML文件的URL为:http://www.baidu.com。
下面是具体步骡:
1) HTTP客户初始化一个与服务器主机http://www.baidu.com中的HTTP服务器的TCP连接。HTTP服务器使用默认端口号80监听来自HTTP客户的连接建立请求。
2) HTTP客户经由与TCP连接相关联的本地套接字发出—个HTTP请求消息。这个消息中包含路径名/somepath/index.html。
3) HTTP服务器经由与TCP连接相关联的本地套接字接收这个请求消息,再从服务器主机的内存或硬盘中取出对象/somepath/index.html,经由同一个套接字发出包含该对象的响应消息。
4) HTTP服务器告知TCP关闭这个TCP连接(不过TCP要到客户收到刚才这个响应消息之后才会真正终止这个连接)。
5) HTTP客户经由同一个套接字接收这个响应消息。TCP连接随后终止。该消息标明所封装的对象是一个HTML文件。客户从中取出这个文件,加以分析后发现其中有10个JPEG对象的引用。
6) 给每一个引用到的JPEG对象重复步骡1-4。
上述步骤之所以称为使用非持久连接,原因是每次服务器发出一个对象后,相应的TCP连接就被关闭,也就是说每个连接都没有持续到可用于传送其他对象。每个TCP连接只用于传输一个请求消息和一个响应消息。就上述例子而言,用户每请求一次那个web页面,就产生11个TCP连接。
持久连接:
非持久连接的缺点
1) 客户得为每个待请求的对象建立并维护一个新的连接。对于每个这样的连接,TCP得在客户端和服务器端分配TCP缓冲区,并维持TCP变量。对于有可能同时为来自数百个不同客户的请求提供服务的web服务器来说,这会严重增加其负担。
2) 如前所述,每个对象都有2个RTT的响应延长——一个RTT用于建立TCP连接,另—个RTT用于请求和接收对象。
3) 每个对象都遭受TCP缓启动,因为每个TCP连接都起始于缓启动阶段。不过并行TCP连接的使用能够部分减轻RTT延迟和缓启动延迟的影响。
而在持久连接情况下,服务器在发出响应后让TCP连接继续打开着。同一对客户/服务器之间的后续请求和响应可以通过这个连接发送。整个Web页面(上例中为包含一个基本HTMLL文件和10个图像的页面)自不用说可以通过单个持久TCP连接发送:甚至存放在同一个服务器中的多个web页面也可以通过单个持久TCP连接发送。
通常,HTTP服务器在某个连接闲置一段特定时间后关闭它,而这段时间通常是可以配置的。持久连接分为不带流水线(without pipelining)和带流水线(with pipelining)两个版本。如果是不带流水线的版本,那么客户只在收到前一个请求的响应后才发出新的请求。这种情况下,web页面所引用的每个对象(上例中的10个图像)都经历1个RTT的延迟,用于请求和接收该对象。与非持久连接2个RTT的延迟相比,不带流水线的持久连接已有所改善,不过带流水线的持久连接还能进一步降低响应延迟。不带流水线版本的另一个缺点是,服务器送出一个对象后开始等待下一个请求,而这个新请求却不能马上到达。这段时间服务器资源便闲置了。
HTTP/1.1的默认模式使用带流水线的持久连接。这种情况下,HTTP客户每碰到一个引用就立即发出一个请求,因而HTTP客户可以一个接一个紧挨着发出各个引用对象的请求。服务器收到这些请求后,也可以一个接一个紧挨着发出各个对象。如果所有的请求和响应都是紧挨着发送的,那么所有引用到的对象一共只经历1个RTT的延迟(而不是像不带流水线的版本那样,每个引用到的对象都各有1个RTT的延迟)。另外,带流水线的持久连接中服务器空等请求的时间比较少。与非持久连接相比,持久连接(不论是否带流水线)除降低了1个RTT的响应延迟外,缓启动延迟也比较小。其原因在于既然各个对象使用同一个TCP连接,服务器发出第一个对象后就不必再以一开始的缓慢速率发送后续对象。相反,服务器可以按照第一个对象发送完毕时的速率开始发送下一个对象。
四、HTTP协议的使用
最常见的应用就是浏览器了,这个大家都会用。下面介绍下JAVA中如何发送HTTP请求。
- 使用JDK发送HTTP请求
Java代码
- package com.asiainfo.test;
- import java.io.BufferedReader;
- import java.io.DataOutputStream;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.net.HttpURLConnection;
- import java.net.URL;
- public class HttpTest {
- public static void readContentFromGet() throws IOException {
- URL getUrl = new URL(<a href="http://www.baidu.com">http://www.baidu.com</a>);
- HttpURLConnection connection = (HttpURLConnection) getUrl.openConnection();
- connection.connect();
- // 发送数据到服务器并使用Reader读取返回的数据
- BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String aline;
- while ((aline = reader.readLine()) != null) {
- System.out.println(aline);
- }
- reader.close();
- // 断开连接
- connection.disconnect();
- }
- public static void readContentFromPost() throws IOException {
- URL postUrl = new URL(<a href="http://www.baidu.com">http://www.baidu.com</a>);
- HttpURLConnection connection = (HttpURLConnection) postUrl.openConnection();
- // 打开读写属性,默认均为false
- connection.setDoOutput(true);
- connection.setDoInput(true);
- // 设置请求方式,默认为GET
- connection.setRequestMethod("POST");
- connection.setUseCaches(false);
- // 配置连接的Content-type,配置为application/x-www-form-urlencoded的意思是正文是urlencoded编码过的form参数,
- //下面我们可以看到我们对正文内容使用URLEncoder.encode进行编码
- connection.setRequestProperty(" Content-Type "," application/x-www-form-urlencoded ");
- // 连接,从postUrl.openConnection()至此的配置必须要在 connect之前完成,
- // 要注意的是connection.getOutputStream()会隐含的进行调用 connect(),所以这里可以省略
- // connection.connect();
- DataOutputStream out = new DataOutputStream(connection.getOutputStream());
- String content = "username=name&password=passwd";
- out.writeBytes(content);
- out.flush();
- out.close(); // flush and close
- BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String aline;
- while ((aline = reader.readLine()) != null) {
- System.out.println(aline);
- }
- reader.close();
- }
- public static void main(String[] args) {
- try {
- readContentFromGet();
- readContentFromPost();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- 使用Apache的HttpClient发送Http请求
Java代码
- package com.zhuanqian.util;
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
- import org.apache.commons.httpclient.HttpClient;
- import org.apache.commons.httpclient.HttpException;
- import org.apache.commons.httpclient.NameValuePair;
- import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
- import org.apache.commons.httpclient.methods.GetMethod;
- import org.apache.commons.httpclient.methods.PostMethod;
- import org.apache.commons.httpclient.methods.RequestEntity;
- import org.apache.commons.lang.StringUtils;
- publicclass HttpUtil {
- publicstatic HttpClient getHttpClient(){ HttpClient client = new HttpClient();
- // client.getHostConfiguration().setProxy("proxy.bmcc.com.cn", 8080);
- // client.getParams().setAuthenticationPreemptive(true);
- // //如果代理需要密码验证,这里设置用户名密码
- // client.getState().setProxyCredentials(AuthScope.ANY, new UsernamePasswordCredentials("ngcrm_xubo","xcv.2134"));
- client.getHttpConnectionManager().getParams().setConnectionTimeout(0);
- client.getHttpConnectionManager().getParams().setSoTimeout(0);
- return client;
- }
- publicstatic HttpResponse doGet(String url) throws HttpException, IOException{
- HttpClient client = getHttpClient();
- GetMethod method = new GetMethod(url);
- DefaultHttpMethodRetryHandler retry = new DefaultHttpMethodRetryHandler(0, false);
- method.getParams().setParameter("http.method.retry-handler", retry);
- method.getParams().setContentCharset("GB2312");
- // method.addRequestHeader("Content-Type", "text/html; charset=UTF-8");
- int status = client.executeMethod(method);
- HttpResponse response = new HttpResponse();
- response.setStatusCode(status);
- response.setResult(method.getResponseBodyAsString());
- return response;
- }
- publicstatic HttpResponse doPost(String url, Map parameters, String json) throws Exception {
- HttpClient client = getHttpClient();
- PostMethod method = new PostMethod(url);
- DefaultHttpMethodRetryHandler retry = new DefaultHttpMethodRetryHandler(0, false);
- method.getParams().setParameter("http.method.retry-handler", retry);
- if(StringUtils.isNotBlank(json)){
- RequestEntity objRequestEntity = new ByteArrayRequestEntity(json.getBytes())
- method.setRequestEntity(objRequestEntity);
- }
- if(parameters != null && !parameters.isEmpty()){
- List list = new ArrayList();
- Set keys = parameters.keySet();
- String item;
- for(Iterator iter = keys.iterator(); iter.hasNext(); ){
- item = (String)iter.next();
- list.add(new NameValuePair(item, (String)parameters.get(item)));
- }
- method.setQueryString((NameValuePair[])list.toArray(new NameValuePair[0]));
- }
- int statusCode = client.executeMethod(method);
- HttpResponse response = new HttpResponse();
- response.setStatusCode(statusCode);
- response.setResult(method.getResponseBodyAsString());
- return response;
- }
- }
五、HTTP协议之状态保持
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
虽然HTTP协议本身是没有状态的,但是我们可以通过其他的手段来实现状态保持,最常用的就是Cookie和Session。Session也可以使用Cookie来实现。
Cookie机制:服务器在响应消息中通过Set-Cookie头将Cookie内容返回给客户端,客户端在新的请求中通过Cookie发送给服务器,从而实现了会话的保持。
Session机制:Session是一种服务器机制,当客户端请求创建一个Session时,服务器首先检查请求中是否存在Session标识,如果有将根据此标识查找出来使用,如果查不到,或者Session已过期,则重新创建一个,然后将其返回给客户端使用。
六、HTTP协议之长连接
HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接)。
从HTTP/1.1起,默认都开启了Keep-Alive,保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。但是Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。
之前就遇到过一个长连接引起的问题。当时项目部署有一个Http接口,需要进行压力测试,因为觉得很简单,所以自己写了一个程序压了一下。发现过一段时间,就会报错,端口不足。代码如下:
Java代码
- while (true) {
- HttpClient client = new HttpClient();
- HttpMethod method = new GetMethod("http://www.apache.org");
- try {
- client.executeMethod(method);
- byte[] responseBody = method.getResponseBody();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
过检查才发现原因:method.releaseConnection()实际上没有关闭连接,只是将链接返回给了ConnectionManager,然后ConnectionManager会根据算法来关闭连接。
这样当循环发起Http请求时,就会不断打开新端口,直到端口不足。然后参考网上其他同学的做法,主动将Connection断开后,发现本地还是端口还是不足,严重怀疑端口不能立即释放。
后来想到,在使用jmeter进行压测的时候,从来没有出现过这种情况。好在jmeter是java写的,我就翻了翻jmeter的代码,终于找到了答案。
原来秘诀在于,需要将HttpClient重复使用即可。于是代码优化成下面这样后,就完美运行无压力了。
Java代码
- HttpClient client = new HttpClient();
- HttpMethod method = new GetMethod("http://www.apache.org");
- while (true) {
- try {
- client.executeMethod(method);
- byte[] responseBody = method.getResponseBody();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
最后,这是有个网上有位同学建议的几本书,我也木有看过,记下来,万一哪天就能看懂了呢。哈哈
1.O‘Reilly - HTTP Pocket Reference:这是一本比较简短的介绍HTTP协议的书,可以作为入门读物
2.O‘Reilly - HTTP The Definitive Guide:这是一本宝典级别的书,因为它包含的内容实在多,可以作为全面学习的HTTP协议的首选读物
3.Sams - HTTP Developers Handbook:这是比HTTP The Definitive Guide稍微比HTTP The Definitive Guide简单。不过从我的感觉,这本书比HTTP The Definitive Guide要好,因为它篇幅比较少,介绍的是HTTP精髓,我认为这本书应该是web程序员的首选读物
http://z1041950008.iteye.com/blog/2190509