打开github,在github上创建新项目:

Repository name: anydoor

Descripotion: Tiny NodeJS Static Web server

选择:public

选择:Initialize this repository with a README

添加gitignore文件:Add .gitignore:Node

添加License文件:Add a license: MIT License

git clone 该项目地址到本地文件夹

.gitignore

https://git-scm.com/docs/gitignore

.npmignore

https://docs.npmjs.com/misc/developers

代码一致性

https://editorconfig.org/

ESLint

https://editorconfig.org/

安装一个颜色插件chalk

npm init //初始化项目

npm -i chalk

NodeJS在服务器上构建web server

const http = require('http');
const chalk = require('chalk');
const conf = require('./config/defaultConf') const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type','text/plain'); // 输出是文本
res.end('Hello My Friends!');
}); server.listen(conf.port, conf.hostname, () => {
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server started at ${chalk.green(addr)}`)
});

输入 node app.js:

Server started at http://127.0.0.1:9000

在网页可以输出结果:

Hello My Friends!

可以改为html代码显示效果,改变'Content-Type'为'text/html':

const http = require('http');
const chalk = require('chalk');
const conf = require('./config/defaultConf') const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type','text/html'); // 可以改为html输出效果
res.write('<html>')
res.write('<body>')
res.write('Hello My Friends!');
res.write('</body>')
res.write('</html>')
res.end();
}); server.listen(conf.port, conf.hostname, () => {
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server started at ${chalk.green(addr)}`)
});

为了调试方便,安装supervisor

sudo npm -g install supervisor

输入命令supervisor app.js

Running node-supervisor with

program 'app.js'

--watch '.'

--extensions 'node,js'

--exec 'node'

Starting child process with 'node app.js'

实现效果:如何是目录,输出目录下所有文件,如何是文件,输出文件内容:

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs');
const conf = require('./config/defaultConf') const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url);
fs.stat(filePath, (err, stats) => {
if (err) {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end(`${filePath} is not a directory or file`);
return;
} if (stats.isFile()) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
// fs.readFile(filePath, (err, data) => {
// res.end(data);
// }); //读完才开始,响应速度慢,不推荐
fs.createReadStream(filePath).pipe(res);
} else if (stats.isDirectory()) {
fs.readdir(filePath, (err, files) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(files.join(','))
});
}
});
}); server.listen(conf.port, conf.hostname, () => {
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server started at ${chalk.green(addr)}`)
});

需要解决回调地狱的问题:

修改为两个文件,app.js 和route.js

app.js:

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const conf = require('./config/defaultConf')
const route = require('./helper/route') const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url);
route(req, res, filePath);
}); server.listen(conf.port, conf.hostname, () => {
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server started at ${chalk.green(addr)}`)
});

使用了promisify函数,并用同步解决异步问题: asyc和await两个都不能少!

route.js

const fs = require('fs');
const promisify = require('util').promisify; // 去回调
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir); module.exports = async function (req, res, filePath) {
try {
const stats = await stat(filePath);
if (stats.isFile()) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
fs.createReadStream(filePath).pipe(res);
} else if (stats.isDirectory()) {
const files = readdir(filePath);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(files.join(','))
}
} catch(ex) {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end(`${filePath} is not a directory or file`);
}
}

上面出现错误:修改代码如下,readdir前面漏了await

const fs = require('fs');
const promisify = require('util').promisify; // 去回调
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir); module.exports = async function (req, res, filePath) {
try {
const stats = await stat(filePath); //不加await会出现不把当成异步
if (stats.isFile()) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
fs.createReadStream(filePath).pipe(res);
} else if (stats.isDirectory()) {
const files = await readdir(filePath);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(files.join(','))
}
} catch(ex) {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end(`${filePath} is not a directory or file\n }`);
}
}

安装并使用handlebars

npm i handlebars

模板文件dir.tpl:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<style media="screen">
body {
margin: 30px;
}
a {
display: block;
font-size: 30px;
}
</style>
</head>
<body>
{{#each files}}
<a href="{{../dir}}/{{file}}">[{{icon}}] - {{file}}</a>
{{/each}}
</body>
</html>

配置文件:

module.exports = {
root: process.cwd(),
hostname: '127.0.0.1',
port:9000,
compress: /\.(html|js|css|md)/
};

压缩文件,可以使用js内置的压缩方法,可以大大节省带宽和下载速度:

const {createGzip, createDeflate} = require('zlib');

module.exports = (rs, req, res) => {
const acceptEncoding = req.headers['accept-encoding'];
if(!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
return rs;
}else if(acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
return rs.pipe(createGzip());
}else if(acceptEncoding.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'defalate');
return rs.pipe(createDeflate());
}
};

核心处理代码route.js:

const fs = require('fs');
const path = require('path');
const Handlebars = require('handlebars');
const promisify = require('util').promisify; // 去回调
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const config = require('../config/defaultConf'); //require可以放心使用相对路径
const mime = require('./mime');
const compress = require('./compress'); const tplPath = path.join(__dirname, '../template/dir.tpl');
const source = fs.readFileSync(tplPath); //只执行一次,下面内容之前必须提前加载好,所以用同步
const template = Handlebars.compile(source.toString()); module.exports = async function (req, res, filePath) {
try {
const stats = await stat(filePath); //不加await会出现不把当成异步
if (stats.isFile()) {
const contentType = mime(filePath);
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
let rs = fs.createReadStream(filePath);
if (filePath.match(config.compress)) {
rs = compress(rs, req, res);
}
rs.pipe(res);
} else if (stats.isDirectory()) {
const files = await readdir(filePath);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
const dir = path.relative(config.root, filePath);
const data = {
title: path.basename(filePath),
dir: dir?`/${dir}`:'',
// files // ES6语法简写 files:files
files: files.map(file => {
return {
file,
icon: mime(file)
}
})
};
res.end(template(data));
}
} catch(ex) {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end(`${filePath} is not a directory or file\n }`);
}
}

服务器相关代码app.js:

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const conf = require('./config/defaultConf')
const route = require('./helper/route') const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url);
route(req, res, filePath);
}); server.listen(conf.port, conf.hostname, () => {
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server started at ${chalk.green(addr)}`)
});

文件传输类型mime.js:

const path = require('path');

const mimeTypes = {
'323': 'text/h323',
'acx': 'application/internet-property-stream',
'ai': 'application/postscript',
'aif': 'audio/x-aiff',
'aifc': 'audio/x-aiff',
'aiff': 'audio/x-aiff',
'asf': 'video/x-ms-asf',
'asr': 'video/x-ms-asf',
'asx': 'video/x-ms-asf',
'au': 'audio/basic',
'avi': 'video/x-msvideo',
'axs': 'application/olescript',
'bas': 'text/plain',
'bcpio': 'application/x-bcpio',
'bin': 'application/octet-stream',
'bmp': 'image/bmp',
'c': 'text/plain',
'cat': 'application/vnd.ms-pkiseccat',
'cdf': 'application/x-cdf',
'cer': 'application/x-x509-ca-cert',
'class': 'application/octet-stream',
'clp': 'application/x-msclip',
'cmx': 'image/x-cmx',
'cod': 'image/cis-cod',
'cpio': 'application/x-cpio',
'crd': 'application/x-mscardfile',
'crl': 'application/pkix-crl',
'crt': 'application/x-x509-ca-cert',
'csh': 'application/x-csh',
'css': 'text/css',
'dcr': 'application/x-director',
'der': 'application/x-x509-ca-cert',
'dir': 'application/x-director',
'dll': 'application/x-msdownload',
'dms': 'application/octet-stream',
'doc': 'application/msword',
'dot': 'application/msword',
'dvi': 'application/x-dvi',
'dxr': 'application/x-director',
'eps': 'application/postscript',
'etx': 'text/x-setext',
'evy': 'application/envoy',
'exe': 'application/octet-stream',
'fif': 'application/fractals',
'flr': 'x-world/x-vrml',
'gif': 'image/gif',
'gtar': 'application/x-gtar',
'gz': 'application/x-gzip',
'h': 'text/plain',
'hdf': 'application/x-hdf',
'hlp': 'application/winhlp',
'hqx': 'application/mac-binhex40',
'hta': 'application/hta',
'htc': 'text/x-component',
'htm': 'text/html',
'html': 'text/html',
'htt': 'text/webviewhtml',
'ico': 'image/x-icon',
'ief': 'image/ief',
'iii': 'application/x-iphone',
'ins': 'application/x-internet-signup',
'isp': 'application/x-internet-signup',
'jfif': 'image/pipeg',
'jpe': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'application/x-javascript',
'latex': 'application/x-latex',
'lha': 'application/octet-stream',
'lsf': 'video/x-la-asf',
'lsx': 'video/x-la-asf',
'lzh': 'application/octet-stream',
'm13': 'application/x-msmediaview',
'm14': 'application/x-msmediaview',
'm3u': 'audio/x-mpegurl',
'man': 'application/x-troff-man',
'mdb': 'application/x-msaccess',
'me': 'application/x-troff-me',
'mht': 'message/rfc822',
'mhtml': 'message/rfc822',
'mid': 'audio/mid',
'mny': 'application/x-msmoney',
'mov': 'video/quicktime',
'movie': 'video/x-sgi-movie',
'mp2': 'video/mpeg',
'mp3': 'audio/mpeg',
'mpa': 'video/mpeg',
'mpe': 'video/mpeg',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'mpp': 'application/vnd.ms-project',
'mpv2': 'video/mpeg',
'ms': 'application/x-troff-ms',
'mvb': 'application/x-msmediaview',
'nws': 'message/rfc822',
'oda': 'application/oda',
'p10': 'application/pkcs10',
'p12': 'application/x-pkcs12',
'p7b': 'application/x-pkcs7-certificates',
'p7c': 'application/x-pkcs7-mime',
'p7m': 'application/x-pkcs7-mime',
'p7r': 'application/x-pkcs7-certreqresp',
'p7s': 'application/x-pkcs7-signature',
'pbm': 'image/x-portable-bitmap',
'pdf': 'application/pdf',
'pfx': 'application/x-pkcs12',
'pgm': 'image/x-portable-graymap',
'pko': 'application/ynd.ms-pkipko',
'pma': 'application/x-perfmon',
'pmc': 'application/x-perfmon',
'pml': 'application/x-perfmon',
'pmr': 'application/x-perfmon',
'pmw': 'application/x-perfmon',
'pnm': 'image/x-portable-anymap',
'pot,': 'application/vnd.ms-powerpoint',
'ppm': 'image/x-portable-pixmap',
'pps': 'application/vnd.ms-powerpoint',
'ppt': 'application/vnd.ms-powerpoint',
'prf': 'application/pics-rules',
'ps': 'application/postscript',
'pub': 'application/x-mspublisher',
'qt': 'video/quicktime',
'ra': 'audio/x-pn-realaudio',
'ram': 'audio/x-pn-realaudio',
'ras': 'image/x-cmu-raster',
'rgb': 'image/x-rgb',
'rmi': 'audio/mid',
'roff': 'application/x-troff',
'rtf': 'application/rtf',
'rtx': 'text/richtext',
'scd': 'application/x-msschedule',
'sct': 'text/scriptlet',
'setpay': 'application/set-payment-initiation',
'setreg': 'application/set-registration-initiation',
'sh': 'application/x-sh',
'shar': 'application/x-shar',
'sit': 'application/x-stuffit',
'snd': 'audio/basic',
'spc': 'application/x-pkcs7-certificates',
'spl': 'application/futuresplash',
'src': 'application/x-wais-source',
'sst': 'application/vnd.ms-pkicertstore',
'stl': 'application/vnd.ms-pkistl',
'stm': 'text/html',
'svg': 'image/svg+xml',
'sv4cpio': 'application/x-sv4cpio',
'sv4crc': 'application/x-sv4crc',
'swf': 'application/x-shockwave-flash',
't': 'application/x-troff',
'tar': 'application/x-tar',
'tcl': 'application/x-tcl',
'tex': 'application/x-tex',
'texi': 'application/x-texinfo',
'texinfo': 'application/x-texinfo',
'tgz': 'application/x-compressed',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'tr': 'application/x-troff',
'trm': 'application/x-msterminal',
'tsv': 'text/tab-separated-values',
'txt': 'text/plain',
'uls': 'text/iuls',
'ustar': 'application/x-ustar',
'vcf': 'text/x-vcard',
'vrml': 'x-world/x-vrml',
'wav': 'audio/x-wav',
'wcm': 'application/vnd.ms-works',
'wdb': 'application/vnd.ms-works',
'wks': 'application/vnd.ms-works',
'wmf': 'application/x-msmetafile',
'wps': 'application/vnd.ms-works',
'wri': 'application/x-mswrite',
'wrl': 'x-world/x-vrml',
'wrz': 'x-world/x-vrml',
'xaf': 'x-world/x-vrml',
'xbm': 'image/x-xbitmap',
'xla': 'application/vnd.ms-excel',
'xlc': 'application/vnd.ms-excel',
'xlm': 'application/vnd.ms-excel',
'xls': 'application/vnd.ms-excel',
'xlt': 'application/vnd.ms-excel',
'xlw': 'application/vnd.ms-excel',
'xof': 'x-world/x-vrml',
'xpm': 'image/x-xpixmap',
'xwd': 'image/x-xwindowdump',
'z': 'application/x-compress',
'zip': 'application/zip'
} module.exports = (filePath) => {
let ext = path.extname(filePath).split('.').pop().toLowerCase(); if (!ext) {
ext = filePath;
}
return mimeTypes[ext]||mimeTypes['txt'];
};

range

  • range:bytes = [start]-[end]
  • Accept-Range:bytes
  • Content-Range:bytes start-end/total

增加range.js

module.exports = (totalSize, req, res) => {
const range = req.headers['range'];
if(!range) {
return {code:200};
} const sizes = range.match(/bytes=(\d*)-(\d*)/);
const end = sizes[2] || totalSize - 1;
const start = sizes[1] || totalSize - end; if(start > end || start < 0 || end > totalSize) {
return {code:200};
} res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Range', `bytes ${start}-${end}/${totalSize}`);
res.setHeader('Content-Length', end - start);
return {
code: 206,
start: parseInt(start),
end: parseInt(end)
}
};

修改了route.js部分代码:

let rs;
const {code, start, end} = range(stats.size, req, res);
if(code === 200) {
rs = fs.createReadStream(filePath);
}else{
rs = fs.createReadStream(filePath, {start, end});
}

用curl可以查看内容:

curl -r 0-10 -i http://127.0.0.1:9000/LICENSE

显示结果,使用range拿到了文件的部分内容:

HTTP/1.1 200 OK

Content-Type: text/plain

Accept-Ranges: bytes

Content-Range: bytes 0-10/1065

Content-Length: 10

Date: Wed, 12 Dec 2018 05:10:45 GMT

Connection: keep-alive

MIT Licens

缓存

缓存原理图

缓存header

  • Expires, Cache-Control
  • If-Modified-Since / Last-Modified
  • If-None-Match/ETag文件改变就变化的值

cache.js

const {cache} = require('../config/defaultConf');

function refreshRes(stats, res) {
const {maxAge, expires, cacheControl, lastModified, etag} = cache; if(expires) {
res.setHeader('Expires', (new Date(Date.now() + maxAge*1000)).toUTCString());
} if(cacheControl) {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
} if(lastModified) {
res.setHeader('Last-Modified', stats.mtime.toUTCString());
} if(etag) {
res.setHeader('ETag',`${stats.size}-${stats.mtime}`);
}
} module.exports = function isFresh(stats, req, res) {
refreshRes(stats, res); const lastModified = req.headers['if-modified-since'];
const etag = req.headers['if-none-match']; // 没有给,第一次
if(!lastModified && !etag) {
return false;
} if(lastModified && lastModified !== res.getHeader('Last-Modified')) {
return false;
}
if(etag && etag !== res.getHeader('ETag')) {
return false;
} return true; //缓存可用
};

在加载资源之前,可以添加:

if(isFresh(stats, req, res)) {
res.statusCode = 304;
res.end();
return;
}

安装命令行工具:npm i yargs

index.js命令行代码:

// process.argv  -p --port=8080
// 现有工具 commander yargs const yargs = require('yargs');
const Server = require('./app'); const argv = yargs
.usage('anywhere [options]')
.option('p', {
alias: 'port',
describe: '端口号',
default: 9000
})
.option('h', {
alias: 'hostname',
describe: 'host',
default: '127.0.0.1'
})
.option('d', {
alias: 'root',
describe: 'root path',
default: process.cwd()
})
.version()
.alias('v', 'version')
.help()
.argv; const server = new Server(argv);
server.start();

app.js

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const conf = require('./config/defaultConf');
const route = require('./helper/route');
const openUrl = require('./helper/openUrl'); class Server { constructor (config) {
this.conf = Object.assign({}, conf, config);
} start() {
const server = http.createServer((req, res) => {
const filePath = path.join(this.conf.root, req.url);
route(req, res, filePath, this.conf);
}); server.listen(this.conf.port, this.conf.hostname, () => {
const addr = `http://${this.conf.hostname}:${this.conf.port}`;
console.info(`Server started at ${chalk.green(addr)}`)
openUrl(addr);
});
}
} module.exports = Server;

Node项目实战-静态资源服务器的更多相关文章

  1. 使用Node.js搭建静态资源服务器

    对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如 ...

  2. 原生node写一个静态资源服务器

    myanywhere 用原生node做一个简易阉割版的anywhere静态资源服务器,以提升对node与http的理解. 相关知识 es6及es7语法 http的相关网络知识 响应头 缓存相关 压缩相 ...

  3. NodeJS4-8静态资源服务器实战_构建cli工具

    Cli(command-line interface),中文是 命令行界面,简单来说就是可以通过命令行快速生成自己的项目模板等功能(比较熟悉的是vue-cli脚手架这些),把上述写的包做成Cli工具. ...

  4. [Node]创建静态资源服务器

    项目初始化 .gitignore cnpm i eslint -D eslint --init得到.eslintrc.js .eslintrc.js module.exports = { 'env': ...

  5. 极简 Node.js 入门 - 5.3 静态资源服务器

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  6. node静态资源服务器的搭建----访问本地文件夹(搭建可访问静态文件的服务器)

    我们的目标是实现一个可访问静态文件的服务器,即可以在浏览器访问文件夹和文件,通过点击来查看文件. 1.先创建一个文件夹anydoor,然后在该文件夹里npm init一个package.json文件, ...

  7. 使用node搭建静态资源服务器

    安装 npm install yumu-static-server -g 使用 shift+鼠标右键  在此处打开Powershell 窗口 server # 会在当前目录下启动一个静态资源服务器,默 ...

  8. 使用 Express 实现一个简单的 SPA 静态资源服务器

    背景 限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案. 为 SPA 应用添加入口 ...

  9. Nginx——静态资源服务器(一)

    java web的项目中,我们经常将项目部署到Tomcat或者jetty上,可以通过Tomcat或者jetty启动的服务来访问静态资源.但是随着Nginx的普及,用Nginx来作为静态资源服务器,似乎 ...

随机推荐

  1. 6.计算字段 ---SQL

    提示:客户端与服务器的格式在SQL语句内可完成的许多转换和格式化工作都可以直接在客户端应用程序内完成.但一般来说,在数据库服务器上完成这些操作比在客户端中完成要快得多. 一.拼接字段 拼接(conca ...

  2. gedit配置

    编辑 \(\rightarrow\) 首选项 \(\rightarrow\) 插件 \(\rightarrow\) 外部工具 启用 进入工具 \(\rightarrow\) Manage Extern ...

  3. Core2.0 项目到2.1

    Core2.0 项目到2.1 https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_10.html .NET Core 2.1 终于发布了, 赶紧升级一下. 一. ...

  4. 什么是语义化的HTML?有何意义?为什么要做到语义化?

    一.什么是语义化的HTML? 语义化的HTML就是正确的标签做正确的事情,能够便于开发者阅读和写出更优雅的代码的同时让网络爬虫很好地解析. 二.为什么要做到语义化? 1.有利于SEO,有利于搜索引擎爬 ...

  5. Spark Mllib里决策树二元分类使用.areaUnderROC方法计算出以AUC来评估模型的准确率和决策树多元分类使用.precision方法以precision来评估模型的准确率(图文详解)

    不多说,直接上干货! Spark Mllib里决策树二元分类使用.areaUnderROC方法计算出以AUC来评估模型的准确率 具体,见 Hadoop+Spark大数据巨量分析与机器学习整合开发实战的 ...

  6. php时间戳存在8小时误差

    当将PHP时间戳转化为正常的时间格式一般的操作方法如下: $mytime=time(); echo $mytime.'<br />'; echo date('Y-m-d H:i:s',$m ...

  7. FloatHelper

    function FloatHelper() { } FloatHelper.prototype.showFloater = function (Target, Title, Action, Acti ...

  8. NHibernate中创建User类报错问题

    前两天刚开始学习NHibernate架构,照着前辈的例子打了一遍运行之后没问题,然后自己创建了一个User的Model发现一运行就报User附近有错误,然后就检查,类写的没错用了virtual,Use ...

  9. vue or react mvvm里的文字上下滚动

    1.jQuery 时候实现 上下滚动很简单,基本上一个animateTop就可以了 2. vue等MVVM就有些麻烦了,因为不推荐操作DOM,专注于数据 我们可以使用 css3 transition: ...

  10. 【持续更新】Java 时间相关

    直接上代码: import java.util.*; import java.text.SimpleDateFormat; public class HelloWorld { public stati ...