首页 > 技术文章 > HTTP报文详解

lhyblog 2016-09-28 10:25 原文

HTTP报文详解

一、    基本概念

GETPOSTHTTP中常用的请求方式,面试经常问到其区别?更深的会让你手写一个请求报文?---网易

HTTP是面向文本传输的,报文中每个字段都是一些ASCII码流,各个字段的长度是不确定的;HTTP报文分为请求报文和响应报文。

 

 

二、       HTTP请求报文

HTTP请求报文分为4个部分:请求行、请求头部、空行和请求数据.

 

2.1 请求行

请求行由请求方法、URL字段和HTTP版本号3个字段组成,使用空格隔开。比如常见的:GET /index.html HTTP/1.1。

注意HTTP请求方法:GET、POST、HEAD、PUT、DELETE、CONNECT、OPTIONS等。

 

2.1.1 GET请求实例讲解

   GET是最常见的请求方式,当客户端从服务器读取文档时,当点击网页上的链接时或者通过浏览器输入网址浏览网页时,一般使用的都是GET方式。GET方法要求服务端将URL定位的资源放在响应报文中的数据部分,回送给客户端。

   使用GET方法时,请求参数和对应的值附加在URL后面,利用一个“?”代表URL结束和请求参数的开始,不同的参数用&隔开;不同的浏览器对字符限制有所不同,一般最多识别1024个字节。传递参数长度收到限制;

   下面以用google搜索domety为例说下GET请求:

 

可以看到:GET请求一般是没有请求数据部分的,或者说请求数据直接附加在URL后面给服务器了,所以直接空行结束即可。

 

2.1.2 POST请求实例讲解

GET方式请求由于URL长度的限制,一般不能传送较多的数据。这时候考虑使用POST请求。POST方法将请求参数封装在HTTP请求数据中,以键值对形式存在,可以传输大量数据。

 

 

2.1.3 HEAD请求

HEAD就像GET一样,只是服务端收到HEAD请求后只返回响应头,而不会发送响应内容;当我们只需要查看某个页面的状态时,HEAD是很高效的,因为在传输的过程中省去了页面的内容。

 

2.2 请求头部

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:

User-Agent:产生请求的浏览器类型。

Accept:客户端可识别的内容类型列表。

Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。

 

2.3 空行

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。

2.4 请求内容

请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-TypeContent-Length

 

三、       HTTP响应报文

HTTP响应也由三个部分组成,分别是:状态行、消息报头、响应正文。在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status line)通过提供一个状态码来说明所请求的资源情况。

状态行格式如下:

HTTP-Version Status-Code Reason-Phrase CRLF

其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。

  • 1xx:指示信息--表示请求已接收,继续处理。
  • 2xx:成功--表示请求已被成功接收、理解、接受。
  • 3xx:重定向--要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误--请求有语法错误或请求无法实现。
  • 5xx:服务器端错误--服务器未能实现合法的请求。

常见状态代码、状态描述的说明如下。

  • 200 OK:客户端请求成功。
  • 400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务。
  • 404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
  • 500 Internal Server Error:服务器发生不可预期的错误。
  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

 

 

下面给出一个HTTP响应报文例子

 

HTTP/1.1 200 OK

Date: Sat, 31 Dec 2005 23:59:59 GMT

Content-Type: text/html;charset=ISO-8859-1

Content-Length: 122

 

<html>

<head>

<title>Wrox Homepage</title>

</head>

<body>

<!-- body goes here -->

</body>

</html>

 

四、       一个POST上传文件的例子

参考:http://www.tuicool.com/articles/muYfquQ

在开发中,我们使用的比较多的HTTP请求方式基本上就是GET、POST。其中GET用于从服务器获取数据,POST主要用于向服务器提交一些表单数据,例如文件上传等。而我们在使用HTTP请求时中遇到的比较麻烦的事情就是构造文件上传的HTTP报文格式,这个格式虽说也比较简单,但也比较容易出错。今天我们就一起来学习HTTP POST的报文格式以及通过Java来模拟文件上传的请求。

首先我们来看一个POST的报文请求,然后我们再来详细的分析它。

4.1 POST报文格式

POST /api/feed/ HTTP/1.1

Accept-Encoding: gzip

Content-Length: 225873

Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp

Host: www.myhost.com

Connection: Keep-Alive

 

--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp

Content-Disposition: form-data; name="lng"

Content-Type: text/plain; charset=UTF-8

Content-Transfer-Encoding: 8bit

 

116.361545

--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp

Content-Disposition: form-data; name="lat"

Content-Type: text/plain; charset=UTF-8

Content-Transfer-Encoding: 8bit

 

39.979006

--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp

Content-Disposition: form-data; name="images"; filename="/storage/emulated/0/Camera/jdimage/1xh0e3yyfmpr2e35tdowbavrx.jpg"

Content-Type: application/octet-stream

Content-Transfer-Encoding: binary

 

 

这里是图片的二进制数据

--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp--

这里我们提交的是经度、纬度和一张图片(图片数据比较长,而且比较杂乱,这里省略掉了)。

4.2 格式分析

4.2.1请求头分析

我们先看报文格式中的第一行:

POST /api/feed/ HTTP/1.1

这一行就说明了这个请求的请求方式,即为POST方式,要请求的子路径为/api/feed/,例如我们的服务器地址为www.myhost.com,然后我们的这个请求的完整路径就是 www.myhost.com/api/feed/,最后说明了HTTP协议的版本号为1.1。

Accept-Encoding: gzip
Content-Length: 225873
Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Host: www.myhost.com
Connection: Keep-Alive

 

这几个header的意思分别为服务器返回的数据需要使用gzip压缩、请求的内容长度为225873、内容的类型为"multipart/form-data"、请求参数分隔符(boundary)为OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp、请求的根域名为www.myhost.com、HTTP连接方式为持久连接( Keep-Alive)。

其中这里需要注意的一点是分隔符,即boundary。 boundary用于作为请求参数之间的界限标识,例如参数1和参数2之间需要有一个明确的界限,这样服务器才能正确的解析到参数1和参数2。但是分隔符并不仅仅是boundary,而是下面这样的格式:-- + boundary。例如这里的boundary为 OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp,那么参数分隔符则为:

--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp

不管boundary本身有没有这个"--",这个"--"都是不能省略的。

我们知道HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接(HTTP协议为无连接的协议);当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后续请求时,Keep-Alive功能避免了建立或者重新建立连接。

 

如上图中,左边的是关闭Keep-Alive的情况,每次请求都需要建立连接,然后关闭连接;右边的则是Keep-Alive,在第一次建立请求之后保持连接,然后后续的就不需要每次都建立、关闭连接了, 启用Keep-Alive模式肯定更高效,性能更高,因为避免了建立/释放连接的开销 。

http 1.0中默认是关闭的,需要在http头加入"Connection: Keep-Alive",才能启用Keep-Alive;http 1.1中默认启用Keep-Alive,如果加入"Connection: close ",才关闭。目前大部分浏览器都是用http1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,所以是否能完成一个完整的Keep- Alive连接就看服务器设置情况。

4.2.2请求实体分析

请求实体其实就是HTTP POST请求的参数列表,每个参数以请求分隔符开始,即-- + boundary。例如下面这个参数。

--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Content-Disposition: form-data; name="lng"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
 
116.361545

上面第一行为--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp,也就是--加上boundary内容, 最后加上一个换行 (这个换行不能省略),换行的字符串表示为"\r\n" 。第二行为Content-Disposition和参数名,这里的参数名为lng,即经度。 Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名,这里我们不过多关注。第三行为 Content-Type,即 WEB 服务器告诉浏览器自己响应的对象的类型 ,还有指定字符编码为UTF-8。 第四行是 描述的是消息请求(request)和响应(response)所附带的实体对象(entity)的传输形式, 简单文本数据我们设置为8bit,文件参数我们设置为binary就行 。然后添加两个换行之后才是参数的具体内容。例如这里的参数内容为116.361545。

注意这里的每行之间都是使用“\r\n”来换行的,最后一行和参数内容之间是两个换行。文件参数也是一样的格式,只是文件参数的内容是字节流。

这里要注意一下,普通文本参数和文件参数有如下两个地方的不同,因为其内容本身的格式是不一样的。

普通参数:

Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

文件参数:

Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

参数实体的最后一行是: --加上boundary加上--,最后换行,这里的 格式即为: --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp--。

4.3 模拟文件上传请求

public static void uploadFile(String fileName) {
        try {
  // 换行符
  final String newLine = "\r\n";
  final String boundaryPrefix = "--";
  // 定义数据分隔线
  String BOUNDARY = "========7d4a6d158c9";
  // 服务器的域名
  URL url = new URL("www.myhost.com");
  HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  // 设置为POST情
  conn.setRequestMethod("POST");
  // 发送POST请求必须设置如下两行
  conn.setDoOutput(true);
  conn.setDoInput(true);
  conn.setUseCaches(false);
  // 设置请求头参数
  conn.setRequestProperty("connection", "Keep-Alive");
  conn.setRequestProperty("Charsert", "UTF-8");
  conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
  OutputStream out = new DataOutputStream(conn.getOutputStream());
  // 上传文件
  File file = new File(fileName);
  StringBuilder sb = new StringBuilder();
  sb.append(boundaryPrefix);
  sb.append(BOUNDARY);
  sb.append(newLine);
  // 文件参数,photo参数名可以随意修改
  sb.append("Content-Disposition: form-data;name=\"photo\";filename=\"" + fileName
          + "\"" + newLine);
  sb.append("Content-Type:application/octet-stream");
  // 参数头设置完以后需要两个换行,然后才是参数内容
  sb.append(newLine);
  sb.append(newLine);
  // 将参数头的数据写入到输出流中
  out.write(sb.toString().getBytes());
  // 数据输入流,用于读取文件数据
  DataInputStream in = new DataInputStream(new FileInputStream(
          file));
  byte[] bufferOut = new byte[1024];
  int bytes = 0;
  // 每次读1KB数据,并且将文件数据写入到输出流中
  while ((bytes = in.read(bufferOut)) != -1) {
      out.write(bufferOut, 0, bytes);
  }
  // 最后添加换行
  out.write(newLine.getBytes());
  in.close();
  // 定义最后数据分隔线,即--加上BOUNDARY再加上--。
  byte[] end_data = (newLine + boundaryPrefix + BOUNDARY + boundaryPrefix + newLine)
          .getBytes();
  // 写上结尾标识
  out.write(end_data);
  out.flush();
  out.close();
  // 定义BufferedReader输入流来读取URL的响应
//       BufferedReader reader = new BufferedReader(new InputStreamReader(
//               conn.getInputStream()));
//       String line = null;
//       while ((line = reader.readLine()) != null) {
//           System.out.println(line);
//       }
        } catch (Exception e) {
  System.out.println("发送POST请求出现异常!" + e);
  e.printStackTrace();
        }
    }

 

4.4 使用Apache Httpmime上传文件

/**
     * @param fileName 图片路径
     */
    public static void uploadFileWithHttpMime(String fileName) {
        // 定义请求url
        String uri = "www.myhost.com";
        // 实例化http客户端
        HttpClient httpClient = new DefaultHttpClient();
        // 实例化post提交方式
        HttpPost post = new HttpPost(uri);
        // 添加json参数
        try {
  // 实例化参数对象
  MultipartEntity params = new MultipartEntity();
  // 图片文本参数
  params.addPart("textParams", new StringBody(
          "{'user_name':'我的用户名','channel_name':'却道明','channel_address':'(123.4,30.6)'}",
          Charset.forName("UTF-8")));
  // 设置上传文件
  File file = new File(fileName);
  // 文件参数内容
  FileBody fileBody = new FileBody(file);
  // 添加文件参数
  params.addPart("photo", fileBody);
  params.addPart("photoName", new StringBody(file.getName()));
  // 将参数加入post请求体中
  post.setEntity(params);
  // 执行post请求并得到返回对象 [ 到这一步我们的请求就开始了 ]
  HttpResponse resp = httpClient.execute(post);
  // 解析返回请求结果
  HttpEntity entity = resp.getEntity();
  InputStream is = entity.getContent();
  BufferedReader reader = new BufferedReader(new InputStreamReader(is));
  StringBuffer buffer = new StringBuffer();
  String temp;
  while ((temp = reader.readLine()) != null) {
      buffer.append(temp);
  }
  System.out.println(buffer);
        } catch (UnsupportedEncodingException e) {
  e.printStackTrace();
        } catch (ClientProtocolException e) {
  e.printStackTrace();
        } catch (IOException e) {
  e.printStackTrace();
        } catch (IllegalStateException e) {
  e.printStackTrace();
        }
    }

 

 

推荐阅读