最近学习了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. hive集成kerberos

    1.票据的生成 kdc服务器操作,生成用于hive身份验证的principal 1.1.创建principal # kadmin.local -q “addprinc -randkey hive/yj ...

  2. python3 Paramiko模块学习

    简介 ssh是一个协议,OpenSSH是其中一个开源实现,paramiko是Python的一个库,实现了SSHv2协议(底层使用cryptography). 有了Paramiko以后,我们就可以在Py ...

  3. Kubernetes kubectl 命令概述

    kubectl用于运行Kubernetes集群命令的管理工具. 语法 kubectl [command] [TYPE] [NAME] [flags] command:指定要在一个或多个资源执行的操作 ...

  4. 20189220 余超《Linux内核原理与分析》第九周作业

    理解进程调度时机跟踪分析进程调度与进程切换的过程 本章的基础知识总结 一般来说,进程调度分为三种类型:中断处理过程(包括时钟中断.I/O 中断.系统调用和异常)中,直接调用schedule,或者返回用 ...

  5. Lararel安装和虚拟主机配置

    Laravel 对系统有些要求,当然,所有这些要求 Laravel Homestead 虚拟机都能满足,因此强烈推荐你使用 Homestead 作为你的开发环境. 当然,假如你不使用 Homestea ...

  6. 菜鸟学IT之分布式文件系统

    作业来源:https://edu.cnblogs.com/campus/gzcc/GZCC-16SE1/homework/3310 1.目录操作 在HDFS中为hadoop用户创建一个用户目录(had ...

  7. 安装mininet 一直显示 ‘Cloning into openflow'

    问题描述. 安装mininet卡在了下载openflow. git clone --branch 2.2.2 git@github.com:mininet/mininet.git ,然后输入命令./i ...

  8. Unity内存优化之视频讲解

    视频为中文讲解,mp4格式,大小3.05GB 目录   扫码时备注或说明中留下邮箱 付款后如未回复请至https://shop135452397.taobao.com/ 联系店主

  9. cas 3.5.3服务器搭建+spring boot集成+shiro模拟登录(不修改现有shiro认证架构)

    因为现有系统外部接入需要,需要支持三方单点登录.由于系统本身已经是微服务架构,由多个业务独立的子系统组成,所以有自己的用户认证微服务(不是cas,我们基础设施已经够多了,现在能不增加就不增加).但是因 ...

  10. 运行okvis-mono

    ./build/okvis_app_synchronous config/config_fpga_p2_euroc1.yaml ../mav0