zoukankan      html  css  js  c++  java
  • NIO开发Http服务器(3):核心配置和Request封装

    最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室。我们是做WEB开发的,整天围着tomcatnginx转,所以选择了一个新的方向,就是自己开发一个简单的Http服务器,在总结Java NIO的同时,也加深一下对http协议的理解。

    项目实现了静态资源(htmlcssjs和图片)和简单动态资源的处理,可以实现监听端口、部署目录、资源过期的配置。涉及到了NIO缓冲区、通道和网络编程的核心知识点,还是比较基础的。

    之前的两篇文章介绍了项目的部署、运行和开发环境等,从本篇文章开始详细介绍下项目的代码,本文首先讲核心的配置类、配置文件和http请求的封装。

    文章目录:

    NIO开发Http服务器(1):项目下载、打包和部署

    NIO开发Http服务器(2):项目结构

    NIO开发Http服务器(3):核心配置和Request封装

    NIO开发Http服务器(4):Response封装和响应

    NIO开发Http服务器(5-完结):HttpServer服务器类

    Github地址:

    https://github.com/xuguofeng/http-server

    一、核心配置类

    1、ContentTypeUtil工具类

    这个类使用一个静态Map保存资源后缀和ContentType的对应关系,主要支持:htmlcssjsjpgjpegpnggificotxt

    提供一个静态方法通过资源后缀获取对应的ContentType

     1 public static String getContentType(String suffix) {
     2     // 如果资源uri后缀为空,直接返回text/html
     3     if (StringUtil.isNullOrEmpty(suffix)) {
     4         return contentTypes.get(HTML);
     5     }
     6     // 根据后缀从contentTypes获取
     7     String contentType = contentTypes.get(suffix);
     8     // 如果没有获取到,返回application/octet-stream
     9     if (contentType == null) {
    10         return APPLICATION_OCTET_STREAM;
    11     }
    12     return contentType;
    13 }

    2、ResponseUtil工具类

    这个类定义了一些常用的响应状态码和响应首行字符串,如下:

    1 public static final int RESPONSE_CODE_200 = 200;
    2 public static final int RESPONSE_CODE_304 = 304;
    3 public static final int RESPONSE_CODE_404 = 404;
    4 public static final int RESPONSE_CODE_500 = 500;
    5 
    6 public static final String RESPONSE_LINE_200 = "HTTP/1.1 200 OK";
    7 public static final String RESPONSE_LINE_304 = "HTTP/1.1 304 Not Modified";
    8 public static final String RESPONSE_LINE_404 = "HTTP/1.1 404 Not Found";
    9 public static final String RESPONSE_LINE_500 = "HTTP/1.1 500 Internal Server Error";

    其他的WEB_ROOTRESPONSE_PAGE_404CHARSET都不再使用,已经被配置文件替代

    提供一个静态方法根据指定状态码获取对应的响应首行字符串

     1 public static String getResponseLine(int status) {
     2     switch (status) {
     3     case RESPONSE_CODE_200:
     4         return RESPONSE_LINE_200;
     5     case RESPONSE_CODE_304:
     6         return RESPONSE_LINE_304;
     7     case RESPONSE_CODE_404:
     8         return RESPONSE_LINE_404;
     9     case RESPONSE_CODE_500:
    10         return RESPONSE_LINE_500;
    11     }
    12     return RESPONSE_LINE_200;
    13 }

    3、HttpServerConfig和server.properties配置文件

    先看一下server.properties配置文件

     1 # 服务监听端口
     2 server.port=8082
     3 # 服务部署根目录
     4 server.root=WebContent
     5 # 404页面
     6 server.404.page=WebContent/404.html
     7 
     8 # 编码
     9 request.charset=utf-8
    10 response.charset=utf-8
    11 
    12 # 静态资源过期设置
    13 expires.jpg=120000
    14 expires.jpeg=120000
    15 expires.png=120000
    16 expires.js=60000
    17 expires.css=60000
    18 expires.ico=7200000
    19 
    20 # 动态资源配置
    21 servlet.test.do=org.net5ijy.nio.servlet.TestServlet

    配置比较简单,其中

    server.root配置web站点的部署目录,默认使用当前工作目录下面的WebContent目录

    server.404.page配置资源404时的响应页面

    expires.xxx可以配置一些静态资源的过期时间,单位为毫秒。响应时会在header中添加Expires响应头

    servlet.xxx可以配置动态资源的处理类型,xxx部分配置资源的uri,值为处理这个请求的类的全限定名

    然后看一下HttpServerConfig类,这个会读取server.properties配置文件,使用单例模式保存配置,对外提供相关方法获取配置信息以便程序使用

    代码比较简单,就不再做过多的介绍了

    二、Request接口和HttpRequest类

    1、Request接口

    该接口定义了Request对象需要有的核心方法

     1 // 获取请求方法
     2 String getMethod();
     3 // 获取请求资源uri
     4 String getRequestURI();
     5 // 获取客户端请求的协议版本
     6 String getProtocol();
     7 // 获取请求的主机名
     8 String getHost();
     9 // 获取请求的端口
    10 int getPort();
    11 // 获取请求资源的Content-Type
    12 String getContentType();
    13 // 获取全部请求参数
    14 Map<String, String> getParameters();
    15 // 获取指定的请求参数的值
    16 String getParameter(String paramaterName);
    17 // 获取全部请求头
    18 Map<String, String> getHeaders();
    19 // 获取指定的请求头的值
    20 String getHeader(String headerName);
    21 // 获取请求里面携带的cookie
    22 List<Cookie> getCookies();

    2、HttpRequest类

    该类实现了Request接口

    内部保存请求的uri、请求方法、协议版本、hostport、参数、请求头和cookie

    构造方法需要传入客户端请求的字符串,内部进行解析

    3、解析请求uri、请求方法、协议版本

     1 // 所在平台的行分隔符
     2 String lineSeparator = System.getProperty("line.separator");
     3 
     4 // 获取请求行
     5 String requestLine = body.substring(0, body.indexOf(lineSeparator));
     6 // 获取请求方法和资源uri
     7 String[] requestLines = requestLine.split("\s+");
     8 this.method = requestLines[0];
     9 this.protocol = requestLines[2];
    10 
    11 // 解析请求uri
    12 String[] uri = requestLines[1].split("\?");
    13 this.requestURI = uri[0];

    4、获取请求头

     1 // 截取请求头和请求主体
     2 body = body.substring(body.indexOf(lineSeparator) + 2);
     3 
     4 // 获取请求头
     5 int num = 0;
     6 String[] headerAndParameter = body.split(lineSeparator);
     7 for (; num < headerAndParameter.length; num++) {
     8     String headerLine = headerAndParameter[num];
     9     // 遍历到请求主体上面的空白行就停止
    10     if (StringUtil.isNullOrEmpty(headerLine)) {
    11         break;
    12     }
    13     // 获取第一个“:”的下标
    14     int indexOfMaohao = headerLine.indexOf(":");
    15     if (indexOfMaohao == -1) {
    16         continue;
    17     }
    18     String headerName = headerLine.substring(0, indexOfMaohao).trim();
    19     String headerValue = headerLine.substring(indexOfMaohao + 1).trim();
    20     this.headers.put(headerName, headerValue);
    21 }
    • 把请求头和请求参数从请求字符串中截取出来
    • 按行分割
    • 遍历分割后的字符串数组
    • 解析出请求头的行,再使用:”进行分割
    • 最后把请求头放入Map

    5、获取请求cookie

     1 String cookieHeader = headers.get("Cookie");
     2 if (!StringUtil.isNullOrEmpty(cookieHeader)) {
     3     String[] cookiesArray = cookieHeader.split(";\s*");
     4     for (int i = 0; i < cookiesArray.length; i++) {
     5         String cookieStr = cookiesArray[i];
     6         if (!StringUtil.isNullOrEmpty(cookieStr)) {
     7             String[] cookieArray = cookieStr.split("=");
     8             if (cookieArray.length == 2) {
     9                 Cookie c = new Cookie(cookieArray[0], cookieArray[1], -1);
    10                 this.cookies.add(c);
    11                 if (log.isDebugEnabled()) {
    12                     log.debug("Recieve request cookie " + c);
    13                 }
    14             }
    15         }
    16     }
    17 }
    • headers中获取出Cookie的头
    • 使用空白分割Cookie
    • 遍历分割后的数组
    • 再使用=”分割每一个元素
    • 创建Cookie对象并放入List

    6、获取请求参数

    获取请求参数分为两部分

    第一部分是解析uri后面的如arg1=val1&arg2=val2形式的参数,代码如下:

    // 解析uri后面跟的请求参数
    if (uri.length > 1) {
        this.parameters.putAll(resolveRequestArgs(uri[1]));
    }

    此处使用了一个私有方法解析如arg1=val1&arg2=val2形式的参数,方法如下:

     1 private Map<String, String> resolveRequestArgs(String args) {
     2     Map<String, String> map = new HashMap<String, String>();
     3     if (!StringUtil.isNullOrEmpty(args)) {
     4         String[] argss = args.split("&+");
     5         for (int i = 0; i < argss.length; i++) {
     6             String arg = argss[i];
     7             String[] nameAndValue = arg.split("\s*=\s*");
     8             if (nameAndValue.length == 2) {
     9                 map.put(nameAndValue[0], nameAndValue[1]);
    10             } else if (nameAndValue.length == 1) {
    11                 map.put(nameAndValue[0], "");
    12             }
    13         }
    14     }
    15     return map;
    16 }
    • 使用&+”分割参数字符串
    • 遍历分割后的数组
    • 再使用=”分割元素
    • 把分割后的数据放入Map中并返回

    另外一部分是使用POST方式发送的请求参数,支持JSONarg1=val1&arg2=val2形式的参数,代码如下:

     1 // 获取请求参数
     2 num++;
     3 StringBuilder builder = new StringBuilder();
     4 for (; num < headerAndParameter.length; num++) {
     5     builder.append(headerAndParameter[num]);
     6 }
     7 String requestArgs = builder.toString();
     8 
     9 if (!StringUtil.isNullOrEmpty(requestArgs)) {
    10     if (requestArgs.indexOf("{") > -1) {
    11         ObjectMapper mapper = new ObjectMapper();
    12         try {
    13             @SuppressWarnings("unchecked")
    14             Map<String, String> map = mapper.readValue(requestArgs, Map.class);
    15             this.parameters.putAll(map);
    16         } catch (Exception e) {
    17         }
    18     } else if (requestArgs.indexOf("&") > -1) {
    19         this.parameters.putAll(resolveRequestArgs(requestArgs));
    20     }
    21 }

    此处使用了jackson

     1 <dependency>
     2     <groupId>com.fasterxml.jackson.core</groupId>
     3     <artifactId>jackson-core</artifactId>
     4     <version>2.9.4</version>
     5 </dependency>
     6 <dependency>
     7     <groupId>com.fasterxml.jackson.core</groupId>
     8     <artifactId>jackson-databind</artifactId>
     9     <version>2.9.4</version>
    10 </dependency>

    7、几个核心方法实现方式

    getHost()方法

    1 public String getHost() {
    2     String host = this.headers.get("Host");
    3     if (!StringUtil.isNullOrEmpty(host)) {
    4         this.host = host.split(":")[0];
    5     }
    6     return this.host;
    7 }

    getPort()方法

    1 public int getPort() {
    2     String host = this.headers.get("Host");
    3     if (!StringUtil.isNullOrEmpty(host) && host.indexOf(":") > -1) {
    4         this.port = Integer.parseInt(host.split(":")[1]);
    5     }
    6     return this.port;
    7 }

    getContentType()方法

    1 public String getContentType() {
    2     if (this.requestURI.indexOf(".") == -1) {
    3         return ContentTypeUtil.getContentType(ContentTypeUtil.HTML);
    4     }
    5     String suffix = this.requestURI.substring(this.requestURI.lastIndexOf(".") + 1);
    6     return ContentTypeUtil.getContentType(suffix);
    7 }
  • 相关阅读:
    gc的real时间比user时间长
    java 反射: 当Timestamp类型的属性值为null时,设置默认值
    多线程注意点
    多线程socket编程示例
    一个类有两个方法,其中一个是同步的,另一个是非同步的; 现在又两个线程A和B,请问:当线程A访问此类的同步方法时,线程B是否能访问此类的非同步方法?
    含有Date和Timestamp的Java和Json互相转化
    java bean、List、数组、map和Json的相互转化
    并发抢购
    SQL 性能调优日常积累【转】
    String和包装类IntegerDoubleLongFloatCharacter 都是final类型
  • 原文地址:https://www.cnblogs.com/xugf/p/9603434.html
Copyright © 2011-2022 走看看