从零开始手写Web-Server

从零开始手写Web-Server

  • 简介
  • 1 反射
    • 1.1 什么是反射
    • 1.2 获得Class的三种方式
    • 1.3 用Class来创建对象
    • 1.4 示例代码(顺便理解下利用接口解耦)
  • 2 XML
    • 2.1 什么是XML
    • 2.2 解析XML(SAX方式)
    • 2.3 解析webXML
      • 2.3.1 Entity.java
      • 2.3.2 Mapping.java
      • 2.3.4 Servlet.java 一个服务接口
      • 2.3.5 RegisterServlet.java 注册服务类
      • 2.3.6 LoginServlet.java 登录服务类
      • 2.3.7 WebContext.java URL到类路径的映射器
      • 2.3.8 解析器代码
      • 2.3.9 运行结果
  • 3 HTML
    • 3.1什么是HTML
    • 3.2 HTML结构
      • 3.2.1 固定结构
      • 3.2.2 常用标签
      • 3.2.3 尝试提交表单
  • 4 HTTP协议
    • 4.1 简介
    • 4.2 HTTP请求报文
    • 4.3 相应协议
  • 5 手写服务器
    • 5.1 获取请求协议
    • 5.2 返回响应协议
    • 5.3 封装响应信息
    • 5.4 封装请求协议
    • 5.5 处理中文
    • 5.6 构建请求参数的KV映射
  • 6 引入Servlet
  • 7 整合webxml
  • 8 高效分发器
  • 9 经典404及首页处理(完结)

简介

要实现手写Web-Server,需要9大知识储备

  1. OOP面向对象
  2. 容器(Collection、Map)
  3. IO
  4. 多线程
  5. 网络编程
  6. 反射
  7. XML解析
  8. HTML
  9. HTTP协议

由于前五个为Java基础知识,因此,我们直接从第六项开始学习,如果你已经掌握了这些知识,请直接跳过,去看后面的实现部份。

1 反射

1.1 什么是反射

把Java类中的各种结构方法(类名、属性、构造器、方法)映射成一个个Java对象。反射技术可以对一个类进行解剖,反射是框架设计的灵魂。
假如我们有一个Person类

class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

然后我们想创建Person类的对象,传统方法是这样:

Person person = new Person("不识不知" , 18);

然而反射则需要先拿到这个类的信息,然后再创建对象

1.2 获得Class的三种方式

拿到这个类的信息,也就是拿到这个Class,共有三种方法:

//1、通过Object类的getClass()方法:(需要先实例化一个对象)
Class clz = person.getClass();
//2、通过对象实例方法获取对象:(需要先实例化一个对象)
Class clz = person.class;
//3、类的全路径:(不需要实例对象)
Class clz = Class.forName("包名.类名");

我们推荐的反射方法为第三种:Class.forName("完整路径");

1.3 用Class来创建对象

有空构造函数的类:直接用字节码文件获取实例

Person person = (Person)clz.newInstance();//会调用空参构造器(如果没有则会报错)
Person person = (Person)clz.getConstructor().newInstance();//更推荐

含参构造函数的类:先获取构造器,再通过该构造器获取实例

//1、获取构造器
//这里的参数要注意与Person类构造函数的参数相对应
Constroctor const = clz.getConstructor(String.class,int.class);
//2、通过构造器对象的newInsttance方法进行对象的初始化
//对应位置填入参数
Object obj = const.newInstance("不识不知",18);
Person person = (Person)obj;

总结一下:

//含参构造器
Person person = (Person)clz.getConstructor(String.class,int.class).newInstance("不识不知",18);
//无参构造器
Person person = (Person)clz.getConstructor().newInstance();//更推荐

1.4 示例代码(顺便理解下利用接口解耦)

package Reflaction;

import java.lang.reflect.InvocationTargetException;

public class TestReflection {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, InvocationTargetException {

        Class clz = Class.forName("Reflaction.Boy");
        Person person = (Person) clz.getConstructor(String.class).newInstance("不识");
        person.Go();

        Class clz2 = Class.forName("Reflaction.Girl");
        Person person2 = (Person) clz2.getConstructor(String.class).newInstance("不知");
        person2.Go();
    }
}

interface Person {
    void Go();
}

class Boy implements Person {

    String name;

    public Boy(String name) {
        this.name = name;
    }

    @Override
    public void Go() {
        System.out.println("I am Boy!" + "My name is " + name + " .");
    }
}

class Girl implements Person {
    String name;

    public Girl(String name) {
        this.name = name;
    }

    @Override
    public void Go() {
        System.out.println("I am Girl!" + "My name is " + name + " .");
    }
}

运行结果

I am Boy!My name is 不识 .
I am Girl!My name is 不知 .

Process finished with exit code 0

Class.forName()的参数可以是字符串!这样就为我们设计框架提供了方便。

2 XML

2.1 什么是XML

XML是一种通用的数据交换格式,它的平台无关性、语言无关性、系统无关性、给数据集成与交互带来了极大的方便。XML在不同的语言环境中解析方式都是一样的,只不过实现的语法不同而已。
例如这样一个xml文件:test.xml


<persons>
    <person>
        <name>至尊宝name>
        <age>9000age>
    person>
    <person>
        <name>白晶晶name>
        <age>7000age>
    person>
persons>

解释一下,XML文件就像一棵树,我画了一张图:
从零开始手写Web-Server_第1张图片
这张图的内容就是上面XML文件所包含的内容。

2.2 解析XML(SAX方式)

SAX解析是一种自上而下的流解析,整个文件从顶行向下走一遍,即完成解析任务。

解析步骤很简单,可分为以下四个步骤

  1. 获取解析工厂
  2. 从解析工厂获取解析器
  3. 编写处理器PersonHandler和解析类Person
  4. 加载处理器
  5. 开始解析
  6. 拿到数据
  7. 按照需求处理数据

为了便于理解以上步骤,我编写了下面的程序,整个步骤可以在main函数中体现,并且我配上了中文注释。解析的文件是XML包下的test.xml,内容为上面的至尊宝白晶晶,请配合使用。

package XML;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class TestXML {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        //SAX解析 以后以下代码可以直接复制,不需要重复敲打。
        //1、获取解析工厂
        SAXParserFactory factory = SAXParserFactory.newInstance();
        //2、从解析工厂获取解析器
        SAXParser parser = factory.newSAXParser();
        //3、编写处理器PersonHandler和解析类Person
        //4、加载处理器
        PersonHandler handler = new PersonHandler();
        //5、开始解析
        parser.parse(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
                        .getResourceAsStream("XML/test.xml"))
                , handler);
        //6、拿到数据
        List<Person> persons = handler.getPersons();
        //7、查看数据
        for (Person p : persons) {
            System.out.println(p.getName() + "--->" + p.getAge());
        }
    }
}

class PersonHandler extends DefaultHandler {
    private List<Person> persons;//用于保存结果 可在startDocument()处初始化
    private Person person;//用于每次解析
    private String tag;//存储操作的标签

    public List<Person> getPersons() {
        return persons;
    }

    /**
     * 仅在解析文档开始时被执行一次
     *
     * @throws SAXException
     */
    @Override
    public void startDocument() throws SAXException {
        System.out.println("---解析文档开始---");
        //初始化容器
        persons = new ArrayList<>();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        System.out.println(qName + "--->解析开始");
        if (null != qName) {
            tag = qName;//存储当前的标签
            if (tag.equals("person")) {//当开始解析一个person时创建一个Person对象
                person = new Person();
            }
        }
    }

    /**
     * 每次解析的内容模块
     *
     * @param ch
     * @param start
     * @param length
     * @throws SAXException
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contents = new String(ch, start, length).trim();
        if (null != tag) {//处理tag为空的问题
            if (tag.equals("name")) {
                person.setName(contents);
            } else if (tag.equals("age")) {
                if (contents.length() > 0) {
                    person.setAge(Integer.valueOf(contents));
                }
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.println(qName + "--->解析结束");
        tag = null;//解析完毕丢弃标签
        if (qName.equals("person")) {//当结束解析一个person时将person加入到容器中
            persons.add(person);
        }
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
        System.out.println("---解析文档结束---");
    }
}

/**
 * 与XML文件内容格式相对应的Person解析类
 */
class Person {
    String name;
    int age;

    public Person() {
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

运行结果:

---解析文档开始---
persons--->解析开始
person--->解析开始
name--->解析开始
name--->解析结束
age--->解析开始
age--->解析结束
person--->解析结束
person--->解析开始
name--->解析开始
name--->解析结束
age--->解析开始
age--->解析结束
person--->解析结束
persons--->解析结束
---解析文档结束---
至尊宝9000
白晶晶7000

Process finished with exit code 0

2.3 解析webXML

现在考虑这样一个xml文件:test2.xml


<web-app>
    <servlet>
        <servlet-name>loginservlet-name>
        <servlet-class>XML.WEBXML.MyClasses.LoginServletservlet-class>
    servlet>
    <servlet>
        <servlet-name>regservlet-name>
        <servlet-class>XML.WEBXML.MyClasses.RegisterServletservlet-class>
    servlet>
    <servlet-mapping>
        <servlet-name>loginservlet-name>
        <url-pattern>/loginurl-pattern>
        <url-pattern>/gurl-pattern>
    servlet-mapping>
    <servlet-mapping>
        <servlet-name>regservlet-name>
        <url-pattern>/regurl-pattern>
    servlet-mapping>
web-app>

与之前不同的是:

  • 原先只有Person,而现在出现了servlet和servlet-mapping,所以我们需要建立两个解析类Entity和Mapping。
  • 原先每一个属性只出现一次,而url-pattern允许有多个值,所以我们需要用容器,由于这里取值不会重复,所以我们选择Map。

2.3.1 Entity.java

servlet块所对应的解析类类

package XML.WEBXML;

/**
 * 对应的XML代码块为
 * 
 * login
 * com.shsxt.LoginServlet
 * 
 */
public class Entity {
    //类名
    private String name;
    //类路径
    private String clz;

    public Entity() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClz() {
        return clz;
    }

    public void setClz(String clz) {
        this.clz = clz;
    }
}

2.3.2 Mapping.java

servlet-mapping块的解析类

package XML.WEBXML;

import java.util.HashSet;
import java.util.Set;

/**
 * 对应的XML代码块为
 * 
 * login
 * /login
 * /g
 * 
 */
public class Mapping {
    //URL
    private Set<String> patterns;
    //类名
    private String name;


    public Mapping() {
        patterns = new HashSet<>();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<String> getPatterns() {
        return patterns;
    }

    public void addPattern(String pattern) {
        this.patterns.add(pattern);
    }
}

2.3.4 Servlet.java 一个服务接口

LoginServlet和RegisterServlet都要实现本接口

package XML.WEBXML.MyClasses;

/**
 * 配合反射使用的接口
 */
public interface Servlet {
    void service();
}

2.3.5 RegisterServlet.java 注册服务类

package XML.WEBXML.MyClasses;

public class RegisterServlet implements Servlet {
    @Override
    public void service() {
        System.out.println("RegisterServlet");
    }
}

2.3.6 LoginServlet.java 登录服务类

package XML.WEBXML.MyClasses;

public class LoginServlet implements Servlet {
    @Override
    public void service() {
        System.out.println("LoginServlet");
    }
}

2.3.7 WebContext.java URL到类路径的映射器

WebContext是一个映射器,完成一个URL到类路径的映射。

package XML.WEBXML;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 映射器
 */
public class WebContext {
    private List<Mapping> mappings = null;
    private List<Entity> entities = null;

    //key-->servlet-name value--->servlet-class
    private Map<String, String> mappingsMap = new HashMap<>();
    //key-->url-pattern value--->servlet-name
    private Map<String, String> entitiesMap = new HashMap<>();


    public WebContext(List<Mapping> mappings, List<Entity> entities) {
        this.mappings = mappings;
        this.entities = entities;
        build();
    }

    /**
     * 构建映射
     */
    private void build() {
        for (Entity e :
                entities) {
            entitiesMap.put(e.getName(), e.getClz());
        }
        for (Mapping m :
                mappings) {
            for (String s :
                    m.getPatterns()) {
                mappingsMap.put(s, m.getName());
            }
        }
    }

    /**
     * 通过URL找类路径
     *
     * @param pattern URL
     * @return 类路径
     */
    public String getClz(String pattern) {
        String name = mappingsMap.get(pattern);
        String Clz = entitiesMap.get(name);
        return Clz;
    }
}

2.3.8 解析器代码

这里的解析器和上一节的类似,来看看我们做了何种修改。

package XML.WEBXML;

import XML.WEBXML.MyClasses.Servlet;
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.ArrayList;
import java.util.List;
import java.util.Objects;

public class TestServlet {
    public static void main(String[] args) throws Exception {
        //SAX解析 以后以下代码可以直接复制,不需要重复敲打。
        //1、获取解析工厂
        SAXParserFactory factory = SAXParserFactory.newInstance();
        //2、从解析工厂获取解析器
        SAXParser parser = factory.newSAXParser();
        //3、编写处理器PersonHandler和解析类Person
        //4、加载处理器
        WebHandler handler = new WebHandler();
        //5、开始解析
        parser.parse(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
                        .getResourceAsStream("XML/WEBXML/test2.xml"))
                , handler);
        //URL--->类路径的映射器
        WebContext webContext = new WebContext(handler.getMappings(), handler.getEntities());
        //假设你输入了/reg
        String name = webContext.getClz("/reg");
        //开始反射
        Class clz = Class.forName(name);
        Servlet servlet = (Servlet) clz.getConstructor().newInstance();
        servlet.service();
    }
}

class WebHandler extends DefaultHandler {
    private List<Mapping> mappings;//用于保存结果 可在startDocument()处初始化
    private List<Entity> entities;//用于保存结果 可在startDocument()处初始化
    private Mapping mapping;//用于每次解析
    private Entity entity;//用于每次解析

    private String father_tag;//存储标签名
    private String tag;//存储属性标签


    public List<Mapping> getMappings() {
        return mappings;
    }

    public List<Entity> getEntities() {
        return entities;
    }

    /**
     * 仅在解析文档开始时被执行一次
     *
     * @throws SAXException
     */
    @Override
    public void startDocument() throws SAXException {
        System.out.println("---解析文档开始---");
        //初始化容器
        mappings = new ArrayList<>();
        entities = new ArrayList<>();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        System.out.println(qName + "--->解析开始");
        if (null != qName) {
            tag = qName;//存储当前属性
            switch (tag) {
                case "servlet-mapping":
                    mapping = new Mapping();
                    father_tag = qName;//存储当前标签
                    break;
                case "servlet":
                    entity = new Entity();
                    father_tag = qName;//存储当前标签
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 每次解析的内容模块
     *
     * @param ch
     * @param start
     * @param length
     * @throws SAXException
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contents = new String(ch, start, length).trim();
        if (null != tag && null != father_tag) {//处理tag fathertag为空的问题
            switch (father_tag) {
                case "servlet":
                    switch (tag) {
                        case "servlet-name":
                            entity.setName(contents);
                            break;
                        case "servlet-class":
                            entity.setClz(contents);
                            break;
                    }
                    break;
                case "servlet-mapping":
                    switch (tag) {
                        case "servlet-name":
                            mapping.setName(contents);
                            break;
                        case "url-pattern":
                            mapping.addPattern(contents);
                            break;
                    }
                    break;
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.println(qName + "--->解析结束");
        tag = null;//解析完毕丢弃tag
        switch (qName) {
            case "servlet-mapping":
                mappings.add(mapping);
                father_tag = null;//解析完毕丢弃标签
                break;
            case "servlet":
                entities.add(entity);
                father_tag = null;//解析完毕丢弃标签
                break;
            default:

        }
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
        System.out.println("---解析文档结束---");
    }
}

2.3.9 运行结果

---解析文档开始---
web-app--->解析开始
servlet--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
servlet-class--->解析开始
servlet-class--->解析结束
servlet--->解析结束
servlet--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
servlet-class--->解析开始
servlet-class--->解析结束
servlet--->解析结束
servlet-mapping--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
url-pattern--->解析开始
url-pattern--->解析结束
url-pattern--->解析开始
url-pattern--->解析结束
servlet-mapping--->解析结束
servlet-mapping--->解析开始
servlet-name--->解析开始
servlet-name--->解析结束
url-pattern--->解析开始
url-pattern--->解析结束
servlet-mapping--->解析结束
web-app--->解析结束
---解析文档结束---
RegisterServlet

Process finished with exit code 0

3 HTML

3.1什么是HTML

超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言,HTML 运行在浏览器上,由浏览器来解析。

发展路径1
发展路径2
HTML
前面讲过的XML
HTML 5

目前一个完整的网页一般包括三个部分。

骨架
样式
交互
HTML网页
HTML 5
CSS
JavaScript

JavaScript,简称JS,是动态语言。

3.2 HTML结构

3.2.1 固定结构

<html>---开始标签
<head>---网页上的控制信息
    <title>---页面标题

    title>
head>

<body>
页面显示的内容
body>
html>---带/的都是结束标签

3.2.2 常用标签

  • h1~h6
  • p
  • div
  • span
  • form
  • input

例子:form表单

<html>
<head>
    <title>
        登录
    title>
head>
<body>
<h1>表单的使用h1>
<pre>
    post:提交,其内容被放在http请求内容体中,因此量大,请求参数不写在URL中,相对安全。<br>
    get:获取,本意是获取服务器上某一个路径的文件,因此请求参数写在URL内,所以相对不安全。<br>
    action:所请求的web服务器资源,即URL。<br>
    name:后端(服务器JavaEE)使用的,因此必须存在,否则数据不能提交。<br>
    id:前端(JavaScript)使用,因此可以不存在,不影响数据提交。<br>

    在method为post的情况下,submit是提交到action所指向的服务器地址上,内容体的数据结构为<br>
    (Key,Value)。其中key是name的值,value是控件输入的值。
pre>
<form method="post" action="http://localhost:8888/index.html">
    用户名:<input type="text" name="uname" id="uname"/>
    密码:<input type="password" name="pwd" id="pwd"/>
    <input type="submit" value="登录"/>
form>
body>
html>

显示结果是这样的:
从零开始手写Web-Server_第2张图片

3.2.3 尝试提交表单

当我们输入用户名和密码,点击登录按钮后,就会向action的地址发送http请求了,并且这个请求的方法是post,我们不妨点击试一下。
从零开始手写Web-Server_第3张图片
可以看到,浏览器顶部的网址正是我们action的内容。这代表着,我们的浏览器已经向这个地址http://localhost:8888/index.html发送了一个http包,包的内容是刚刚我们所填写的用户名和密码。

我们还没有编写服务器,因此网页是无法访问,不要急,马上就到服务器部分了。

4 HTTP协议

4.1 简介

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。

对于要手写服务器的我们来说,只需要知道,GET方法和POST方法不一样就够了,我们后面只需要用到这两个方法。

4.2 HTTP请求报文

第一行为请求行,接下来为请求头部,隔一个空行接下来为请求内容。
http请求报文的结构如下:

说明 位置1 位置2 位置3 位置4 位置5 位置6 位置7
请求行 请求方法(GET/POST) 空格 URL 空格 协议版本 回车符 换行符
请求头部 头部字段名 : 回车符 换行符
请求头部 头部字段名 : 回车符 换行符
空行 回车符 换行符
请求内容 xxxx xxxx xxxx xxxx xxxx xxxx xxxx

这里是一个POST方法的报文

POST /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Content-Length: 24
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150

uname=greathd&pwd=123456

这里有一个GET方法的报文

GET /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150

可以看到,GET方式的请求一般不包含”请求内容”部分,请求数据以地址的形式表现在请求行。地址链接如下:

4.3 相应协议

HTTP响应也由三个部分组成,分别是:状态行、响应头部、响应内容。
如下所示,HTTP响应的格式与请求的格式十分类似:
状态行(status-line)
响应头部(headers)
空行(blank line)
响应内容(response-body)
以下是一个响应报文

HTTP/1.1 200 OK
Date: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 122

<html>
<head>
<title>Wrox Homepage</title>
</head>
<body>
<!-- body goes here -->
</body>
</html>

如你所见,响应报文与请求报文唯一的区别在第一行。

说明 位置1 位置2 位置3 位置4 位置5 位置6 位置7
状态行 协议/版本 空格 状态码 空格 状态说明 回车符 换行符
响应头部 头部字段名 : 回车符 换行符
响应头部 头部字段名 : 回车符 换行符
空行 回车符 换行符
响应内容 xxxx xxxx xxxx xxxx xxxx xxxx xxxx

状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。

状态码 含义
1xx 指示信息–表示请求已接收,继续处理。
2xx 成功–表示请求已被成功接收、理解、接受。
3xx 重定向–要完成请求必须进行更进一步的操作。
4xx 客户端错误–请求有语法错误或请求无法实现。
5xx 服务器端错误–服务器未能实现合法的请求。

现在有一些常见的状态代码、状态描述的说明如下。

状态码 状态描述 含义
200 OK 客户端请求成功。
400 Bad Request 客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden 服务器收到请求,但是拒绝提供服务。
404 Not Found 请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error 服务器发生不可预期的错误。
503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

好了,知道了这些以后,就可以开始手写服务器了!

5 手写服务器

5.1 获取请求协议

由于HTTP协议的底层是TCP/IP,因此,我们使用ServerSocket接收连接,然后读取请求内容。

Created with Raphaël 2.2.0 开始 创建ServerSocket 建立连接获取Socket 通过输入流获取请求协议 结束

示例代码

package SERVER;

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

/**
 * 目标:使用ServerSocket建立与浏览器的连接,获取请求协议。
 */
public class Server {
    private ServerSocket serverSocket;
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

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

    /**
     * 连接处理
     */
    public void receive(){
        try {
            Socket client = serverSocket.accept();
            System.out.println("一个客户端建立了连接...");
            //获取请求协议
            InputStream is = client.getInputStream();
            byte[] datas = new byte[1024*1024];//1M的空间
            int len = is.read(datas);//全部读取
            String requestinfo = new String(datas,0,len);//构造成字符串
            System.out.println(requestinfo);
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客户端错误...");
        }
    }
    /**
     * 停止服务
     */
    public void stop(){
    }
}

运行程序后,使用RESTer模拟http请求,点send后可以看到输出结果
从零开始手写Web-Server_第4张图片
输出结果:

一个客户端建立了连接...
GET /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150; _gid=GA1.1.2100199394.1559657416



Process finished with exit code 0

5.2 返回响应协议

Created with Raphaël 2.2.0 开始 1、准备内容 2、获取字节数长度(注意不是字符数长度) 3、拼接响应协议(注意空格和换行) 4、使用输出流输出 结束

修改后的reveive方法代码如下:

    /**
     * 连接处理
     */
    public void receive() {
        try {
            Socket client = serverSocket.accept();
            System.out.println("一个客户端建立了连接...");
            //获取请求协议
            InputStream is = client.getInputStream();
            byte[] datas = new byte[1024 * 1024];//1M的空间
            int len = is.read(datas);//全部读取
            String requestinfo = new String(datas, 0, len);//构造成字符串
            System.out.println(requestinfo);

            //StringBuilder更方便构造String 这里构造的是响应内容
            StringBuilder content = new StringBuilder();
            content.append("");
            content.append("");
            content.append(""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            content<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"服务器响应成功"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            content<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"");
            content.append("");
            content.append("");
            content.append("bsbz server终于回来了...");
            content.append("");
            content.append("");
            //注意这里size要计算的一定是字节数而非字符数!
            int size = content.toString().getBytes().length;

            StringBuilder responsinfo = new StringBuilder();
            String blank = " ";
            String CRLF = "\r\n";
            //返回
            //1、响应行:HTTP/1.1 200 OK
            responsinfo.append("HTTP/1.1").append(blank);
            responsinfo.append(200).append(blank);
            responsinfo.append("OK").append(CRLF);
            //2、响应头:(最后一行存在空行)
            /**
             * Date: Sat, 31 Dec 2005 23:59:59 GMT
             * Server:bsbz Server/0.0.1;charset=GBK
             * Content-Type:text/html;charset=ISO-8859-1
             * Content-Length: 122
             */
            responsinfo.append("Date:").append(new Date()).append(CRLF);
            responsinfo.append("Server:").append("bsbz Server/0.0.1;charset=GBK").append(CRLF);
            responsinfo.append("Content-Type:").append("text/html;charset=ISO-8859-1").append(CRLF);
            responsinfo.append("Content-Length:").append(size).append(CRLF);
            responsinfo.append(CRLF);
            //3、正文:
            responsinfo.append(content.toString());
            //4、写出到客户端
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
            bw.write(responsinfo.toString());
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客户端错误...");
        }
    }

测试结果:
从零开始手写Web-Server_第5张图片

5.3 封装响应信息

这是一个封装类,负责构造和发送相应信息,经过封装后,我们可以只关注内容和状态码。

package SERVER;

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

public class Response {
   private BufferedWriter bw;
   //协议头信息(状态行与请求头 回车)
   private StringBuilder headinfo;
   //正文
   private StringBuilder content;
   private int len = 0;


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

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

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

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

   //动态添加内容
   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 code) throws IOException {
       //构建头信息
       creatHeadInfo(code);
       //加头信息
       bw.append(headinfo);
       //加内容信息
       bw.append(content);
       //推送
       bw.flush();
   }
   //构建头信息
   private void creatHeadInfo(int code) {
       //1、响应行:HTTP/1.1 200 OK
       headinfo.append("HTTP/1.1").append(BLANK);
       headinfo.append(code).append(BLANK);
       switch (code) {
           case 200:
               headinfo.append("OK").append(CRLF);
               break;
           case 404:
               headinfo.append("NOT FOUND").append(CRLF);
               break;
           case 505:
               headinfo.append("SERVER ERROR").append(CRLF);
               break;
       }
       //2、响应头:(最后一行存在空行)
       /**
        * Date: Sat, 31 Dec 2005 23:59:59 GMT
        * Server:bsbz Server/0.0.1;charset=GBK
        * Content-Type:text/html;charset=ISO-8859-1
        * Content-Length: 122
        */
       headinfo.append("Date:").append(new Date()).append(CRLF);
       headinfo.append("Server:").append("bsbz Server/0.0.1;charset=GBK").append(CRLF);
       headinfo.append("Content-Type:").append("text/html;charset=ISO-8859-1").append(CRLF);
       headinfo.append("Content-Length:").append(len).append(CRLF);
       headinfo.append(CRLF);
   }
}

修改后的receive方法,先构建内容,再给定状态码,最后由封装的响应类准备头部信息并发送。
代码如下:

/**
 * 连接处理
 */
public void receive() {
    try {
        Socket client = serverSocket.accept();
        System.out.println("一个客户端建立了连接...");
        //获取请求协议
        InputStream is = client.getInputStream();
        byte[] datas = new byte[1024 * 1024];//1M的空间
        int len = is.read(datas);//全部读取
        String requestinfo = new String(datas, 0, len);//构造成字符串
        System.out.println(requestinfo);
        //关注了内容
        Response response = new Response(client);
        response.print("");
        response.print("");
        response.print(""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        response<span class="token punctuation">.</span><span class="token function">print</span><span class="token punctuation">(</span><span class="token string">"服务器响应成功"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        response<span class="token punctuation">.</span><span class="token function">print</span><span class="token punctuation">(</span><span class="token string">"");
        response.print("");
        response.print("");
        response.print("bsbz server终于回来了...");
        response.print("");
        response.print("");
        //关注了状态码
        response.pushToBrowser(200);
    } catch (IOException e) {
        e.printStackTrace();
        System.out.println("客户端错误...");
    }
}

5.4 封装请求协议

这部分我们要做的就是通过分解字符串来获取method、URL、请求参数,这里注意GET方法的请求参数在URL后,POST方法的请求参数还可以在请求体中。
代码如下:

package SERVER;

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

public class Request {
    //协议信息
    private String requestInfo;
    //请求方式
    private String method;
    //请求URL
    private String url;
    //请求参数
    private String queryStr;

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

    private Request() {

    }

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

    public Request(InputStream is) {
        byte[] datas = new byte[1024 * 1024];
        int len;
        try {
            len = is.read(datas);
            requestInfo = new String(datas, 0, len);//构造成字符串
            System.out.println(requestInfo);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //分解字符串
        parseRequestInfo();
    }

    private void parseRequestInfo() {
        System.out.println("-----开始分解-----");
        System.out.println("--1、获取请求方式--");
        this.method = this.requestInfo.substring(0, this.requestInfo.indexOf("/")).trim().toLowerCase();
        System.out.println(method);
        System.out.println("--2、获取请求的URL--");//若包含请求参数,则url为?之前
        //1)获取/的位置
        int startIdx = this.requestInfo.indexOf("/") + 1;
        //2)获取HTTP/的位置
        int endIdx = this.requestInfo.indexOf("HTTP/");
        //3)分割字符串
        this.url = this.requestInfo.substring(startIdx, endIdx).trim();
        //4)获取?的位置
        int queryIdx = this.url.indexOf("?");
        if (queryIdx >= 0) {//表示存在请求参数
            String[] urlArray = this.url.split("\\?");
            this.url = urlArray[0].trim();
            queryStr = urlArray[1].trim();
        }
        System.out.println(this.url);
        System.out.println("---3获取请求体参数,如果方法为GET则已经获取,如果是POST可能在请求体中---");
        if (method.equals("post")) {
            String qStr = this.requestInfo.substring(this.requestInfo.lastIndexOf(CRLF)).trim();
            if (null == queryStr) {
                queryStr = qStr;
            } else {
                queryStr += "&" + qStr;
            }
        }
        queryStr = null == queryStr ? "" : queryStr;
        System.out.println(method + "--->" + url + "--->" + queryStr);
    }
}

运行结果如下:

一个客户端建立了连接...
GET /index.html?hello=3&why=5 HTTP/1.1
Host: localhost:8888
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1892484369.1559541150


-----开始分解-----
--1、获取请求方式--
get
--2、获取请求的URL--
index.html
---3获取请求体参数,如果方法为GET则已经获取,如果是POST可能在请求体中---
get--->index.html--->hello=3&why=5

Process finished with exit code 0

5.5 处理中文

以下代码可以将String转化编码格式,调用时传入源String和编码格式即可。
编码格式如"“utf-8”。

    /**
     * 处理中文
     * @param value
     * @param enc
     * @return
     */
    private String decode(String value, String enc) {
        try {
            return java.net.URLDecoder.decode(value, enc);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return value;
    }

5.6 构建请求参数的KV映射

这里要注意,一个Key可能会对应多个Value值,比如传入参数爱好,爱好可以有多个。
修改后的Request类如下

package SERVER;

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

public class Request {
    //协议信息
    private String requestInfo;
    //请求方式
    private String method;
    //请求URL
    private String url;
    //请求参数
    private String queryStr;
    private Map<String, List<String>> parameterMap;

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

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

    public Request(InputStream is) {
        parameterMap = new HashMap<>();
        byte[] datas = new byte[1024 * 1024];
        int len;
        try {
            len = is.read(datas);
            requestInfo = new String(datas, 0, len);//构造成字符串
            System.out.println(requestInfo);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //分解字符串
        parseRequestInfo();
    }

    private void parseRequestInfo() {
        System.out.println("-----开始分解-----");
        System.out.println("--1、获取请求方式--");
        this.method = this.requestInfo.substring(0, this.requestInfo.indexOf("/")).trim().toLowerCase();
        System.out.println(method);
        System.out.println("--2、获取请求的URL--");//若包含请求参数,则url为?之前
        //1)获取/的位置
        int startIdx = this.requestInfo.indexOf("/") + 1;
        //2)获取HTTP/的位置
        int endIdx = this.requestInfo.indexOf("HTTP/");
        //3)分割字符串
        this.url = this.requestInfo.substring(startIdx, endIdx).trim();
        //4)获取?的位置
        int queryIdx = this.url.indexOf("?");
        if (queryIdx >= 0) {//表示存在请求参数
            String[] urlArray = this.url.split("\\?");
            this.url = urlArray[0].trim();
            queryStr = urlArray[1].trim();
        }
        System.out.println(this.url);
        System.out.println("---3获取请求体参数,如果方法为GET则已经获取,如果是POST可能在请求体中---");
        if (method.equals("post")) {
            String qStr = this.requestInfo.substring(this.requestInfo.lastIndexOf(CRLF)).trim();
            if (null == queryStr) {
                queryStr = qStr;
            } else {
                if (qStr.length() > 0)
                    queryStr += "&" + qStr;
            }
        }
        queryStr = null == queryStr ? "" : queryStr;
        System.out.println(method + "--->" + url + "--->" + queryStr);
        //转成Map fav=1&fav=2&uname=lhd&age=18&others=
        convertMap();
    }

    //处理请求参数为Map
    private void convertMap() {
        //1、分割字符串
        String[] keyValues = this.queryStr.split("&");
        for (String queryStr : keyValues) {
            //2、再次分割字符串 =
            String[] kv = queryStr.split("=");
            kv = Arrays.copyOf(kv, 2);//永远保证有两个长度
            //获取key和value
            String key = kv[0];
            String value = kv[1] == null ? "" : decode(kv[1], "utf-8");
            //存储到map中
            if (!parameterMap.containsKey(key)) {//第一次
                parameterMap.put(key, new ArrayList<>());
            }
            parameterMap.get(key).add(value);
        }
    }

    /**
     * 处理中文
     * @param value
     * @param enc
     * @return
     */
    private String decode(String value, String enc) {
        try {
            return java.net.URLDecoder.decode(value, enc);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return value;
    }

    /**
     * 通过name获取对应的多个值
     *
     * @param name
     * @return
     */
    public String[] getParameterValues(String name) {
        List<String> values = this.parameterMap.get(name);
        if (null == values || values.size() < 1) {
            return null;
        }
        return values.toArray(new String[0]);
    }

    /**
     * 通过name获取对应的1个值
     *
     * @param name
     * @return
     */
    public String getParameterValue(String name) {
        String[] values = getParameterValues(name);
        return values == null ? null : values[0];
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getQueryStr() {
        return queryStr;
    }
}

6 引入Servlet

目标:解耦业务代码
首先我们需要一个接口:Servlet

package Servlet;

/**
 * 服务器小脚本接口
 */
public interface Servlet {
    void service(Request request, Response response);
}

让我们的业务处理类来实现这个接口,如注册类:

package Servlet;


public class RegisterServlet implements Servlet {
    @Override
    public void service(Request request, Response response) {
        response.print("注册成功了!");
    }
}

同样的,登录类也实现这个接口:

package Servlet;


public class LoginServlet implements Servlet {
    @Override
    public void service(Request request, Response response) {
        //业务代码
        response.print("");
        response.print("");
        response.print(""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        response<span class="token punctuation">.</span><span class="token function">print</span><span class="token punctuation">(</span><span class="token string">"服务器响应成功"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        response<span class="token punctuation">.</span><span class="token function">print</span><span class="token punctuation">(</span><span class="token string">"");
        response.print("");
        response.print("");
        response.print("欢迎回来" + request.getParameterValue("uname"));
        response.print("");
        response.print("");
    }
}

可以看到,登录类里我把原先的业务逻辑放进来了,因此,server中的receive方法就相应的变成了这样:

/**
 * 连接处理
 */
public void receive() {
    try {
        Socket client = serverSocket.accept();
        System.out.println("一个客户端建立了连接...");
        //获取请求协议
        Request request = new Request(client);
        //获取响应协议
        Response response = new Response(client);
        //通过Servlet解耦了业务代码
        Servlet servlet = null;
        if (request.getUrl().equals("login")) {
            servlet = new LoginServlet();
        } else if (request.getUrl().equals("reg")) {
            servlet = new RegisterServlet();
        } else {
            //首页...
        }
        servlet.service(request, response);
        //关注了状态码
        response.pushToBrowser(200);
    } catch (IOException e) {
        e.printStackTrace();
        System.out.println("客户端错误...");
    }
}

这样,我们就实现了业务处理和请求响应的解耦。

7 整合webxml

还记得我们最开始所讲的webxml解析和反射吗?现在要派上用场了。
我们的webxml.xml内容如下:


<web-app>
    <servlet>
        <servlet-name>LoginServletservlet-name>
        <servlet-class>Servlet.LoginServletservlet-class>
    servlet>
    <servlet-mapping>
        <servlet-name>LoginServletservlet-name>
        <url-pattern>/loginurl-pattern>
        <url-pattern>/gurl-pattern>
    servlet-mapping>
    <servlet>
        <servlet-name>RegisterServletservlet-name>
        <servlet-class>Servlet.RegisterServletservlet-class>
    servlet>
    <servlet-mapping>
        <servlet-name>RegisterServletservlet-name>
        <url-pattern>/regurl-pattern>
    servlet-mapping>
    <servlet>
        <servlet-name>OtherServletservlet-name>
        <servlet-class>Servlet.OtherServletservlet-class>
    servlet>
    <servlet-mapping>
        <servlet-name>OtherServletservlet-name>
        <url-pattern>/ourl-pattern>
    servlet-mapping>
web-app>

首先是用于解析xml文件的类,我们不用过多文件了,在这里,放到一个.java文件里。
WebHandler.java

package Servlet;

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

/**
 * 目标:
 * 1、使用ServerSocket建立与浏览器的连接,获取请求协议。
 * 2、返回响应协议
 * 3、内容可以动态添加
 * 4、关注状态码,拼接好响应的协议信息
 */
public class Server {
    private ServerSocket serverSocket;

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

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

    /**
     * 连接处理
     */
    public void receive() {
        try {
            Socket client = serverSocket.accept();
            System.out.println("一个客户端建立了连接...");
            //获取请求协议
            Request request = new Request(client);
            //获取响应协议
            Response response = new Response(client);
            //通过Servlet解耦了业务代码
            Servlet servlet = WebApp.getServletFromURL(request.getUrl());
            if (null != servlet) {
                servlet.service(request, response);
                //关注了状态码
                response.pushToBrowser(200);
            } else {
                //错误
                response.pushToBrowser(404);
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客户端错误...");
        }
    }

    /**
     * 停止服务
     */
    public void stop() {

    }
}

是不是非常简单?

8 高效分发器

到目前为止,我们都是单线程,为了提高性能,我们来设计分发器。

package Servlet;

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

public class Dispatcher implements Runnable {
    private Socket client;
    Request request;
    Response response;

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

    @Override
    public void run() {
        try {
            //通过Servlet解耦了业务代码
            Servlet servlet = WebApp.getServletFromURL(request.getUrl());
            if (null != servlet) {
                servlet.service(request, response);
                //关注了状态码
                response.pushToBrowser(200);
            } else {
                //没找到
                response.pushToBrowser(404);
            }
        } catch (Exception e) {
            //错误
            try {
                response.pushToBrowser(500);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

    }

    private void release() {
        try {
            client.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

那么相应的,Server端的receive就相应变成

/**
 * 连接处理
 */
public void receive() {
    while (isRuning) {
        try {
            Socket client = serverSocket.accept();
            System.out.println("一个客户端建立了连接...");
            //多线程处理
            new Thread(new Dispatcher(client)).start();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客户端错误...");
        }
    }
}

9 经典404及首页处理(完结)

package Servlet;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class Dispatcher implements Runnable {
    private Socket client;
    Request request;
    Response response;

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

    @Override
    public void run() {
        try {
            if (request.getUrl() == null || request.getUrl().equals("")) {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("Servlet/index.html");
                BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                String line = null;
                while ((line = br.readLine()) != null) {
                    response.println(line);
                }
                br.close();
                response.pushToBrowser(200);
                return;
            }
            //通过Servlet解耦了业务代码
            Servlet servlet = WebApp.getServletFromURL(request.getUrl());
            if (null != servlet) {
                servlet.service(request, response);
                //关注了状态码
                response.pushToBrowser(200);
            } else {
                //没找到
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("Servlet/error.html");
                BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                String line = null;
                while ((line = br.readLine()) != null) {
                    response.println(line);
                }
                br.close();
                response.pushToBrowser(404);
            }
        } catch (Exception e) {
            e.printStackTrace();
            //错误
            try {
                response.pushToBrowser(500);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

    }

    private void release() {
        try {
            client.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

完结撒花!

你可能感兴趣的:(Java)