图片服务器项目

项目源码

项目源码地址:https://gitee.com/StoneLib/picture-server

项目简介

该项目基于 web 下的 servlet 开发,具有简单的图片的上传、图片的查看和删除的功能,用户可以在这里上传自己的照片,进行存储与观看

项目描述

  • 使用 JavaScript 编辑前端页面进行 Ajax 请求,简单使用 CSS 美化界面;
  • 使用 Servlet、JSON 完成前后端的交互和分离;
  • 简单的 Web 服务器设计开发能力(Servlet)
  • 使用数据库(Mysql)JDBC 操作 MySQL
  • 数据库设计(根据实际场景设计数据库表结构)
  • 前后端交互的 API 的设计(基于HTTP协议)
  • 学习测试 HTTP 服务器,Postman
  • 使用 HTML、CSS、JavaScript技术构造一个简单的网页

服务器设计

服务器设计

  1. MySQL 本质上也是一个服务器程序
  2. 数据库中存储的图片的属性(元信息);图片正文,以文件的形式直接存在磁盘上;数据库中就记录一个 path 对应到磁盘上的文件
  3. 校验和:通过一个更短的字符串,来验证整体数据是否正确,短的字符串是根据原串内容通过一定的规则来计算出来的。
  4. md5:图片的md5校验和 —— 字符串哈希算法

服务器 API 设计(前后端交互接口设计)

JSON:一种数据组织的格式,格式为键值对的结构。此处有使用 JSON 完成数据的序列化,方便进行网络传输

// JSON 只是一个数据格式,和编程语言无关(JavaScript)

正式开始设计前后端交互 API:

  1. 新增图片
请求:
POST/image
Content-Type:multipart/form-data
//正文内容 包含图片自身的一些信息图片正文的二进制内容

响应:
HTTP /1.1 200 OK
{// 上传成功
​ “ok":true// 上传失败"ok":false"reason":"具体失败原因"
}
  1. 查看所有图片的属性
请求:GET/image
响应:
HTTP /1.1 200 OK
[ // 响应成功{
​ imageId: 1,
​ imageName: "a.png",
​ size: 10,
​ uploadTime: "20220815",
​ path: "./image/a.png",
​ md5:"dq2ew4"},{ ........ }
]
响应失败:
HTTP /1.1 200 OK 
[
 // 上传成功
​ “ok":true// 上传失败"ok":false"reason":"具体失败原因"
]
1.API 具体的设定有很多方式,可以用200表示成功,404表示失败;
2.也可以使用 body 中的 OK 字段 ,true 表示成功,false 表示失败;
3.还可以使用 [] 有内容表示成功,为空表示失败。
  1. 查看指定图片属性
请求:
GET / image?imageId = [具体的数值]
响应:
HTTP/1.1 200 OK
{ // 响应成功
 imageId: 1,
​ imageName: "a.png",
​ size: 10,
​ uploadTime: "20220815",
​ path: "./image/a.png",
​ md5:"dq2ew4"
}
HTTP/1.1 200 OK
{ // 响应失败"ok": false,"reason":"具体出错的原因"
}
  1. 删除指定图片属性
请求:
DELETE/image?imageId = [具体图片的ID]
响应:
HTTP/1.1 200 OK
{ // 响应成功"ok": true,
}
HTTP/1.1 200 OK
{ // 响应失败"ok": false,"reason":"具体出错的原因"
}
服务器实现代码的时候就可以判定方法,如果是 DELETE 方法,就执行删除操作
删除也不一定非得用 DELETE 方法,
例如:GET / image?imageId=xxx&delete=1
  1. 查看指定图片的内容
请求:
GET / imageShow?imageId = [具体图片的id]
响应:
HTTP/1.1 200 OK
// 响应成功
Content-Type: image/png [图片的二进制内容]
HTTP/1.1 200 OK
{ // 响应失败
​ ok: false,
​ reason:"具体出错的原因"
}

数据库操作

1.先创建 DBUtil 封装一下获取数据库连接的过程, dao 数据访问层:这里的类围绕着数据操作展开

package dao;

import java.sql.Connection;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


public class DBUtil {
    //3306后面跟的是数据库的名字
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/数据库?characterEncoding=utf8&useSSL=false";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "数据库密码";
    private static DataSource dataSource = null;
    public static DataSource getDataSource(){
        // 通过这个方法来创建DataSource的实例
        // 保证线程安全:
        // 1.先加锁 2.二次判断  3.使用volatile关键字
        if(dataSource == null){
            synchronized (DBUtil.class){
                if(dataSource == null){
                    dataSource = new MysqlDataSource();
                    MysqlDataSource tempDataSource = (MysqlDataSource) dataSource;
                    tempDataSource.setURL(URL);
                    tempDataSource.setUser(USERNAME);
                    tempDataSource.setPassword(PASSWORD);
                }
            }
        }
        return dataSource;
    }
    public static Connection getConnection(){
        try {
            return getDataSource().getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){
       //注意关闭顺序,先连接的后关闭
        try {
            if(resultSet != null){
                resultSet.close();
            }
            if(statement != null){
                statement.close();
            }
            if(connection != null){
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

2.建立数据库中的表

create table image_table (imageId int not null primary key auto_increment,
imageName varchar(50),
size int,
uploadTime varchar(50),
contentType varchar(50),
path varchar(1024),
md5 varchar(1024));

代码实现

封装数据库操作(DAO层)

  • DBUtil 封装获取数据库连接的操作,上述已经给出
  • Image 对应到一个图片对象(包括图片的相关属性)
public class Image {
    private int imageId;
    private String imageName;
    private int size;
    private String uploadTime;
    private String contentType;
    private String path;
    private String md5;
    // 再利用 IDEA 快捷键 alt+enter+insert  对上述属性进行 set 和 get 方法的创建
}
  • ImageDao 是 Image 对象的管理器,借助这个类完成 Image 对象的增删查
  1. 增加图片
public void insert(Image image) {
        // 1.获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2.创建片拼接SQL语句
        PreparedStatement statement = null;
        try {
            String sql = "insert into image_table values(null, ?, ?, ?, ?, ?, ?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1,image.getImageName());
            statement.setInt(2,image.getSize());
            statement.setString(3,image.getUploadTime());
            statement.setString(4,image.getContentType());
            statement.setString(5,image.getPath());
            statement.setString(6,image.getMd5());
            // 3.执行SQL语句
            int ret = statement.executeUpdate();
            if(ret != 1){
                //程序出现问题,抛出一个异常
                throw new ImageServerException("插入数据库错误");
            }
        } catch (SQLException | ImageServerException e) {
            e.printStackTrace();
        } finally {
            // 4.关闭连接和statement对象
            DBUtil.close(connection,statement,null);
        }
    }
  1. 查看图片
// 查看所有图片
public List<Image> selectAll() {
        List<Image> images = new ArrayList<>();
        // 1.获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2.构造 SQL 语句
        String sql = "select * from image_table";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        // 3.执行 SQL 语句
        try {
            statement = connection.prepareStatement(sql);
            resultSet = statement.executeQuery();
            // 4.处理结果集
            while (resultSet.next()) {
                Image image = new Image();
                image.setImageId(resultSet.getInt("imageId"));
                image.setImageName(resultSet.getString("imageName"));
                image.setSize(resultSet.getInt("size"));
                image.setUploadTime(resultSet.getString("uploadTime"));
                image.setContentType(resultSet.getString("contentType"));
                image.setPath(resultSet.getString("path"));
                image.setMd5(resultSet.getString("md5"));
                images.add(image);
            }
            return images;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5.关闭连接和 statement 对象
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }

    // 根据 imageId 查看指定图片
    public Image selectOne(int imageId) {
        // 1.获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2.构造 SQL 语句
        String sql = "select * from image_table where imageId = ?";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        // 3.执行 SQL 语句
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1,imageId);
            resultSet = statement.executeQuery();
            // 4.处理结果集
            if (resultSet.next()) {
                Image image = new Image();
                image.setImageId(resultSet.getInt("imageId"));
                image.setImageName(resultSet.getString("imageName"));
                image.setSize(resultSet.getInt("size"));
                image.setUploadTime(resultSet.getString("uploadTime"));
                image.setContentType(resultSet.getString("contentType"));
                image.setPath(resultSet.getString("path"));
                image.setMd5(resultSet.getString("md5"));
                return image;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5.关闭连接和 statement 对象
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }
  1. 删除图片
public void delete(int imageId) {
        //1.获取数据库连接
        Connection connection = DBUtil.getConnection();
        // 2.创建并片接sql语句
        // ?作为占位符
        String sql = "delete from image_table where imageId = ?";
        // 3.执行sql语句
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1,imageId);
            int ret = statement.executeUpdate();
            if(ret != 1){
                throw new ImageServerException("删除数据库操作失败");
            }
        } catch (SQLException | ImageServerException e) {
            e.printStackTrace();
        }finally {
            // 4.关闭连接
            DBUtil.close(connection,statement,null);
        }
    }
  1. 根据 MD5 进行查看,此方法和 根据指定 imageId 查看的方法油异曲同工之处
ImageDao 有一个 selectAll 方法,查出所有数据库中的数据。如果数据库中内容只有几千条,这样查找可以;但若是数据库有几亿条数据,这样查就会非常低效,很可能直接把数据库或者应用程序运行崩溃。
更科学的方法是指定一些其他筛选的条件(分页)

单元测试- 把一个类/方法看作是一个单元进行测试

  • 一旦出现问题,就能及时发现,BUG 发现地越早,解决成本就越低
  • 软件开发的核心任务 —— 管理软件的复杂程度(不要让软件变得更复杂)
  • 封装是一种有效的管理复杂程度的手段,让类的使用者不需要关注类具体实现细节(还是需要关注这个对象是什么类型)
  • 多态是封装的更进一步,使用者不仅不需要知道类的具体实现功能,也不需要关注这个对象的类型是什么

基于 Servlet 搭建服务器

  • 安装 Servlet,根据 maven 进行安装
  • 创建一个类,继承 HttpServlet 父类,并且重写这个父类中的一些重要方法
  • HttpServlet 中提供的 doXXX 系列,和 HTTP 协议的方法是一一对应的
  1. doGet 方法 —— 查看图片
@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 考虑到查看所有图片和查看指定图片属性
        // 通过是否 URL 中带有 imageId 参数进行区分
        // 存在 imageId 查看指定图片属性,否咋查看所有图片属性
        // 例如:URL / image?imageId=100
        // imageId 的值就是 100
        // 如果 URL 中不存在 imageId 那么返回 null
        String imageId = req.getParameter("imageId");
        if (imageId == null || imageId.equals("")) {
            // 查看所有图片属性
            selectAll(req, resp);
        } else {
            // 查看指定图片
            selectOne(imageId,resp);
        }
    }

    private void selectAll(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("application/json;charset=utf-8");
        // 1.创建一个 ImageDao 对象,并查找数据
        ImageDao imageDao = new ImageDao();
        List<Image> images = imageDao.selectAll();
        // 2.将查找到的结果转换成Json格式的字符串,然后写回给resp对象
        Gson gson = new GsonBuilder().create();
        // jsonData 就是一个 json 格式的字符串,和之前约定的格式是一样的
        // 重点体会下列代码,是方法的核心,gson 帮我们完成了大量的格式转换工作
        // 只要把之前的相关字段约定成统一的命名,下面的操作就可一步到位
        String jsonData = gson.toJson(images);
        resp.getWriter().write(jsonData);

    }

    private void selectOne(String imageId, HttpServletResponse resp) throws IOException {
        resp.setContentType("application/json;charset=utf-8");
        // 1.创建一个 ImageDao 对象,并查找数据
        ImageDao imageDao = new ImageDao();
        Image image = imageDao.selectOne(Integer.parseInt(imageId));
        // 2.将查找到的结果转换成Json格式的字符串,然后写回给resp对象
        Gson gson = new GsonBuilder().create();
        String jsonData = gson.toJson(image);
        resp.getWriter().write(jsonData);
    }
  1. doPost 方法 —— 上传图片
@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1.获取图片的属性信息,并且写入数据库
        // a.创建factory对象和upload对象
        FileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        // b.通过upload对象进一步解析请求,解析HTTP请求中body的内容
        // FileItem代表一个文件对象,HTTP支持一个请求同时上传多个文件
        List<FileItem> items = null;
        try {
            items = upload.parseRequest(req);
        } catch (FileUploadException e) {
            // 出现异常说明解析出错
            e.printStackTrace();
            // 告诉客户端出现的具体的错误
            resp.setContentType("application/json; charset=utf-8");
            resp.getWriter().write("{\"ok\":false,\"reason\":\"请求解析失败\"}");
            return;
        }
        // c.将FileItem中的属性提取出来,转换成Image对象,存入数据库中
        // 当前只考虑一张图片的情况
        FileItem fileItem = items.get(0);
        Image image = new Image();
        image.setImageName(fileItem.getName());
        image.setSize((int) fileItem.getSize());
        // 手动获取时间,并转成格式化日期
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        image.setUploadTime(simpleDateFormat.format(new Date()));
        image.setContentType(fileItem.getContentType());
        // MD5 暂时不用要求
        image.setMd5(DigestUtils.md5Hex(fileItem.get()));
        // 构造一个路径来进行保存
        image.setPath("./image/" + image.getMd5());
        // 存到数据库
        ImageDao imageDao = new ImageDao();

        // 看看数据库中是否存在相同的MD5的图片,不存在,返回 null
        Image existImage = imageDao.selectByMd5(image.getMd5());

        imageDao.insert(image);

        // 2.获取图片的内容信息,并且写入磁盘
        if (existImage == null) {
            File file = new File(image.getPath());
            try {
                fileItem.write(file);
            } catch (Exception e) {
                e.printStackTrace();
                resp.setContentType("application/json; charset=utf-8");
                resp.getWriter().write("{\"ok\":false,\"reason\":\"写磁盘失败\"}");
                return;
            }
        }

        // 3.给客户端返回一个结果数据
        /*resp.setContentType("application/json; charset=utf-8");
        resp.getWriter().write("{\"ok\":true}");*/

        //上传之后可以直接看到新上传的图片
        resp.sendRedirect("images.html");
    }
  1. doDelete方法 —— 删除图片
@Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json; charset=utf-8");
        // 1.先获取到请求中的 imageId
        String imageId = req.getParameter("imageId");
        if (imageId == null || imageId.equals("")) {
            resp.setStatus(200);
            resp.getWriter().write("{ \"ok\": false, \"reason\": \"请求解析失败\"}");
            return;
        }
        // 2.创建 ImageDao 对象,查看到该图片对象对应的相关属性(为了知道这个图片对应文件路径)
        ImageDao imageDao = new ImageDao();
        Image image = imageDao.selectOne(Integer.parseInt(imageId));
        if (image == null) {
            // 此时请求中传入的 id 在数据库中不存在
            resp.setStatus(200);
            resp.getWriter().write("{ \"ok\": false, \"reason\": \"imageId 在数据库中不存在\"}");
            return;
        }
        // 3.删除数据库中的记录
        imageDao.delete(Integer.parseInt(imageId));
        // 4.删除本地磁盘文件
        File file = new File(image.getPath());
        file.delete();
        resp.setStatus(200);
        resp.getWriter().write("{ \"ok\": true }");
    }
  1. ImageServletShow
<servlet></servlet> 标签
告诉 Tomcat 当前这个 Servlet 对应到代码中的哪个类
<servlet-mapping></servlet-mapping> 标签
告诉 Tomcat 当前这个 Servlet 对应到的 URL 的 path 是什么

HTTP 服务器

  • 启动 HTTP 服务器,直接基于 socket,伪代码如下
serverStart
 Socket socket
 socket.bind(服务器IP + 端口号)
 while(true) {
 Socket newSocket = socket.accept();
 // 读取数据(客户端发来数据,读到的数据相当于一份完整的HTTP请求)
 Byte[] inputBuffer = newSocket.read();
 // 解析读到的 HTTP 请求格式的数据
 HttpServletRequest req = parse(inputBuffer);
 // 调用自己编写的代码(ImageServlet)
 // 根据 url 中的 path 决定创建哪个对象
 // h 对象本质上是一个 ImageServlet
 // 根据 web.xml
 HttpServlet h = build(req.getUrl)
 if(req.getMethod().equals("GET")) {
 h.doGet(req,resp);
 } else if(req.getMethod().equals("GET")) {
 h.doGet(req,resp);
 } else if() {}
 // 执行完上述方法之后,再把 resp 对象转为字符串,写回到 socket 中
 newSocket.write(resp.toString());
 }
  • Servlet 可以理解成一种编程框架
    当前已经有许多现成的代码,只有一少部分需要用户进行编写,完成整个工作
  • Servlet 需要做的核心工作,创建一个 Servlet 类,完成如何 根据 HTTP 请求,计算生成 HTTP 响应

前端页面

1.关于前端代码的知识

  • HTML:网页的骨架
  • CSS:描述网页上组件的样式(颜色、位置、大小、字体、背景)
  • JavaScript:描述前端页面上的一些动作(和用户具体交互的行为)
  • 关于这些的教程:w3school 在线教程
  • ul 表示无序列表
  • li 嵌套在 ul 内部,列表中具体的某个条目
    2.为了实现页面的上传
  • 把搜索框改成了文件上传按钮
  • 新增了一个提交按钮
  • 修改了 form 属性,新增 action,method,enctype
    3.测试 HTML 显示效果的时候,有两种方式
  • 在 IDEA 直接点击浏览器图标(只是在本地进行测试)
  • 部署到服务器上,通过浏览器远程访问服务器
    4.浏览器看到的页面内容可能和源代码中的内容有一定差别,这些都是通过 JS 动态渲染的
    5.把网页上显示的这些预览图片替换成服务器上保存的图片,img 标签中 src 改成服务器上存储的图片的 url 就可以
    6.需要获取到服务器上所有的图片的 url(ImageServlet),需要通过 JS 先获取所有照片的属性,再分别加载每一个图片,使用 JS 完成
    7.此处引入 Vue JS 的框架来帮助我们更方便编写代码 —— 主流的前端框架 Vue、React(Facebook)、Auglar(Google)
    8.Vue 中的命令
  • v-for:循环访问一个数据
  • v-bind:把数据绑定到 html 标签上的某个属性
  • v-on:绑定某种事件的处理函数
  • 如果是在标签内部使用 Vue 对象中的数据,就需要使用插值表达式
  • 如果是在 标签属性 中使用 Vue 对象中的数据,不需要使用插值表达式,但是需要搭配 Vue 的命令
    9.接下来通过浏览器中 JS 代码请求服务器,获取到服务器上都有哪些图片,把这个数据作为 Vue 渲染的依据(Vue 对象中 images 数组)
    10.原来页面的渲染过程
  • 先加载图片
  • 再根据图片大小设定图片的位置,设定显示图片的空间大小
    11.现在使用 ajax 进行渲染
  • 页面尝试获取图片大小,并设定显示图片的空间(当前图片还没获取到,也不知图片大小)
  • 通过 ajax 获取图片内容
  • 改进上传操作,上传成功之后,自动掉转到主页 index.html,使用 HTTP 重定向(302可以完成重定向)
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值