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

项目实现了静态资源(html、css、js和图片)和简单动态资源的处理,可以实现监听端口、部署目录、资源过期的配置。涉及到了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的对应关系,主要支持:html、css、js、jpg、jpeg、png、gif、ico、txt

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

 public static String getContentType(String suffix) {
// 如果资源uri后缀为空,直接返回text/html
if (StringUtil.isNullOrEmpty(suffix)) {
return contentTypes.get(HTML);
}
// 根据后缀从contentTypes获取
String contentType = contentTypes.get(suffix);
// 如果没有获取到,返回application/octet-stream
if (contentType == null) {
return APPLICATION_OCTET_STREAM;
}
return contentType;
}

2、ResponseUtil工具类

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

 public static final int RESPONSE_CODE_200 = 200;
public static final int RESPONSE_CODE_304 = 304;
public static final int RESPONSE_CODE_404 = 404;
public static final int RESPONSE_CODE_500 = 500; public static final String RESPONSE_LINE_200 = "HTTP/1.1 200 OK";
public static final String RESPONSE_LINE_304 = "HTTP/1.1 304 Not Modified";
public static final String RESPONSE_LINE_404 = "HTTP/1.1 404 Not Found";
public static final String RESPONSE_LINE_500 = "HTTP/1.1 500 Internal Server Error";

其他的WEB_ROOT、RESPONSE_PAGE_404、CHARSET都不再使用,已经被配置文件替代

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

 public static String getResponseLine(int status) {
switch (status) {
case RESPONSE_CODE_200:
return RESPONSE_LINE_200;
case RESPONSE_CODE_304:
return RESPONSE_LINE_304;
case RESPONSE_CODE_404:
return RESPONSE_LINE_404;
case RESPONSE_CODE_500:
return RESPONSE_LINE_500;
}
return RESPONSE_LINE_200;
}

3、HttpServerConfig和server.properties配置文件

先看一下server.properties配置文件

 # 服务监听端口
server.port=8082
# 服务部署根目录
server.root=WebContent
# 404页面
server.404.page=WebContent/404.html # 编码
request.charset=utf-8
response.charset=utf-8 # 静态资源过期设置
expires.jpg=120000
expires.jpeg=120000
expires.png=120000
expires.js=60000
expires.css=60000
expires.ico=7200000 # 动态资源配置
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对象需要有的核心方法

 // 获取请求方法
String getMethod();
// 获取请求资源uri
String getRequestURI();
// 获取客户端请求的协议版本
String getProtocol();
// 获取请求的主机名
String getHost();
// 获取请求的端口
int getPort();
// 获取请求资源的Content-Type
String getContentType();
// 获取全部请求参数
Map<String, String> getParameters();
// 获取指定的请求参数的值
String getParameter(String paramaterName);
// 获取全部请求头
Map<String, String> getHeaders();
// 获取指定的请求头的值
String getHeader(String headerName);
// 获取请求里面携带的cookie
List<Cookie> getCookies();

2、HttpRequest类

该类实现了Request接口

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

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

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

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

4、获取请求头

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

5、获取请求cookie

 String cookieHeader = headers.get("Cookie");
if (!StringUtil.isNullOrEmpty(cookieHeader)) {
String[] cookiesArray = cookieHeader.split(";\\s*");
for (int i = 0; i < cookiesArray.length; i++) {
String cookieStr = cookiesArray[i];
if (!StringUtil.isNullOrEmpty(cookieStr)) {
String[] cookieArray = cookieStr.split("=");
if (cookieArray.length == 2) {
Cookie c = new Cookie(cookieArray[0], cookieArray[1], -1);
this.cookies.add(c);
if (log.isDebugEnabled()) {
log.debug("Recieve request cookie " + c);
}
}
}
}
}
  • 从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形式的参数,方法如下:

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

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

 // 获取请求参数
num++;
StringBuilder builder = new StringBuilder();
for (; num < headerAndParameter.length; num++) {
builder.append(headerAndParameter[num]);
}
String requestArgs = builder.toString(); if (!StringUtil.isNullOrEmpty(requestArgs)) {
if (requestArgs.indexOf("{") > -1) {
ObjectMapper mapper = new ObjectMapper();
try {
@SuppressWarnings("unchecked")
Map<String, String> map = mapper.readValue(requestArgs, Map.class);
this.parameters.putAll(map);
} catch (Exception e) {
}
} else if (requestArgs.indexOf("&") > -1) {
this.parameters.putAll(resolveRequestArgs(requestArgs));
}
}

此处使用了jackson库

 <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.4</version>
</dependency>

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

getHost()方法

 public String getHost() {
String host = this.headers.get("Host");
if (!StringUtil.isNullOrEmpty(host)) {
this.host = host.split(":")[0];
}
return this.host;
}

getPort()方法

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

getContentType()方法

 public String getContentType() {
if (this.requestURI.indexOf(".") == -1) {
return ContentTypeUtil.getContentType(ContentTypeUtil.HTML);
}
String suffix = this.requestURI.substring(this.requestURI.lastIndexOf(".") + 1);
return ContentTypeUtil.getContentType(suffix);
}

NIO开发Http服务器(3):核心配置和Request封装的更多相关文章

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

    最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室.我们是做WEB开发的,整天围着tomcat.n ...

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

    最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室.我们是做WEB开发的,整天围着tomcat.n ...

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

    最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室.我们是做WEB开发的,整天围着tomcat.n ...

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

    最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室.我们是做WEB开发的,整天围着tomcat.n ...

  5. ASP.NET Core搭建多层网站架构【6-注册跨域、网站核心配置】

    2020/01/29, ASP.NET Core 3.1, VS2019, NLog.Web.AspNetCore 4.9.0 摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站 ...

  6. vs2015 无法连接到已配置的开发web服务器

    问题一:vs2015 无法连接到已配置的开发web服务器 最靠谱方法如下:(和防火墙开启无关联, 注册表增加什么键值无关联 , 缺失asp.net core.Dll文件无关联 ) 分析,因为前提是你的 ...

  7. 学习GO第一天,自我感觉可麻利的开干了-GO语言配置、开发、服务器部署

    学习GO第一天,自我感觉可麻利的开干了-GO语言配置.开发.服务器部署 第一步下载 go sdk https://golang.org/dl/ https://storage.googleapis.c ...

  8. 高性能Web服务器Nginx的配置与部署研究(7)核心模块之主模块的非测试常用指令

    1. error_log 含义:指定存储错误日志的文件 语法:error_log <file> [debug|info|notice|warn|error|crit] 缺省:${prefi ...

  9. VS提示无法连接到已配置的开发web服务器的解决方法

    VS2013每次启动项目调试好好的,今天出现了提示“提示无法连接到已配置的开发web服务器“,使用环境是本地IISExpress,操作系统为windows10,之前也出现过就是重启电脑又好了,这次是刚 ...

随机推荐

  1. CSS系列之后代选择器、子选择器和相邻兄弟选择器

    后代选择器比子选择器的范围大,包含子选择器,且包含子选择器的“子孙”选择器,后代选择器使用"空格"符号间隔选择器 子选择器:子选择器只是父选择器的一级子元素,使用"> ...

  2. ICEM-缺口圆柱

    原视频下载地址:https://pan.baidu.com/s/1bpahxd9 密码: bpp7

  3. 2019 SDN第6次上机作业

    1.作业要求: 作业链接 参考资料: Ryu控制器的API文档:ryu.app.ofctl_rest Ryu的拓扑展示 助教博客:基于RYU restful api实现的VLAN网络虚拟化 2.具体操 ...

  4. RocketMQ官方启动脚本不支持jdk11的修改

    以rocketmq4.5.0为例 主要涉及classpath和启动配置. nameserver: vim /opt/rocketmq/bin/runserver.sh 删除 -XX:+UseConcM ...

  5. db2常用操作命令

    1. 打开命令行窗口 #db2cmd 2. 打开控制中心 # db2cmd db2cc 3. 打开命令编辑器 db2cmd db2ce =====操作数据库命令===== 4. 启动数据库实例 #db ...

  6. Java3d 案例程序

    今天偶尔翻出了很久以前写的java3d程序,很怀念曾经探索java3d解析.渲染ifc数据的日子 package com.vfsd.test0621; import java.applet.Apple ...

  7. 持久化机器学习模型(joblib方式)

    import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression ...

  8. [LeetCode] 254. Factor Combinations 因子组合

    Numbers can be regarded as product of its factors. For example, 8 = 2 x 2 x 2; = 2 x 4. Write a func ...

  9. [LeetCode] 445. Add Two Numbers II 两个数字相加之二

    You are given two linked lists representing two non-negative numbers. The most significant digit com ...

  10. python爬虫4猫眼电影的Top100

    1 查看网页结构 (1)确定需要抓取的字段 电影名称 电影主演 电影上映时间 电影评分 (2) 分析页面结构 按住f12------->点击右上角(如下图2)---->鼠标点击需要观察的字 ...