自定义JavaWeb服务

本文介绍了使用Java实现一个简单的微Tomcat服务,处理静态和动态资源。通过ServerSocket监听端口,接收客户端请求,解析HTTP报文,实现静态资源的读取和发送,以及动态资源的处理逻辑,包括Servlet工厂和反射机制。此外,还涉及到了数据持久化和动态资源处理类的设计与实现。
摘要由CSDN通过智能技术生成

引言

基于java语言实现了“微Tomcat”服务,实现静态资源和动态资源(自定义架构),为学习Java经典的SpringMVC和JavaEE框架打个前站。巩固JavaSE基本知识和面向对象及多线程方面。整个项目构思建立在HTTP协议和Socket网络套接字上,是希望Java爱好者不要只顾基本和框架的使用上,还得理解底层的构建思想,为之后的软件构架或平台框架打下(每个Java程序员都应该有的梦想)良好的基础。

一、静态资源环境

  1. 静态资源的根目录 ROOT

D:\tomcat8.5\webapps\ROOT
  1. 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类,可以修改绑定的端口号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值