在第一篇中我们构造了http消息的请求和响应结构,进行了请求报文的解析,响应报文的发送,就像一个人先有了四肢,这一节我们来构造最重要的部分,就是躯干。先来看设计图,再上代码,最后总结。 首先来看设计图: 先看一个自定义的XML文件,文件中定义了这个工程中需要用到的组件,这些组件可以定义在xml文件中,也可以定义在数据库中,也可以定义在属性文件中。就像绝大多数的JAVA框架发展史一样,现在大部分组件的定义都是用Annotation实现,由容器自动扫描加载了,具体用哪种方式定义,没有好坏的分别,在哪种场景下最合适就用哪种。 <?xml version="1.0" encoding="UTF-8"?> <common-server> <business-servlet project="googleServer" desc="business server...10.0.0.1"> <servlet name="GoogleServlet" class="nioHttpServer.GoogleServlet"/> <servlet name="MicroSoftServlet" class="nioHttpServer.MicroSoftServlet"/> <!-- ....more business servlets here....--> </business-servlet> <!-- ThreadPool,Class name and invoked method name,multiple.cpu * count --> <threadPools> <pool port="8081" timeout="10000" method="process" trcount="100"> nioHttpServer.CommonProcess </pool> <!-- example one begin--> <pool port="8082" timeout="10000" method="process" trcount="100"> nioHttpServer.GoogleProcess </pool> <!-- example two begin--> <pool port="8083" timeout="10000" method="process" trcount="100"> nioHttpServer.MicroSoftProcess </pool> </threadPools> <!-- business action class and method--> <services> <service method="personVerify" class="nioHttpServer.UserAction" desc="custom verify..."/> <!-- example one begin--> <service method="loginAction" class="nioHttpServer.UserAction" desc="custom login..."/> <!-- example two begin--> <service method="register" class="nioHttpServer.UserAction" desc="custom register..."/> </services> </common-server> 大概解释一下这个配置文件 第一部分是自定义Servlet,也就是这个工程的业务主调用类,一个业务模块可以定义一个servlet,属性project就可以用作是HTTP请求url中的第一个关键字。 第二部分是线程池,包括主线程类,线程池的属性信息,这里遵循Tomcat的设计原则,将servlet打入线程池执行,实现单Servlet多线程的模式。 第三部分是具体的业务Action定义,就是最具体的业务处理类,和业务处理方法。 代码部分,先来看第一个,Servlet的预加载类 package nioHttpServer; import javax.servlet.http.HttpServlet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class ServletContext { private static ServletContext context; public Map<String, Map<String, HttpServlet>> projectMap = new HashMap<>(); private ServletContext() { init(); } public static ServletContext getContext() { if (context == null) { context = new ServletContext(); } return ServletContext.context; } public void init() { try { Iterator it = BusinessServer.SERVER.servlertMap.keySet().iterator(); while (it.hasNext()) { String servName = it.next().toString(); String servClass = BusinessServer.SERVER.servlertMap.get(servName); Map<String, HttpServlet> map = new HashMap<>(); Class<?> servletClass = Class.forName(servClass); HttpServlet newInstance = (HttpServlet) servletClass.getConstructor(new Class[]{}).newInstance(new Object[]{}); map.put(servName, newInstance); projectMap.put(BusinessServer.SERVER.projectName, map); System.out.println("Servlet loaded successful:NAME=["+servName+"],[CLASS:"+servClass+"]"); } } catch (Exception e) { e.printStackTrace(); System.out.println("Servlet loaded Exception"); } } public Map<String, HttpServlet> getProject(String projectName) { return projectMap.get(projectName); } } 在这个类中,我会将系统启动时加载到内存中的XML配置文件拿出来,检索里面的Servlet信息,并依次实例化后存放在一个Map中等待后续使用,由于Servlet和Project名都已经在项目中配置,所以对非法链接和请求的屏蔽很容易做到。 Servlet有了,下一步就是执行这个Serlvet所需的线程 package nioHttpServer; import nioHttpServer.niosrv.NioHttpPrtWriter; import nioHttpServer.niosrv.NioHttpRequest; import nioHttpServer.niosrv.NioHttpResponse; import javax.servlet.http.HttpServlet; import java.util.Map; import java.util.UUID; public class CommonProcess implements Runnable { private HttpServlet servlet = null; private NioHttpRequest request; private NioHttpResponse response; public CommonProcess(NioHttpRequest request, NioHttpResponse response) { this.request = request; this.response = response; } public void process() { boolean flag = false; try { flag = request.parseHead(BusinessServer.SERVER.projectName); } catch (Exception e) { try { NioHttpPrtWriter writer = (NioHttpPrtWriter) response.getWriter(0); writer.writeAsString("Error During Read Head From Http Request!"); } catch (Exception ex) { ex.printStackTrace(); } e.printStackTrace(); System.out.println("HTTP Request read error,error HEAD=["+request.getNioHead()+"]"); } if (flag) { String project = request.getContextPath(); String servlet = request.getServletPath(); ServletContext container = BusinessServer.SERVER.servletContext; Map<String, HttpServlet> servletMap = container.getProject(project); if (null != servlet && !("").equals(servlet)) { this.servlet = servletMap.get(servlet); } else { this.servlet = servletMap.get(BusinessServer.SERVER.defaultName); } if (this.servlet != null) { try { BusinessServer.SERVER.submitServiceProcess(this); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Thread pool run error,Thread Name = [" + this.getClass().getSimpleName() + "],Exception=" + e.toString()); } } else { try { NioHttpPrtWriter writer = (NioHttpPrtWriter) response.getWriter(0); writer.writeAsString("Error Http Request Servlet Head! ServletName does not exist!"); } catch (Exception e) { e.printStackTrace(); } System.out.println("Socket closed normally,ERROR HEAD=["+request.getNioHead()+"]"); } } else { try { NioHttpPrtWriter writer = (NioHttpPrtWriter) response.getWriter(0); writer.writeAsString("Error Http Request Head!"); } catch (Exception e) { e.printStackTrace(); } System.out.println("Socket closed normally,ERROR HEAD=["+request.getNioHead()+"]"); } } public void run() { //Thread in thread pool,UUID to flag it as a unique thread UUID uuid = UUID.randomUUID(); try { System.out.println("[BEGIN] THREAD [" + uuid.toString() + "],THREAD NAME=["+Thread.currentThread().getName()+"]"); long start =System.currentTimeMillis(); this.servlet.service(request, response); long cost= System.currentTimeMillis()-start; System.out.println("[END] THREAD [" + uuid.toString() + "],"+cost+"] mil seconds,THREAD NAME=["+Thread.currentThread().getName()+"]"); } catch (Exception e) { e.printStackTrace(); try { NioHttpPrtWriter writer = (NioHttpPrtWriter) response.getWriter(0); writer.writeAsString("Exception Happened During The Process"); } catch (Exception ex) { ex.printStackTrace(); } System.out.println("EXCEPTION Thread[" + uuid.toString() + "] closed"); } } } 这是主线程类,主线程类可以配置多个,系统收到请求后,会调用这个类中的process方法,process方法会根据Http请求得到对应的Servlet信息,然后去Servlet上下文中获取合法的Servlet实例,如果找到匹配的Servlet,则将自己放入已经加载好的线程池中,并执行获取到的这个Servlet实例的Service方法。如果找不到匹配的Servlet实例,则返回错误信息。 Servlet有了,执行主线程也有了,现在写一个服务,启动监听端口吧 package nioHttpServer; import nioHttpServer.niosrv.NioHttpRequest; import nioHttpServer.niosrv.NioHttpResponse; import java.io.IOException; import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class ComNioServer { private Selector selector; private ServerSocketChannel serverSocketChannel; private String processClazz; private String processMethod; private Thread listenThread; public void nioServerInit(int port, int timeOut, String processClazz, String processMethod) throws Exception { this.processClazz = processClazz; this.processMethod = processMethod; try { synchronized (Selector.class) { selector = Selector.open(); } serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); ServerSocket socket = serverSocketChannel.socket(); socket.setSoTimeout(timeOut); socket.setReuseAddress(true); socket.bind(new InetSocketAddress(port)); System.out.println("Server Startup Success:PROCESS CLASS=["+processClazz+"];PORT=["+port+"];"); Thread listenThread = new Thread(new HandleTheSocketChannel()); listenThread.start(); System.out.println("Server listener Thread:THREAD ID=["+listenThread.getId()+"]**********Server Startup Successful**********"); } catch (IOException e) { e.printStackTrace(); System.out.println("Server listener Thread Startup **********Server Startup Fail!**********"); } } class HandleTheSocketChannel implements Runnable { @Override public void run() { try { startListen(); } catch (Exception x) { x.printStackTrace(); System.out.println( "Unable to run replication listener."); } } } private void startListen() { while (true) { try { Selector selector = this.selector; int sel = selector.select(); if (sel == 0) { continue; } Set<SelectionKey> selectKeys = selector.selectedKeys(); Iterator keyIter = selectKeys.iterator(); while (keyIter.hasNext()) { SelectionKey key = (SelectionKey) keyIter.next(); keyIter.remove(); if (!key.isValid()) { continue; } if (key.isAcceptable()) { acceptServerSocket(key); } if (key.isReadable()) { readServerSocketAndWriteBack(key); } else { key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE)); } } } catch (Throwable t) { t.printStackTrace(); } } } private void acceptServerSocket(SelectionKey key) { SocketChannel client = null; ServerSocketChannel server = (ServerSocketChannel) key.channel(); try { client = server.accept(); if (client == null) { return; } client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } catch (IOException e) { System.out.println("Socket listener startup Exception:" + e); try { client.close(); } catch (IOException e1) { e1.printStackTrace(); } } } private void readServerSocketAndWriteBack(SelectionKey key) { NioHttpRequest request = new NioHttpRequest(key); NioHttpResponse response = new NioHttpResponse(key, selector); try { Class clazz = Class.forName(processClazz); Class[] paramTypes = {NioHttpRequest.class, NioHttpResponse.class}; Object[] params = {request, response}; Object obj = clazz.getConstructor(paramTypes).newInstance(params); Method method = clazz.getMethod(processMethod); method.invoke(obj, null); } catch (Exception e) { e.printStackTrace(); System.out.println("Thread[" + processClazz + "]invoke Exception"); } } public Thread getListenThread() { return listenThread; } public void setListenThread(Thread listenThread) { this.listenThread = listenThread; } } 在这个类中我启动了Nio服务,服务的端口和各种属性信息,是通过配置文件加载,如果配置多个主线程类,多个端口,则系统启动时自动开启多个端口来监听请求,不同的端口执行不同的主线程类,每个主线程中都可以调用系统中的Servlet和Services。 Servlet有了,主线程有了,服务也有了,下面我们开始启动服务吧 package nioHttpServer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; public enum BusinessServer { SERVER(); public Map<String, String> servlertMap; public String projectName; public String defaultName; public ServletContext servletContext; public Map<String, ExecutorService> businessPoolMap = new HashMap<>(); private ExecutorService objectTasks; public void submitServiceProcess(CommonProcess process) { businessPoolMap.get(process.getClass().getSimpleName()).submit(process); } public static void main(String[] args) { try{ for(int i=0;i<2;i++){ ComNioServer server = new ComNioServer(); int port = 8080+i; server.nioServerInit(port, 3600, "nioHttpServer.CommonProcess", "process"); } }catch (Exception e){ e.printStackTrace(); } } } 在服务启动类中,我读取了配置文件中的Servlet信息,进行预先实例化,读取了线程池信息,在启动时开启线程池,最后在main方法中启动了2个端口,当然这里应该是读取配置文件启动的,最后看看启动效果,分别启动了8080端口和8081端口的两个监听。 最后来总结一下设计图 系统启动时加载配置文件,分别预加载自定义的Servlet和线程池,并定义好每个端口下的主线程类。然后启动服务,等待消息,收到请求后,根据报文信息获取到对应的Servlet,并在主线程中执行这个Servlet的Service方法,并打入线程池。 这样讲可能比较好理解,因为讲流程时是从前到后的,先收到请求,再一步一步处理。而设计开发时实际是逆向的,因为需要使用大量的反射机制完成动态的加载,最后再等待请求的到来。到这里,框架层面的代码就基本完成,下一篇讲讲怎么把实际的业务嵌入到这个结构中。 文章导航 Using Java NIO Socket as the underlying foundation, manually parse HTTP requests and responses, and implement a customized server similar to Tomcat (1) Java NIO Socket をベースに、Tomcat と同様のカスタマイズされた http サーバーを実装し、Tomcat を自分で開発する (2)