实现一个简易的http服务器

准备找工作,把自己以前做的项目做一下整理,回顾一下知识

使用技术

基于Java BIO,多线程,Socket网络编程,xml解析

具备功能

Request封装Http请求报文

Response封装HTTP响应报文

Dispatcher实现分发器功能,进行Servlet的分发

xmlParse解析web.xml文件

支持Get和Post两种请求

文件结构

在这里插入图片描述

项目主要是红线框中的内容,其余的部分是自己写服务器之前的一些学习,不影响项目,只是不舍得删除所以留着

HTTPServer

下面只放了重要部分的代码

值得注意的是,这个服务器基于BIO,那么如果只开一个主线程必然会在socket的accept部分阻塞,所以bio的解决方案就是开新的进程,一个客户端连接开一个线程,但这种方式仍然是同步阻塞的(最后会讨论方案可改进的地方,以及另一种方案NIO)

        while (isRunning){
            try{
                Socket client = serverSocket.accept();
                System.out.println("一个客户端建立连接");
                //多线程处理
                new Thread(new Dispatcher(client)).start();
            }catch (Exception e){
                e.printStackTrace();
                System.out.println("客户端错误");
            }
        }
HTTPRequest的封装

首先补充一下基础的知识,HTTP请求的格式如下
在这里插入图片描述

例如:(空格是存在的!虽然看不太出来的样子)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7yAv0HI-1607672604851)(http博客/image-20201211105649318.png)]

这里主要是解析请求,需要解析出什么呢?请求方式,请求url,请求参数(需要注意的是请求参数根据get和post请求方式不同,会有一点点的区别,比如post请求参数不但可能出现在输入的网址中,也可能在请求体中出现)

这里主要放了解析请求行部分的内容,像处理中文,以及将分解出来的请求参数存为map等等细节的处理没有放代码

    private void parseRequestInfo(){
        System.out.println("----------分解----------");
        //1.获取请求方式 开头到第一个/
        method = requestInfo.substring(0,requestInfo.indexOf("/")).toLowerCase();
        method = method.trim();
        System.out.println(method);
        //1.获取请求uri, 第一个/到HTTP/
        //uri可能包含请求参数,只取前面的部分
        int startIdx = requestInfo.indexOf("/")+1;//取第一个/的位置
        int endIdx = requestInfo.indexOf("HTTP/");//取HTTP/的位置
        uri = requestInfo.substring(startIdx,endIdx).trim();
        //处理请求参数
        int queryIdx = uri.indexOf("?");
        if (queryIdx >= 0){//存在请求参数
            String[] uriArray = uri.split("\\?");//需要转义
            uri = uriArray[0];
            queryStr = uriArray[1];
        }
        System.out.println(uri);

        //获取请求参数,是get则获取到了,post可能在请求体中还存在
        if (method.equals("post")){
            String qStr = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
            if(null == queryStr){
                queryStr = qStr;
            }else {
                queryStr += "&"+qStr;
            }
        }
        queryStr = null==queryStr?"":queryStr;//http://localhost:8888/aaa,出现了queryStr为null的情况,处理null的情况
        System.out.println(method+"----"+uri+"-------"+queryStr);
        //处理请求参数为Map pare=1&para=2
        convertMap();
    }
HTTPResponse的封装

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vqcJAqcm-1607672604852)(C:\Users\SSM\AppData\Roaming\Typora\typora-user-images\image-20201210233951253.png)]

封装响应报文主要的工作是:

①:严格按照HTTP响应报文的格式封装响应头,只有按照HTTP的格式封装,浏览器才能正确解析显示出你想要的内容

②:统计请求报文的长度(这里需要注意,要求出字节的长度,而不是求字符的长度!!不然可能会显示内容不完全!)

    //动态添加内容
    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;
    }

    private void createHeadInfo(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 500:
                headInfo.append("SERVER ERROR").append(CRLF);
                break;
        }
        headInfo.append("Date:").append(new Date()).append(CRLF);
        headInfo.append("Content-type:text/html;charset=UTF-8").append(CRLF);
        headInfo.append("Content-length:").append(len).append(CRLF);
        headInfo.append(CRLF);
    }

    public void pushToBrowser(int code) throws IOException {
        if (null == headInfo){
            code = 505;
        }
        createHeadInfo(code);
        bw.append(headInfo);
        bw.append(content);
        bw.flush();
    }
Dispatcher分发器

Dispatcher实现了Runnable接口,每次接受一个客户端连接都会创建一个线程进行处理

根据http请求报文的uri找到符合的Servlet(通过uri找到servlet主要利用了反射技术,在下面会写到这部分内容),然后将封装好的请求报文发送出去

    public void run() {
        try{
            Servlet servlet = WebApp.getServletFromUrl(request.getUri());
            if(null != servlet){
                servlet.service(request,response);
                //关注状态码
                response.pushToBrowser(200);
            }else {
                //错误页面
                response.println("");
                response.pushToBrowser(404);
            }
        }catch (Exception e){
            e.printStackTrace();
            try {
//                response.println("发生错误");
                response.pushToBrowser(500);
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        release();
    }
Servlet部分

一个Servlet的接口,有一个service方法,其余的xxxServlet类都实现该接口,并重写service方法

例如:Servlet接口

public interface Servlet {
    void service(Request request, Response response);
}

例如:LoginServlet类

public class LoginServlet implements Servlet {
    @Override
    public void service(Request request, Response response) {
        StringBuilder content = new StringBuilder();
        response.print("<html>");
        response.print("<head>");
        response.print("<title>");
        response.print("页面");
        response.print("</title>");
        response.print("</head>");
        response.print("<body>");
        response.print("come back。"+request.getParameter("username"));
        response.print("</body>");
        response.print("</html>");
    }
}
XML解析

xml解析的一篇文章
link.
首先来看web.xml文件

<?xml version="1.0" encoding="utf-8"?>
<web-app>
    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>ssm.server.Myservlect.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/log</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>reg</servlet-name>
        <servlet-class>ssm.server.Myservlect.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>reg</servlet-name>
        <url-pattern>/reg</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>others</servlet-name>
        <servlet-class>ssm.server.Myservlect.OtherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>others</servlet-name>
        <url-pattern>/others</url-pattern>
    </servlet-mapping>
</web-app>

xml问价需要两个对象来存储,一个存储servlet,一个存储servlet-mapping

servlet对象

public class IServlet {
    private String name;
    private String clz;

    public IServlet(){
    }

    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;
    }
}

servletmapping对象

public class IServletMapping {
    private String name;
    private Set<String> patterns;

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

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

    public String getName() {
        return name;
    }

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

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

    public void setPatterns(Set<String> patterns) {
        this.patterns = patterns;
    }
}

上面是准备工作,下面解析xml,必须明确解析xml是为了什么?

解析是为了从url-pattern拿到url后找到对应的类对象,通过反射技术拿到需要分发到哪个servlet。

详细步骤

①从web.xml中的servlet-mapping中 的url-pattern找到对应的servlet-name,例如:这里有一个url-pattern是/log(/login也是可以的),找到它对应的servlet-name是login

  <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/log</url-pattern>
    </servlet-mapping>

②将servlet-mapping中得到的servlet-name去servlet中找对应的servlet-name,看servlet中的servlet-name对应的servlet-class,这个时候拿到了calss就可以使用反射获取Class对象

<servlet>
    <servlet-name>login</servlet-name>
    <servlet-class>ssm.server.Myservlect.LoginServlet</servlet-class>
</servlet>

解析部分的代码为红色部分的内容

webapp是主要的解析流程,具体的解析分离出来放在了webhandler文件中,webcontext主要是解析后得到的结果

WebApp

public class WebApp {
    private static WebContext webContext;
    static {
        try {
            //1.获取解析工厂
            SAXParserFactory factory = SAXParserFactory.newInstance();
            //2.从解析工厂获取解析器
            SAXParser parse = factory.newSAXParser();
            //3.编写处理器
            //4.加载文档Document注册处理器
            WebHandler handler = new WebHandler();
            //5.解析
            parse.parse(Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("ssm/server/web.xml"),handler);

            //获取数据
            webContext = new WebContext(handler.getiServlets(),handler.getiServletMappings());

        }catch (Exception e){
            System.out.println("解析配置文件错误");
        }
    }
    public static Servlet getServletFromUrl(String url){
        String className = webContext.getClz("/"+url);
        Class<?> clz;
        try {
            clz = Class.forName(className);
            Servlet servlet = (Servlet)clz.getConstructor().newInstance();
            return servlet;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

WebHandler

class WebHandler extends DefaultHandler {
    private List<IServlet> iServlets;//容器
    private List<IServletMapping> iServletMappings;//容器
    private IServlet iServlet;//web.xml中的servlet标签作为一个对象
    private IServletMapping iServletMapping;//web.xml中的servlet-mapping标签作为一个对象
    private String tag;//存储操作标签
    private boolean isMapping = false;//因为sevlet和servletmapping里面有相同的name属性赋值,需要一个区分的标识


    @Override
    public void startDocument() throws SAXException {
        iServlets = new ArrayList<IServlet>();
        iServletMappings = new ArrayList<IServletMapping>();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if (null != qName){
            tag = qName;//存储标签名
            if (tag.equals("servlet")){
                iServlet = new IServlet();
                isMapping = false;
            }
            else if (tag.equals("servlet-mapping")){
                iServletMapping = new IServletMapping();
                isMapping = true;
            }
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contents = new String(ch,start,length).trim();
        if (null != tag){
            //因为endElement那里丢弃第二个如</xxx>后面的内容,将tag=null丢弃
            //是为了防止第二次赋值把前面的值覆盖为第二次内容空
            if (isMapping){
                //给servlet-mapping的属性赋值
                if (tag.equals("servlet-name")){
                    iServletMapping.setName(contents);
                }else if (tag.equals("url-pattern")){
                    iServletMapping.addPattern(contents);
                }
            }else {
                //给servlet的属性赋值
                if (tag.equals("servlet-name")){
                    iServlet.setName(contents);
                }else if (tag.equals("servlet-class")){
                    iServlet.setClz(contents);
                }
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (null != qName){
            if (qName.equals("servlet")){
                iServlets.add(iServlet);
            }else if (qName.equals("servlet-mapping")){
                iServletMappings.add(iServletMapping);
            }
        }
        tag = null;//将tag段丢弃
    }


    public List<IServlet> getiServlets() {
        return iServlets;
    }
    public List<IServletMapping> getiServletMappings() {
        return iServletMappings;
    }
}

WebContext

/**
 * @author ssm
 * 主要功能:通过URL路径找到对应的class
 */
public class WebContext {
    private List<IServlet> iServlets = null;
    private List<IServletMapping> iServletMappings = null;

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

    public WebContext(List<IServlet> iServlets, List<IServletMapping> iServletMappings) {
        this.iServlets = iServlets;
        this.iServletMappings = iServletMappings;

        //分别将List数据存到为Map
        for(IServlet iServlet: iServlets){
            iservletMap.put(iServlet.getName(),iServlet.getClz());
        }
        for(IServletMapping iServletMapping: iServletMappings){
            for (String pattern: iServletMapping.getPatterns()){
                iservletmappingMap.put(pattern,iServletMapping.getName());
            }
        }
    }


/*
 <servlet-mapping>中的url-pattern ---> 找servlet-name --->
 <servlet>中的servlet-name ---> 找servlet-class,
 究极目标通过URL路径找到对应的class
 (求class用处:由Class反射对应的对象)
 */
    public String getClz(String pattern){
        String name = iservletmappingMap.get(pattern);
        String clz = iservletMap.get(name);
        return clz;
    }
}

码云地址:https://gitee.com/vampire-boom/http-server.

思考:

实现的只是一个简易的http服务器,基于BIO,每当有客户端来连接服务器都开一个新线程去处理,同步阻塞,因为内存资源很容易被消耗完,有很多客户端连接的时候就会出现在浏览器输入一个网址,一直显示加载的情况。

有一种不同的工作方式的io流NIO,NIO主要有三个重要的概念:selector,bytebuffer,channel,NIO开启一个线程可以处理多个客户端请求,监听到事件再去处理

还有一个地方可以完善,接受请求的时候我的是默认一次性接受到所有的数据,但其实会出现TCP的粘包问题,这里可改进

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值