JavaEE--从文件上传、下载入门Java web

目录

前言

从文件上传、下载入门Java web

文件上传Demo

简单的BS架构服务端Demo

***:一个ServerSocket就是后台服务?

依赖Tomcat实现后台服务

***:一个servlet就是后台服务?

***:Tomcat做了什么?

***:(实战)实现简单的Servlet容器


前言

带着问题学java系列博文之java基础篇。从问题出发,学习java知识。


从文件上传、下载入门Java web

JavaWeb,主要指以Java语言为基础,利用Servlet、JSP等技术开发动态页面,方便用户通过浏览器与服务器后台交互。Java Web应用程序可运行在一个轻量级的Servlet容器中,比如Tomcat。那服务端到底是如何实现,浏览器又是如何和服务端交互,为什么java web要Tomcat,Tomcat又做了什么呢?让我们从文件上传、下载开始,逐步了解java网络编程,入门Java web。

 

文件上传Demo

public class TcpServer {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8080);
        while (true) {
            Socket socket = server.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String fileName = System.currentTimeMillis() + ".txt";
                        File file = new File("src\\com\\zst\\javabasedemo\\net\\upload");
                        if (!file.exists()) {
                            file.mkdirs();
                        }
                        byte[] bytes = new byte[1024];
                        int len = 0;
                        InputStream inFromClient = socket.getInputStream();
                        FileOutputStream fos = new FileOutputStream(file + File.separator + fileName);
                        while ((len = inFromClient.read(bytes)) != -1) {
                            fos.write(bytes, 0, len);
                        }
                        DataOutputStream outToClient = new DataOutputStream(socket.getOutputStream());
                        outToClient.writeUTF("上传成功");
                        fos.close();
                        socket.close();
                    } catch (IOException e){
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}


public class TcpClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost",8080);
        FileInputStream fis = new FileInputStream("src\\com\\zst\\javabasedemo\\file\\test.txt");
        OutputStream outToServer = socket.getOutputStream();
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = fis.read(bytes)) != -1){
            outToServer.write(bytes,0,len);
        }
        //添加这个表示关闭输入,文件上传结束,防止阻塞
        socket.shutdownOutput();
        DataInputStream in = new DataInputStream(socket.getInputStream());
        System.out.println(in.readUTF());
        fis.close();
        socket.close();
    }
}

如上demo,使用BIO搭建:ServerSocket作为服务端,客户端使用Socket与服务端实现通讯。当客户端与服务端连接建立后,客户端向服务端上传文件,服务端接收后保存在本地upload文件夹下。

ServerSocket和Socket是java.net包下的用于网络编程的两个类,服务端初始化ServerSocket后,进行端口绑定,然后就由ServerSocket进行监听,阻塞等待客户端socket连接事件。一旦监听到客户端连接,就创建一个socket实例用于与客户端通讯。注意此时,客户端有一个socket,服务端也有一个socket,它们不是同一个。ServerSocket和Socket通讯是在传输层,基于TCP/IP协议。

***:注意最后要写入文件结束标志,否则服务端while循环永远读不到-1,也就无法退出循环,导致线程一直运行,不关闭io,释放资源。

有了文件上传,那文件下载其实也很简单,就是将整个过程反过来就行了。文件上传、下载无非都是一个文件读取、传输、写入的过程,其中文件读取和写入就是传统的io输入、输出流操作,网络数据传输就交给了Socket。客户端和服务端都各自持有一个Socket实例,要想向对方发送内容,就使用Socket.getOutputStream得到输出流,写入内容即可;要想获取对方发送的内容,就使用Socket.getInputStream得到输入流,读取内容即可。

可以看到,我们目前还是在客户端和服务端之间通讯,也就是常说的CS架构。而我们实际使用更多的是浏览器发送一个请求,服务端响应,也就是BS架构。而java语言最大的特点也是跨平台,跨语言;对网络编程也有良好的支持。所以经常使用Java来开发服务端,使用HTML/JavaScript等开发前端,用浏览器访问,做BS架构的应用。

 

简单的BS架构服务端Demo

***:一个ServerSocket就是后台服务?

CS架构,必须我们实现客户端,通过socket通讯;其实BS架构也是一样的,浏览器与后端服务之间通讯也是通过socket,http协议底层采用tcp/ip协议。一个请求的完整过程是:由浏览器建立一个socket连接,向服务端写入url和参数等内容;服务端等待连接建立后拿到socket,通过socket读取浏览器发送过来的内容,处理完毕,向浏览器写入响应内容,并且为了适配浏览器的规范,还要加入特殊的头信息。

public class BSServerDemo {
    private static ExecutorService service = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        //循环执行,持续监听客户端连接事件
        while (true){
            Socket socket = serverSocket.accept();
            //监听到一个客户端连接(浏览器发起一次请求),开启一个子线程处理请求
            service.execute(new RequestTask(socket));
        }
    }

    private static class RequestTask implements Runnable{

        private Socket socket;

        public RequestTask(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                //读取浏览器发送过来的第一行内容
                String line = reader.readLine();
                //解析,拿到请求方式、请求路径和参数
                String[] info = line.split(" ");
                System.out.println("请求方式是:"+info[0]);
                String url = info[1].substring(1);
                String path = url;
                String params = "";
                if (url.contains("?")){
                    params = url.substring(url.indexOf("?"));
                    path = url.substring(0,url.indexOf("?"));
                    System.out.println("请求参数是:"+params);
                }
                System.out.println("请求路径是:"+path);

                //获取socket输入流,回应浏览器
                OutputStream outToBrowser = socket.getOutputStream();
                //由于是向浏览器响应,需要特殊格式,写入特殊格式头,否则浏览器不会解读回应的内容
                outToBrowser.write("HTTP/1.1 200 OK\r\n".getBytes());
                outToBrowser.write("Content-type:text/html;charset=utf-8\r\n".getBytes());
                outToBrowser.write("\r\n".getBytes());

                if (path.equals("hello")){
                    //响应hello请求,注意浏览器会将请求url进行url编码(UTF-8)后再发送过来
                    String[] split = params.split("=");
                    //中文参数需要经过url解码后转换为具体的字符串
                    String name = URLDecoder.decode(split[1], "UTF-8");
                    outToBrowser.write(("你好"+name+",欢迎回来").getBytes());
                    reader.close();
                    socket.close();
                } else if (path.contains(".html")){
                    //创建文件输入流,读取文件
                    FileInputStream fis = new FileInputStream(path);
                    //读取浏览器想获取的对应路径文件,并写入socket输入流,响应浏览器
                    byte[] bytes = new byte[1024];
                    int len = 0;
                    while ((len = fis.read(bytes)) != -1){
                        outToBrowser.write(bytes,0,len);
                    }
                    fis.close();
                    reader.close();
                    socket.close();
                } else {
                    outToBrowser.write("没有对应的资源".getBytes());
                    reader.close();
                    socket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

启动后台服务之后,在浏览器中输入:

JavaEE--从文件上传、下载入门Java web_第1张图片

如上图,此时后台服务就响应了一串字串,并且拿到了传递的参数。

JavaEE--从文件上传、下载入门Java web_第2张图片

如上图,浏览器发起请求,后端服务拿到具体路径后,读取本地文件(从《Java基础篇--IO》我们知道此时相对路径加上运行路径就是绝对路径),响应给浏览器(其实就是浏览器从服务器下载hello.html文件)。

上面的服务端流程梳理如下:

1.启动ServerSocket,绑定8080端口,持续监听浏览器的连接请求;

2.一旦监听到浏览器的连接,就开启一个子线程执行这个请求的具体业务;

3.通过socket的inputstream得到浏览器发送过来的url和具体参数等信息;

4.处理完毕,通过socket的outputstream向浏览器发送响应数据,为了遵循浏览器规范,特意先写入响应头。

从这里也验证了,浏览器发起请求其实就是通过socket与服务端建立连接,然后通过socket进行网络通讯。

 

整个写下来,发现上面的服务端demo其实也不是很复杂,就是一个BIO+多线程编程而已,原来后台服务也不神秘呀。难道后台服务就是一个ServerSocket的事?而且讲到现在我们连Tomcat都还没接触到!

很显然,后台服务这样实现肯定是不行的,原因主要有几点:

1.首先BIO是阻塞IO,服务端监听客户端连接的过程中主线程阻塞无法做其它的事情;

2.每一个客户端连接来了就会起一个子线程去执行具体业务,Demo中是用了一个固定大小为10的线程池来作为执行池,当请求非常多的时候就会存在请求排队,响应延迟现象;

3.我们需要在Task中去根据获取到的url来执行相应的业务,每增加一个请求路径,就需要修改Task代码,增加一个else if判断,这与开闭原则不符;

4.随着请求路径增多,Task的else if判断越来越多,整个task代码将会越来越多,可以预见最后一个Task动不动就有好几万行,阅读和维护起来将有多痛苦。

 

依赖Tomcat实现后台服务

显然上面的通过一个ServerSocket,自己编码实现后台服务的方案存在很多缺点,而且每次都需要自己编写同样的一部分代码(ServerSocket启动,监听连接事件,请求参数获取,头信息写入等);所以大佬们为java web封装了一套规范,将公有代码进行封装,极大的简便了编码,让我们码农只需要关注后台业务逻辑开发即可——这也就是接下来要讲的Servlet和Servlet容器方案。

***:一个servlet就是后台服务?

上面的文件上传和简单的BS服务端Demo,都是我们自己编写一个java程序,通过main方法入口,启动应用;服务端都是依赖ServerSocket实现的,通讯依赖Socket。这和我们常见的后台服务完全不一样,我们通常都是一个Servlet,加一个web.xml(如果用注解的话,连web.xml都不需要),然后部署到Tomcat,一个后台服务就完成了。那一个Servlet就是后台服务吗?我们从具体代码一探究竟。

还是和上面的ServerSocket服务一样,一个是拿到参数,然后返回字串;一个是拿到具体地址,然后返回html页面。这次改用Servlet实现,依赖Tomcat容器运行。代码如下:

public class HelloServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {}

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        try {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            String method = request.getMethod();
            System.out.println("请求方式是:"+method);
            String name = request.getParameter("name");
            System.out.println("请求参数是:"+name);
            String uri = request.getRequestURI();
            System.out.println("请求uri是:"+uri);
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().write("你好"+name+",欢迎回来");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {}
}


public class HtmlServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {}

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setContentType("text/html;charset=utf-8");
        String uri = request.getRequestURI();
        System.out.println("uri:"+uri);
        String path = request.getParameter("path");
        //创建文件输入流,读取文件
        FileReader fr = new FileReader(path);
        //读取浏览器想获取的对应路径文件,并写入socket输入流,响应浏览器
        char[] data = new char[258];
        int len = 0;
        while ((len = fr.read(data)) != -1){
            response.getWriter().write(data,0,len);
        }
        fr.close();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {}
}


    
    
        hello
        servlet.HelloServlet
        
        5
    
    
    
        hello
        /hello
    


    
        html
        servlet.HtmlServlet
        1
    
    
        html
        /html
    

运行结果如下图:

JavaEE--从文件上传、下载入门Java web_第3张图片

JavaEE--从文件上传、下载入门Java web_第4张图片

如上代码,只是实现了两个类,两个类都实现了一个接口“Servlet”;然后不管是读取请求参数还是写回响应数据,都是使用两个已经提供好的对象ServletRequest和ServletResponse。再加上一份web.xml配置文件,之后就是由Idea根据配置部署在Tomcat中运行即可。

有没有发现使用Servlet实现后台服务好简单,不需要启动ServerSocket监听,也不需要开启子线程处理浏览器请求,不管是获取请求参数还是写回响应数据,都有对象方法支持,甚至我们连Main方法入口都没有。其实上述这些东西都是由Tomcat提前做好了,这也正是Servlet容器需要做的主要工作,Servlet程序需要运行就必须依赖Servlet容器。

 

***:Tomcat做了什么?

对比上面的两个后台服务,可以梳理出Tomcat等Servlet容器主要做了以下几件事情:

1.启动ServerSocket监听客户端连接请求;

2.根据web.xml配置,实现请求路径和servlet实例之间的映射,并初始化创建servlet实例;

3.封装socket.inputstream成ServletRequest对象,并实现对应方法;

4.封装socket.outputStream成ServletResponse对象,并实现对应方法;

5.处理请求线程池管理、请求排队、拒绝策略等;

6.根据路径映射找到servlet实例处理具体请求。

 

***:(实战)实现简单的Servlet容器

按照上面梳理的主要事项,我们一步一步实现后,其实就是一个Servlet容器。当揭开Tomcat的神秘面纱后,发现其实Tomcat之类的Servlet容器也是可以慢慢理解其核心的,甚至我们自己来封装实现,说不定也能写出比Tomcat更好的Servlet容器呢!

容器入口:

/**
 * 简单实现Tomcat
 * 1.读取配置,初始化参数、线程池,servlet实例(自定义的Servlet)与路径映射等
 * 2.启动ServerSocket,监听连接事件
 * 3.封装request和response对象
 * 4.找到具体的servlet实例,执行对应方法
 */
public class MyTomcat {
    private static int port = 8080;
    public static final ConcurrentHashMap servletMapping = new ConcurrentHashMap();
    private static ExecutorService pool;

    public static void main(String[] args) {
        try {
            //1.初始化参数、线程池等
            init();
            //2.启动ServerSocket,监听连接事件
            ServerSocket server = new ServerSocket(port);
            while (true){
                //2.1 持续监听客户端连接
                Socket socket = server.accept();
                //2.2 提交请求task,交给线程池处理
                pool.execute(new SocketTask(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void init(){
        //初始化线程池
        pool = Executors.newFixedThreadPool(100);
        //读取web.xml,初始化Servlet实例和路径映射
        InputStream resourceAsStream = MyTomcat.class.getClassLoader().getResourceAsStream("web.xml");
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();
            List elements = rootElement.elements();
            for (int i = 0, length=elements.size(); i < length; i++) {
                Element element = elements.get(i);
                List es = element.elements();
                for (int j = 0, lgth=es.size(); j < lgth; j++) {
                    Element element2 = es.get(j);
                    String ename1 = element2.getName().toString();
                    if ("servlet-name".equals(ename1) && "servlet".equals(element.getName().toString())) {
                        String servletName = element2.getStringValue();
                        Element ele2 = element.element("servlet-class");
                        String classname = ele2.getStringValue();
                        List elements2 = rootElement.elements("servlet-mapping");
                        for (int k = 0, lk=elements2.size(); k < lk; k++) {
                            Element element4 = elements2.get(k);
                            List es3 = element4.elements();
                            for (int op = 0, opp=es3.size(); op < opp; op++) {
                                if ("servlet-name".equals(es3.get(op).getName().toString())
                                        && servletName.equals(es3.get(op).getStringValue())) {
                                    Element element7 = element4.element("url-pattern");
                                    String urlPattern = element7.getStringValue();
                                    servletMapping.put(urlPattern, (MyServlet) Class.forName(classname).newInstance());
                                    System.out.println("==> 加载 "+ classname + ":" +urlPattern);
                                }
                            }

                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != resourceAsStream) {
                try {
                    resourceAsStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

请求处理类:

/**
 * 请求处理类
 * 1.封装request和response对象
 * 2.根据请求url查找容器,找到则调用servlet实例方法处理请求;未找到则响应错误提示
 */
public class SocketTask implements Runnable {
    private Socket socket;

    public SocketTask(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            // 封装自定义request对象
            MyServletRequest request = new MyServletRequest(socket.getInputStream());
            // 封装自定义response对象
            MyServletResponse response = new MyServletResponse(socket.getOutputStream());
            String url = request.getUrl();
            // 从映射mapping中获取servlet实例,处理具体请求
            MyServlet servlet = (MyServlet) MyTomcat.servletMapping.get(url);
            if (null != servlet) {
                // 容器中找到了对应的servlet实例,调用实例方法处理请求
                servlet.service(request, response);
            } else {
                // 容器中不存在处理该请求的Servlet
                OutputStream outputStream = response.getOutputStream();
                outputStream.write((MyServletResponse.RESPONSE_HEADER + "Welcome! error: Cannot find the servlet!").getBytes());
                outputStream.flush();
                outputStream.close();
            }
            if (!"/favicon.ico".equals(url)) {
                // 简单记录我们自己的访问日志
                //logRecord();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != socket) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

自定义的Request类:

/**
 * 自定义的request对象
 * 1.拿到处理类传递过来的inputStream,读取浏览器发送过来的信息
 * 2.解析发送过来的信息,拿到请求方式、URL、参数等
 * 3.对参数进行URL解码,防止中文乱码
 */
public class MyServletRequest {

    // 请求方式
    private String method;
    // 请求URL
    private String url;
    // 携带参数
    private Map paramMap = new HashMap();


    public MyServletRequest(InputStream inputStream) {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        try {
            String line = bufferedReader.readLine();
            if (null != line && line.length() > 0) {
                String[] split = line.split(" ");
                if (split.length == 3) {
                    this.method = split[0];
                    String allUrl = split[1];
                    if (allUrl.contains("?")) {
                        this.url = allUrl.substring(0, allUrl.indexOf("?"));
                        String params = allUrl.substring(allUrl.indexOf("?") + 1);
                        String[] paramArray = params.split("&");
                        for (String param : paramArray) {
                            String[] paramValue = param.split("=");
                            if (paramValue.length == 2) {
                                paramMap.put(paramValue[0], paramValue[1]);
                            }
                        }
                    } else {
                        this.url = allUrl;
                    }
                    if (allUrl.endsWith("ico")) {
                        return;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getParameter(String name){
        try {
            String nameEncode = paramMap.get(name);
            //防止中文乱码,进行URL解码
            String nameStr = URLDecoder.decode(nameEncode, "utf-8");
            return nameStr;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

自定义的response类:

/**
 * 自定义的response对象
 * 1.拿到处理类传递过来的outputStream,封装为BufferWriter,便于写入内容;
 * 2.写入特殊的头信息,适配浏览器规范;
 */
public class MyServletResponse {

    private OutputStream outputStream;
    private BufferedWriter bufferedWriter;

    // 添加Response响应头
    public static final String RESPONSE_HEADER=
            "HTTP/1.1 200 \r\n"
                    + "Content-Type: text/html;charset=utf-8\r\n"
                    + "\r\n";

    public MyServletResponse(OutputStream outputStream) {
        this.outputStream = outputStream;
        this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        //为了符合浏览器规范,写入特殊头信息
        try {
            bufferedWriter.write(RESPONSE_HEADER);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public OutputStream getOutputStream() {
        return outputStream;
    }

    public BufferedWriter getWriter(){
        return bufferedWriter;
    }
}

自定义Servlet(满足自定义容器规范):

/**
 * 自定义的Servlet
 * 与MyTomcat对应,自定义的Servlet容器仅识别自定义的Servlet
 */
public abstract class MyServlet {
    public void service(MyServletRequest request, MyServletResponse response) {
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }

    public abstract void doGet(MyServletRequest request, MyServletResponse response);

    public abstract void doPost(MyServletRequest request, MyServletResponse response);
}

至此,一个自定义的支持MyServlet的容器就完成了。

接下来我们要开发BS架构的后台服务,只需要实现具体的类(继承MyServlet),然后在web.xml中做好mapping配置。

public class HelloServlet extends MyServlet {
    public void doGet(MyServletRequest request, MyServletResponse response) {
        doPost(request,response);
    }

    public void doPost(MyServletRequest request, MyServletResponse response) {
        String url = request.getUrl();
        System.out.println("url:"+url);
        String name = request.getParameter("name");
        try {
            response.getWriter().write("你好"+name+",欢迎回来!");
            response.getWriter().close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class HtmlServlet extends MyServlet {
    public void doGet(MyServletRequest request, MyServletResponse response) {
        doPost(request,response);
    }

    public void doPost(MyServletRequest request, MyServletResponse response) {
        try {
            String uri = request.getUrl();
            System.out.println("uri:"+uri);
            String path = request.getParameter("path");
            //创建文件输入流,读取文件
            FileReader fr = new FileReader(path);
            //读取浏览器想获取的对应路径文件,并写入socket输入流,响应浏览器
            char[] data = new char[258];
            int len = 0;
            while ((len = fr.read(data)) != -1){
                response.getWriter().write(data,0,len);
            }
            fr.close();
            response.getWriter().close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}




    
        html
        com.zst.servlet.HtmlServlet
    
    
        html
        /html
    

    
        hello
        com.zst.servlet.HelloServlet
    
    
        hello
        /hello
    

运行MyTomcat,浏览器测试如下图:

JavaEE--从文件上传、下载入门Java web_第5张图片

JavaEE--从文件上传、下载入门Java web_第6张图片


以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!

你可能感兴趣的:(java,后端,面试)