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

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

项目实现了静态资源(htmlcssjs和图片)和简单动态资源的处理,可以实现监听端口、部署目录、资源过期的配置。涉及到了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类

 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 }
View Code

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 }
View Code

7、write和newLine方法

 1 private void newLine() throws IOException {
 2     this.write("
");
 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方法会把指定的参数字符串输出到响应输出通道

原文地址:https://www.cnblogs.com/xugf/p/9603622.html