从零开始手写Web-Server

简介

要实现手写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

<?xml version = "1.0" encoding = "UTF-8"?>
<persons>
    <person>
        <name>至尊宝</name>
        <age>9000</age>
    </person>
    <person>
        <name>白晶晶</name>
        <age>7000</age>
    </person>
</persons>

解释一下,XML文件就像一棵树,我画了一张图:
在这里插入图片描述
这张图的内容就是上面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

<?xml version = "1.0" encoding = "UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>XML.WEBXML.MyClasses.LoginServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>reg</servlet-name>
        <servlet-class>XML.WEBXML.MyClasses.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/g</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>reg</servlet-name>
        <url-pattern>/reg</url-pattern>
    </servlet-mapping>
</web-app>

与之前不同的是:

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

2.3.1 Entity.java

servlet块所对应的解析类类

package XML.WEBXML;

/**
 * 对应的XML代码块为
 * <servlet>
 * <servlet-name>login</servlet-name>
 * <servlet-class>com.shsxt.LoginServlet</servlet-class>
 * </servlet>
 */
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代码块为
 * <servlet-mapping>
 * <servlet-name>login</servlet-name>
 * <url-pattern>/login</url-pattern>
 * <url-pattern>/g</url-pattern>
 * </servlet-mapping>
 */
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><!--h1标签为标题-->
<pre><!--pre标签的内容写成什么样,表现出来就是什么样-->
    post:提交,其内容被放在http请求内容体中,因此量大,请求参数不写在URL中,相对安全。<br><!--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>

显示结果是这样的:
在这里插入图片描述

3.2.3 尝试提交表单

当我们输入用户名和密码,点击登录按钮后,就会向action的地址发送http请求了,并且这个请求的方法是post,我们不妨点击试一下。
在这里插入图片描述
可以看到,浏览器顶部的网址正是我们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空格协议版本回车符换行符
请求头部头部字段名:回车符换行符
请求头部头部字段名:回车符换行符
空行回车符换行符
请求内容xxxxxxxxxxxxxxxxxxxxxxxxxxxx

这里是一个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
状态行协议/版本空格状态码空格状态说明回车符换行符
响应头部头部字段名:回车符换行符
响应头部头部字段名:回车符换行符
空行回车符换行符
响应内容xxxxxxxxxxxxxxxxxxxxxxxxxxxx

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

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

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

状态码状态描述含义
200OK客户端请求成功。
400Bad Request客户端请求有语法错误,不能被服务器所理解。
401Unauthorized请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403Forbidden服务器收到请求,但是拒绝提供服务。
404Not Found请求资源不存在,举个例子:输入了错误的URL。
500Internal Server Error服务器发生不可预期的错误。
503Server 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后可以看到输出结果
在这里插入图片描述
输出结果:

一个客户端建立了连接...
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("<html>");
            content.append("<head>");
            content.append("<title>");
            content.append("服务器响应成功");
            content.append("</title>");
            content.append("</head>");
            content.append("<body>");
            content.append("bsbz server终于回来了...");
            content.append("</body>");
            content.append("</html>");
            //注意这里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("客户端错误...");
        }
    }

测试结果:
在这里插入图片描述

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("<html>");
        response.print("<head>");
        response.print("<title>");
        response.print("服务器响应成功");
        response.print("</title>");
        response.print("</head>");
        response.print("<body>");
        response.print("bsbz server终于回来了...");
        response.print("</body>");
        response.print("</html>");
        //关注了状态码
        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("<html>");
        response.print("<head>");
        response.print("<title>");
        response.print("服务器响应成功");
        response.print("</title>");
        response.print("</head>");
        response.print("<body>");
        response.print("欢迎回来" + request.getParameterValue("uname"));
        response.print("</body>");
        response.print("</html>");
    }
}

可以看到,登录类里我把原先的业务逻辑放进来了,因此,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内容如下:

<?xml version = "1.0" encoding = "UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>Servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/g</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>RegisterServlet</servlet-name>
        <servlet-class>Servlet.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>RegisterServlet</servlet-name>
        <url-pattern>/reg</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>OtherServlet</servlet-name>
        <servlet-class>Servlet.OtherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>OtherServlet</servlet-name>
        <url-pattern>/o</url-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();
        }
    }
}

完结撒花!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值