手写httpserver

作者: z七夜 | 来源:发表于2018-06-04 18:14 被阅读8次

    需求:
    简单实现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

    相关文章

      网友评论

        本文标题:手写httpserver

        本文链接:https://www.haomeiwen.com/subject/wzjysftx.html