最近学习了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
一、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方法会把指定的参数字符串输出到响应输出通道