引言
基于java语言实现了“微Tomcat”服务,实现静态资源和动态资源(自定义架构),为学习Java经典的SpringMVC和JavaEE框架打个前站。巩固JavaSE基本知识和面向对象及多线程方面。整个项目构思建立在HTTP协议和Socket网络套接字上,是希望Java爱好者不要只顾基本和框架的使用上,还得理解底层的构建思想,为之后的软件构架或平台框架打下(每个Java程序员都应该有的梦想)良好的基础。
一、静态资源环境
静态资源的根目录 ROOT
D:\tomcat8.5\webapps\ROOT
servlet.xml文件
放在项目目录下
二、静态资源服务器
2.1 什么是Tomcat
Tomcat是Apache下开源项目 ,主要功能是为本地资源提供Web访问(HTTP协议),资源包含静态资源(html网页、CSS样式,JavaScript脚本及woff字体、图片等资源)和动态资源(JSP, Servlet)。
2.2 实现静态资源服务器的思路
1) 通过 ServerSocket 绑定指定端口(80) 开启服务
2) 通过 ServerSocket的accept()接收客户的请求连接,设计了Runnable接口实现类 Client,接收连接的客户端Socket对象,在它run进行接收报文和发送报文处理。
3) 接收报文: 将字节输入流(socket.getInputStream()) 包装成缓冲字符流,读取第一行,获取请求路径 path; 读取请求头的属性,当读取数据的长度小于等于2(空行)时结束。
4) 通过path路径 从服务器指定目录(ROOT),查找文件资源,如果不存在,则指定ROOT目录的 error-404.html资源。
5) 设置读取文件的大小 Content-Length和文件类型 Content-Type的响应头, 并向客户端发送响应头信息
6) 读取请求资源的数据,以字节流的方式向客户端发送。
2.3 代码实现
package day27.webserver;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Client implements Runnable{
private static final String ROOT = "D:\\tomcat8.5\\webapps\\ROOT"; // 服务器资源存放的位置
private static HashMap<String, String> headers;
static {
headers = new HashMap<>();
headers.put("Connection","close"); // 短连接, 即请求响应结束之后,当前的Socket不保留
headers.put("Accept-Ranges","bytes");
headers.put("Content-Length","1111");
headers.put("Content-Type","image/png");
}
private Socket socket;
private String path;
public Client(Socket socket) {
this.socket = socket;
}
public void receive_request(){
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
String s = reader.readLine(); // GET /index.html HTTP/1.1
this.path = s.split("\\s+")[1];
System.out.println(this.socket.getInetAddress().getHostAddress()+" GET "+this.path+" 200 OK");
try {
while ((s = reader.readLine()).length()>2) {}
System.out.println("--header close--");
}catch (Exception e){}
// 验证请求path是否为 /
if(this.path.equals("/")){ // 主页地址
this.path = "index.html";
}else {
// GET请求不读取Body, /index.html, /assert/js/ace-extra-min.js
// 将请求资源路径转化为系统的文件资源路径, mac或linux系统不需要 转化
this.path = this.path.replace("/", "\\").substring(1);
// 将路径中的参数去掉,路径上的参数是动态资源,对于静态资源不需要的
// path => index.html?q=123&n=1&t=333
if (this.path.contains("?"))
this.path = this.path.substring(0, this.path.indexOf("?"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getContentType(String filename){
if(filename.endsWith(".png")){
return "image/png";
}else if(filename.endsWith(".js")){
return "application/javascript";
}else if(filename.endsWith(".css")){
return "text/css";
}else if(filename.endsWith(".woff2")){
return "font/woff2";
}else if(filename.endsWith(".woff")){
return "font/woff";
}else if(filename.endsWith(".html")){
return "text/html";
}else if(filename.endsWith(".jpg")){
return "image/jpeg";
}else if(filename.endsWith(".gif")){
return "image/gif";
}
else{
return "text/*";
}
}
void response(){
if(this.path.endsWith(".html")){
headers.remove("Accept-Ranges");
}
// 根据请求资源的文件路径,构造ROOT目录中资源文件对象
File file = new File(ROOT, this.path);
if(!file.exists()){ // 验证资源是否存在
file = new File(ROOT, "error-404.html");
}
headers.put("Content-Length", ""+file.length());
headers.put("Content-Type", getContentType(file.getName()));
try {
// 先发送响应头报文
OutputStream os = this.socket.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os));
writer.write("HTTP/1.1 200 OK\r\n");
for (String header_name : headers.keySet()) {
writer.write(header_name+": "+headers.get(header_name)+"\r\n");
}
writer.write("\r\n");
writer.flush();
// 写body数据
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[8192];
int len=-1;
while ((len=fis.read(buffer)) != -1){
os.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
System.out.println(socket.getInetAddress().getHostAddress()+"已连接");
// Connection: close;短连接(即请求-响应结束时,当前线程也结束)
// Connection: keep-alive; 长连接, 请求响应结束时,当前线程保留,等待下一次请求和响应
receive_request();
response();
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class HTTPServer extends Thread{
ServerSocket ss;
ExecutorService clientPool; // 客户端请求处理的线程池
HTTPServer(int port){
super("HTTPServer");
try {
ss = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
clientPool = Executors.newCachedThreadPool(); // 弹性创建或销毁线程
}
@Override
public void run() {
while (true){
try {
Socket client = ss.accept();
// 基于线程池方式,执行客户端请求与响应的任务
clientPool.execute(new Client(client));
} catch (IOException e) {
System.out.println("接收请求连接超时");
}
}
}
public static void main(String[] args) {
System.out.println("启动Web服务器, bind 80");
new HTTPServer(80).start();
}
}
三、实现动态资源服务器
3.3 动态资源的定义与区分
动态资源: 请求的资源是需要java代码(程序)进行处理的,可以理解为请求路径 对应是Java的一段代码。
区分静态与动态的
/index.html 静态的
/index.js 静态的
/assets/images/icon-up.png 静态的
/login.do 动态的
/logout.do 动态的
动态资源的请求报文:
GET /login.do?name=disen&pwd=123 HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
POST /login.do HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
name=disen&pwd=123
3.4 实现动态资源服务器的思路
WEB服务器类,可以处理静态、动态的资源。
主要功能的假设:
1) 绑定端口,启动服务(异步处理)
2) 设计单例Servlet工厂类,加载xml文件中的所有servlet资源, 并提供查找指定请求动态资源的方法,返回servlet中的class处理类完整路径
3) 当客户端请求连接时,创建Client类对象,传入客户端Socket对象, 将Client对象放在线程池中,由线程池来执行。
4)Client类
4.1) 接收请求报文,解析第一行,获取请求方法和请求路径
4.2) 接收请求头信息,可处理,也可不处理;
4.3) 判断请求资源是静态的还是动态的
4.3.3) 静态资源,按静态资源处理
4.3.4) 获取Servlet工厂类实例,通过它的查找方法,获取处理类
4.3.4.1) 如果不存在,则按404处理
4.3.4.2) 反射方式,创建处理类的实例,并依据请求方法,调用实例的doGet或doPost()
4.3.4.3) 接收doGet/doPost()处理的结果Response
4.3.4.4) 依据Reponse实例,发送响应的报文。
4.4) 在调用处理类的doGet或doPost()方法之前,需要处理路径参数或请求体的数据
封装成Request类对象
3.3 构架设计
3.3.1 WebServer类
支持静态资源和动态资源的访问
实现功能点如下:
1) 开启指定端口的服务
2) 接收客户端的请求, 并客户端封装到WebClient类中, 同时放在线程池中执行
package day27.webserver;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 实现Web服务器:支持静态资源和动态资源的访问
public class WebServer extends Thread{
private ServerSocket ss;
private ExecutorService clientPool;
public WebServer(int port){
try {
ss = new ServerSocket(port);
clientPool = Executors.newScheduledThreadPool(1000);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true){
try {
Socket socket = ss.accept();
clientPool.execute(new WebClient(socket));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
try {
System.out.println("已启动Web服务,"+ InetAddress.getLocalHost().getHostAddress() + " 绑定端口: 8000");
} catch (UnknownHostException e) {
e.printStackTrace();
}
new WebServer(8000).start();
}
}
3.3.4 WebClient类
处理客户端请求报文和响应报文
实现功能点如下:
1) 接收客户端发送请求报文 receive_request()
1.1) 获取请求路径和请求方法, 数据保存在path和method的String变量中
1.2)请求头的属性和属性值, 数据保存在 requestHeaders的Map集合中
1.3) 依据请求路径,验证是动态的、静态的、主页(静态的)
2)静态资源响应, path=index.html, static_response()
2.1) 通过File对象,从ROOT目录中加载文件 path
2.2) 构造响应报文,添加或更新Content-Type和Content-Length两个响应头的属性值,读取不同的静态文件具有不同的大小和文件类型。
2.3)发送报文
2.4)发送读取文件的数据
3) 动态资源的请求参数和请求体数据的接收, dynamic_path_handler(BufferedReader )
3.3)获取请求路径的参数, 存储在 params的Map集合中
3.4) 获取POST请求体的数据, 需要按单个字节读取,存储在 forms的Map集合中
4) 动态资源的处理及响应处理后的数据(响应报文), dynamic_response()
4.1) 依据path,请求客户端的IP及requestHeaders、params和forms,构造Request类对象
4.2) 通过 动态资源处理类的工厂 ServletFactory, 获取path路径对应的处理类(IServlet接口的实现类)
4.3)基于Java的反射机制,创建IServlet接口实现类的实例
4.4)如果请求方法是GET,则调用IServlet.doGet(),传入Request对象
4.5) 如果请求方法是POST,则调用IServlet.doPost()方法,传入Request对象
4.6)依据IServlet处理方法返回 的Response,向客户端发送信息的响应报文
第一行:HTTP/1.1 响应状态码 状态码对应的文本名称
第二行开始: 写响应头 response.headers;
响应头: 空行结束
响应体: response.body byte[]
3.3.3 IServlet接口
为get和post两种请求方法,提供处理方法接口
package day27.http;
public interface IServlet {
Response doGet(Request request);
Response doPost(Request request);
}
3.3.4 AbsServlet抽象类
提供一些简单方法,让具体业务处理类中使用
public Response htmlResponse(String filename);
依据ROOT目录下的文件名,快速构造一个Response响应类对象
public Response redirect(String path);
依据path构造一个重写向的Response类实例
package day27.http;
import day27.config.Config;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public abstract class AbsServlet implements IServlet{
public Response redirect(String path){
Response response = new Response();
response.setStatus(301, "Moved Permanently");
response.addHeader("Location", path);
return response;
}
public Response htmlResponse(String fileName) throws IOException {
File htmlFile = new File(Config.ROOT, fileName);
FileInputStream fis = new FileInputStream(htmlFile);
ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 内存流,用于存储读取的字节数据
byte[] buffer = new byte[8192];
int len = -1;
while ((len=fis.read(buffer)) != -1){
bos.write(buffer, 0, len);
}
Response response = new Response();
response.addHeader("Content-Type", "text/html;charset=utf-8");
response.addHeader("Content-Length", ""+htmlFile.length());
// 设置response的响应数据
response.setBody(bos.toByteArray());
return response;
}
}
3.3.5 Request类
请求报文中的数据封装到Request中,方便提供给动态资源处理方法
package day27.http;
import java.util.HashMap;
import java.util.Map;
public class Request {
private String path; // 请求path路径
private String remote_ip; // 请求的IP地址
private Map<String, String> params; // 路径参数
private Map<String, String> forms; // 表单参数
public Request(String path, String remote_ip) {
this.path = path;
this.remote_ip = remote_ip;
params = new HashMap<>();
forms = new HashMap<>();
}
public void addParameter(String name, String value){
params.put(name, value);
}
public void addFormData(String name, String value){
forms.put(name, value);
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getRemote_ip() {
return remote_ip;
}
public void setRemote_ip(String remote_ip) {
this.remote_ip = remote_ip;
}
public Map<String, String> getParams() {
return params;
}
public Map<String, String> getForms() {
return forms;
}
}
3.3.6 Response类
动态资源处理方法返回的对象,表示处理的结果,方便WebClient获取响应报文需要的信息(状态码、状态码名称 、响应头、响应体)
package day27.http;
import java.util.HashMap;
import java.util.Map;
public class Response {
private int status;
private String status_msg;
private Map<String, String> headers;
private byte[] body;
public Response() {
status = 200;
status_msg = "OK";
headers = new HashMap<>();
}
public void addHeader(String name, String value){
headers.put(name, value);
}
public void setStatus(int status, String msg){
this.status = status;
this.status_msg = msg;
}
public String getStatusMessage(){
return this.status +" "+this.status_msg;
}
public int getStatus() {
return status;
}
public String getStatus_msg() {
return status_msg;
}
public Map<String, String> getHeaders() {
return headers;
}
public byte[] getBody() {
return body;
}
public void setBody(byte[] body){
this.body = body;
}
}
3.3.7 ServletFactory工厂类
实现动态资源请求路径和动态资源处理类的映射关系 ,即通过请求路径获取它的处理类(IServlet接口实现类)的类全名。
内部加载 servlet.xml文件, 事先获取请求路径和处理类的映射信息。
工厂类设计为单例模式,在任意地方获取它的实例
package day27.http;
import day27.config.Config;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.util.HashMap;
import java.util.Map;
// 加载 servlet.xml文件
public class ServletFactory {
private static ServletFactory instance = new ServletFactory();
public static ServletFactory getInstance(){
return instance;
}
private Map<String,String> servlets;
private ServletFactory(){
servlets = new HashMap<>();
loadXml();
}
private void loadXml(){
try {
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
saxParser.parse(Config.SERVLET_CONFIG_PATH, new DefaultHandler(){
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equals("servlet")){
servlets.put(attributes.getValue("path"), attributes.getValue("class"));
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean exists(String path){
return servlets.containsKey(path.substring(0, path.indexOf(".")));
}
public String get(String path){
return servlets.get(path.substring(0, path.indexOf(".")));
}
public static void main(String[] args) {
String s = ServletFactory.getInstance().get("login.do");
System.out.println(s);
}
}
3.3.8 Config接口
Config类配置静态资源访问 的根目录 ROOT和动态资源的映射文件 SERVLET_CONFIG_PATH
package day27.config;
public interface Config {
String ROOT = "D:\\tomcat8.5\\webapps\\ROOT";
String SERVLET_CONFIG_PATH = "servlet.xml";
}
3.3.9 数据持久化
提供User类对象的保存和加载
package day27.data;
import java.io.Serializable;
public class User implements Serializable {
public static final long serialVersionUID = 2L;
private String name;
private String pwd;
public User(String name, String pwd) {
this.name = name;
this.pwd = pwd;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
package day27.data;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class UserData {
private UserData(){}
private static UserData ud = new UserData();
public static UserData getInstance(){ return ud;}
public List<User> load(){
ObjectInputStream bis = null;
try {
bis = new ObjectInputStream(new FileInputStream("users.dat"));
Object o = bis.readObject();
return (List<User>) o;
} catch (Exception e) {
}finally {
if(bis!=null){
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public boolean addUser(User user){
List<User> users = load();
if(users == null){
users = new ArrayList<>();
}
users.add(user);
ObjectOutputStream bos = null;
try {
bos = new ObjectOutputStream(new FileOutputStream("users.dat"));
bos.writeObject(users);
} catch (IOException e) {
e.printStackTrace();
}finally {
if(bos!=null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
public static void main(String[] args) {
// UserData.getInstance().addUser(new User("disen", "disen666"));
// UserData.getInstance().addUser(new User("admin", "admin123"));
List<User> users = UserData.getInstance().load();
System.out.println(users);
}
}
3.4 动态资源处理类
3.4.1 LoginServlet处理类
1) get请求,响应login.html文件的数据
2) post请求,获取请求体forms表单数据,从持久化的用户列表中查找登录用户,如果存在,则重定向到 index.html页面,不存在,则返回 login.html页面
package day27.actions;
import day27.data.User;
import day27.data.UserData;
import day27.http.AbsServlet;
import day27.http.Request;
import day27.http.Response;
import java.io.*;
import java.util.List;
import java.util.Map;
public class LoginServlet extends AbsServlet {
@Override
public Response doGet(Request request) {
System.out.println("login data:" + request.getParams());
try {
return htmlResponse("login.html");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public Response doPost(Request request) {
Map<String, String> forms = request.getForms();
System.out.println("form data: " + forms);
if (forms != null && !forms.isEmpty()) {
try {
String name = forms.get("name");
String pwd = forms.get("pwd");
List<User> users = UserData.getInstance().load();
for (User user : users) {
if(user.getName().equals(name) && user.getPwd().equals(pwd)){
return redirect("/index.html");
}
}
return htmlResponse("login.html");
} catch (IOException e) {
e.printStackTrace();
try {
return htmlResponse("error-500.html");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
return null;
}
}
3.4.2 RegistServlet类
1)get 请求 获取 regist.html页面
2)post请求,获取请求体的form表单数据,构造User类对象,并持久化保存
package day27.actions;
import day27.data.User;
import day27.data.UserData;
import day27.http.AbsServlet;
import day27.http.Request;
import day27.http.Response;
import java.io.IOException;
import java.util.List;
public class RegistServlet extends AbsServlet {
@Override
public Response doGet(Request request) {
try {
return htmlResponse("regist.html");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public Response doPost(Request request) {
String name = request.getForms().get("name");
String pwd = request.getForms().get("pwd");
List<User> users = UserData.getInstance().load();
for (User user : users) {
if(user.getName().equals(name)) {
try {
return htmlResponse("regist.html");
} catch (IOException e) {
e.printStackTrace();
}
}
}
UserData.getInstance().addUser(new User(name, pwd));
return redirect("/login.do"); // 重写向
}
}
3.4.3 添加动态资源映射
添加到servlet.xml文件
<servlets>
<servlet path="login" class="day27.actions.LoginServlet" />
<servlet path="regist" class="day27.actions.RegistServlet" />
</servlets>
3.3 启动WebServer服务
直接运行WebServer类,可以修改绑定的端口号