
1.2 HTTP请求与响应
超文本传输协议(HyperText Transfer Protocol,HTTP)是用于从WWW服务器传输超文本到本地浏览器的应用层传输协议。其简捷、快速的方式,非常适合用于分布式超媒体信息系统。HTTP是在1990年提出的,经过多年的使用与发展,得到不断的完善和扩展。
HTTP工作于客户端-服务器架构之上,如图1.9所示。浏览器作为HTTP客户端通过URL向HTTP服务器(即Web服务器)发送所有请求,Web服务器根据接收到的请求向客户端发送响应信息。

图1.9 客户端-服务器架构
1991年,HTTP/0.9版发布,该版本非常简单,只有一个GET命令。1996年5月,HTTP/1.0版本发布,其内容大大增加,任何格式的内容都可以发送,这使得互联网不仅可以传输文字,还可以传输图像、视频及二进制文件,HTTP/1.0为互联网的发展奠定了基础。1997年1月,HTTP/1.1版本发布,它进一步完善了HTTP,直到现在HTTP/1.1仍是较为流行的版本。2015年,HTTP/2版本发布,但未得到普及应用。下面以HTTP/1.1版本的协议为例,对HTTP请求以及HTTP响应格式进行说明。
1.2.1 HTTP请求报文格式
HTTP请求由三部分组成:请求行、消息报头、请求正文。其中的部分消息报头和实体内容是可选的,消息报头和请求正文之间要用空行隔开。HTTP请求消息的基本格式如图1.10所示。

图1.10 HTTP请求消息的基本格式
一个简单的HTTP请求消息如下所示。
GET /1.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
第一部分为请求行,用来说明请求类型、要访问的资源以及所使用的HTTP版本。其中,GET说明HTTP的请求方法为GET,“/1.html”为要访问的资源,该行的最后一部分说明使用的是HTTP/1.1版本。其中请求方法有多个,各个方法的具体含义如表1.4所示。
表1.4 HTTP的请求方法

第二部分为请求报头,是紧接在请求行之后的部分,用来说明服务器要使用的附加信息。从第二行起为请求报头开始,Host指出请求的目的地,User-Agent是浏览器类型检测逻辑的重要基础,该信息由浏览器来定义,并且在每个请求中自动发送,以及其他一些在HTTP请求解析中有用的信息。下面介绍一些比较常用的HTTP请求报头以及各个报头的含义。
1. Host
Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常是从HTTP URL中提取出来的,在发送请求时,该报头域是必需的。例如,在浏览器中输入“http://localhost:8080/index.html”,浏览器发送的请求消息中就会包含Host请求报头域的内容,代码如下。
Host:localhost:8080
2. Accept
Accept请求报头域用于指定客户端接受哪些类型的信息。例如,“Accept:image/gif”表明客户端希望接收“GIF”图像格式的资源;“Accept:text/html”表明客户端希望接收html文本。
3. Accept-Charset
Accept-Charset请求报头域用于指定客户端接受的字符集。例如,“Accept-Charset:gb2312”,如果在请求消息中没有设置这个域,则表示任何字符集都可以接受。
4. Accept-Encoding
Accept-Encoding请求报头域类似于Accept,但它只用于指定可接受的内容编码。例如,“Accept-Encoding:gzip.deflate”,如果请求消息中没有设置这个域,则表示客户端对各种内容编码都能接受。
5. Accept-Language
Accept-Language请求报头域类似于Accept,但它只用于指定一种自然语言。例如,“Accept-Language:zh-cn”,如果请求消息中没有设置这个报头域,则表示客户端对各种语言都能接受。
6. Authorization
Authorization请求报头域用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到的服务器响应代码为401(未授权),则客户端会发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
7. User-Agent
用户登录到一些网站时,往往会看到一些欢迎信息,其中列出了客户端操作系统的名称和版本,以及所使用的浏览器的名称和版本,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息的。User-Agent请求报头域允许客户端将它的操作系统、浏览器及其他属性告诉服务器。
第三部分是一个空行,请求报头结束后,必须添加一个空行,即使第四部分的请求数据为空,也必须有空行。
第四部分是请求正文,请求正文也称请求主体,在其中可以添加任意其他数据,这些数据都是按照“key=value”的格式设置参数名与参数值信息的,多个参数之间使用“&”进行分隔。该例的请求正文为空。带有请求正文的HTTP请求消息示例代码如下。
GET /1.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
username=admin&password=123456
1.2.2 HTTP响应报文格式
一般情况下,服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息。HTTP响应也由三部分组成:状态行、响应报头和响应正文。图1.11所示为HTTP响应消息的基本格式。

图1.11 HTTP响应消息的基本格式
一个简单的HTTP响应消息如下所示。
HTTP/1.1 200 OK
Date: Tue, 16 May 2017 07:23:05 GMT
Content-Length: 97
Content-Type: text/html;charset=UTF-8
Last-Modifield: Tue, 16 May 2017 04:14:19 GMT
<html>
<head>
<title>HttpServer</title>
</head>
<body>
hello world
</body>
</html>
第一部分是状态行,由HTTP的版本号、状态码和状态消息三部分组成。第一行中的“HTTP/1.1200 OK”表明此HTTP的版本号为1.1,状态码为200,状态消息为“OK”,代表服务器成功接收了请求并做出了最终的正确回应。HTTP响应中包含了很多响应状态码,表1.5列举了常见的几种响应状态码,并对各个状态码的含义做出了解释。
表1.5 常用响应状态码及其说明

第二部分是响应报头,用来说明客户端要使用的一些附加信息。其中,Date表示生成响应的日期和时间。而Content-Type指定了响应正文的MIME类型是文本类型,并且是文本中的HTML类型,响应正文的编码类型是UTF-8。Content-Length说明了响应正文的长度。Last-Modifield指明资源最终修改的时间。
第三部分是一个空行,响应报头后面的这个空行也是必需的。
第四部分是响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文,在这里是一个符合HTML语法标准的字符串。
1.2.3 URL
HTTP请求通常由HTTP客户端发起,并建立一个到服务器指定端口的TCP连接,HTTP服务器则在该端口监听客户端发送过来的请求。一旦收到请求,服务器会向客户端发回一个状态行(如“HTTP/1.1200 OK”)以及响应的消息,消息的消息体可能是请求的文件、错误消息,或者是其他一些信息。通过HTTP请求的资源由统一资源标示符(URL)来标识。
URL是一种特殊类型的统一资源定位符,用于确定网络中具体资源的位置。URL包含了用于查找某个资源的足够信息,其具体格式如下。
http://host[:port]/[abs_path]
其中,http表示要通过HTTP来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用默认端口80;abs_path指定请求资源的URL;如果URL中没有给出abs_path,那么当它作为请求URL时,必须以“/”的形式给出,该工作一般由浏览器自动完成。
1.2.4 简单的Web服务器
学习完HTTP的内容后,思考一下,如何通过Web服务器将HTML文件,使用HTTP在互联网上进行共享?
下面使用ServerSocket来发布一个Web服务,让浏览器通过HTTP来连接这个Web服务,Web服务接收浏览器发送过来的HTTP请求,并对HTTP请求进行解析,封装到Request对象中,Request类的定义如下。
1 public class Request {
2 private Map<String,String> requestHeadMap = new HashMap<String,String>();
3 private String method; //请求方法
4 private String resourcePath; //请求路径
5 private Map<String,String> parameter = new HashMap<String,String>(); //请求参数
6 public void setRequestHead(String key,String value){
7 requestHeadMap.put(key, value);
8 }
9 public String getHead(String key){
10 return requestHeadMap.get(key);
11 }
12 public void setParameter(String key,String value){
13 parameter.put(key, value);
14 }
15 public String getParameter(String key){
16 return parameter.get(key);
17 }
18 public String getMethod() {
19 return method;
20 }
21 public void setMethod(String method) {
22 this.method = method;
23 }
24 public String getResourcePath() {
25 return resourcePath;
26 }
27 public void setResourcePath(String resourcePath) {
28 this.resourcePath = resourcePath;
29 }
30 }
首先,定义Web服务类为HttpServer,在该类中创建一个ServerSocket对象,占用8080端口,并循环等待浏览器连接,当浏览器发送请求连接服务器后,HttpServer针对当前连接启动线程,进行HTTP请求处理,进而继续等待浏览器连接,HttpServer代码如下。
1 public class HttpServer {
2 private ServerSocket server;
3 public HttpServer(){
4 try {
5 server = new ServerSocket(8080);
6 } catch (IOException e) {
7 System.out.println("服务无法启动");
8 e.printStackTrace();
9 System.exit(1);
10 }
11 }
12 /**
13 * 启动Web服务器接收客户端的HTTP请求,对请求进行解析处理,生成响应
14 */
15 public void run(){
16 while(true){
17 try {
18 //等待客户端连接
19 Socket socket = server.accept();
20 RequestProcess requestPro = new RequestProcess(socket);
21 //启动线程进行请求处理
22 Thread thread = new Thread(requestPro);
23 thread.start();
24 } catch (IOException e) {
25 System.out.println("客户端连接异常");
26 e.printStackTrace();
27 }
28 }
29 }
30 }
在上述代码中,RequestProcess是对浏览器发送的HTTP请求进行处理的工具类,该类主要从Socket连接中获得完整的HTTP请求消息,并封装到请求对象Request中,再根据请求中的请求资源路径,将具体资源文件封装到HTTP响应Responce对象中,Responce类的定义如下。
1 public class Responce {
2 private int respCode = 200; //响应状态码
3 private byte[] respBody; //响应正文
4 private Map<String,String> respHead = new HashMap<String,String>();//响应头
5 public void setHead(String key,String value){
6 respHead.put(key, value);
7 }
8 public int getRespCode() {
9 return respCode;
10 }
11 public void setRespCode(int respCode) {
12 this.respCode = respCode;
13 }
14 public byte[] getRespBody() {
15 return respBody;
16 }
17 public void setRespBody(byte[] respBody) {
18 this.respBody = respBody;
19 }
20 public Map<String, String> getRespHead() {
21 return respHead;
22 }
23 public void setRespHead(Map<String, String> respHead) {
24 this.respHead = respHead;
25 }
26 }
最后根据资源文件的处理情况,设置Responce对象中的响应状态码,将最终的响应结果对象解析成HTTP响应格式,通过Socket连接将HTTP响应发送给浏览器,浏览器进行接收,RequestProcess请求处理类的具体代码如下。
1 public class RequestProcess implements Runnable{
2 private Socket socket;
3 public RequestProcess(Socket socket){
4 this.socket = socket;
5 }
6 @Override
7 public void run() {
8 try {
9 //获取客户端的请求详情
10 Request req = getReauest();
11 //根据请求获取资源,生成响应对象,设置响应正文,并设置响应状态码
12 Responce resp = requestProcess(req);
13 //将响应正文返回给客户端
14 printResponce(resp);
15 } catch (IOException e) {
16 System.out.println("无法读取客户端的连接");
17 e.printStackTrace();
18 }
19
20 }
21 private Request getReauest() throws IOException{
22 InputStream io = this.socket.getInputStream();
23 Request request = new Request();
24 BufferedReader br = new BufferedReader(new InputStreamReader(io));
25 String line;
26 //处理请求行
27 line = br.readLine();
28 String[] requestLine = line.split(" ");
29 request.setMethod(requestLine[0]); //请求方法
30 request.setResourcePath(requestLine[1]); //请求路径
31 //处理请求头
32 while((line = br.readLine())!=null&&!"".equals(line)){
33 String[] heads = line.split(":");
34 request.setRequestHead(heads[0],heads[1]);
35 }
36 return request;
37 }
38 private Responce requestProcess(Request request) throws IOException {
39 Responce resp = new Responce();
40 String resource = request.getResourcePath();
41 FileInputStream fr = null;
42 try {
43 File file = new File(System.getProperty("user.dir") + resource);
44 fr = new FileInputStream(file);
45 byte[] body = new byte[(int)file.length()];
46 fr.read(body);
47 resp.setRespBody(body);
48 } catch (FileNotFoundException e) {
49 resp.setRespCode(404);
50 }finally{
51 if(fr!=null){
52 fr.close();
53 }
54 }
55 return resp;
56 }
57
58 private void printResponce(Responce resp) throws IOException{
59 System.out.println("开始生成响应");
60 OutputStream out = this.socket.getOutputStream();
61 PrintStream ps = new PrintStream(out);
62 StringBuffer sb = new StringBuffer();
63 //设置响应行
64 sb.append("HTTP/1.1 200 OK\n\r");
65 //设置响应头
66 Set<Entry<String, String>> set = resp.getRespHead().entrySet();
67 for(Entry<String, String> entry:set){
68 sb.append(entry.getKey())
69 .append(":")
70 .append(entry.getValue())
71 .append("\n\r");
72 }
73 //设置响应正文
74 sb.append("\n\r");
75 if(resp.getRespBody()!=null){
76 sb.append(new String(resp.getRespBody()));
77 }
78 ps.println(sb.toString());
79 ps.flush();
80 ps.close();
81 }
82 }
通过Run启动Web服务程序,并在当前工程的根目录下创建html页面1.html,通过浏览器进行访问,可以看到响应结果,如图1.12和图1.13所示。

图1.12 浏览器请求HttpServer

图1.13 查看HttpServer的响应结果
1 public class Run {
2 public static void main(String[] args) {
3 HttpServer server = new HttpServer();
4 server.run();
5 }
6 }