最近学习了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

一、Response响应

1、Cookie类

 public class Cookie {

     private String name;
private String value;
private long age;
private String path = "/";
private String domain; public Cookie() {
super();
} public Cookie(String name, String value, long age) {
super();
this.name = name;
this.value = value;
this.age = age;
} // getter and setter
}

2、Response接口

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

 // 设置http响应状态码
void setResponseCode(int status); // 设置http响应的Content-Type
void setContentType(String contentType); // 设置header
void setHeader(String headerName, String headerValue); // 添加一个cookie到响应中
void addCookie(Cookie cookie); // 设置响应编码字符集
void setCharsetEncoding(String charsetName); // 响应
void response(); // 获取当前请求所对应的客户端socket通道
@Deprecated
SocketChannel getOut(); // 把指定的字符串写入响应缓冲区
void print(String line); // 把指定的字符串写入响应缓冲区,末尾有换行符
void println(String line);

二、HttpResponse实现类

1、核心字段

 // 时间格式化工具
private static SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); // 编码字符集
private CharsetEncoder encoder; // 响应的Content-Type
private String contentType = "text/html;charset=utf-8"; // 响应状态码
private int status = 0; // 响应头
private Map<String, String> headers = new HashMap<String, String>(); // 响应cookie
private List<Cookie> cookies = new ArrayList<Cookie>(); // 本地资源输入通道
private FileChannel in; // 客户端输出通道
private SocketChannel out; // 动态资源生成的数据
private StringBuilder content = new StringBuilder(); // 获取服务器配置
HttpServerConfig config = HttpServerConfig.getInstance();

2、构造方法

提供两个构造方法

 public HttpResponse(SocketChannel sChannel) {
// 获取GBK字符集
Charset c1 = Charset.forName(config.getResponseCharset());
// 获取编码器
this.encoder = c1.newEncoder();
// 获取Content-Type
this.setContentType(ContentTypeUtil.getContentType(ContentTypeUtil.HTML));
this.headers.put("Date", sdf.format(new Date()));
this.headers.put("Server", "nginx");
this.headers.put("Connection", "keep-alive");
// 客户端输出通道
this.out = sChannel;
}

此方法初始化编码字符集、设置基础的响应头

下面的构造方法比前一个多了一些内容:根据资源uri获取本地资源输入通道、设置资源的Expires头,所以在请求静态资源时使用这个方法创建Response对象

 public HttpResponse(Request req, SocketChannel sChannel) {

     this(sChannel);

     // 获取请求资源URI
String uri = req.getRequestURI(); // 获取本地输入通道
this.getLocalFileChannel(uri); // 设置Content-Type
this.setContentType(req.getContentType()); // 设置静态资源过期响应头
int expires = config.getExpiresMillis(this.contentType);
if (expires > 0) {
long expiresTimeStamp = System.currentTimeMillis() + expires;
this.headers.put("Expires", sdf.format(new Date(expiresTimeStamp)));
}
}

3、从请求uri获取本地输入通道

这是一个私有方法,会尝试根据参数uri到站点root下面寻找资源文件,并且打开输入通道。

如果打开通道正常,则设置200响应码,设置Content-Length响应头。

如果抛出NoSuchFileException异常设置404响应码。

如果是其他的异常设置500响应码

 private void getLocalFileChannel(String uri) {
// 打开本地文件
try {
this.in = FileChannel.open(Paths.get(config.getRoot(), uri),
StandardOpenOption.READ);
// 设置Content-Length响应头
this.setHeader("Content-Length", String.valueOf(in.size()));
// 设置响应状态码200
this.setResponseCode(ResponseUtil.RESPONSE_CODE_200);
} catch (NoSuchFileException e) {
// 没有本地资源被找到
// 设置响应状态码404
this.setResponseCode(ResponseUtil.RESPONSE_CODE_404);
// 关闭本地文件通道
this.closeLocalFileChannel();
} catch (IOException e) {
// 打开资源时出现异常
// 设置响应状态码500
this.setResponseCode(ResponseUtil.RESPONSE_CODE_500);
// 关闭本地文件通道
this.closeLocalFileChannel();
}
}

4、setCharsetEncoding方法

 public void setCharsetEncoding(String charsetName) {
// 获取GBK字符集
Charset c1 = Charset.forName(charsetName);
// 获取编码器
this.encoder = c1.newEncoder();
}

5、response方法

  • 输出响应首行
  • 输出响应头
  • 输出cookie
  • 打印一个空白行后,输出响应主体
  • 最后关闭输入通道
 public void response() {
try {
// 输出响应首行
this.writeResponseLine();
// 输出Header
this.writeHeaders();
// 输出全部cookie
this.writeCookies(); // 再输出一个换行,目的是输出一个空白行,下面就是响应主体了
this.newLine(); //
if (this.status == ResponseUtil.RESPONSE_CODE_304) {
return;
} // 输出响应主体
if (in != null && in.size() > 0) {
// 输出本地资源
long size = in.size();
long pos = 0;
long count = 0; while (pos < size) {
count = size - pos > 31457280 ? 31457280 : size - pos;
pos += in.transferTo(pos, count, out);
}
} else {
// 输出动态程序解析后的字符串
this.write(content.toString());
}
} catch (IOException e) {
} finally {
// 关闭本地文件通道
this.closeLocalFileChannel();
}
}

6、writeResponseLine、writeHeaders、writeCookies方法

这几个私有方法分别用于输出响应首行、输出响应头和响应cookie

 private void writeResponseLine() throws IOException {
this.write(ResponseUtil.getResponseLine(this.status));
this.newLine();
} private void writeHeaders() throws IOException {
Set<Entry<String, String>> entrys = this.headers.entrySet();
for (Iterator<Entry<String, String>> i = entrys.iterator(); i.hasNext();) {
Entry<String, String> entry = i.next();
String headerContent = entry.getKey() + ": " + entry.getValue();
this.write(headerContent);
this.newLine();
}
} private void writeCookies() throws IOException {
for (Cookie cookie : this.cookies) {
String name = cookie.getName();
String value = cookie.getValue();
if (StringUtil.isNullOrEmpty(name)
|| StringUtil.isNullOrEmpty(value)) {
continue;
}
// 构造cookie响应头
StringBuilder s = new StringBuilder("Set-Cookie: ");
// cookie名字和值
s.append(name);
s.append("=");
s.append(value);
s.append("; ");
// 设置过期时间
long age = cookie.getAge();
if (age > -1) {
long expiresTimeStamp = System.currentTimeMillis() + age;
s.append("Expires=");
s.append(sdf.format(new Date(expiresTimeStamp)));
s.append("; ");
}
// 设置path
String path = cookie.getPath();
if (!StringUtil.isNullOrEmpty(path)) {
s.append("Path=");
s.append(path);
s.append("; ");
}
// 设置domain
String domain = cookie.getDomain();
if (!StringUtil.isNullOrEmpty(domain)) {
s.append("Domain=");
s.append(domain);
s.append("; ");
}
// http only
s.append("HttpOnly");
// 写到响应通道
this.write(s.toString());
this.newLine();
}
}

7、write和newLine方法

 private void newLine() throws IOException {
this.write("\n");
} private void write(String content) throws IOException {
CharBuffer cBuf = CharBuffer.allocate(content.length());
cBuf.put(content);
cBuf.flip();
ByteBuffer bBuf = this.encoder.encode(cBuf);
this.out.write(bBuf);
}

newLine方法会输出一个换行符

write方法会把指定的参数字符串输出到响应输出通道

NIO开发Http服务器(4):Response封装和响应的更多相关文章

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

    最近学习了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. Netty精粹之JAVA NIO开发需要知道的

    学习Netty框架以及相关源码也有一小段时间了,恰逢今天除夕,写篇文章总结一下.Netty是个高效的JAVA NIO框架,总体框架基于异步非阻塞的设计,基于网络IO事件驱动,主要贡献在于可以让用户基于 ...

  6. 使用C#开发HTTP服务器系列之访问主页

    各位朋友大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是http://qinyuanpei.com.在这个系列文章的第一篇中,我们着重认识和了解了HTTP协议,并在此基础上实现了一个可交互的W ...

  7. .Net Core 跨平台开发实战-服务器缓存:本地缓存、分布式缓存、自定义缓存

    .Net Core 跨平台开发实战-服务器缓存:本地缓存.分布式缓存.自定义缓存 1.概述 系统性能优化的第一步就是使用缓存!什么是缓存?缓存是一种效果,就是把数据结果存在某个介质中,下次直接重用.根 ...

  8. 一步一步开发Game服务器(三)加载脚本和服务器热更新(二)完整版

    上一篇文章我介绍了如果动态加载dll文件来更新程序 一步一步开发Game服务器(三)加载脚本和服务器热更新 可是在使用过程中,也许有很多会发现,动态加载dll其实不方便,应为需要预先编译代码为dll文 ...

  9. 使用Go开发web服务器

    原文链接 Go(Golang.org)是在标准库中提供HTTP协议支持的系统语言,通过他可以快速简单的开发一个web服务器.同时,Go语言为开发者提供了很多便利.这本篇博客中我们将列出使用Go开发HT ...

随机推荐

  1. sudo rm /var/cache/apt/archives/lock sudo rm /var/lib/dpkg/lock

    原因:历史软件(包)更新(安装)未完成就退出了系统 解决办法:杀死该进程 sudo rm /var/cache/apt/archives/lock sudo rm /var/lib/dpkg/lock ...

  2. Healthcare in Azure

  3. IntelliJ IDEA 2018.2,WebStorm 2018.2破解

    一.IntelliJ IDEA 2018.2.4破解: 可参考:https://www.cnblogs.com/iathanasy/p/9469280.html 二.WebStorm 2018.2.4 ...

  4. linux core 性能

    apt-get install lrzsz apt-get install vim apt-get install -y net-tools apt-get install -y procps htt ...

  5. [代码质量] Maintainability Index (MI)

    转载自: http://www.projectcodemeter.com/cost_estimation/help/GL_maintainability.htm ProjectCodeMeter Ma ...

  6. 升级ruby的版本 https://gems.ruby-china.com/

    升级ruby版本,有时候安装ruby的版本过低,需要进行升级,例如安装在centos6.7安装fpm需要ruby版本在1.9以上. 1.主机环境如下: 1 [root@test ~]# cat /et ...

  7. 【spring源码分析】IOC容器解析

    参考: https://www.iteye.com/topic/1121913(自动注入bean的扫描器) https://m.imooc.com/mip/article/34150(循环依赖的解决方 ...

  8. signal(SIGPIPE, SIG_IGN)(转)

    signal(SIGPIPE, SIG_IGN) 当服务器close一个连接时,若client端接着发数据.根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发 ...

  9. python 把带小数的浮点型字符串转换为整数的解决方案

    以下内容在python中完全可以接受: 将整数的字符串表示形式传递给 int 将float的字符串表示形式传递给 float 但是,如果你将float型的字符串传递给int将会得到错误. >&g ...

  10. 安卓之Android.mk多文件以及动态库编译

    1.多文件编译 多文件编译共有两种方式: (1) 在Android.mk中一一添加 LOCAL_PATH:= $(call my-dir) #定义当前模块的相对路径 include $(CLEAR_V ...