http简介
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。
HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。
http是万维网的一套标准协议,规范了客户端与服务端通讯的报文格式,状态码等。
如果没有这个标准协议,那么所有基于http的通讯就无法完成,大家各自编写不同的格式规范,那么客户端和服务端就无法适配不同。正是有了统一规范的标准协议,并且大家都遵守这些协议,浏览器才能很好的和服务端进行通讯。
HTTP 工作原理
HTTP协议工作于客户端-服务端架构上(C-S)。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。
注意: 我们一般常说的的B-S指的是浏览器和服务端。浏览器就是客户端,所有的通讯都是有浏览器来完成,但是为了保证客户端机器(PC电脑)的安全,浏览器在通讯过程中有一些限制,比如跨域等安全限制。
Web服务器有:Apache服务器,IIS服务器(Internet Information Services)等。Web服务器根据接收到的请求后,向客户端发送响应信息。HTTP默认端口号为80,但是你也可以改为8080或者其他端口。
-
CGI(Common Gateway Interface) 是 HTTP 服务器与你的或其它机器上的程序进行“交谈”的一种工具,其程序须运行在网络服务器上。
-
浏览器显示的内容都有 HTML、XML、GIF、Flash 等,浏览器是通过 MIME Type 区分它们,决定用什么内容什么形式来显示。
-
MIME Type 是该资源的媒体类型,MIME Type 不是个人指定的,是经过互联网(IETF)组织协商,以 RFC(是一系列以编号排定的文件,几乎所有的互联网标准都有收录在其中) 的形式作为建议的标准发布在网上的,大多数的 Web 服务器和用户代理都会支持这个规范。媒体类型通常通过 HTTP 协议,由 Web 服务器告知浏览器的,更准确地说,是通过 Content-Type 来表示的。例如:Content-Type:text/HTML。
HTTP三点注意事项:
-
HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。我们也称这样的连接为短连接。
-
HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
-
HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。思考在无状态的情况下基于B-S模式的应用系统是如何保存用户的登录状态的
HTTP 报文格式
客户端请求报文
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
请求头是可以多个任意键值对的,比如我们平时经常用的授权码,jsessionId等
服务端返回报文
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
http请求方法(method)
方法 | 描述 |
---|---|
GET | 请求指定的页面信息,并返回实体主体 |
HEAD | 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。 POST 请求可能会导致新的资源的建立和/或已有资源的修改。 |
PUT | 从客户端向服务器传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定的页面 |
CONNECT | 协议中预留给能够将连接改为管道方式的代理服务器 |
OPTIONS | 允许客户端查看服务器的性能 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 |
PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 |
http响应头
应答头 | 说明 |
---|---|
Allow | 服务器支持哪些请求方法(如GET、POST等) |
Content-Encoding | 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。 利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩, 但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。 因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept-Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面 |
Content-Length | 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStream,完成后查看其大小,然后把该值放入Content-Length头,最后通过byteArrayStream.writeTo(response.getOutputStream()发送内容。 |
Content-Type | 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。 |
Date | 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。 |
Expires | 应该在什么时候认为文档已经过期,从而不再缓存它? |
Last-Modified | 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。 |
Location | 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。 |
Refresh | 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader("Refresh", "5; URL=http://host/path")让浏览器读取指定的页面。 注意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV="Refresh" CONTENT="5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。 注意Refresh的意义是"N秒之后刷新本页面或访问指定页面",而不是"每隔N秒刷新本页面或访问指定页面"。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV="Refresh" ...>。 注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。 |
Server | 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。 |
Set-Cookie | 设置和页面关联的Cookie。Servlet不应使用response.setHeader("Set-Cookie", ...),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。 |
WWW-Authenticate | 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的应答中这个头是必需的。例如,response.setHeader("WWW-Authenticate", "BASIC realm=\"executives\"")。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess) |
HTTP content-type
Content-Type(内容类型),一般是指网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,这就是经常看到一些 PHP 网页点击的结果却是下载一个文件或一张图片的原因。
Content-Type 标头告诉客户端实际返回的内容的内容类型。
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something
- 常见的媒体格式类型如下:
类型 | 描述 |
---|---|
text/html | HTML格式 |
text/plain | 纯文本格式 |
text/xml | XML格式 |
image/gif | gif图片格式 |
image/jpeg | jpg图片格式 |
image/png | png图片格式 |
- 以application开头的媒体格式类型:
column1 | column2 |
---|---|
content1 | content2 |
application/xhtml+xml | XHTML格式 |
application/xml | XML数据格式 |
application/atom+xml | Atom XML聚合格式 |
application/json | JSON数据格式 |
application/pdf | pdf格式 |
application/msword | Word文档格式 |
application/octet-stream | 二进制流数据(如常见的文件下载) |
application/x-www-form-urlencoded | 表单中默认的格式,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式) |
- 另外一种常见的媒体格式是上传文件之时使用的:
类型 | 描述 |
---|---|
text/html | HTML格式 |
multipart/form-data | 需要在表单中进行文件上传时,就需要使用该格式 |
具体的文件content-type请参考:
https://www.runoob.com/http/http-content-type.html
content-type 是非常重要的一个响应头,浏览器就是根据不同的content-type来做出不同的响应机制。比如之前就有人经常遇到什么时候图片是直接显示,什么时候图片是下载。
扩展训练
下面我们通过最基础的servlet代码,以及原生的js代码来理解写servlet和http县相关的知识。
html的显示
一般我们显示html都是通过html页面或者是jsp页面。其实不论是html还是jsp在response报文中都会设置content-type 为 text/html。正是因为content-type浏览器接收到response报文后才会解析html的bom结构渲染出结果。
- servlet代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置:响应内容类型
response.setContentType("text/html;charset=utf-8");
// response.setContentType("text/plain;charset=utf-8");
// response.setHeader("Content-Encoding", "utf-8");
// 输出文本
PrintWriter out = response.getWriter();
StringBuilder html = new StringBuilder();
// html.append("<!DOCTYPE html> <html> <head>");
// html.append("<meta http-equiv=\"Content-Type\" content=\"text/xml; charset=utf-8\" />");
html.append("<h1> " + message + " </h1>");
html.append("<input placeholder=\"请输入你的姓名\">");
html.append("</head>");
// html.append("<body>");
// html.append("</html>");
out.write(html.toString());
}
- web.xml servlet配置
<servlet>
<servlet-name>DemoServlet</servlet-name>
<servlet-class>http.DemoServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DemoServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
注意观察以上代码,什么时候输出纯文本,什么时候输出html
- 纯文本
- html
图片的显示
通过servlet显示图片,又或者是图片下载。同样也是浏览器什么时候认为应该打开图片下载窗口,什么时候应该直接显示该图片。
-
servlet代码
java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置:响应内容类型
// response.setContentType("application/octet-stream ");
// response.setContentType("multipart/form-data");
// response.setContentType("text/plain");
response.setContentType("image/*");
response.setContentType("image/jpeg");
// response.setHeader("content-disposition", "attachment;filename=test.jpg");// 得到向客户端输出二进制数据的对象 ServletOutputStream outStream = response.getOutputStream(); // 以byte流的方式打开文件 FileInputStream fis = new FileInputStream(this.getClass().getResource("/paris-1836415_1280.jpg").getFile()); // 读数据 byte data[] = new byte[1024]; while (fis.read(data) > 0) { outStream.write(data); } fis.close(); outStream.close();
}
* web.xml配置
```xml
<servlet>
<servlet-name>ImageServlet</servlet-name>
<servlet-class>http.ImageServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ImageServlet</servlet-name>
<url-pattern>/image</url-pattern>
</servlet-mapping>
页面的重定向
一般我们通过页面redirect实现页面重定向,注意重定向是由浏览器完成的,所以浏览器的url地址栏会改变,这个是servlet的forward是不一样的,注意区分下。其实我们也可以通过直接设置response的响应header实现重定向
- servlet代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置:响应内容类型
response.addHeader("Refresh","0; URL=/image");
}
这里web.xml配置就不展示了,应该都理解了。
- 效果
现在浏览器地址输入refresh的url
最终会重定向到image中
附件上传
stream解析
一般我们如果要通过http实现附件上传,conten-type 需要设置成multipart/form-data。
由于现在很多框架都做了封装,所以很多时候我们并不需要关注在附件上传的过程中request的报文格式,通过框架可以获取到附件信息、流信息、表单信息。
如果conten-type是multipart/form-data,其实整个请求的内容都是在stream流中,包括表单信息。并且会有一定的格式,如果我们不引用任何其他jar包,其实也是可以做到解析stream流中的数据并且获取文件以及表单信息。
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class EfengPayload {
private byte[] reqdata;
private byte[] boundary;
Map<String, String> payloadinfo;
EfengPayload(HttpServletRequest req) throws ServletException, IOException {
this.initInputData(req);
}
private void initInputData(HttpServletRequest req) throws ServletException, IOException {// 初始化payload
boundary = req.getHeader("Content-Type").split("boundary=")[1].getBytes();// 获取payload的随机分割线
int ra;
InputStream in = req.getInputStream();// 获取输入流
BufferedInputStream br = new BufferedInputStream(in);// 缓冲流接收输入流
int byteslen = req.getContentLength();// 获取输入流长度
reqdata = new byte[byteslen];// 新建保存payload所有数据数组
for (int i = 0; (ra = br.read()) != -1; i++) {
reqdata[i] = (byte) ra;// 从缓冲流读取所有的数据赋值给reqdata,之后都是对这个数组进行操作
}
br.close();
in.close();
payloadinfo = new HashMap<String, String>();// 新建map,格式
// name->[0,1,2];0:代表内容起始位1:代表下一个内容的分割线起始位,2:当前内容起始位
int a[] = new int[256];// 存放每个分割线所处位置的标记数组
int aindex = 0;// 上面a数组的配套下标
for (int i = 0; i < reqdata.length; i++) {// 从头到尾遍历输入流数据
if (reqdata[i] == boundary[0]) {
for (int j = 1; j < boundary.length; j++) {
if (reqdata[i + j] != boundary[j])
break;
if (j == boundary.length - 1) {// 如果有一段与boundary(随机字母分割线)匹配则将当前位置记录
a[aindex++] = i;// 记录当前a数组位置
}
}
}
}
byte[] namebyte = " name=\"".getBytes();// name特征byte数组 例如name="xxxxx
boolean finded = false;// 分辨当前数据块是否找到
for (int j = 0; j < aindex - 1; j++) {
int xb = a[j];// 获取当前数据块的起始坐标数值
finded = false;// 每次进入前设置没找到
for (int k = xb; k < a[j + 1]; k++) {
if (reqdata[k] == namebyte[0]) {
for (int m = 1; m < namebyte.length; m++) {
if (reqdata[k + m] != namebyte[m])
break;
if (m == namebyte.length - 1) {// 从namebyte的第一位到最后一位都匹配成功
finded = true;// 找到name
StringBuilder mapname = new StringBuilder();
int n;
for (n = k + m + 1; ; n++) {// 请求数据从name=“位置+1处开始收集name信息
if (reqdata[n] == (byte) '\"')// 如果”意味着name收集结束,通常格式name=“xxx”
break;
mapname.append((char) reqdata[n]);// 填入当前位
}
for (int o = n + 1; o < a[j + 1]; o++) {// 继续往下一直搜索直到搜索到数据开头处
if (reqdata[o] == (byte) '\n'
&& reqdata[o - 2] == (byte) '\n') {// 特征符合
payloadinfo.put(mapname.toString(), o + 1
+ "," + a[j + 1] + "," + a[j]);// map中记录当前数据块的name
// ,内容起始位,下一个内容的分割线起始位,当前内容起始位
break;
}
}
}
if (finded)// 如果找到跳出当前循环
break;
}
}
if (finded)// 如果找到跳出当前循环
break;
}
}
}
public byte[] getPayloadByteValue(String name) {
String[] indexs = payloadinfo.get(name).split(",");// 私有map中记录的各块的基本信息
int bstart = Integer.parseInt(indexs[0]), bend = Integer
.parseInt(indexs[1]);// 数据起始位与下一数据头(头分割线)的起始位
for (int j = bend; ; j--) {
if (reqdata[j] == 10 && reqdata[j - 1] == 13) {
bend = j - 1;// 找到特征,确定结束位置
break;
}
}
byte[] retbyt = new byte[bend - bstart];// 确定返回数组大小
for (int i = bstart; i < bend; i++) {// 取出区间数据
retbyt[i - bstart] = reqdata[i];
}
return retbyt;
}
public String getPayloadValue(String name) throws UnsupportedEncodingException {
StringBuilder retstr = new StringBuilder();
byte[] bytename = getPayloadByteValue(name);
return new String(bytename, "utf-8");
}
public String getPayloadOthers(String name, String type) {
if ("Content-Type".equals(type))
type = "\n" + type + ": ";
else
type = (" " + type + "=\"");
byte[] typebyte = type.getBytes();
int end = Integer.parseInt(payloadinfo.get(name).split(",")[0]);// 0代表数据起始位置
int start = Integer.parseInt(payloadinfo.get(name).split(",")[2]);// 2代表数据前面标记的起始位置
for (int i = start; i < end; i++) {
if (reqdata[i] == typebyte[0]) {
for (int j = 1; j < typebyte.length; j++) {
if (reqdata[i + j] != typebyte[j])
break;
if (j == typebyte.length - 1) {
String rtsstr = "";
for (int k = i + j + 1; ; k++) {
if ((char) reqdata[k] == ';'
|| (char) reqdata[k] == '\"'
|| (char) reqdata[k] == '\n'
|| (char) reqdata[k] == (char) 13)//回车
return rtsstr;
rtsstr += (char) reqdata[k];
}
}
}
}
}
return null;
}
public BufferedImage getImage(String name) throws IOException {
byte nameByte[] = this.getPayloadByteValue(name);
ByteArrayInputStream imageinput = new ByteArrayInputStream(nameByte);
BufferedImage buf = ImageIO.read(imageinput);
return buf;
}
}
以上代码是解析整个request stream流数据的整个代码。
分析下过程,流的解析比较麻烦的就是不可见,因为流数据都是byte。整个解析思路是通过边界获取数据块,这里的数据库快就是表单中的每一个form iteam。边界是htpp随机生成的,如下图中boundary=----WebKitFormBoundaryHkkX3jGyBTVrSxMH,----WebKitFormBoundaryHkkX3jGyBTVrSxMH就是边界值,也就是分隔符。
。
所以第一步我们是要通过request header获取请求头中的边界值。
boundary = req.getHeader("Content-Type").split("boundary=")[1].getBytes();// 获取payload的随机分割线
获取到边界值,然后就是讲stream流转成byte数组并解析。这里我们将stream转成String字符串,简单看下整个流中的数据格式。
下面一大串非法字符,其实就是图片的byte流。
拉倒最后,form-data,就是普通的表单信息了。
本文中通过表单上传附件,后端解析stream数据,获取其中的图片字节内容,然后输出到前端页面。
- 后端代码,读取流中的图片内容后,通过response.getOutPutStream()向浏览器输出图片
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
EfengPayload efeng = new EfengPayload(request);
ImageIO.write(
efeng.getImage("uploadFile"),
efeng.getPayloadOthers("uploadFile", "Content-Type").split("/")[1],
response.getOutputStream());
System.out.println(efeng.getPayloadValue("username"));//获取前端name为username的组件值
}
- 前端代码通过form表单提交附件以及其他表单
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件上传实例 </title>
</head>
<body>
<h1>文件上传实例</h1>
<form method="post" action="/parse-stream" enctype="multipart/form-data">
选择一个文件:
<input type="file" name="uploadFile"/>
<input type="text" name="name"/>
<br/><br/>
<input type="submit" value="上传"/>
</form>
</body>
</html>
commons-fileupload
servlet中实现附件上传也可以引用commons-fileupload,commons-fileupload内部也是对流做了解析。实现代码如下
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检测是否为多媒体上传
if (!ServletFileUpload.isMultipartContent(request)) {
// 如果不是则停止
PrintWriter writer = response.getWriter();
writer.println("Error: 表单必须包含 enctype=multipart/form-data");
writer.flush();
return;
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存临界值 - 超过后将产生临时文件并存储于临时目录中
factory.setSizeThreshold(MEMORY_THRESHOLD);
// 设置临时存储目录
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload(factory);
// 设置最大文件上传值
upload.setFileSizeMax(MAX_FILE_SIZE);
// 设置最大请求值 (包含文件和表单数据)
upload.setSizeMax(MAX_REQUEST_SIZE);
// 中文处理
upload.setHeaderEncoding("UTF-8");
// 构造临时路径来存储上传的文件
// 这个路径相对当前应用的目录
String uploadPath = request.getServletContext().getRealPath("./") + File.separator + UPLOAD_DIRECTORY;
// 如果目录不存在则创建
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
// 解析请求的内容提取文件数据
@SuppressWarnings("unchecked")
List<FileItem> formItems = upload.parseRequest(request);
if (formItems != null && formItems.size() > 0) {
// 迭代表单数据
for (FileItem item : formItems) {
// 处理不在表单中的字段
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
String filePath = uploadPath + File.separator + fileName;
File storeFile = new File(filePath);
// 在控制台输出文件的上传路径
System.out.println(filePath);
// 保存文件到硬盘
item.write(storeFile);
request.setAttribute("message",
"文件上传成功!");
}
}
}
} catch (Exception ex) {
request.setAttribute("message",
"错误信息: " + ex.getMessage());
}
// 跳转到 message.jsp
request.getServletContext().getRequestDispatcher("/message.jsp").forward(
request, response);
}
前端html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件上传实例 </title>
</head>
<body>
<h1>文件上传实例</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
选择一个文件:
<input type="file" name="uploadFile"/>
<input type="text" name="name"/>
<br/><br/>
<input type="submit" value="上传"/>
</form>
</body>
</html>
FormData表单提交
该种方式是通过FormData对象来封装表单数据,实现表单异步提交。
- 前端代码
function upload() {
var form = document.getElementById("upload-form");
var formData = new FormData(form);
var oReq = new XMLHttpRequest();
oReq.onreadystatechange = function () {
if (oReq.readyState == 4) {
if (oReq.status == 200) {
document.getElementById('result').append(oReq.responseText)
}
}
}
oReq.open("POST", "/stream");
oReq.send(formData);
}
- 后端代码
/**
* 上传数据并且直接将内容输出
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String str = IOUtils.toString(request.getInputStream(), "utf-8");
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write(str);
}
这种方式和form提交是一样,FormData不是所有浏览器都支持,以往通过ajax序列化表单是之前前端UI比较常用的提交方式。
stream流上传
流方式是直接获取file文件中的byte内容,通过XmlHttpRequest提交byte数据,后端接收到数据后不用解析流,可以直接将流数据存储。本案例中将byte转成string,通过response将string输出到客户端,观察下string内容的区别。
function uploadStream() {
debugger
var dataFile = document.getElementById('uploadFile').files[0]; //<input type="file" id="file" value="上传文件" />
var data = dataFile.slice(0, dataFile.size);//表示取文件的0到100k大小的数据
var oReq = new XMLHttpRequest();
oReq.onreadystatechange = function () {
if (oReq.readyState == 4) {
if (oReq.status == 200) {
document.getElementById('result').append(oReq.responseText)
}
}
}
oReq.open("POST", "/stream");
oReq.send(data);
}
通过该种方式,可以实现文件断点续传以及分片上传。还有FileReader对象使用起来更加方便。
非常规图片预览
纯后端实现图片预览
- 前端代码
function preview() {
debugger
var dataFile = document.getElementById('uploadFile').files[0]; //<input type="file" id="file" value="上传文件" />
var data = dataFile.slice(0, dataFile.size);//表示取文件的0到100k大小的数据
var oReq = new XMLHttpRequest();
oReq.onreadystatechange = function () {
if (oReq.readyState == 4) {
if (oReq.status == 200) {
document.getElementById('result').innerHTML = "";
var img = new Image()
img.src = "data:image/jpeg;base64," + oReq.responseText;
document.getElementById('result').append(img)
}
}
}
oReq.open("POST", "/preview");
oReq.send(data);
}
- 后端代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
BASE64Encoder encoder = new BASE64Encoder();
String img = encoder.encode(IOUtils.toByteArray(request.getInputStream()));
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write(img);
}
当前主流的浏览器支持FileReader对象,可以直接通过FileReader对象在前端实现图片的预览
function getFileContext() {
var reader=new FileReader();
//需要的参数是图片
var file=document.querySelector("#myFile").files;
// 没有返回值,将其结果储存在result中,无法判断文件是否读完
reader.readAsDataURL(file[0]);
reader.onload=function () {
// 展示出来:得到的reader.result为二进制文件base64 data:image/jpeg;base64...
console.log(reader.result);
document.querySelector("#img").src=reader.result;
}
问题,思考下当前有些富文本编辑器中实现的截图粘贴图片实现图片上传,其实过程就是从剪切板中读取图片字节内容,直接通过流方式上传图片保存后,返回后端图片的访问路径,前端构建image展现图片
总结
-
http是基于C-S架构的通讯协议,服务端其实只需要开启SocketServer,监听某个端口即可。
-
http常用的客户端有很多,我们一般用的最多的是浏览器,比如一个超链接,form submit就是由浏览器来发起的request请求。
-
常用的还有XmlHttpRequest,是属于js的http客户端api类,还有我们常用的java,httpclient都属于通讯中的客户端。这些客户端都可以先服务端发起request请求。
上图中浏览器可以监控 XmlHttpRequest 请求。
-
response中的返回类型很重要,这决定了浏览器做出何种响应,注意我们以往用的jsp内部工作原理是转成了servlet然后out.println(jsp中不是直接使用response,采用的是jspWrite对象)输出html结构的string内容,content-type是text/html。
-
在servlet中是无法和客户端直接交互的,也就是说B-S架构服务端无法向客户端直接发送报文消息,只能通过response告诉客户端要干什么,比如设置cookie ,刷新页面,缓存页面等。
-
response向客户端打印内容分为response.getOutputStream()和response.getWriter()。
-
response.getOutputStream().write(),常用于流输出,比如文件下载等,也常用于文件的显示(前提是浏览器支持该文件内容显示,比如图片显示,PDF显示等)。
-
response.getWriter().write(),文本内容输出,比如json、html等普通文本输出。write不能输出对象,比如Java中的object不能通过write输出,如果需要输出object信息需要向将对象转成json格式的文本。
-
response.getWriter().println(),print和write是一样的,print内部调用的也是write,但是print是可以输出object对象的。
-
response.getWrite()常用于异步的request,比如ajax请求的返回信息等。
-
当前主流的一些框架,前端UI、JS后端的Servlet 、MVC等框架对基础的http以及servlet 都做了大量的封装,所以具体使用我们依然需要学习官方的教程,比如mvc、vue、angular、ant-designer等框架。
-
对于IT技术人员来说,了解并掌握底层的基础知识,我们在学习其他框架是就会更加容易理解,深入学习底层的技术知识后,对其他框架的学习会有很大的帮助,有时候可能只需要看看快速手册,帮助文档等就能理解框架部分组件的运行原理,在遇到实际问题时才能精准的分析定位问题。
-
工作中常用的框架,比如mvc,tomcat,当我们遇到404,500,或者是参数不匹配,后端无法接收到参数,参数类型转换失败等等,参数值不对,等等。通过以上知识学会从底层去分析问题的原因,学会通过浏览器分析header头,request body等定位出request请求过程中,框架中的那个步骤出现问题。比如我们常见的编码、日期转换。又比如mvc中 RequestParam和RequestBody等参数注解的区别。