04.HTTP
一、HTTP
HTTP是HyperText Transfer Protocol的缩写,即超文本传输协议(对比,HTML,超文本标记语言)。
它是一种请求/响应式的协议(双方通讯的格式),客户端在与服务器端建立连接后,就可以向服务器端发送请求,这种请求被称作HTTP请求,服务器端接收到请求后会做出响应,称为HTTP响应,客户端与服务器端在HTTP下的交互过程如图所示。
从图中可以清楚地看到客户端与服务器端使用HTTP通信的过程,接下来总结一下HTTP协议的特点,具体如下:
(1)支持客户端(浏览器就是一种Web客户端)/服务器模式。
(2)简单快速:客户端向服务器请求服务时,只需传送请求方式和路径。常用的请求方式有GET、POST等,每种方式规定了客户端与服务器联系的类型不同。由于HTTP简单,使得HTTP服务器的程序规模小,因而通信速度很快。
(3)灵活:HTTP允许传输任意类型的数据,正在传输的数据类型由Content-Type加以标记。
(4)无状态:HTTP是无状态协议。无状态是指协议对于事务处理没有记忆能力,如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。
二、HTTP 1.0和HTTP 1.1区别
1、HTTP1.0
基于HTTP 1.0协议的客户端与服务器在交互过程中需要经过建立连接、发送请求信息、回送响应信息、关闭连接4个步骤,具体交互过程如图所示。
从图中可以看出,客户端与服务器建立TCP(传输控制协议)连接后,每次只能处理一个HTTP请求。
对于内容丰富的网页来说,这样的通信方式明显有缺陷。例如,下面的一段HTML代码:
<html> <body> <img src="/image01.jpg"/> <img src="/image02.jpg"/> <img src="/image03.jpg"/> </body> </html>
上面的HTML文档中包含三个<img>标记,由于<img>标记的src属性指明的是图片的URL地址,因此,当客户端访问这些图片时,需要发送三次请求,并且每次请求都需要与服务器重新建立连接。如此一来,必然导致客户端与服务器端交互耗时,影响网页的访问速度。
2、HTTP1.1
为了克服上述HTTP1.0的缺陷,HTTP1.1版本支持持久连接,也就是说在一个TCP连接上可以传送多个HTTP请求和响应,从而减少了建立和关闭连接的消耗和延时,如图所示。
从图中可以看出,当客户端与服务器端建立连接后,客户端可以向服务器端发送多个请求,并且在发送下个请求时,无需等待上次请求的返回结果。但服务器必须按照接受客户端请求的先后顺序依次返回响应结果,以保证客户端能够区分出每次请求的响应内容。
由此可见,HTTP1.1不仅继承了HTTP1.0的优点,而且有效解决了HTTP1.0的性能问题,显著地减少浏览器与服务器交互所需要的时间。
三、HTTP消息
当用户在浏览器中访问某个URL地址、单击网页的某个超链接或者提交网页上的form表单时,浏览器都会向服务器发送请求数据,即HTTP请求消息。服务器接收到请求数据后,会将处理后的数据回送给客户端,即HTTP响应消息。
HTTP请求消息和HTTP响应消息统称为HTTP消息。
在HTTP消息中,除了服务器端的响应实体内容(HTML网页、图片等)以外,其他信息对用户都是不可见的,要想观察这些“隐藏”的信息,需要借助一些网络查看工具。例如:HttpWatch、Fiddler、Firefox浏览器的Firebug这类抓包工具。
另外IE浏览器的“F12开发人员工具”功能也能够查看网络数据情况。
浏览器和服务器通信的HTTP消息可以通过单击“网络”按钮进行查看。
为了更好地理解HTTP消息,我们启动配置好的Tomcat,浏览上节课编写的HelloTomcat项目,来详细学习HTTP消息。
我们选中第一条URL,点击“详细信息”按钮,切换到详细界面。
先来看一下“请求标头”,这里显示的是请求头信息,“F12开发人员工具”以键值对的形式展示出来,第一行是请求行,后面其余几行是请求消息头。
切换到“响应标头”,这里显示的是响应头信息。第一行是响应状态行,后面其余是响应消息头。
切换到“请求正文”,这里是请求体,但是由于我们的请求方式是GET方式,所以,请求体是空的。因为,表单提交的内容作为URL地址的参数提交了,并没有放到请求体中。
如果请求方式是POST,这里就会显示表单提交的参数。我们找到之前注册信息的那个html文件,将其发布到Tomcat中,运行注册一下查看消息,form.html代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta charset="UTF-8"> <title>注册</title> </head> <body> <fieldset> <legend>注册新用户</legend> <form action="form.html" name="注册申请表" method="POST"> <table cellpadding="2" align="center"> <tr> <td align="right">用户名:</td> <td><input type="text" name="username" /></td> </tr> <tr> <td align="right">密码:</td> <td><input type="password" name="password" /></td> </tr> <tr> <td align="right">性别:</td> <td> <input type="radio" name="sex" value="male" /> 男 <input type="radio" name="sex" value="female" /> 女 </td> </tr> <tr> <td align="right">兴趣:</td> <td><input type="checkbox" name="interest" value="film" />看电影 <input type="checkbox" name="interest" value="code" />敲代码 <input type="checkbox" name="interest" value="game" />玩游戏 </td> </tr> <tr> <td colspan="2" align="center"> <input type="submit" value="注册" /> <input type="reset" value="重填" /> </td> </tr> </table> </form> </fieldset> </body> </html>
切换到“响应正文”,这里是响应体。响应体实际上就是服务器返回的html文件内容。
下面我们来详细看看消息的具体内容。
四、HTTP请求消息
一个完整的请求消息是由请求行、请求消息头和实体内容(请求正文)三部分组成。
1、HTTP请求行
HTTP请求行位于请求消息的第一行,它包括三个部分,分别是请求方式、资源路径以及所使用的HTTP版本,具体示例如下:
GET /HelloTomcat/index.jsp HTTP/1.1 或 POST /HelloTomcat/form.html HTTP/1.1
其中,GET/POST是请求方式,/HelloTomcat/或/HelloTomcat/form.html是请求资源路径,HTTP/1.1是通信使用的协议版本。需要注意的是,请求行中的每个部分需要用空格分隔,最后要以回车换行结束。
在HTTP的请求消息中,请求方式有GET、POST、HEAD、OPTIONS、DELETE、TRACE、PUT和CONNECT共8种,每种方式都指明了操作服务器中指定URI资源的方式,它们表示的含义如表所示。
请求方式 |
含义 |
GET |
向特定资源发出请求(请求指定页面信息,并返回实体主体) |
POST |
向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 |
HEAD |
向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 |
PUT |
向指定资源位置上传其最新内容 |
DELETE |
请求服务器删除Request-URI所标识的资源(请求服务器删除页面) |
TRACE |
回显服务器收到的请求,主要用于测试或诊断 |
CONNECT |
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器,保留将来使用。 |
OPTIONS |
返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。 |
其中最常用的就是GET和POST方式
(1)GET方式
当用户在浏览器地址栏中直接输入某个URL地址或者单击网页上的一个超链接时,浏览器将使用GET方式发送请求。如果将网页上的form表单的method属性设置为“GET”或者不设置method属性(默认值是GET),当用户提交表单时,浏览器也将使用GET方式发送请求。
如果浏览器请求的URL中有参数部分,在浏览器生成的请求消息中,参数部分将附加在请求行中的资源路径后面。先来看一个URL地址,具体如下:
http://localhost:8080/HelloTomcat/form.html?username=lihuawei&password=123456
在上述URL中,“?”后面的内容为参数信息。参数是由参数名和参数值组成的,并且中间使用等号(=)进行连接。需要注意的是,如果URL地址中有多个参数,参数之间需要用“&”分隔。
当浏览器向服务器发送请求消息时,上述URL中的参数部分会附加在要访问的URI资源后面,具体如下所示:
GET /HelloTomcat/form.html?username=lihuawei&password=123456 HTTP/1.1
需要注意的是,使用GET方式传送的数据量有限,最多不能超过1KB(1024字节)。
(2)POST方式
如果网页上form表单的method属性设置为“POST”,当用户提交表单时,浏览器将使用POST方式提交表单内容,并把各个表单元素及数据作为HTTP消息的实体内容发送给服务器,而不是作为URI地址的参数传递。另外,在使用POST方式向服务器传递数据时,Content-Type消息头会自动设置为“application/x-www-form-urlencoded”,Content-Length消息头会自动设置为实体内容的长度,具体示例如下:
对于使用POST方式传递的请求信息,服务器端程序会采用与获取URI后面参数相同的方式来获取表单各个字段的数据。
需要注意的是,在实际开发中,通常都会使用POST方式发送请求,其原因主要有两个:
- POST传输数据大小无限制
由于GET请求方式是通过请求参数传递数据的,因此最多可传递1KB(1024字节)的数据。而POST请求方式是通过实体内容传递数据的,因此可以传递数据的大小没有限制。
- POST比GET请求方式更安全
由于GET请求方式的参数信息都会在URL地址栏明文显示,而POST请求方式传递的参数隐藏在实体内容中,用户是看不到的,因此,POST比GET请求方式更安全。
2、HTTP请求消息头
在HTTP请求消息中,请求行之后,便是若干请求消息头。请求消息头主要用于向服务器端传递附加消息,例如,客户端可以接收的数据类型(Accept)、压缩方法、语言(Accept-Language)以及发送请求的超链接所属页面的URL地址等信息,具体示例如下所示:
Accept:text/html, application/xhtml+xml, */*,能够处理的MIME(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展)类型;
Accept-Encoding:gzip, deflate,用于指定客户端能够进行解码的数据编码方式,这里的编码方式通常指的是某种压缩方式;
Accept-Language:zh-CN,头字段用于指定客户端期望服务器返回哪个国家语言的文档,如果指定多个国家的语言,用逗号分隔;
3、HTTP请求实体内容(请求正文)
如果使用的请求方式是GET,HTTP是没有实体内容的,参数都在URL中了。
如果使用的请求方式是POST,HTTP实体内容就是表单提交的参数。
五、HTTP响应消息
一个完整的响应消息主要包括响应状态行、响应消息头和实体内容(响应正文)。
1、HTTP响应状态行
HTTP响应状态行位于响应消息的第一行,它包括三个部分,分别是HTTP版本、一个表示成功或错误的整数代码(状态码)和对状态码进行描述的文本信息,具体示例如下:
HTTP/1.1 200 OK
上面的示例就是一个HTTP响应消息的状态行,其中HTTP 1.1是通信使用的协议版本(200是状态码),OK是状态描述,说明客户端请求成功。需要注意的是,请求行中的每个部分需要用空格分隔,最后要以回车换行结束。
关于协议版本和文本信息,都比较容易理解,而HTTP的状态码对读者来说则比较陌生,接下来就针对HTTP的状态码进行具体分析。
状态代码由三位数字组成,表示请求是否被理解或被满足。HTTP响应状态码的第一个数字定义了响应的类别,后面两位没有具体的分类,第一个数字有5种可能的取值,状态码列表如下:
分类 |
分类描述 |
1** |
信息,服务器收到请求,需要请求者继续执行操作 |
2** |
成功,操作被成功接收并处理 |
3** |
重定向,需要进一步的操作以完成请求 |
4** |
客户端错误,请求包含语法错误或无法完成请求 |
5** |
服务器错误,服务器在处理请求的过程中发生了错误 |
其中比较常见的的状态码如下:
200:OK,请求成功。一般用于GET与POST请求;
404:Not Found,请求的资源没有找到。说明客户端错误的请求了不存在的资源;
500:Internal Server Error,请求的资源找到了,但是服务器内部出现了错误,无法完成请求;
302:Found,找到,但是重定向。表示服务器要求浏览器重新再发一个请求,服务器会发送一个响应头location,指定新请求的URL地址;
304:Not Modified,未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,客户端在请求一个页面后,服务器第一次响应200,客户端浏览器会在本地建立一个缓存文件,并且有Last-Modified时间,第二次请求该资源时,会在请求中包含If-Modified-Since还给服务器(这个时间就是缓存文件的Last-Modified,就是浏览器缓存文件的最后修改时间),服务器端会比较If-Modified-Since与文件真实的Last-Modified时间是否相同。如果相同,说明服务器文件和客户端浏览器缓存的文件一致,没有修改,服务器会返回响应码304,并且不会返回响应实体内容(没有响应正文),节省响应时间,节省传输成本,表示客户端浏览器中缓存的就是最新版的响应实体内容。如果不相同,服务器的文件发生了改变,服务器会返回响应码200,并且重新返回响应实体内容。这个一般只对HTML静态页面有效,对于JSP动态页面一般不这样使用。当我们反复请求服务器上的一个HTML页面时,就会见到304。
2、HTTP响应消息头
在HTTP响应消息中,第一行为响应状态行,紧接着的是若干响应消息头,服务器端通过响应消息头向客户端传递附加信息,包括服务器软件名称(Server)、被请求资源需要的认证方式、客户端请求资源的最后修改时间(Last-Modified)、重定向地址等信息。HTTP响应消息头的具体示例如下所示:
Server:Apache-Coyote/1.1,服务器软件名称和版本信息;
Accept-Ranges:bytes,服务器接受客户端使用以bytes为单位的Range请求;
Last-Modified:Mon, 25 Mar 2019 05:57:55 GMT,文档的最后修改时间,使用的是GMT时间,和中国标准时会有8个小时的时区差。
Content-Type:text/html,响应实体内容的MIME类型(服务器端返回的响应实体内容);
Content-Length:1246,响应实体内容的长度(字节数);
Date :Mon, 25 Mar 2019 06:45:35 GMT 响应的时间,使用的是GMT时间,和中国标准时会有8个小时的时区差。
3、HTTP响应实体内容
HTTP响应实体内容就是服务器返回给客户端浏览器的HTML代码,用于显示服务器返回的数据。
六、HTTP其他头字段
设置网页不缓存,以下三个字段一起使用:
Pragma:no-cache 对于HTTP1.0有效
Cache-Control:no-cache 对于HTTP1.1有效
Expires:0 设置为0,表示当前网页过期,不用缓存。
Refresh:3;url=http://www.baidu.com 表示在3秒钟之后,自动跳转到另一个页面。
html中的<meta>标签可以用来模拟响应消息头:
<meta http-equiv="Refresh" content="5; url=http://www.sdbi.edu.cn">
七、Java随机生成验证码图片
我们学习了HTTP的相关知识后,我们可以利用HTTP去反复测试,来暴力破解Web系统的用户名和密码。为此,人们发明了验证码来防止计算机的暴力破解。我们下面来学习一下Java如何生成随机验证码。
定义一个类,VerifyCode.java,定义方法createImage(),用于创建一张验证码的图片,我们使用BufferedImage类来作为该方法的返回值;定义方法output(),用于生成一张JPEG类型的图片文件;定义方法getText(),用于得到验证码的文本,用来验证用户输入的验证码。代码如下:
import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; import java.util.Random; import javax.imageio.ImageIO; public class VerifyCode { private int w = 150; // 宽度 private int h = 70; // 高度 private Random r = new Random(); // 获得一个随机数对象 // 定义有哪些字体 private String[] fontNames = { "宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312" }; // 定义有哪些验证码的随机字符 private String codes = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; // 生成背景色 private Color bgColor = new Color(250, 250, 250); // 生成的验证码文本 private String text; // 生成随机颜色 private Color randomColor() { int red = r.nextInt(200); int green = r.nextInt(200); int blue = r.nextInt(200); return new Color(red, green, blue); } // 生成随机字体 private Font randomFont() { int index = r.nextInt(fontNames.length); String fontName = fontNames[index]; // 字体名称 int style = r.nextInt(4); // 字体样式(PLAIN=0,BOLD=1,ITALIC=2,BOLD|ITALIC=3) int size = r.nextInt(5) + 50; // 字体大小(50-54之间) return new Font(fontName, style, size); } // 画干扰线 private void drawLine(BufferedImage image) { int num = 3; // 干扰线数量 Graphics2D g2 = (Graphics2D) image.getGraphics(); // 得到图片画笔 for (int i = 0; i < num; i++) { int x1 = r.nextInt(w / 5); // 起点x坐标,起点尽量靠左,0到五分之一处 int y1 = r.nextInt(r.nextInt(h - 20) + 10); // 起点y坐标,中间,10到60之间 int x2 = r.nextInt(w / 5) + w / 5 * 4; // 终点x坐标,靠右,右五分之一 int y2 = r.nextInt(h); // 终点y坐标,中间,10到60之间 g2.setStroke(new BasicStroke(1.5F));// 设置线条特征,1.5F为线的宽度 g2.setColor(randomColor()); // 干扰线颜色随机 g2.drawLine(x1, y1, x2, y2); // 画线 } } // 得到codes的长度内的随机数,并使用charAt取得随机数位置上的codes中的字符 private char randomChar() { int index = r.nextInt(codes.length()); return codes.charAt(index); } // 创建一张验证码的图片 public BufferedImage createImage() { // 得到一个图片缓冲区 BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = (Graphics2D) image.getGraphics(); // 得到图片画笔 g2.setColor(bgColor); // 给画笔设置颜色 g2.fillRect(0, 0, w, h); // 使用画笔填充指定的矩形 g2.setColor(Color.RED); // 给画笔设置颜色 g2.drawRect(0, 0, w - 1, h - 1); // 使用画笔绘制指定的矩形 StringBuilder stringBuilder = new StringBuilder(); // 向图中画四个字符 for (int i = 0; i < 4; i++) { String s = randomChar() + ""; stringBuilder.append(s); int x = i * w / 4; // 计算字符x坐标位置 g2.setFont(randomFont()); // 设置画笔字体 g2.setColor(randomColor()); // 设置画笔颜色 g2.drawString(s, x, h - 20); // 在图片上写字符 } text = stringBuilder.toString(); drawLine(image); // 绘制干扰线 return image;// 返回图片 } // 得到验证码的文本,用来验证用户输入的验证码 public String getText() { return text; } // 定义输出的对象和输出文件流 // 注意第2个参数类型指定为OutputStream,以后可以用于向response中输出图片 public void output(BufferedImage bi, OutputStream output) throws IOException { ImageIO.write(bi, "JPEG", output); } }
以上代码中,随机字符数组中只有55个字符,没有0,1,i,l,I,o,O这7个字符,因为这7个字符容易出现不好识别的情况。
另外,我们在写一个类VerifyCodeTest.java,用于测试验证码是否能够正确生成。代码如下:
import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.io.IOException; public class VerifyCodeTest { public static void main(String[] args) throws IOException { VerifyCode code = new VerifyCode(); BufferedImage image = code.createImage(); // 获得验证码图片对象 code.output(image, new FileOutputStream("D:/project_javaweb/image.jpg")); System.out.println("验证码的真实文本是:" + code.getText()); // 输出验证码文本 } }
如果我们不是需要画干扰线,而是在图片上加上一些噪点,我们可以这样修改drawLine()方法。
private void drawLine(BufferedImage image) { int num = 500; // 噪点个数 Graphics2D g2 = (Graphics2D) image.getGraphics(); for (int i = 0; i < num; i++) { int x = r.nextInt(width); int y = r.nextInt(heigth); g2.setColor(randomColor()); g2.setStroke(new BasicStroke(2.0F)); // 线的粗细 g2.drawLine(x, y, x, y); // 起点终点横纵坐标一样,就是画点 } }