为了账号安全,请及时绑定邮箱和手机立即绑定

手写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());
                }
            }

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

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

最后:我已将文件抽好,

webp

image.png

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

效果:


webp

image.png

webp

image.png


webp

image.png

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



作者:z七夜
链接:https://www.jianshu.com/p/2c608b1baccf


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消