手写httpserver

需求:
简单实现http服务器功能,服务器运行之后,可以自定义servlet,完成指定功能,浏览器访问,后台可以处理请求,并返回相应内容

写在前面

http协议基于TCP/IP协议,本例是用socket做底层实现的
socket相关看这篇(https://www.jianshu.com/p/651fd7718450

1.简易Server端构建

socket构建server端,浏览器不同方式访问,查看不同的请求

public class Server2 {
    private static final String CRLF="\r\n";
    
    private ServerSocket serverSocket;
    
    public static void main(String[] args) {
        Server2 server = new Server2();
        server.start();
    }

    /**
     * 服务器启动方法
     * @throws IOException 
     */
    public void start(){
        try {
            serverSocket = new ServerSocket(8888);
            this.recive();
        } catch (Exception e) {
            e.printStackTrace();
            //关闭server
        }
    }
    
    /**
     * 服务器接收客户端方法
     */
    public void recive() {
        try {
            Socket client = serverSocket.accept();
            //得到客户端
            
            StringBuilder sb =new StringBuilder();
            
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            String mString = null;
            while ((mString = bufferedReader.readLine()).length()>0) {
                sb.append(mString);
                sb.append(CRLF);
                if (mString==null) {
                    break;
                }
            }
            System.out.println(sb.toString());
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    /**
     * 关闭服务器方法
     */
    public void stop(){
        //CloseUtils.closeSocket(server);
    }
}

GET请求:

GET /index?name=123&psw=fdskf HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

POST请求:

POST /index HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Content-Length: 34
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; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

username=fdsfgsdfg&pwd=fdsfadsfasd

对两种不同的方式的请求信息解析

2.Request封装

先解析下请求:

第一行: 请求方式 请求资源 HTTP协议版本
后面几行是一些协议,客户端支持的数据格式

如果是POST请求,请求参数会放在最后一行,和上面一行有空行间隔,如果是GET方式,请求参数会放在第一行

2.1 得到浏览器的请求信息

通过构造方法将socket的输入流传入,读取解析

2.2 得到请求方式与请求资源

解析请求信息,字符串截取第一行,然后截取方法,根据方法判断,如果是GET,那么请求资源与请求参数在第一行,如果是POST,第一行是请求资源,最后一行是请求参数,然后解析请求资源

public Request(InputStream inputStream) {
        this();
        this.inputStream = inputStream;
        
        //从输入流中取出请求信息
        try {
            byte[] data = new byte[20480];
            int len = inputStream.read(data);
            requestInfo=new String(data, 0, len);
            //解析请求信息
            parseRequestInfo();
        } catch (Exception e) {
            return;
        }
        
    }
    /**
     * 解析请求信息
     * @param requestInfo2
     */
    private  void parseRequestInfo() {
        
        if(requestInfo==null || requestInfo.trim().equals("")) {
            return;
        }
        String paramentString="";//保存请求参数
        //得到请求第一行数据
        String firstLine = requestInfo.substring(0, requestInfo.indexOf(CRLF));
        
        //第一个/的位置
        int index = firstLine.indexOf("/");
        this.method = firstLine.substring(0,index).trim();
        
        String urlString = firstLine.substring(index,firstLine.indexOf("HTTP/")).trim();
        //判断请求方式
        if (method.equalsIgnoreCase("post")) {
            
            url = urlString;
            //最后一行就是参数
            paramentString = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
        }else if (method.equalsIgnoreCase("get"))  {
            if (!urlString.contains("?")) {
                this.url = urlString;
            }else {
                //分割url
                String[] urlArray = urlString.split("\\?");
                this.url = urlArray[0];
                paramentString = urlArray[1];
            }
            
        }
        
        if (paramentString!=null&&!paramentString.trim().equals("")) {
            //解析请求参数
            parseParament(paramentString);
        }
        
        
    }
    
    /**
     * 解决中文乱码
     * @param value
     * @param code
     * @return
     */
    private String decode(String value,String code) {
        try {
            return URLDecoder.decode(value, code);
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
    
    /**
     * 解析请求参数,放在数组里面
     *   name=12&age=13&fav=1&fav=2
     */
    private void parseParament(String paramentString) {
        
        String[] paramentsArray = paramentString.split("&");
        
        for(String string:paramentsArray) {
            //某个键值对的数组
            String[] paramentArray = string.split("=");
            //如果该键没有值,设值为null
            if (paramentArray.length==1) {
                paramentArray = Arrays.copyOf(paramentArray, 2);
                paramentArray[1]=null;
            }
            
            String key = paramentArray[0];
            String value = paramentArray[1]==null?null:decode(paramentArray[1].trim(), "utf8");
            
            //分拣法
            if (!paramentMap.containsKey(key)) {
                paramentMap.put(key,new ArrayList<String>());
            }
            
            //设值
            ArrayList<String> values = paramentMap.get(key);
            values.add(value);
        }
        
        
    }

2.3 根据name得到请求参数的值

上一步解析完数据之后,会把请求参数放在Map中,key为请求参数name,value为请求参数值,根据key得到值

/**
     * 根据key得到多个值
     */
    public String[] getParamenters(String name) {
        ArrayList<String> values =null;
        if ((values=paramentMap.get(name))==null) {
            return null;
        }else {
            return  values.toArray(new String[0]);
        }
    }
    
    /**
     * 根据key得到值
     */
    public String getParamenter(String name) {
        
        if ((paramentMap.get(name))==null) {
            return null;
        }else {
            return getParamenters(name)[0];
        }
    }

2.4 解决中文乱码

前台提交数据时候,中文有时候会乱码,


    /**
     * 解决中文乱码
     * @param value
     * @param code
     * @return
     */
    private String decode(String value,String code) {
        try {
            return URLDecoder.decode(value, code);
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

3. 封装response

当得到浏览器请求之后,需要给浏览器响应
响应格式:
响应:

  • HTTP协议版本,状态码
  • 响应头
  • 响应正文

3.1 得到服务器的输出流

构造方法传入socket的输出流

3.2 构造响应头

常见响应码:200 404 500
根据状态码,构建不同的响应头,

3.3 构建方法,外界传入响应正文

暴露一个方法,用于外界传入响应值,与状态码

3.4 构建响应正文

根据不同响应码构建响应正文,如404,返回一个NOT FOUNF页面,
500返回一个SERVER ERROR 页面,如果是200,就返回正常界面,

3.5 构建推送数据到客户端的方法

将响应推送到客户端

4. 封装servlet

将响应与请求封装在一个servlet类中,主要是实现业务逻辑,不做其他事情 , 在server端直接new 一个servlet类,调用业务方法

public class Servlet {

    public void service(Request request,Response response){
        response.print("<html>\r\n" + 
                "<head>\r\n" + 
                "    <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\r\n" + 
                "</head>\r\n" + 
                "<body>\r\n" + 
                "欢迎你\r\n" + request.getParamenter("username")+
                "</form>\r\n" + 
                "</body>\r\n" + 
                "</html>");
    }
}

5. 处理不同请求的server

想让server可以处理不同请求,/login 是做登录请求 /reg 是做注册请求
需要多线程,当有请求过来之后,创建一个线程处理相关的请求与响应,每个请求的线程互不影响,

5.1 创建一个转发器

每有一个客户端连接,就会创建一个线程,处理改客户端的请求与响应

public class Dispatcher implements Runnable{

    private Request request;
    private Response response;
    private Socket client;
    
    private int code=200;
    
    public Dispatcher(Socket client) {
        try {
            client = client;
            request = new Request(client.getInputStream());
            response = new Response(client.getOutputStream());
        } catch (IOException e) {
            code=500;
            return;
        }
    }
    
    
    @Override
    public void run() {
        
        Servlet servlet = new Servlet();
        servlet.service(request, response);
        response.pushToclient(code);
        
        CloseUtils.closeSocket(client);
    }

}

5.2 创建上下文对象,存放servlet与对应的mapping

使用工厂模式,得到不同url得到不同的servlet,

public class ServletContext {
    /*
     *    有LoginServlet   设值别名  login     
     *    访问login  /login  /log
     */
    //存放servlet的别名
    private Map<String, Servlet> servletMap;
    //存放url对应的别名
    private Map<String, String> mappingMap;
    
    public ServletContext() {

        servletMap = new HashMap<>();
        mappingMap = new HashMap<>();
    }
public class WebApp {
    
    private static ServletContext context;
    
    static {
        context = new ServletContext();
        //存放servlet 和其对应的别名
        Map<String, Servlet> servletMap = context.getServletMap();
        
        servletMap.put("login", new LoginServlet());
        servletMap.put("register", new RegisterServlet());
        
        
        Map<String, String> mappingMap = context.getMappingMap();
        mappingMap.put("/login", "login");
        mappingMap.put("/", "login");
        mappingMap.put("/register", "register");
        mappingMap.put("/reg", "register");
        
    }
    
    public static Servlet getServlet(String url) {
        
        if (url==null || url.trim().equals("")) {
            return null;
        }else {
            return context.getServletMap().get(context.getMappingMap().get(url));
        }
    }

}

6.反射获取servlet对象

根据请求的url, 在mappingmap中找到servlet的别名,根据servlet的别名在servletmap中得到servlet对象,map存对象过于耗费内存,并且,每次添加一个servlet,都要更改这个文件,所以讲servlet的配置,卸载xml文件中,读取xml文件

<?xml version="1.0" encoding="UTF-8"?>

 <web-app>
     <servlet>
        <servlet-name>login</servlet-name>别名
        <servlet-class>jk.zmn.server.demo4.LoginServlet</servlet-class>类的全路径
     </servlet>
     <servlet-mapping>配置映射
        <servlet-name>login</servlet-name>别名
        <url-pattern>/login</url-pattern> 访问路径
        <url-pattern>/</url-pattern> 访问路径
     </servlet-mapping>  
      <servlet>
        <servlet-name>reg</servlet-name>
        <servlet-class>jk.zmn.server.demo4.RegisterServlet</servlet-class>
     </servlet>
     
     <servlet-mapping>
        <servlet-name>reg</servlet-name>
        <url-pattern>/reg</url-pattern> 
     </servlet-mapping>
 </web-app>

6.1 解析xml文件

首先要先解析xml配置文件,得到servlet及其映射,

6.2 根据解析到的数据,动态添加到map中

//获取解析工厂
        try {
            SAXParserFactory factory =SAXParserFactory.newInstance();
            //获取解析器
            SAXParser sax =factory.newSAXParser();
            //指定xml+处理器
            WebHandler web = new WebHandler();
            sax.parse(Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("jk/zmn/server/demo4/web.xml")
                    ,web);
            
            //得到所有的servlet 和别名
            List<ServletEntity> entityList = web.getEntityList();
            List<MappingEntity> mappingList = web.getMappingList();
            context = new ServletContext();
            //存放servlet 和其对应的别名
            Map<String, String> servletMap = context.getServletMap();
            
            for(ServletEntity servletEntity: entityList) {
                servletMap.put(servletEntity.getName(),servletEntity.getClz());
            }

            //存放urlpatten和servlet别名
            Map<String, String> mappingMap = context.getMappingMap();
            for(MappingEntity mappingEntity:mappingList) {
                List<String> urlPattern = mappingEntity.getUrlPattern();
                for(String url:urlPattern) {
                    mappingMap.put(url, mappingEntity.getName());
                }
            }

不用每次都直接修改这个文件,直接在配置文件中配置就行

##########################################################

最后:我已将文件抽好,

image.png

servlet包,是用户自定义包,新建的servlet必须要继承servlet类,
web.xml必须在src目录下,配置servlet也需按照格式配置,
运行core java application, 浏览器访问你的项目,就可以正常运行了,

效果:


image.png
image.png

image.png

本例存在诸多bug,请多多指教,在读取请求信息时候,我是直接读了20480个字节,实际上应该一个一个字节的读,但是写不出来,希望大佬们帮帮忙
源码:https://gitee.com/zhangqiye/httpserver
qq群:552113611

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,656评论 18 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,403评论 1 92
  • 花插春头,飞云天羞,挪步再回眸。 口哨音盈,粉蝶迷芳踪。 故叶落尘肥,风舟载香波。 心暖空湛宝身隐,木语斜影径自幽。
    念观诗词文化阅读 183评论 0 2
  • “爹,娘呢?” “你娘在厨房!” “爹,明天周末,来帮一下忙!” “好的!”爹并不善言辞,我们也就习惯了长话短说,...
    三千晚风阅读 1,269评论 7 19
  • 张总第一时间赶到现场~ 参加到抢险救灾中~ 变电处马主任在清理污泥浊水的时候~ 溅到一身~ 只希望大家在工作的时候...
    梅子吉祥如意怀德阅读 221评论 0 1