Java实现简单静态-动态http服务器

编写静态-动态http服务器

用例图:

image-20210811093356093

实现需求:
实现静态-动态服务器的目的是为了更深的理解HTTP协议和Servlet 底层原生的原理。

实现描述:

​ 用户通过浏览器提交一个http 请求到指定的服务器的启动端口,在HttpServer 中,Java通过Socket 封装好得到http的请求流和响应流,通过HttpPrase方法将Http请求流封装成HttpRequest 对象,然后读取指定目录中的html,css,js 文件,转换为文件流,通过响应流封装好的HttpResponse 对象将数据返回给浏览器,实现一个类似nginx 的静态服务器。

通过修改实现类,实现可以配置映射路径的动态服务器,类似于:

  • 访问/ 即可访问/index.html

项目目录:

  • -java
    • -ServerApplication
    • -handler
      • -impl
        • -DynamicHandler
        • -StaticHandler
      • -Handler
      • -HttpPrase
    • -http
    • -server
      • -HttpServer
      • -HttpWorkerThread

1、编写HttpServer类

作用:

  • -绑定端口号
  • -创建Socket 对象,接收请求响应流
  • -创建新线程,专门用于处理请求和响应
/**
 * @author HaiPeng Wang
 * @date 2021/8/5 11:22
 * @Description:
 */
public class HttpServer {


    /*端口号*/
    private int port;
    
    public HttpServer(int port) {
        this.port = port;
    }

    public void start(){
        try {
            ServerSocket serverSocket = new ServerSocket();
            /*绑定端口号*/
            serverSocket.bind(new InetSocketAddress(port));
            
            System.out.println("Server start in "+ port);
            /*通过死循环不断的接收浏览器发来的请求,并专门给他创建一个线程去处理他的请求和响应*/
            while (true){
                /*建立新连接*/
                Socket  cilentSocket = serverSocket.accept();
                cilentSocket.setSoTimeout(1000);
                System.out.println("A new connection : "+cilentSocket.getInetAddress());
                /*为每一个连接创建一个线程*/
                new HttpWorkerThread(cilentSocket).start();
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

}

2、编写HttpWorkerThread类

作用:

  • -调用HttpPrase.prase()
  • -创建HttpResponse 对象
  • -调用Handler.handle 方法处理请求和响应
/**
 * @author HaiPeng Wang
 * @date 2021/8/5 11:26
 * @Description:
 */
public class HttpWorkerThread extends Thread{

    public Socket cilentSocket;

    public HttpWorkerThread(Socket cilentSocket) {
        this.cilentSocket = cilentSocket;
    }

    @Override
    public void run() {
        /*根据Http协议解析Http请求*/
        try{
            System.out.println("线程:" + this.getId());
            /*通过Socket 获取InputStream创建转换器对象*/
            HttpParse httpParse = new HttpParse(cilentSocket.getInputStream());
            /*调用转换器方法,将InputStream 中的请求报文信息封装成HttpRequest*/
            HttpRequest httpRequest = httpParse.prase();
            /*通过Socket 获取OutputStream 封装成响应对象*/
            HttpResponse httpResponse = new HttpResponse(cilentSocket.getOutputStream());
            /*创建处理器,类似于Servlet 中的Service 方法*/
            Handler handler = new StaticHandler();
//            Handler dynamicHandler = new DynamicHandler();
            handler.handle(httpRequest,httpResponse);

        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3、编写HttpRequest类

作用:

  • -存储InputStream 中的信息
/**
 * @author HaiPeng Wang
 * @date 2021/8/5 11:34
 * @Description:
 */
public class HttpRequest {

    /*Http 输入流*/
    private InputStream inputStream;

    /*Http 请求方法*/
    private String method;

    /*Http 请求行*/
    private String line;

    /*Http 请求头,用HashMap 存储*/
    private Map<String,String> headers = new HashMap<>();

    /*Http 请求体*/
    private String body;

    /*Http 请求路径 比如说:localhost:1024/test/url 则url = “/test/url”*/
    private String url;


    public HttpRequest(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public InputStream getInputStream() {
        return inputStream;
    }

    public String getMethod() {
        return method;
    }

    public String getLine() {
        return line;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public String getHeader(String name){
        return headers.get(name);
    }

    public void addHeader(String name,String value){
        headers.put(name, value);
    }

    public void setLine(String result) {
        this.line = result;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }
}

4、编写HttpPrase类

作用:

  • -封装InputStream 返回HttpRequest 对象
    • -封装请求头
    • -封装请求行
    • -封装请求体

Http 协议:

  • 是一种超文本传输协议,基于TCP/IP协议来传输超文本到本地浏览器
  • 是一种无连接协议:每次连接只处理一个请求,服务器处理完请求,处理后做出响应,浏览器接收到响应则端开连接,这样可以节省传输时间
  • 是一种无状态协议:对事务处理没有记忆功能

Http请求报文:

image-20210811103154190

public class HttpParse {

    /*Http 输入流*/
    private InputStream is;

    /*持有HttpRequest 对象*/
    private HttpRequest httpRequest;

    /*将输入流根据/r/n 转换成字符串数组*/
    private String[] requestStr;

    /*必须通过输入流去创建*/
    public HttpParse(InputStream is) {
        this.is = is;
        httpRequest = new HttpRequest(is);
    }


    /**
     * 解析请求行
     * @return 请求行字符串
     */
    public HttpRequest prase() throws IOException {
        HttpRequest httpRequest = parseToString()
                .requestLine()
                .requestHeader()
                .requestBody();
        return httpRequest;
    }

    /**
     * 解析请求行
     * @return
     * @throws IOException
     */
    public HttpParse requestLine() throws IOException {
        String line = requestStr[0];
        String[] str1 = line.split(" ");
        httpRequest.setLine(line);  //设置完整响应行
        httpRequest.setMethod(str1[0]); //设置响应方法
        httpRequest.setUrl(str1[1]); //设置url
        return this;
    }

    /**
     * 解析请求头
     * @return 请求头Map
     */
    public HttpParse requestHeader() throws IOException {
        int i = 1;
        while (i < requestStr.length && requestStr[i].equals("") ){
            String[] strings = requestStr[i].split(":");
            if (strings[0].toLowerCase().equals("host")){
                strings[1] = strings[1] + ":" +strings[2];
            }
            httpRequest.addHeader(strings[0],strings[1]);  //添加每一个响应头到请求头中
            i++;
        }
        return this;
    }

    /**
     * 解析请求体
     * @return 请求体字符串
     */
    public HttpRequest requestBody(){
        int i = requestStr.length-1;
        String body = requestStr[i];
        httpRequest.setBody(body);
        return this.httpRequest;
    }

    /**
     * 读取请求数据流并根据/r/n转换成字符串数组
     * @return
     * @throws IOException
     */
    public HttpParse parseToString() throws IOException {

        this.httpRequest = new HttpRequest(is);
        String result = new String();
        StringBuffer stringBuffer = new StringBuffer();
        try {
            byte[] buf = new byte[128];
            int size = 0;
            while (( size = is.read(buf,0,buf.length)) != -1) {
                for (int i = 0; i < buf.length ; i ++){
                    stringBuffer.append((char) buf[i]);
                }
            }
        }catch (SocketTimeoutException e){
            /**
             * 如果浏览器没有接收到响应数据,那么是不会主动去端开连接的
             * 这样就导致InputStream 是一个无限的流
             * 但是这样在读取的时候就会造成read 方法的阻塞
             * 这时利用tSocket.setSoTimeout(1000); 设置read 等待的时间,超出时间则扔出异常
             * 这时就可以主动的结束read 阻塞跳出来然后进行继续的步骤
             * 否则浏览器就会一直卡着转圈圈
             */
        }
        this.requestStr = stringBuffer.toString().split("\r\n");
        return this;
    }
}

5、编写HttpResponse类

/**
 * @author HaiPeng Wang
 * @date 2021/8/5 15:12
 * @Description:
 */
public class HttpResponse {

    /*枚举类 存储StatusCode 和 StatusDescription*/
    private HttpStatus httpStatus;

    /*响应头信息*/
    private Map<String,String> headers = new HashMap<>();

    /*ContentType 信息,用于存储文件后缀名和*/
    private static Map<String,String> type = new HashMap<>();




    static{
        /**
         * 初始化type对象,存储后缀名和contentType 的对应关系
         */
        type.put("css","text/css;charset=utf-8");
        type.put("js","text/js;charset=utf-8");
        type.put("html","text/html;charset=utf-8");
        type.put("json","text/json;charset=utf-8");
        type.put("png","application/x-png");
    }

    {
        /**
         * 初始化headers 相当于设置默认值
         */
        headers.put("Content-Type","text/html;charset=utf-8 ");
//        headers.put("Content-Length","4096");
        /**
         * 报错net::ERR_CONTENT_LENGTH_MISMATCH
         * 我以为设计成个死的长度就行,结果发现这样也不行
         * 不行就不行吧,查看response.length 这个头字段的含义发现一般的浏览器会自动的去计算长度
         * content length 的长度是消息实体的传输长度,一个是传输长度和实体长度是不同的
         * 消息实体这里就表示响应体,单位是字节
         * 那我这里就不去计算来让他自己去计算吧
         * 
         * 如果conten length 的长度大于实际长度则会被阻塞然后报错
         * 如果小于则会被截断
         * 所以他必须是精确的,有的浏览器都有计算的功能,但是有的浏览器老一些的其实是没有计算功能的
         */
        headers.put("Connection","close");
    }

    private OutputStream os;


    public HttpResponse(OutputStream os) {
        this.os = os;
    }

    public void setHttpStatus(HttpStatus httpStatus) {
        this.httpStatus = httpStatus;
    }

    public OutputStream getOs() {
        return os;
    }

    public void addHeader(String name,String value){
        headers.put(name,value);
    }

    public String getHeader(String name){
        return headers.get(name);
    }

    public void setContentType(String value){
        headers.put("Content-Type",value);
    }
    public String getContentType(){
        return this.headers.get("Content-Type");
    }

    public void setContentLength(Integer value){
        headers.put("Content-Length",value.toString());
    }
    public String getContentLength(){
        return this.headers.get("Content-Length");
    }

    public Map<String, String> getType() {
        return type;
    }

    /**
     * 根据HttpResponse 对象生成响应字符串
     * @return
     */
    public String loadResponse(){
        String lankLine = "\r\n";
        String blank = " ";
        String result = "";
        String colon = ":";
        result += "HTTP/1.1" + blank + this.httpStatus.getCode() + blank + this.httpStatus.getMsg()+ lankLine;
        Set<String> keySet = this.headers.keySet();
        for (String key : keySet){
            result += key + colon + this.headers.get(key) + lankLine ;
        }
        result += lankLine;
        return result;
    }

    /**
     * 将字符串写回前端
     * @param str
     * @throws IOException
     */
    public void write(String str) throws IOException {
        os.write(str.getBytes(StandardCharsets.UTF_8));
        os.flush();
        os.close();
    }

    /**
     * 用字符串+字节流的形式写回前端
     * @param str
     * @param bytes
     * @throws IOException
     */
    public void write(String str,byte[] bytes) throws IOException {
        os.write(str.getBytes(StandardCharsets.UTF_8));
        os.write(bytes);
        os.flush();
        os.close();
    }

    /**
     * 将对象转换成json 写回前端
     * @param t
     * @param <T>
     * @throws IOException
     */
    public<T> void writeBody(T t) throws IOException {
        this.httpStatus = HttpStatus.SC_OK;
        this.setContentType("application/json");
        String result = loadResponse();
        String json = "{lalalala}";
        result += json;
        write(result);
    }
}

Http 响应报文和请求报文的本质就是约定好的一种形式的字符串

6、编写HttpStatus枚举类

作用:

  • -表示响应码和描述
/**
 * @author HaiPeng Wang
 * @date 2021/8/5 15:13
 * @Description:
 */
public enum HttpStatus {

    NOT_FOUNT(404,"NotFound"),
    SC_OK(200,"OK"),
    BAD_REQUEST(200,"BadRequest"),
    SERVER_ERROR(400,"ServerError");
    private int code;

    private String msg;

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    HttpStatus(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

7、编写Handler接口

作用:

  • -这里使用了灵活的策略模式,只需要更改Handler 的实现类,即可实现静态和动态的转换
/**
 * @author HaiPeng Wang
 * @date 2021/8/5 15:10
 * @Description:请求处理接口
 */
public interface Handler {


    public void handle(HttpRequest request, HttpResponse httpResponse) throws IOException;
}

8、编写StaticHandler实现类

作用:

  • -读取指定路径中的文件,并将文件传输给浏览器
  • -根据不同的文件后缀名设置不同的content-type
  • -编写error 响应
/**
 * @author HaiPeng Wang
 * @date 2021/8/5 15:11
 * @Description:静态服务器实现类
 */
public class StaticHandler implements Handler {


    /**
     * 解析文件,将文件返回前端
     * @param request
     * @param response
     */
    @Override
    public void handle(HttpRequest request, HttpResponse response) throws IOException {
        sendFile(request,response);
    }

    public void error(HttpRequest request,HttpResponse response) throws IOException {
        response.setHttpStatus(HttpStatus.NOT_FOUNT);
        String result = response.loadResponse();
        response.write(result);
    }

    public void sendFile(HttpRequest request,HttpResponse response) throws IOException {
        String staicPath = "W:\\桌面文件\\2021暑期云泽\\会议记录\\20210804-Java-Tomcat\\20210804-Java-Tomcat\\html";
        /*替换Url中的分隔符*/
        String url = request.getUrl().replace("/",File.separator);
        String path = staicPath + url;
        String[] strs = url.split("\\.");
        String contentType = response.getType().get(strs[1]);
        response.setContentType(contentType);
        File file = new File(path);
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(file);
        }catch (Exception e){
            error(request,response);
        }
        byte[] bytes = inputStream.readAllBytes();
        response.setHttpStatus(HttpStatus.SC_OK);
        response.write(response.loadResponse(),bytes);
    }
}

9、编写DynamicHandler实现类

作用:

  • -实现根据映射完成动态访问
  • -小细节,每一个Server 的mapping映射信息都应该是由每一个server 对象所共享的,这样这些映射和server 对象有相同的生命周期
/**
 * @author HaiPeng Wang
 * @date 2021/8/8 19:50
 * @Description:动态实现类
 */
public class DynamicHandler implements Handler {

    private Map<String,String> maps = new HashMap<>();
    {
        maps.put("/cgxz","/cgxz/index.html");
        maps.put("/css/base.css","/css/base.css");
        maps.put("/css/header.css","/css/header.css");
        maps.put("/css/footer.css","/css/footer.css");
        maps.put("/css/index.css","/css/index.css");
        maps.put("/images/banner.png","/images/banner.png");
        maps.put("/images/img1.png","/images/img1.png");
        maps.put("/js/index.js","/js/index.js");
    }

    @Override
    public void handle(HttpRequest request, HttpResponse response) throws IOException {
        String staicPath = "W:\\桌面文件\\2021暑期云泽\\会议记录\\20210804-Java-Tomcat\\20210804-Java-Tomcat\\html";
        String url = null;
        try {
            url = maps.get(request.getUrl()).replace("/", File.separator);
        }catch (NullPointerException e){
            error(request,response);
        }
        if (url == "" || url == null){
            error(request,response);
        }
        String path = staicPath + url;
        String[] strs = url.split("\\.");
        String contentType = response.getType().get(strs[1]);
        response.setContentType(contentType);
        File file = new File(path);
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(file);
        }catch (Exception e){
            error(request,response);
        }
        byte[] bytes = inputStream.readAllBytes();
        response.setHttpStatus(HttpStatus.SC_OK);
        response.write(response.loadResponse(),bytes);
    }

    public void addMapping(String realPath,String mapping){
        maps.put(mapping,realPath);
    }

    public void error(HttpRequest request,HttpResponse response) throws IOException {
        response.setHttpStatus(HttpStatus.NOT_FOUNT);
        String result = response.loadResponse();
        response.write(result);
    }
}

10、编写启动类

public class ServerApplication {

    public static void main(String[] args) {
        /*args 是命令行参数的字符串数组,这里可以根据命令行参数获取对应的配置*/
        if (args.length == 0){
            /*给出添加port 的形式*/
            System.out.println("Usage:Java -jar static-server.jar <port>");
        }
        int port = Integer.parseInt(args[0]);
        HttpServer server = new HttpServer(port);
        server.start();
    }
}

11、idea配置命令行参数的方法

1、

image-20210811115433959

2、在1024处进行配置image-20210811115459845

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值