【SpringMvc手写TomCat(WebServer)服务器】

使用HTTP超文本传输协议,了解浏览器与服务器的具体通信流畅,手写TomCat服务器

目录

一、HTTP协议规则

二、Request与Response

1.Request请求

1).请求行

2).消息头

3).消息正文

2.Response响应

1).状态行

2).响应头

3).响应正文

三.代码业务实现

1.启动类

2.线程类(ClientHandler)

3.HttpServletRequest解析请求类

1).解析请求行

2).解析消息头

3).解析消息正文

4.HttpServletResponse响应类

1).发送状态行

2).发送响应头

3).发送正文

5.DispatcherServlet处理类

1).要查看是否为请求业务:

2).非处理业务,那么执行下面请求静态资源的操作

6.HandlerMapping类

7.annotations自定义注解


一、HTTP协议规则

     要求浏览器与服务端之间必须遵循一问一答的规则,即:浏览器与服务端建立TCP连接后需要
先发送一个请求(问)然后服务端接收到请求并予以处理后再发送响应(答)。注意,服务端永远
不会主动给浏览器发送信息

二、Request与Response

1.Request请求

        请求是浏览器发送给服务端的内容,HTTP协议中一个请求由三部分构成:
分别是:请求行,消息头,消息正文。消息正文部分可以没有。

1).请求行

        请求行是一行字符串,以连续的两个字符(回车符和换行符)作为结束这一行的标志。
回车符(不可见字符):在ASC编码中2进制内容对应的整数是13.回车符通常用cr表示。
换行符(不可见字符):在ASC编码中2进制内容对应的整数是10.换行符通常用lf表示。

请求行组成:                                                                                                                                     请求方式(SP)抽象路径(SP)协议版本(CRLF)    注:SP是空格 

GET /myweb/index.html HTTP/1.1
GET / HTTP/1.1

URL地址格式:
协议://主机地址信息/抽象路径

http://localhost:8088/TeduStore/index.html
GET /TeduStore/index.html HTTP/1.1


2).消息头

消息头是浏览器可以给服务端发送的一些附加信息

消息头组成:

消息头由若干行组成,每行结束也是以CRLF标志。
每个消息头的格式为:消息头的名字(:SP)消息的值(CRLF)
消息头部分结束是以单独的(CRLF)标志。

Host: localhost:8088(CRLF)
Connection: keep-alive(CRLF)
Upgrade-Insecure-Requests: 1(CRLF)


3).消息正文

消息正文是2进制数据,通常是用户上传的信息,比如:在页面输入的注册信息,上传的
附件等内容。

GET /index.html HTTP/1.1
Host: localhost:8088
Connection: keep-alive
Upgrade-Insecure-Requests: 1


2.Response响应

响应是服务端发送给客户端的内容。一个响应包含三部分:状态行,响应头,响应正文


1).状态行

状态行是一行字符串(CRLF结尾),并且状态行由三部分组成,格式为:
protocol(SP)statusCode(SP)statusReason(CRLF)
协议版本(SP)状态代码(SP)状态描述(CRLF)

HTTP/1.1 200 OK

状态代码是一个3位数字,分为5类:
1xx:保留
2xx:成功,表示处理成功,并正常响应
3xx:重定向,表示处理成功,但是需要浏览器进一步请求
4xx:客户端错误,表示客户端请求错误导致服务端无法处理
5xx:服务端错误,表示服务端处理请求过程出现了错误


2).响应头

响应头与请求中的消息头格式一致,表示的是服务端发送给客户端的附加信息。


3).响应正文

Content-Type是用来告知浏览器响应正文中的内容是什么类型的数据(图片,页面等等)

Content-Length是用来告知浏览器响应正文的长度,单位是字节。

HTTP/1.1 404 NotFound(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......


三.代码业务实现

本项目的包结构

 

Controller接口:               实现SpringMvc中的@Controller注解

RequestMapping接口:    实现SpringMvc中@RequestMapping注解

controller包:                    测试程序的业务包

ClientHandler:                 多线程分配处理每一个连接信息

DispatcherServlet:           SpringMvc的核心类,处理请求的环节

HandlerMapping:             反射机制扫描请求路径对应的方法(相应的注解的参数)

WebServerApplication:    maven项目的启动类

User:                                测试的用户实体类

EmptyRequestException:处理空请求的异常类

HttpServletRequest:        处理请求,解析请求参数

HttpServletResponse:      发行响应的类

static包:                             测试使用的html静态文件


1.启动类

目的:在maven环境下的启动类上获取网络连接,创建线程处理连接

在构造方法上,项目启动时创建serverSocket ,创建线程连接池

public WebServerApplication(){
         try {
            System.out.println("正在启动服务端...");
            System.out.println("\n" +
                    "  .   ____          _            __ _ _\n" +
                    " /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n" +
                    "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n" +
                    " \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n" +
                    "  '  |____| .__|_| |_|_| |_\\__, | / / / /\n" +
                    " =========|_|==============|___/=/_/_/_/\n" +
                    " :: Spring Boot ::                (v2.7.2)\n" +
                    "\n");
            serverSocket = new ServerSocket(8088);
            threadPool = Executors.newFixedThreadPool(50);  //创建一个大小为50的线程池
            System.out.println("服务器启动完毕...");
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }

 通过start方法获取浏览器的连接,并调用封装的ClientHandler分配当前连接的线程

public void start(){
        try {
            while (true){
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接了...");
                //启动一个线程处理该客户的交互
                ClientHandler handler = new ClientHandler(socket);
                threadPool.execute(handler);    //执行线程体
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }

2.线程类(ClientHandler)

与客户端完成一次HTTP的交互
按照HTTP协议要求,与客户端完成一次交互流程为一问一答
因此,这里分为三步完成该工作:
1:解析请求  目的:将浏览器发送的请求内容读取并整理
2:处理请求  目的:根据浏览器的请求进行对应的处理工作
3:发送响应  目的:将服务端的处理结果回馈给浏览器

public void run() {
        try {
            //1解析请求,实例化请求对象的过程就是解析的过程
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

            //2处理请求
            DispatcherServlet.getService().service(request,response);

            //3发送响应
            response.response();
           
        } catch (IOException e) {
            e.printStackTrace();
        } catch (EmptyRequestException e) {
            //不需要进行处理,报错的代码下面的代码都不会执行
        } finally {
            try {
                //按照HTTP协议要求,一问一答后断开连接
                socket.close();
            } catch (IOException exception) {
                exception.printStackTrace();
            }
        }
    }

3.HttpServletRequest解析请求类

请求包括三部分:

private String method;  //请求方式
private String uri;     //抽象路径
private String protocol;//协议版本

在构造方法中调用处理这三部分的方法:

public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1解析请求行
        parseRequestLine();
        //2解析消息头
        parseHeaders();
        //3解析消息正文
        parseContent();
    }

1).解析请求行

思路:   1.每次读取一行请求的数据(以回车加换行结尾CRLF)

           2.判断读到的数据是否为空 --> 是-->抛出空请求异常

           3.GET /index.html HTTP/1.1                                                                                                                   将读到的字符按照空格(SP)拆分 请求方式method . 抽象路径uri . 协议版本protect

           4.抽象路径部分 : /index.html  静态页面

                                      /reg?name=XXX&password=XXX  业务

            5.url是业务 按照 ? 拆分-->?前半部分为请求业务名requestURI , ?后半部分为请求业务的参数queryString           参数部分按照&拆分 保存在parameters的Map集合中

自定义方法:   每次读取一行数据(每一行都是以回车加换行结尾)

每次读取一行的方法readLine
InputStream in = socket.getInputStream();
int d;
StringBuilder builder = new StringBuilder();
char pre = 'a',cur = 'a';//pre记录上次读到的字符 cur记录本次读取到的字符
while ((d=in.read())!=-1){//read()返回值是读到的一个字节的二进制低八位
    //readline()方法会块读8000个char[],会把消息正文也读进去
    cur = (char) d;
    if (pre==13&&cur==10){//判断是否连续读到回车+换行
        break;
    }
    builder.append(cur);
    pre = cur;
}
return builder.toString().trim();

1>. 解析请求的数据

        得到请求方式 , 抽象路径 , 协议版本

private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();
        if (line.isEmpty()){//若请求行是空字符串,则说明本次是空请求
            throw new EmptyRequestException("request is empty");
        }
        String[] data = line.split("\\s");  
        method = data[0];
        uri = data[1];
        protocol = data[2];
        //进一步解析URI
        parseURI();
    }

2>. 解析URI(parseURI方法)

        判断是业务还是具体的静态页面

String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){//有参数
            //queryString:username=fancq&password=&nickname=chuanqi&age=22
            queryString = data[1];
            //paras:[username=fancq, password=, nickname=chuanqi, age=22]
            parseParameter(queryString);

3>. 解析URI中的参数部分

        分离参数名和值

private void parseParameter(String line){
        //浏览器将汉字通过utf-8转化为2进制-->16进制,支持ISO_8859_1编码从而支持请求行
        //获取拆分参数是在通过utf-8将16进制转换为汉字
        try {
            line = URLDecoder.decode(line,"UTF-8");
            //URLDecoder是JAVA提供的一个API,可以将URL地址中%XX的内容进行解码,汉字在
            //请求行中以十六进制的方式显示
            //"/loginUser?username=%E8%B4%BA%E5%9D%A4&password=123 HTTP/1.1"
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String[] parses = line.split("&");//parse解析
        for(String parse : parses){
            //array:[username,fancq]   若没参数值array:[password]
            String[] array = parse.split("=");
            parameters.put(array[0],array.length>1?array[1]:"");
        }
    }

2).解析消息头

思路:  1.消息头由若干行组成,每行结束也是以CRLF标志。
             每个消息头的格式为:消息头的名字(:SP)消息的值(CRLF)
             消息头部分结束是以单独的(CRLF)标志。

           2.把每次读到的数据按照 ''冒号空格(':\\s')''拆分 并放到headers的Map集合中

           3.循环结束的条件是:读到的数据书否为空串  

例如: Accept-Language: zh-CN,zh;q=0.9(CRLF)(CRLF)

private void parseHeaders() throws IOException{
        String line;
        while (!(line= readLine()).isEmpty()){
            System.out.println("消息头:"+line);
            String[] data = line.split(":\\s");
            headers.put(data[0],data[1]);
        }
    }

3).解析消息正文

思路:  1. 判断请求类型 : get请求 参数在请求路径url中

                                   post请求业务的参数在 正文部分

          2. post请求需要从消息头map集合中获取 正文长度 Content-Length , 再次读取此长度的正文数据得到参数 

          3.从消息头map集合中获取正文类型 Content-Type 判断数据提交的类型

private void parseContent() throws IOException{
        //判断请求方式是否为POST请求
        if("post".equalsIgnoreCase(method)){
            int contentLength = 0;
            if (headers.containsKey("Content-Length")){
                contentLength = Integer.parseInt(headers.get("Content-Length"));
                System.out.println("正文长度:"+contentLength);
            }
            //读取正文数据
            InputStream in = socket.getInputStream();
            byte[] data = new byte[contentLength];
            in.read(data);
            /*
            * 根据Content-Type来分析正文是什么以便进行对应的处理
            * */
            String contentType = headers.get("Content-Type");
            if ("application/x-www-form-urlencoded".equals(contentType)){//是否为form表单
                String line = new String(data,StandardCharsets.ISO_8859_1);
                parseParameter(line);
            }
        }

4.HttpServletResponse响应类

响应包括三部分

状态行 :  协议版本 + 状态码statusCode + 状态描述statusReason

响应头

响应正文

在处理类中会调用相应的set/get方法,为各个变量赋值

自定义方法:   println自定义响应方法(每次相应一条数据)

 private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);//输出流按照字节进行写出
        out.write(data);
        out.write(13);
        out.write(10);
    }

自定义方法:   发送响应前判断是否响应动态数据还是静态页面

//动态数据可以通过该流写出到其内部维护的字节数组中,发送响应是将该数组内容作为正文
private ByteArrayOutputStream out;

private void sendBefore(){
        if (out!=null){//说明有动态数据
            addHeader("Content-Length",out.size()+"");
        }
    }

1).发送状态行

 在处理类中 设置状态行的相关参数发送

private void sendStatusLine() throws IOException{
        //HTTP/1.1 200 OK
        println("HTTP/1.1"+" "+statusCode+" "+statusReason);
    }

2).发送响应头

思路: 从响应头的map集合中获取相关的key-value值

private void sendHeaders() throws IOException{
        Set<Map.Entry<String,String>> entrySet = headers.entrySet();
        for (Map.Entry<String, String> e : entrySet) {//每个Set集合的元素都是一个entry对象
            String name = e.getKey();
            String value = e.getValue();
            println(name+": "+value);
        }
        //单独发送个回车+换行表示响应头发送完毕
        println("");
    }

        在setContentFile方法中(在处理类调用并赋值)

public void setContentFile(File contentFile) {
    this.contentFile = contentFile;
    //添加用于说明正文的响应头Content-Type和Content-Length;
    try {
        String contentType = Files.probeContentType(contentFile.toPath());
        if (contentType!=null){
            addHeader("Content-Type",contentType);
        }//如果不发送contentType头,浏览器会自己去猜,能发尽量发
    } catch (IOException exception) {
        exception.printStackTrace();
    }
    addHeader("Content-Length",contentFile.length()+"");
}

3).发送正文

思路:  1. 在处理类中如果要发送的是动态数据(动态的html页面) 则从 ByteArrayOutputStream数组中获取数据

          2.如果是静态页面, 使用流块读数据响应

private void sendContent() throws IOException{
        OutputStream out = socket.getOutputStream();
        if (this.out!=null){
            byte[] data = this.out.toByteArray();
            out.write(data);//将动态数据作为正文发送给浏览器
        }else if (contentFile!=null){//静态文件
            FileInputStream fis = new FileInputStream(contentFile);
            byte[] buf = new byte[1024*10];//10kb
            int len = 0;//记录每次实际读取的字节数
            while ((len = fis.read(buf)) != -1){
                out.write(buf,0,len);
            }
        }
        //如果为空,响应也可以没有页面
    }


5.DispatcherServlet处理类

用于完成一个http交互流程中处理请求的环节工作

实际上这个类是Spring MVC 框架提供的一个核心类,用于和Web容器(Tomcat)整合,
使得处理请求的环节可以有Spring MVC框架完成

处理类只能有一个, 不能多个同时多个处理对象,因此设置成单例模式

private DispatcherServlet(){}
    private static DispatcherServlet service = new DispatcherServlet();
    public static DispatcherServlet getService(){
        return service;
    }

1).要查看是否为请求业务:

    根据注解的参数调用HandlerMapping中的方法

 Method method = HandlerMapping.getMethod(path);//传入抽象路径RequestUri 获取方法
            if (method!=null){//说明是一个业务
                /**
                 * //通过方法对象可以获取其所属类的类对象
                 * Class cls = method.getDeclaringClass();
                 * Object obj = cls.newInstance();
                 */
                method.invoke(method.getDeclaringClass().newInstance(),
                        request,response);//invoke方法需要传入当前方法的类对象和参数名
                return;
            }

2).非处理业务,那么执行下面请求静态资源的操作

        不是页面-->发送静态404页面

File file = new File(staticDir,path);
        if (file.isFile()){ //浏览器请求的资源是否存在且是一个文件
            //正确响应 "HTTP/1.1 200 OK";
            response.setContentFile(file);
        }else { //不是页面就404
            //响应 "HTTP/1.1 404 NotFound";
            response.setStatusCode(404);
            response.setStatusReason("NotFound");
            file = new File(staticDir,"/root/404.html");
            response.setContentFile(file);
        }

6.HandlerMapping类

反射机制 ---维护请求路径对应的业务处理方法

获取/com/webserver/controller包下的所有.class文件

获取被@Controller修饰的类

获取被@RequestMapping修饰的方法

将参和方法名放在map中

File dir = new File(
                    HandlerMapping.class.getClassLoader().getResource(".").toURI()
            );
            File controllerDir = new File(dir,"/com/webserver/controller");
            File[] subs = controllerDir.listFiles(f->f.getName().endsWith(".class"));
            for (File sub : subs) {
                String name = sub.getName().substring(0,sub.getName().lastIndexOf("."));
                Class cls = Class.forName("com.webserver.controller."+name);
                if (cls.isAnnotationPresent(Controller.class)){
                    Method[] methods = cls.getMethods();
                    for (Method method : methods) {
                        if (method.isAnnotationPresent(RequestMapping.class)){
                            RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                            String value = requestMapping.value();
                            //value:    注解的参数/regUser
                            //mmethod:  注解对应的方法reg()
                            mapping.put(value,method);
                        }
                    }
                }
            }

在DispatcherServlet类中 调用method.invoke(类对象,参数名)执行方法体


7.annotations自定义注解

通过注解,获取controller各个业务的参数 , 从而可以调用指定的方法执行业务

Controller类-->扫描启动类下的特定类

@Target(ElementType.TYPE)//修饰类
@Retention(RetentionPolicy.RUNTIME)//保留级别
public @interface Controller {
}

RequestMapping类-->扫描指定的方法

@Target(ElementType.METHOD)//修饰方法
@Retention(RetentionPolicy.RUNTIME)//保留级别
public @interface RequestMapping {
    String value();//定义参数
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hemuer~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值