NIO开发Http服务器(4):Response封装和响应
最近学习了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类
1 public class Cookie { 2 3 private String name; 4 private String value; 5 private long age; 6 private String path = "/"; 7 private String domain; 8 9 public Cookie() { 10 super(); 11 } 12 13 public Cookie(String name, String value, long age) { 14 super(); 15 this.name = name; 16 this.value = value; 17 this.age = age; 18 } 19 20 // getter and setter 21 }
2、Response接口
该接口定义了Response对象需要有的核心方法
1 // 设置http响应状态码 2 void setResponseCode(int status); 3 4 // 设置http响应的Content-Type 5 void setContentType(String contentType); 6 7 // 设置header 8 void setHeader(String headerName, String headerValue); 9 10 // 添加一个cookie到响应中 11 void addCookie(Cookie cookie); 12 13 // 设置响应编码字符集 14 void setCharsetEncoding(String charsetName); 15 16 // 响应 17 void response(); 18 19 // 获取当前请求所对应的客户端socket通道 20 @Deprecated 21 SocketChannel getOut(); 22 23 // 把指定的字符串写入响应缓冲区 24 void print(String line); 25 26 // 把指定的字符串写入响应缓冲区,末尾有换行符 27 void println(String line);
二、HttpResponse实现类
1、核心字段
1 // 时间格式化工具 2 private static SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 4 5 // 编码字符集 6 private CharsetEncoder encoder; 7 8 // 响应的Content-Type 9 private String contentType = "text/html;charset=utf-8"; 10 11 // 响应状态码 12 private int status = 0; 13 14 // 响应头 15 private Map<String, String> headers = new HashMap<String, String>(); 16 17 // 响应cookie 18 private List<Cookie> cookies = new ArrayList<Cookie>(); 19 20 // 本地资源输入通道 21 private FileChannel in; 22 23 // 客户端输出通道 24 private SocketChannel out; 25 26 // 动态资源生成的数据 27 private StringBuilder content = new StringBuilder(); 28 29 // 获取服务器配置 30 HttpServerConfig config = HttpServerConfig.getInstance();
2、构造方法
提供两个构造方法
1 public HttpResponse(SocketChannel sChannel) { 2 // 获取GBK字符集 3 Charset c1 = Charset.forName(config.getResponseCharset()); 4 // 获取编码器 5 this.encoder = c1.newEncoder(); 6 // 获取Content-Type 7 this.setContentType(ContentTypeUtil.getContentType(ContentTypeUtil.HTML)); 8 this.headers.put("Date", sdf.format(new Date())); 9 this.headers.put("Server", "nginx"); 10 this.headers.put("Connection", "keep-alive"); 11 // 客户端输出通道 12 this.out = sChannel; 13 }
此方法初始化编码字符集、设置基础的响应头
下面的构造方法比前一个多了一些内容:根据资源uri获取本地资源输入通道、设置资源的Expires头,所以在请求静态资源时使用这个方法创建Response对象
1 public HttpResponse(Request req, SocketChannel sChannel) { 2 3 this(sChannel); 4 5 // 获取请求资源URI 6 String uri = req.getRequestURI(); 7 8 // 获取本地输入通道 9 this.getLocalFileChannel(uri); 10 11 // 设置Content-Type 12 this.setContentType(req.getContentType()); 13 14 // 设置静态资源过期响应头 15 int expires = config.getExpiresMillis(this.contentType); 16 if (expires > 0) { 17 long expiresTimeStamp = System.currentTimeMillis() + expires; 18 this.headers.put("Expires", sdf.format(new Date(expiresTimeStamp))); 19 } 20 }
3、从请求uri获取本地输入通道
这是一个私有方法,会尝试根据参数uri到站点root下面寻找资源文件,并且打开输入通道。
如果打开通道正常,则设置200响应码,设置Content-Length响应头。
如果抛出NoSuchFileException异常设置404响应码。
如果是其他的异常设置500响应码
1 private void getLocalFileChannel(String uri) { 2 // 打开本地文件 3 try { 4 this.in = FileChannel.open(Paths.get(config.getRoot(), uri), 5 StandardOpenOption.READ); 6 // 设置Content-Length响应头 7 this.setHeader("Content-Length", String.valueOf(in.size())); 8 // 设置响应状态码200 9 this.setResponseCode(ResponseUtil.RESPONSE_CODE_200); 10 } catch (NoSuchFileException e) { 11 // 没有本地资源被找到 12 // 设置响应状态码404 13 this.setResponseCode(ResponseUtil.RESPONSE_CODE_404); 14 // 关闭本地文件通道 15 this.closeLocalFileChannel(); 16 } catch (IOException e) { 17 // 打开资源时出现异常 18 // 设置响应状态码500 19 this.setResponseCode(ResponseUtil.RESPONSE_CODE_500); 20 // 关闭本地文件通道 21 this.closeLocalFileChannel(); 22 } 23 }
4、setCharsetEncoding方法
1 public void setCharsetEncoding(String charsetName) { 2 // 获取GBK字符集 3 Charset c1 = Charset.forName(charsetName); 4 // 获取编码器 5 this.encoder = c1.newEncoder(); 6 }
5、response方法
- 输出响应首行
- 输出响应头
- 输出cookie
- 打印一个空白行后,输出响应主体
- 最后关闭输入通道
1 public void response() { 2 try { 3 // 输出响应首行 4 this.writeResponseLine(); 5 // 输出Header 6 this.writeHeaders(); 7 // 输出全部cookie 8 this.writeCookies(); 9 10 // 再输出一个换行,目的是输出一个空白行,下面就是响应主体了 11 this.newLine(); 12 13 // 304 14 if (this.status == ResponseUtil.RESPONSE_CODE_304) { 15 return; 16 } 17 18 // 输出响应主体 19 if (in != null && in.size() > 0) { 20 // 输出本地资源 21 long size = in.size(); 22 long pos = 0; 23 long count = 0; 24 25 while (pos < size) { 26 count = size - pos > 31457280 ? 31457280 : size - pos; 27 pos += in.transferTo(pos, count, out); 28 } 29 } else { 30 // 输出动态程序解析后的字符串 31 this.write(content.toString()); 32 } 33 } catch (IOException e) { 34 } finally { 35 // 关闭本地文件通道 36 this.closeLocalFileChannel(); 37 } 38 }
6、writeResponseLine、writeHeaders、writeCookies方法
这几个私有方法分别用于输出响应首行、输出响应头和响应cookie
1 private void writeResponseLine() throws IOException { 2 this.write(ResponseUtil.getResponseLine(this.status)); 3 this.newLine(); 4 } 5 6 private void writeHeaders() throws IOException { 7 Set<Entry<String, String>> entrys = this.headers.entrySet(); 8 for (Iterator<Entry<String, String>> i = entrys.iterator(); i.hasNext();) { 9 Entry<String, String> entry = i.next(); 10 String headerContent = entry.getKey() + ": " + entry.getValue(); 11 this.write(headerContent); 12 this.newLine(); 13 } 14 } 15 16 private void writeCookies() throws IOException { 17 for (Cookie cookie : this.cookies) { 18 String name = cookie.getName(); 19 String value = cookie.getValue(); 20 if (StringUtil.isNullOrEmpty(name) 21 || StringUtil.isNullOrEmpty(value)) { 22 continue; 23 } 24 // 构造cookie响应头 25 StringBuilder s = new StringBuilder("Set-Cookie: "); 26 // cookie名字和值 27 s.append(name); 28 s.append("="); 29 s.append(value); 30 s.append("; "); 31 // 设置过期时间 32 long age = cookie.getAge(); 33 if (age > -1) { 34 long expiresTimeStamp = System.currentTimeMillis() + age; 35 s.append("Expires="); 36 s.append(sdf.format(new Date(expiresTimeStamp))); 37 s.append("; "); 38 } 39 // 设置path 40 String path = cookie.getPath(); 41 if (!StringUtil.isNullOrEmpty(path)) { 42 s.append("Path="); 43 s.append(path); 44 s.append("; "); 45 } 46 // 设置domain 47 String domain = cookie.getDomain(); 48 if (!StringUtil.isNullOrEmpty(domain)) { 49 s.append("Domain="); 50 s.append(domain); 51 s.append("; "); 52 } 53 // http only 54 s.append("HttpOnly"); 55 // 写到响应通道 56 this.write(s.toString()); 57 this.newLine(); 58 } 59 }
7、write和newLine方法
1 private void newLine() throws IOException { 2 this.write("\n"); 3 } 4 5 private void write(String content) throws IOException { 6 CharBuffer cBuf = CharBuffer.allocate(content.length()); 7 cBuf.put(content); 8 cBuf.flip(); 9 ByteBuffer bBuf = this.encoder.encode(cBuf); 10 this.out.write(bBuf); 11 }
newLine方法会输出一个换行符
write方法会把指定的参数字符串输出到响应输出通道