最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室。我们是做WEB开发的,整天围着tomcat、nginx转,所以选择了一个新的方向,就是自己开发一个简单的Http服务器,在总结Java NIO的同时,也加深一下对http协议的理解。
项目实现了静态资源(html、css、js和图片)和简单动态资源的处理,可以实现监听端口、部署目录、资源过期的配置。涉及到了NIO缓冲区、通道和网络编程的核心知识点,还是比较基础的。
本文主要讲解Http响应的封装和输出
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 setter21 }
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 @Deprecated21 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 // 响应cookie18 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-Type12 ????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 ????????// 设置响应状态码40413 ????????this.setResponseCode(ResponseUtil.RESPONSE_CODE_404);14 ????????// 关闭本地文件通道15 ????????this.closeLocalFileChannel();16 ????} catch (IOException e) {17 ????????// 打开资源时出现异常18 ????????// 设置响应状态码50019 ????????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 ????????// 30414 ????????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 ????????// 设置path40 ????????String path = cookie.getPath();41 ????????if (!StringUtil.isNullOrEmpty(path)) {42 ????????????s.append("Path=");43 ????????????s.append(path);44 ????????????s.append("; ");45 ????????}46 ????????// 设置domain47 ????????String domain = cookie.getDomain();48 ????????if (!StringUtil.isNullOrEmpty(domain)) {49 ????????????s.append("Domain=");50 ????????????s.append(domain);51 ????????????s.append("; ");52 ????????}53 ????????// http only54 ????????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方法会把指定的参数字符串输出到响应输出通道
NIO开发Http服务器(4):Response封装和响应
原文地址:https://www.cnblogs.com/xugf/p/9603622.html