最近学习了Java NIO技术,觉得不能再去写一些Hello World的学习demo了,而且也不想再像学习IO时那样编写一个控制台(或者带界面)聊天室。我们是做WEB开发的,整天围着tomcat、nginx转,所以选择了一个新的方向,就是自己开发一个简单的Http服务器,在总结Java NIO的同时,也加深一下对http协议的理解。
项目实现了静态资源(html、css、js和图片)和简单动态资源的处理,可以实现监听端口、部署目录、资源过期的配置。涉及到了NIO缓冲区、通道和网络编程的核心知识点,还是比较基础的。
本文是这个系列文章的最后一篇,主要介绍HttpServer类,该类作用是:
- 打开Selector和ServerSocketChannel,根据HttpServerConfig配置启动监听
- 接收请求连接
- 开启线程读取请求数据、处理请求
文章目录:
NIO开发Http服务器(3):核心配置和Request封装
NIO开发Http服务器(5-完结):HttpServer服务器类
Github地址:
https://github.com/xuguofeng/http-server
1、启动监听
1 selector = Selector.open(); 2 3 // 打开服务端socket通道 4 ServerSocketChannel ssc = ServerSocketChannel.open(); 5 // 设置非阻塞 6 ssc.configureBlocking(false); 7 // 绑定本地端口 8 ssc.bind(new InetSocketAddress(config.getServerPort())); 9 // 把通道注册到Selector 10 ssc.register(selector, SelectionKey.OP_ACCEPT);
2、轮询
1 while (true) { 2 3 int s = selector.select(); 4 // 如果没有就绪的通道直接跳过 5 if (s <= 0) { 6 continue; 7 } 8 // 获取已经就绪的通道的SelectionKey的集合 9 Iterator<SelectionKey> i = selector.selectedKeys().iterator(); 10 11 while (i.hasNext()) { 12 13 // 获取当前遍历到的SelectionKey 14 SelectionKey sk = i.next(); 15 16 // 可连接状态 17 if (sk.isValid() && sk.isAcceptable()) { 18 19 } else if (sk.isValid() && sk.isReadable()) {// 可读取状态 20 21 } 22 i.remove(); 23 } 24 }
3、接收和读取请求数据
接收请求
1 ServerSocketChannel server = (ServerSocketChannel) sk.channel(); 2 SocketChannel clientChannel; 3 try { 4 // 获取客户端channel 5 clientChannel = server.accept(); 6 // 设置非阻塞 7 clientChannel.configureBlocking(false); 8 // 把通道注册到Selector 9 clientChannel.register(selector, SelectionKey.OP_READ); 10 } catch (Exception e) { 11 }
读取数据
1 // 获取通道 2 SocketChannel sChannel = (SocketChannel) sk.channel(); 3 if (socketChannels.get(sChannel.hashCode()) == null) { 4 socketChannels.put(sChannel.hashCode(), sChannel); 5 tp.execute(new RequestHandler(sk)); 6 }
4、请求处理
这是这个类的核心内容,使用RequestHandler处理请求
具体实现如下:
- 从输入通道读取数据,根据配置的解码字符集进行解码
- 创建Request对象
- 尝试根据uri获取动态请求处理类,如果是动态请求,就实例化Servlet对象,调用service方法处理请求
- 输出响应
- 最后关闭客户端输出通道
1 SocketChannel sChannel = null; 2 try { 3 // 获取通道 4 sChannel = (SocketChannel) sk.channel(); 5 // 声明保存客户端请求数据的缓冲区 6 ByteBuffer buf = ByteBuffer.allocate(8192); 7 // 读取数据并解析为字符串 8 String requestBody = null; 9 int len = 0; 10 if ((len = sChannel.read(buf)) > 0) { 11 buf.flip(); 12 requestBody = new String(buf.array(), 0, len); 13 buf.clear(); 14 } 15 if (requestBody == null) { 16 return; 17 } 18 19 // 请求解码 20 requestBody = URLDecoder.decode(requestBody, config.getRequestCharset()); 21 22 // 创建请求对象 23 Request req = new HttpRequest(requestBody); 24 25 // 关闭输入 26 sChannel.shutdownInput(); 27 28 // 根据uri获取处理请求的Servlet类型 29 Class<? extends Servlet> servletClass = config.getServlet(req.getRequestURI()); 30 31 // 创建响应对象 32 Response resp = null; 33 34 // 动态请求 35 if (servletClass != null) { 36 try { 37 Servlet servlet = servletClass.newInstance(); 38 resp = new HttpResponse(sChannel); 39 servlet.service(req, resp); 40 resp.setResponseCode(ResponseUtil.RESPONSE_CODE_200); 41 } catch (Exception e) { 42 resp.setResponseCode(ResponseUtil.RESPONSE_CODE_500); 43 } 44 } else { 45 // 静态请求 46 resp = new HttpResponse(req, sChannel); 47 } 48 49 // 测试,添加cookie 50 if (req.getCookies().isEmpty()) { 51 Cookie c = new Cookie("sessionId", UUID.randomUUID().toString(), 60000); 52 resp.addCookie(c); 53 Cookie c2 = new Cookie("sessionId2", UUID.randomUUID().toString(), 60000); 54 resp.addCookie(c2); 55 } 56 57 // 输出响应 58 resp.response(); 59 60 } catch (IOException e) { 61 } finally { 62 // 关闭通道 63 try { 64 sChannel.finishConnect(); 65 sChannel.close(); 66 socketChannels.remove(sChannel.hashCode()); 67 } catch (IOException e) { 68 } 69 }
5、Servlet接口
处理动态请求的接口,实现类需要在service方法中编写业务处理的程序
1 public interface Servlet { 2 3 void service(Request request, Response response) throws Exception; 4 }
然后在server.properties文件配置