NIO开发Http服务器(3):核心配置和Request封装
最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室。我们是做WEB开发的,整天围着tomcat、nginx转,所以选择了一个新的方向,就是自己开发一个简单的Http服务器,在总结Java NIO的同时,也加深一下对http协议的理解。
项目实现了静态资源(html、css、js和图片)和简单动态资源的处理,可以实现监听端口、部署目录、资源过期的配置。涉及到了NIO缓冲区、通道和网络编程的核心知识点,还是比较基础的。
之前的两篇文章介绍了项目的部署、运行和开发环境等,从本篇文章开始详细介绍下项目的代码,本文首先讲核心的配置类、配置文件和http请求的封装。
文章目录:
NIO开发Http服务器(3):核心配置和Request封装
NIO开发Http服务器(5-完结):HttpServer服务器类
Github地址:
https://github.com/xuguofeng/http-server
一、核心配置类
1、ContentTypeUtil工具类
这个类使用一个静态Map保存资源后缀和ContentType的对应关系,主要支持:html、css、js、jpg、jpeg、png、gif、ico、txt
提供一个静态方法通过资源后缀获取对应的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_ROOT、RESPONSE_PAGE_404、CHARSET都不再使用,已经被配置文件替代
提供一个静态方法根据指定状态码获取对应的响应首行字符串
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、请求方法、协议版本、host、port、参数、请求头和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方式发送的请求参数,支持JSON和arg1=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 }