【JAVA】手写简易服务器(深刻理解Tomcat)

一、知识体系

1、面向对象

特点:概括为封装性、继承性和多态性

2、IO流

用于获取请求协议和发送响应信息

3、多线程

使得服务器能够并发访问

4、xml解析

解析web.xml配置文件

5、反射

概念:把java类中的各种结构(方法、属性、构造器、类名)映射成一个个的java对象

目标:通过用户访问的url解析成类名并获取相应的servlet对象,以此来返回不同的页面

6、http协议

HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议。

底层是基于socket的短链接实现。

目标:
1、使用ServerSocket建立与浏览器的连接, 获取请求协议
2、返回响应协议 (封装响应信息)

二、获取请求协议及封装

1、获取请求协议
//1、创建ServerSocket
ServerSocket serverSocket = new ServerSocket(8888);
//2、建立连接获取Socket
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接...");
//3、通过输入流获取请求协议
InputStream is = client.getInputStream();
paramMap = new HashMap<>();
byte[] datas = new byte[1024*1024];
int len = 0;
try {
    len = is.read(datas);
    String requestInfo = new String(datas,0,len);
} catch (IOException e) {
    e.printStackTrace();
}

请求协议格式:
GET请求:
在这里插入图片描述

POST请求:
在这里插入图片描述

2、封装request

目的:
1、获取用户访问的url和请求参数

根据获取的请求协议,并通过分割字符串获取请求方法、请求url和请求参数(需要注意的是get请求参数是通过?直接跟在url后面,post请求在最后一行,与请求体之间有一个空行)

下面通过封装request并对请求协议进行处理

package com.swust.java.practice.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.*;

/**
 * 封装请求协议
 *  1、获取  method uri 请求参数
 *  2、封装请求参数为map
 */
public class Request {
    // 请求协议信息
    private String requestInfo;
    // 请求方式
    private String method;
    // 请求url
    private String url;
    // 请求参数
    private String queryParam;
    // 处理请求参数
    private Map<String, List<String>> paramMap;

    private final String CRLF = "\r\n";

    public Request() {}

    public Request(Socket client) throws IOException {
        this(client.getInputStream());
    }

    public Request(InputStream is) {
        paramMap = new HashMap<>();
        byte[] data = new byte[1024*1024];
        int len = 0;
        try {
            len = is.read(data);
            requestInfo = new String(data,0,len - 1);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 解析字符串
        parseRequestInfo();
    }

    private void parseRequestInfo(){
        System.out.println("--解析数据--");
        //System.out.println(requestInfo);
        // 1、获取请求方式
        method = requestInfo.substring(0,requestInfo.indexOf("/")).trim().toLowerCase();

        // 2、获取请求路径  路径中可能包含参数
        url = requestInfo.substring(requestInfo.indexOf("/")+1,requestInfo.indexOf("HTTP/")).trim();
        if(url.contains("?")){
            String[] array = url.split("\\?");
            queryParam = array[1].trim();
            url = array[0].trim();
        }
        // 3、获取请求参数  空行之后 请求体
        if(method.equals("post")){
            String param = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
            if(null == queryParam){
                queryParam = param;
            }else{
                queryParam += "&" + param;
            }
        }
        queryParam = null == queryParam ? "" : queryParam;// 避免get请求无参数时为空
        System.out.println("请求方法:"+method);
        System.out.println("请求路径:"+url);
        System.out.println("请求参数:"+queryParam);
        paramConvertToMap(queryParam);
    }
    // 将请求参数由字符串转成map  name=bob&pwd=123&age=23&age=18
    private void paramConvertToMap(String queryParam){
        //1、分割字符串
        String[] array = queryParam.split("&");
        for(String kv : array){
            String[] s = kv.trim().split("=");
            s = Arrays.copyOf(s,2);// 防止出现无参数值的情况
            String key = s[0];
            String value = s[1] == null ? null : decode(s[1],"UTF-8");
            // 2、存储
            if(!paramMap.containsKey(key)){// 当map中不包含key
                paramMap.put(key,new ArrayList<>());
            }
            paramMap.get(key).add(value);
        }
    }
    // 处理中文乱码问题
    private String decode(String value,String enc){
        try {
            return java.net.URLDecoder.decode(value,enc);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }
    // 获取指定key的所有参数值
    public String[] getParamValues(String key){
        List<String> values = paramMap.get(key);
        if(null == values || values.size() < 1){
            return null;
        }
        return values.toArray(new String[0]);
    }
    // 获取指定key的一个参数值
    public String getParam(String key){
        String[] values = getParamValues(key);
        return values == null ? null : values[0];
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getQueryParam() {
        return queryParam;
    }
}

三、返回响应协议及封装

1、返回响应协议
final String BLANK = "";
final String CRLF = "\r\n";
//1、准备内容	
StringBuilder content = new StringBuilder();
content.append("<html>");
content.append("<html>");
content.append("<head>");
content.append("<title>");
content.append("response success (*^▽^*)");
content.append("</title>");
content.append("</head>");
content.append("<body>");
content.append("<h2>登录页面->服务器响应成功!!!</h2>");
content.append("</body>");
content.append("</html>");
//2、获取字节数
int size = content.toString().getBytes().length;
//3、拼接响应协议
StringBuilder headInfo = new StringBuilder();
headInfo.append("HTTP/1.1").append(BLANK)
                .append(statusCode).append(BLANK)
                .append("OK").append(CRLF);// 响应行
headInfo.append("Date:").append(new Date()).append(CRLF)
                .append("Server:").append("hqf Server/0.0.1").append(CRLF)
                .append("Content-type:text/html;charset=UTF-8").append(CRLF)
                .append("Content-length:").append(len).append(CRLF)
                .append(CRLF);// 响应头
//4、使用输出流输出到browser
try {
	BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
   	bw.append(headInfo).append(content).flush();
} catch (IOException e) {
   e.printStackTrace();
}     
2、封装响应协议

目的:
1、能够动态添加内容
2、根据状态码拼接头信息

package com.swust.java.practice.http;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;

/**
 * 封装响应信息
 *  1、动态添加内容
 *  2、根据状态码拼接头信息
 */
public class Response {

    BufferedWriter bw;
    // 协议头信息
    private StringBuilder headInfo;
    // 正文
    private StringBuilder content;
    // 正文字节数
    private int len;

    private final String BLANK = " ";
    private final String CRLF = "\r\n";


    private Response() {
        headInfo = new StringBuilder();
        content = new StringBuilder();
        len = 0;
    }

    public Response(Socket client){
        this();
        try {
            bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
        } catch (IOException e) {
            e.printStackTrace();
            headInfo = null;
        }
    }

    public Response(OutputStream os){
        this();
        bw = new BufferedWriter(new OutputStreamWriter(os));
    }

    // 构建头信息 (状态码)
    private void createHeadInfo(int statusCode){
        // 1、响应行
        headInfo.append("HTTP/1.1").append(BLANK)
                .append(statusCode).append(BLANK);
        switch (statusCode){
            case 200:
                headInfo.append("OK").append(CRLF);
                break;
            case 404:
                headInfo.append("Not Found").append(CRLF);
                break;
            case 500:
                headInfo.append("Server Error").append(CRLF);
                break;
        }
        // 2、响应头
        headInfo.append("Date:").append(new Date()).append(CRLF)
                .append("Server:").append("hqf Server/0.0.1").append(CRLF)
                .append("Content-type:text/html;charset=UTF-8").append(CRLF)
                .append("Content-length:").append(len).append(CRLF)
                .append(CRLF);
    }

    // 动态添加Response内容
    public Response print(String info){
        content.append(info);
        len += info.getBytes().length;
        return this;
    }

    public Response println(String info){
        content.append(info).append(CRLF);
        len += (info + CRLF).getBytes().length;
        return this;
    }

    // 返回响应信息
    public void pushToBrowser(int statusCode){
        if(null == headInfo){
            statusCode = 500;
        }
        createHeadInfo(statusCode);
        try {
            bw.append(headInfo).append(content).flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、引入Servlet

通过引入servlet来处理不同的业务代码

通过接口中的service方法来构建不同的返回内容,response.pushToBrowser来返回不同的状态码

示例:

接口Servlet.java

package com.swust.java.practice.servlet;

import com.swust.java.practice.http.Request;
import com.swust.java.practice.http.Response;

/**
 * 服务器脚本接口
 */
public interface Servlet {

    void service(Request request, Response response);

    void doGet(Request request, Response response);

    void doPost(Request request, Response response);
}

LoginServlet.java

package com.swust.java.practice.servlet;

import com.swust.java.practice.http.Request;
import com.swust.java.practice.http.Response;

public class LoginServlet implements Servlet {

    @Override
    public void service(Request request, Response response) {
        response.print("<html>");
        response.print("<html>");
        response.print("<head>");
        response.print("<title>");
        response.print("response success (*^▽^*)");
        response.print("</title>");
        response.print("</head>");
        response.print("<body>");
        response.print("<h2>登录页面->服务器响应成功!!!</h2>"+request.getParam("name"));
        response.print("</body>");
        response.print("</html>");

    }

    @Override
    public void doGet(Request request, Response response) {

    }

    @Override
    public void doPost(Request request, Response response) {

    }
}

五、解析web.xml配置文件

目标:
1、通过servlet-mapping中的url-pattern获取servlet中的servlet-class(之后使用该类名并通过反射获取对应的servlet对象)
web.xml文件
在这里插入图片描述

//一、通过sax解析文件获取servlet对象和servlet-mapping对象并分别保存到一个列表中
//SAX解析
//1、获取解析工厂
SAXParserFactory factory = SAXParserFactory.newInstance();
//2、从解析工厂获取解析器
SAXParser parser = factory.newSAXParser();
//3、加载文档Document注册处理器
WebHandler handler = new WebHandler();
//4、编写处理器
//5、解析  使用当前类加载器获取xml资源
parser.parse(Thread.currentThread().
                getContextClassLoader().
                getResourceAsStream("com/swust/java/practice/web.xml"),
        handler);
// 获取数据
List<Entity> entities = handler.getEntities();
List<Mapping> mappings = handler.getMappings();
//二、上下文处理
WebContext webContext = new WebContext(entities,mappings);
String name = webContext.getClz(url); // 通过url获取对应类名
//三、根据反射获取Servlet对象
try {
   // 获取类对象
   Class clz = Class.forName(name);
   // 实例化对象
   com.swust.java.practice.servlet.Servlet servlet = (com.swust.java.practice.servlet.Servlet) clz.getConstructor().newInstance();
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
   e.printStackTrace();
}

六、封装分发器

目的:
通过多线程实现并发访问

package com.swust.java.practice.dispatcher;

import com.swust.java.practice.http.Request;
import com.swust.java.practice.http.Response;
import com.swust.java.practice.servlet.Servlet;
import com.swust.java.practice.web.WebApp;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 分发器
 */
public class Dispatcher implements Runnable{

    private Socket client;
    private Request request;
    private Response response;

    public Dispatcher(Socket client) {
        this.client = client;
        try {
            /**
             * 获取请求协议
             */
            this.request = new Request(client);
            /**
             * 返回响应协议
             */
            this.response = new Response(client);
        } catch (IOException e) {
            e.printStackTrace();
            this.release();
        }

    }

    @Override
    public void run() {
        try{
            if(null == request.getUrl() || request.getUrl().equals("")){// 首页
                loadResource("index");
                response.pushToBrowser(200);
                return;
            }
            Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
            if(null != servlet){// 找到了对应的servlet
                servlet.service(request,response);
                // 返回状态码
                response.pushToBrowser(200);
            }else{// 404
                loadResource("404");
                response.pushToBrowser(404);
            }
        }catch (Exception e){
            loadResource("500");
            response.pushToBrowser(500);
        }
        release();// 短链接 提高性能
    }

    // 根据页面名称加载资源
    private void loadResource(String pageName){
        try {
            // 通过类加载器查找资源
            InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("com/swust/java/practice/views/"+pageName+".html");
            byte[] bytes = new byte[1024 * 1024];
            is.read(bytes);
            response.print(new String(bytes));
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 释放资源
    private void release(){
        try {
            this.client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7、完结

Server.java
开启服务、关闭服务和接收请求协议

package com.swust.java.practice;

import com.swust.java.practice.dispatcher.Dispatcher;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 目标:
 *      1、使用ServerSocket建立与浏览器的连接, 获取请求协议
 *      2、返回响应协议 (封装响应信息)
 */
public class Server {

    private ServerSocket serverSocket;

    private boolean isRunning;

    // 开启服务
    public void start(){
        try {
            serverSocket = new ServerSocket(8888);
            isRunning = true;
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服务器启动失败...");
        }
    }

    // 接受连接并进行处理
    public void receive(){
        while (isRunning){
            try {
                // 多线程处理
                Socket client = serverSocket.accept();
                System.out.println("一个客户端建立了连接...");
                new Thread(new Dispatcher(client)).start();
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("客户端连接异常...");
                this.stop();
            }
        }
    }

    // 停止服务
    public void stop(){
        isRunning = false;
        try {
            serverSocket.close();
            System.out.println("服务器已停止");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
        server.receive();
    }

}

项目结构:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值