手写WebServer(简易版SpringBoot)

系列文章目录

请求分析HttpServletRequest

请求业务处理DispatcherServlet

        ---注解机制的引入

        ---实体创建

        ---实体操控类Controller

        ---利用反射机制建立请求方法与请求路径的对应关系HandlerMapping

        ---使用properties文件创建后缀与MIME类型的对应mimeMapping

响应发送HttpServletResponse

html页面的编写


前言

Socket的学习最终往往就是编写一个HTTP服务器了,下面我就分几个系列将手写WebServer的各项工作写出来


一、WebServer的启动——WebServerApplication

public class WebServerApplication {
    private ServerSocket serverSocket;
    private ExecutorService threadPool;

    public WebServerApplication(){
        try{
            System.out.println("正在启动服务器..");
            serverSocket=new ServerSocket(8088);
            threadPool=Executors.newFixedThreadPool(2);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while(true){
                System.out.println("等待客户端连接..");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端已连接!");
                //启动一个线程负责与该客户端进行交互
                ClientHandler handler = new ClientHandler(socket);
                threadPool.execute(handler);//execute:处决,执行
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

二、处理每一个客户端的线程——ClientHandler

public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){//构造器
        this.socket=socket;
    }
    @Override
    public void run() {
        try {
            //1、解析请求
            HttpServletRequest request=new HttpServletRequest(socket);
            HttpServletResponse response=new HttpServletResponse(socket);

            //2、处理请求
            DispatcherServlet servlet=new DispatcherServlet();
            servlet.service(request,response);//往response里边装东西

            //3发送响应
            response.response();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (EmptyRequestException e) {
            //什么也不做,等着finally里边关闭socket
        } finally {
            //http一次交互后断开连接
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

三、请求的解析HTTPServletRequest

        明确请求发送过来是包含三部分的:请求行,消息头,请求正文,我们的HttpServletRequest就是要对其进行分析,将对应的信息提取出来存起来,便于后续处理
        其中,请求头有三部分组成,请求方法 (SP)uri(SP) 协议,我们在方法中定义三个变量,分别用于存放这三个值
        再然后,当使用get方法请求时,uri中会有参数部分,我们需要对uri进一步处理,将参数部分的每一组值存入Map里,供后续使用
        再有就是正文,当 请求使用POST方法发起时,参数部分是位于请求正文的,因此我们每次解析请求的时候也需要解析正文,判断有无正文存在就是看消息头中有无“Content-Length”这个Key,有就可以进行后续处理,注意对参数部分的解析抽成方法,可以对位于uri和正文的消息都可以进行解析,代码复用

    本次的技术细节主要有,HTTP协议在传输过程中使用的字符集是OSI 8859-1,是不支持中文的,当接收传过来的参数信息时有两种:
        一种是位于uri的,另一种是位于正文的,
            位于uri的参数传过来本身就是解析好的OSI 8859 -1编码,可以直接解析参数,注意在转换参数时先按UTF-8解码(这是抽出的解析参数方法应具有的一段代码)
            对于正文中的参数,我们是以字节的形式读取的(此时数据是010101...),
            首先要按OIS 8859-1转码为欧洲字符集(汉字的表达是%E6%9D%8E%E9%91%AB%E6%B3%A2)
            之后在进行参数解码(方法中第一步就是再按UTF-8)解码
        解码完毕后同一装入Map等待后续处理

public class HttpServletRequest {
    //请求行的相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议名称

    private String requestURI;//存URI中"?"左侧的请求部分
    private String queryString;//存uri中"?"右边的参数部分

    //存放请求和参数部分的值
    Map<String,String> parameters=new HashMap<>();
    //消息头相关信息
    private Map<String,String> headers=new HashMap<>();

    private Socket socket;
    /**
     * 实例化请求对象的过程也是解析的过程
     */
    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket=socket;


        //1、1解析请求行
        parseRequestLine();
        //1.2解析消息头
        parseHeaders();
        //1.3解析消息正文
        parseContent();
    }

    /*解析请求行*/
    private void parseRequestLine() throws IOException, EmptyRequestException {
        System.out.println("开始证明!!!");

        String line =readLine();
        //如果请求的是一个空串说明本次是空请求
        if(line.isEmpty()){
            throw new EmptyRequestException();
        }
        System.out.println(line);

        //开始拆分
        String[] temp=line.split("\\s");
        method=temp[0];
        uri=temp[1];
        protocol=temp[2];

        parseUri();//进一步解析uri

        System.out.println("\tmethod:"+method);
        System.out.println("\turi:"+uri);
        System.out.println("\tprotocol:"+protocol);
    }
    /** 进一步解析uri*/
    private void parseUri(){
        /*
            uri是有两种情况的:含参数的和不含有参数的
         */

        String[] t1=uri.split("\\?");
        requestURI=t1[0];
        if(t1.length>1){//处理右边为空,当右边为空时split分割赋值的数组长度就是1
            queryString=t1[1];

            parseParameters(queryString);
        }

        System.out.println("\trequestURI:"+requestURI);
        System.out.println("\tqueryString:"+queryString);
        System.out.println("\tparameters:"+parameters);
    }

    /**
     * 解析参数
     * @param line 格式应当为:name1=value1&name2=value2&...
     */
    private void parseParameters(String line){
        //先进行转码工作
        try {
            line= URLDecoder.decode(line,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String[] t1=line.split("&");//重用t1数组
        for(String data:t1){
            String[] t3=data.split("=");
            parameters.put(t3[0],t3.length>1?t3[1]:null);//判断属性值是否未赋值,
            // 未赋值进行“=”分割得到的结果就是分割数组长度为1
        }
    }

    /*解析消息头*/
    private void parseHeaders() throws IOException {
        while (true){
            String line=readLine();
            if(line.isEmpty()){
                break;
            }
            System.out.println("\t\t消息头:"+line);
            String[] tmp=line.split(":\\s");
            headers.put(tmp[0],tmp[1]);
        }
        System.out.println("消息头集合header:"+headers);
    }
    /*解析消息正文*/
    private void parseContent() throws IOException {
        System.out.println("开始解析消息正文...");
        //通过判断请求中的消息头是否包含Content-Length来判定是否包含有正文
        if(headers.containsKey("Content-Length")){
            int contentLength=Integer.parseInt(headers.get("Content-Length"));
            System.out.println("正文长度:"+contentLength);
            //基于消息头告知的长度创建一个字节数组,用于保存正文内容
            byte[] contentData=new byte[contentLength];
            //读取正文所有字节存入contentData数组中
            InputStream in=socket.getInputStream();
            in.read(contentData);
            //获取消息头
            String contentType=headers.get("Content-Type");
            if("application/x-www-form-urlencoded".equals(contentType)){
                String line =new String(contentData, StandardCharsets.ISO_8859_1);
                System.out.println("正文内容:"+line);
                parseParameters(line);
            }
        }
    }

    /** 重写readLine方法*/
    private String readLine() throws IOException {//外调的方法采用抛出异常,要提醒外面有异常
        InputStream in =socket.getInputStream();
        StringBuilder builder=new StringBuilder();
        int d;
        char pre='a',cur='a';
        
        while ((d=in.read())!=-1){
            cur=(char)d;
            if(pre==CR&&cur==LF){
                break;
            }
            builder.append(cur);
            pre=cur;
        }
        return builder.toString().trim();
    }

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    /**
     * 不能直接把Map返回回去,危险
     * 这里根据key返回value
     * @param name
     * @return
     */
    public String getHeader(String name){
        return headers.get(name);
    }

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    /**
     * 根据给定的参数名获取对应的参数值
     * @param name
     * @return
     */
    public String getParameter(String name){
        return parameters.get(name);
    }
}


总结

本次就只分析解析请求的部分,业务处理以及响应发送下期更新

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值