我的项目_图片服务器

图片服务器

一、项目需求

大家在写博客的时候,是怎么插入图片的呢?
事实上,大家写博客的时候,插入的图片,本质上是往文章中放入了一个链接(URL),而资源对应在另外的一个服务器上。
而我也写了那么多天的博客了,我的博客上也有不少图。我想利用自己所学的知识,自己写一个简单的图片服务器来实现github / 博客中插入图片的问题。

核心功能:
本项目的结构分为两部分,数据存储部分服务器模块,使用MySQL存储图片的属性,将图片内容保存到本地磁盘,服务器向外提供上传图片、获取图片的属性、根据图片的URL获取图片内容和删除图片等API接口。

整体构架:
实现一个 HTTP 服务器,完成对图片的上传、查看(查看属性/内容)、删除操作。
实现一个简单的页面来上传、展示当前的图片。

重要知识点:

  1. 简单的Web服务器设计开发能力(Servlet 的使用)
  2. 使用数据库(MySQL)JDBC 操作 MySQL
  3. 数据库设计(根据实际场景设计数据库表结构)
  4. 前后端交互的 API 的设计(基于HTTP协议)
  5. 认识 JSON 数据格式,学习使用 Java 中的 Gson 这个库操作 JSON 数据
  6. 强化 HTTP 协议的理解,学习测试 HTTP 服务器,Postman 工具的使用
  7. 基于 md5 进行校验
  8. 使用 HTML CSS JavaScript 技术构建一个简单的网页

CSDN:Tomcat 服务器之简介

二、数据库设计

MySQL:

MySQL 本质上也是有一个 服务器 程序;
之前安装数据库,既安装了服务器(本体)(存数据的地方),也安装了客户端(命令行程序);
虽然客户端 与 服务器 安装在同一台机子上,但依旧是通过网络的方式传输。
(MySQL客户端 通过网络的方式 来访问 服务器)(可以有另外的客户端 访问这个服务器);
它既然是个服务器,就可以有很多个客户端同时访问。

在这里插入图片描述
创建文件 db.sql

//进入数据库
mysql -uroot -p

// 创建数据库 java_image_server
create database java_image_server;

//切换数据库
use java_image_server;

//建表
CREATE TABLE `image_table` (
  `imageId` int(11) NOT NULL AUTO_INCREMENT,
  `imageName` varchar(50) DEFAULT NULL,
  `size` int(11) DEFAULT NULL,
  `uploadTime` varchar(50) DEFAULT NULL,
  `contentType` varchar(50) DEFAULT NULL COMMENT '图片类型',
  `path` varchar(1024) DEFAULT NULL,
  `md5` varchar(1024) DEFAULT NULL,
  PRIMARY KEY (`imageId`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8

contentType:正文类型("text/html""image/jpg""image/png" 等)

path:数据库中存储的 图片 的属性
图片正文是以文件的形式直接存在磁盘上的
数据库中就记录一个 path,这个 path 就对应到磁盘上的文件

md5:图片的 md5 校验和 
校验和:通过一个更短的字符串,来验证整体数据是否正确。
	   短的字符串是根据原串内容通过一定的规则来计算出来的。

什么是 md5? MD5_百度百科

这是一种常见字符串 hash 算法, 具有三个特性:
1. 不管源字符串多长, 得到的最终 md5 值都是固定长度
2. 源字符串稍微变化一点点内容, md5 值会变化很大(降低冲突概率)
3. 通过原字符串很容易计算得到 md5 值, 但是根据 md5 推导出原字符串很难(几乎不可能)

查看表结构:
在这里插入图片描述

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

图片存储分为了两个部分,首先就是我们上传的图片信息(包括大小,名字等)这些可以用数据库来进行存储,图片内容部分以二进制方式进行存储。但是由于图片信息存储如果用类进行存储改动比较麻烦,所以就引入了 JSON 格式进行存储。

Json 是一种常见是数据格式组织方式,一种键值对风格的数据格式,方便修改。
(键值对的结构:键不能重复,根据键能够查到值)

Java 中可以使用 Gson 库来完成 Json 的解析和构造。
Gson:goole 搞的一个开源的 JSON 解析库。

在 Maven 中新增 Gson 的依赖:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
</dependency>

前后端交互 API(HTTP 协议具体要构造成什么样子)

写一个简单的 html 来上传图片:

<html>
<head></head>
<body>

<form id="upload-form" action="image" method="post" enctype="multipart/form-data" >
    <input type="file" id="upload" name="upload" /> <br />
    <input type="submit" value="Upload" />
</form>

</body>
</html>

一个HTTP服务器的作用是接收到http请求,并根据请求返回相应的http响应。
需要约定不同的请求来表示不同的操作方式,例如有些请求表示上传图片,有些请求表示查看图片,有些表示删除图片等等。

新增图片:

请求(构造 HTTP 请求):

POST/image
Content-Type:multipart/form-data;

[正文内容 包含图片自身的一些信息]
------WebKitFormBoundary5muoelvEmAAVUyQB
Content-Disposition: form-data; name="filename"; filename="图标.jpg"
Content-Type: image/jpeg

....[图片正文的二进制内容]....

响应(构造 HTTP 响应):

上传成功:
HTTP/1.1 200 OK
{
	"ok":true,
}

上传失败:
{
	"ok":false,
	"reason":"具体失败的原因"
}
查看所有图片属性:

数据库里面存的属性信息,而不是图片的内容
图片属性 与 图片内容 不同

请求: //客户端(浏览器)给服务器发的HTTP请求

GET /image

响应:

成功:(可以查看多个,用 [] 起来)
HTTP/1.1 200 OK
[
	{
		imageId:1,
		imageName:"1.png",
		contentType:"image/png",
		size:1000,
		uploadTime:"20200222",
		path:"./data/1.png"
		md5:"11223344",
	},
	{
		.....
	}
]       

把存储在数据库中的数据以 JSON 这种数据格式,依次返回到客户端(浏览器),     
客户端就可以根据得到的这一系列的信息再决定如何往网页上显示    

失败:
HTTP/1.1 200 OK
[]

API 具体的设定有很多方式。
可以用 200 表示成功,404 表示失败;
也可以使用 body 中的 ok 字段 true 表示成功,false 表示失败;
也可以使用 [] 有内容表示成功,为空表示失败。

查看指定图片属性:

根据 图片id 来查看图片属性

请求:

GET/image?imageId=[具体的数值]

响应:

成功:
HTTP/1.1 200 OK
{
	imageId:1,
	imageName:"1.png",
	contentType:"image/png",
	size:1000,
	uploadTime:"20200222",
	path:"./data/1.png"
	md5:"11223344",
},

失败:
HTTP/1.1 200 OK
{
	"ok":false,
	"reason":"具体失败的原因"
}
查看图片内容:

请求:

GET/imageShow?imageId=[具体的图片id]

响应:

成功:
HTTP/1.1 200 OK
Content-Tye:image/png

[图片的二进制内容]

失败:
HTTP/1.1 200 OK
{
	"ok":false,
	"reason":"具体失败的原因"
}
删除图片:

请求:

DELETE/image?imageId=[具体的图片id]

服务器实现代码的时候就可以判定方法,如果是 DELETE 方法,就执行删除操作。
删除也不一定非得用 DELETE 方法,
例如:
GET/image?imageId=xxx&delete=1

响应:

成功:
HTTP/1.1 200 OK
{
	ok:true,
}

失败:
HTTP/1.1 200 OK
{
	"ok":false,
	"reason":"具体失败的原因"
}

四、图片服务器项目过程详解

先了解一下代码里几个类的基本作用:

DBUtil:封装获取数据库连接的操作
Image:对应到一个图片对象(包含图片的相关属性)
ImageDao:Image对象的管理器,向外提供image_table表的Insert(插入)、SelectAll(查询所有图片信息)、
          SelectOne(查询指定图片信息)Delete(删除指定图片)接口
ImageServlet:约定请求格式来表示不同的操作方式
ImageShowServlet:从磁盘展示文件
封装数据库操作

dao 数据访问层,这里面的类围绕着数据操作展开。

dao 对应到 MVC 中的 Model 层,是对数据库的封装。

创建 DBUtil 类:DBUtil 完整代码
创建一个单例类辅助创建连接。

封装数据库操作(DAO层)
DataSource可以看作数据源,它封装了数据库参数,连接数据库,程序中操作DataSource对象即可对数据库进行增删改查操作。
在这里插入图片描述

线程1 红色箭头部分:dataSource=new MysqlDataSource(); 还没有创建完,
线程2 在执行到 绿色箭头部分 的时候,dataSource 还是 null,线程2 也就会继续往下走,
两个线程 就会把 dataSource 创建两遍。
所以,上图所呈现的代码是线程不安全的。

怎样做到 线程安全? 1、加锁 2、双重判定 3、volatile

	private static volatile DataSource dataSource=null;

    public static DataSource getDataSource(){
        // 通过这个方法来创建 DataSource 的实例
        if(dataSource==null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new MysqlDataSource();
                    
                    // 把之前创建的 URL、USERNAME、PASSWORD 设定进去
                    MysqlDataSource tmpDataSource=(MysqlDataSource)dataSource;
                    tmpDataSource.setURL(URL);
                    tmpDataSource.setUser(USERNAME);
                    tmpDataSource.setPassword(PASSWORD);
                }
            }
        }
        return dataSource;
    }

这段代码详解:
1.通过 synchronized 进行加锁,
  如果不加锁,多线程情况下调用 getDataSource() 这个方法,会出现问题。

2.通过 双重 if 来完成判定
  为什么要通过 双重if 来判断?
  因为加锁操作永远是一个比较耗时、比较低效的操作,不要频繁的设计锁的竞争,
  所以加上一个额外的条件if (dataSource == null) ,
  这样,只有最开始对象没有创建的时候,才尝试获取锁。

3.要在 dataSource 这个属性中加上 volatile 关键字,
  保持 dataSource 是内存可见的,
  这样才能保证你在第一个线程操作去操作这个数值之后,
  其他线程也能及时看到更新(这里volatile解决代码中过度优化的情况)
  private static volatile DataSource dataSource = null;

  close 方法:关闭顺序很重要(谁先打开谁后关闭)

在 Maven 中新增 Gson 的依赖:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
</dependency>    

Image 类:Image 类

按照现有的设定,图片存储的文件路径:./image/文件名
如果有两个图片,内容不同,但是名字相同,此时就可能出现上传失败的情况。
应该让每次上传图片对应的路径都不相同

在这里插入图片描述
每个字段都是一一对应的,此时就可以直接使用 gson 来进行转换。

ImageDao 类(Image对象的管理器): ImageDao 类
提供image_table表的Insert(插入)、SelectAll(查询所有图片信息)、
SelectOne(查询指定图片信息)、Delete(删除指定图片)接口

基于 Servlet 来搭建服务器

安装Servlet:添加依赖

<dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
      <version>3.1.0</version>
      <!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
      <scope>provided</scope>
</dependency>

创建一个类,继承 HttpServlet 父类并且重写这个父类中的一些重要方法:
ImageServlet 类:Fn+Alt+Insert(Override)
在这里插入图片描述

HttpServlet 中提供的方法 doXXX系列
和 HTTP 协议的方法是一一对应的

如果服务器收到的是 GET 请求,就会自动调用到 HttpServlet 的 doGet 方法。
如果服务器收到的是 POST 请求,就会自动调用到 HttpServlet 的 doPost 方法。
如果服务器收到的是 DELETE 请求,就会自动调用到 HttpServlet 的 doDelete 方法。

ImageServlet 类:ImageServlet 类

  1. 上传图片 POST
  2. 查看所有图片信息 GET
  3. 查看指定图片信息 GET
  4. 删除指定图片 DELETE
doGet:
HttpServletRequest req    请求  方法,url,各种header,body
HttpServletResponse resp  响应  状态码,各种header,body

思考:给网页上显示一个 hello world ,修改 req 还是 resp?  
答案:修改resp

doPost:
resp.setContentType("text/html; charset=utf-8");    //解决乱码的问题
resp.setStatus(200);				 // 设置响应状态
resp.getWriter().write("加油");      // 把 “加油” 放到 http 响应的 body 中

ImageShowServlet:ImageShowServlet 类

  1. 查看图片内容

但是代码写完之后,程序还是无法运行?
因为还需要修改 webapp/WEB-INF/web.xml(项目创建的时候会自动生成),把新创建的 Servlet 注册进去。

这个配置文件的目的:
收到的请求中访问的是某个路径对应的 Servlet 类是哪个,进一步执行对应的 servlet 中的代码

web.xml:

    <servlet>
        <servlet-name>ImageServlet</servlet-name>
        <servlet-class>api.ImageServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ImageServlet</servlet-name>
        <url-pattern>/image</url-pattern>
    </servlet-mapping>

    
<servlet> 标签:告诉 Tomcat 当前这个 Servlet 对应到代码中的哪个类
<servlet-mapping> 标签:告诉 Tomcat 当前这个 Servlet 对应到URL 的path是什么

同样的,ImageShowServlet 类也是如此

    <servlet>
        <servlet-name>ImageShowServlet</servlet-name>
        <servlet-class>api.ImageShowServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ImageShowServlet</servlet-name>
        <url-pattern>/imageShow</url-pattern>
    </servlet-mapping>

但是,Servlet 相关的代码执行方式和平时写的代码不一样。平时的代码,是从 main 方法运行的;而 Servlet 里面没有 main 方法,而是靠 Tomcat 来自动调用到 Servlet 的代码。

CSDN:Tomcat 服务器

Tomcat 工作原理:
1.启动的时候要绑定端口号(80802.进入一个主循环
3.在主循环里面,调用 accept,获取到当前的请求的链接
4.读取客户端发送的数据(字符串)
5.把这个字符串按照 HTTP 协议来进行解析
6.解析出的 HTTP 请求的方法和 URL 之后,找到对应的 Servlet,并执行对应 doXXX 方法

在这里插入图片描述
举例:

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    	// 设置响应状态
        resp.setStatus(200);				 
        // 把 “加油” 放到 http 响应的 body 中
        resp.getWriter().write("加油");
    }
1.Tomcat 根据 url 查找映射关系表,找到 api.ImageServlet 类
2.Tomcat 根据 GET 方法,决定给 api.ImageServlet 创建一个对象,并且调用其中的 doGet 方法
3.执行 doGet,往 resp 对象中写了一个 “加油”
4.Tomcat 构造 resp 对象,根据这个对象生成 HTTP 响应报文,再通过 socket 写回给浏览器

在这里插入图片描述
在实现过程中,要记得不断的测试呀!打 war 包

Postman 测试:
​​​​​​在这里插入图片描述
在这里插入图片描述

前端

实现前端页面主要应用的技术:HTML + CSS +JavaScript
W3school

HTML:网页的骨架
CSS:描述网页上组件的样式(位置,颜色,大小,字体,背景。。。)
JavaScript:描述前端页面上的一些动作(和用户具体交互的行为)

HTML CSS JavaScript 都可以写到同一个 HTML 文件中,也可以分开写。
例如:
html 标签的 class 属性,引用了一个 css 的类:
<div class="am-form-group">
    <input type="file" class="am-form-field am-input-sm">
</div>

测试HTML显示效果的时候,有两种方式:
1.在IDEA直接点浏览器图标(只是在本地进行测试)
2.部署到服务器上,通过浏览器远程访问服务器(CSDN:打 war包(打 war包是为了部署到服务器上方便)

写前端时:Ctrl+Shift+I 打开开发者工具
在这里插入图片描述

把网页模板上显示的预览图片替换成 服务器上 保存的图片。
img 标签中 src 改成服务器上存着的图片的 url 就可以了

需要获取到服务器上所有的图片的 url(ImageServlet),需要通过 JS 先获取到所有图片属性,再分别加载每一个图片。
使用 JS 来完成。
引入 Vue JS 的框架则会更方便的编写代码

通过 浏览器中 JS 代码请求服务器,获取到服务器上都有哪些图片,把这个数据作为 Vue 渲染的依据(Vue 对象中 images 数组)

Vue.js

Vue中的命令:

v-for:循环访问一个数据
v-for="image in images"     类似于Java中的 for each
for(Image image:images)

v-bind:把数据绑定到 html 标签上的
v-on:绑定某种事件的处理函数

如果是在标签内部使用 Vue 对象中的数据,就需要使用插值表达式,
如果是在标签属性中使用 Vue 对象的数据,不需要使用差值表达式。

上传:
在这里插入图片描述

ajax:JS 中构造 HTTP 请求发送给服务器的一种实现方式。
使用ajax渲染:
1、页面先尝试获取页面大小,并设定显示图片的空间(当前图片还没获取到,也不知道图片大小)
2、通过ajax获取图片内容

改进上传操作,上传成功之后,自动跳转到主页 index.html(HTTP 重定向(302 可以完成重定向))

删除:
在这里插入图片描述
浏览器给服务器发送一个 DELETE/image?imageId=xxx 这样的请求就可以了 .(ajax 完成)

小细节:
1、简单的防盗链机制
你的图片链接可能被其他人使用(通过一定的机制来限制其他人来使用图片,否则你的图片用的人太多,可能服务器就挂了)

解决办法:可以判定当前请求的 referer 字段(HTTP 请求协议中的 header 的部分),是不是在我们代码中指定的白名单中,如果是,才允许访问
referer 表达当前请求的上个页面的地址
在代码中用一个 hashSet 存一个允许的 Referer 就可以,展示图片的时候判断一个是否在 hashSet 中存在即可

2、优化磁盘存储空间
应用到 MD5,如果两个图片内容完全一样,就在磁盘上只存一份文件就可以了。
通过 MD5 就能判定两个图片内容是否是一样的
图片文件虽然是二进制数据,但是本质上也是字符串,针对图片内容计算 MD5,如果两个图片内容相同,得到的 MD5 一定是相同的;反之,近似的认为 MD5 相同,原图片内容一定相同。
理论上是有可能两个图片内容不同,MD5 相同,但是实际上出现概率极低(MD5 自身算法设计上引起的特性)

实现思路:上传图片的时候,先判定下新图片的 MD5 在数据库中是否存在,如果已经存在了,就不把图片内容写到磁盘上,如果不存在,才写磁盘
1、改上传代码的逻辑,磁盘文件名 用 MD5 值来表示(十六进制的数字)
2、修改 ImageDao,新增一个接口,能够按照 MD5 查找数据库内容
3、修改上传图片的逻辑,根据 MD5 判定,当前图片是否要写磁盘

但是删除咋办呢?多个图片对应一个文件。
删除任何一个图片,都会导致文件就被删掉了。
删除逻辑的调整方式也很简单,通过 selectByMd5 看看当前 md5 值对应的图片在数据库中是否存在,如果不存在这个 md5,才真正删除磁盘文件。

另外

出现异常之后,处理的具体措施:

1.当前接触过的大部分都是 打印调用栈      
  e.printStackTrace();
2.让程序直接终止
3.通过监控报警通知程序员

多态的理解:

软件开发核心任务,管理软件的复杂程度(不要让软件变得更复杂)

封装是一种有效的管理复杂程度的手段,
让类的使用者不需要关注类具体实现细节(还是需要关注这个对象是一个什么类型)。

多态,其实是封装的更近一步,使用者不光不需要知道类的具体实现是啥,
也不需要关注这个对象的类型是什么
总结(这是本项目的框架):

在这里插入图片描述

五、最终效果

在这里插入图片描述

图片服务器完整代码

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值