Java Web程序设计与案例教程(微课版)
上QQ阅读APP看书,第一时间看更新

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 }