• 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方法会把指定的参数字符串输出到响应输出通道

  • 相关阅读:
    Java生成json
    WinForm程序执行JS代码的多种方法以及使用WebBrowser与JS交互
    聚集索引和非聚集索引的区别
    如何编写函数才能提高代码质量
    前端程序员应该知道的15个 jQuery 小技巧
    FileShare枚举的使用(文件读写锁)
    ASP.NET MVC 数据库依赖缓存的实现
    C# 调用一个按钮的Click事件(利用反射)
    解决报错“超时时间已到。超时时间已到,但是尚未从池中获取连接”的方案
    关于浏览器URL中出现会话验证字符说明
  • 原文地址:https://www.cnblogs.com/xugf/p/9603622.html
Copyright © 2020-2023  润新知