http,js,原生实现,详细教程

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或者其他端口。
image.png

  • 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)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
image.png

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等

服务端返回报文

image.png

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/htmlHTML格式
text/plain纯文本格式
text/xmlXML格式
image/gifgif图片格式
image/jpegjpg图片格式
image/pngpng图片格式
  • 以application开头的媒体格式类型:
column1column2
content1content2
application/xhtml+xmlXHTML格式
application/xmlXML数据格式
application/atom+xmlAtom XML聚合格式
application/jsonJSON数据格式
application/pdfpdf格式
application/mswordWord文档格式
application/octet-stream二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded表单中默认的格式,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
  • 另外一种常见的媒体格式是上传文件之时使用的:
类型描述
text/htmlHTML格式
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

  • 纯文本
    image.png
  • html
    image.png

图片的显示

通过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>

image.png

页面的重定向

一般我们通过页面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.png
    最终会重定向到image中

附件上传

stream解析

一般我们如果要通过http实现附件上传,conten-type 需要设置成multipart/form-data。
image.png
由于现在很多框架都做了封装,所以很多时候我们并不需要关注在附件上传的过程中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就是边界值,也就是分隔符。
image.png

所以第一步我们是要通过request header获取请求头中的边界值。

        boundary = req.getHeader("Content-Type").split("boundary=")[1].getBytes();// 获取payload的随机分割线

获取到边界值,然后就是讲stream流转成byte数组并解析。这里我们将stream转成String字符串,简单看下整个流中的数据格式。

image.png
下面一大串非法字符,其实就是图片的byte流。

拉倒最后,form-data,就是普通的表单信息了。
image.png

本文中通过表单上传附件,后端解析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请求。

image.png
上图中浏览器可以监控 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等参数注解的区别。

# servlet   http  

评论

公众号:mumuser

企鹅群:932154986

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×