目前市面上大多数C/S模式的交易类系统,一般都有两种方式。 一种是采用JAVA Socket或者JAVA NIO Socket来做客户端与服务端的通讯模块,自己组织报文结构,来完成交易信息的收发,这种开发模式较复杂,需要开发人员自己维护系统的通讯模块,报文的数据结构,加密模块等等。 第二种是采用http服务器,比如JAVA中最常用的Tomcat,客户端与服务端通过http消息来通讯,开发人员不需要关心服务端与客户端的链路细节,只需要维护好http消息的数据结构,然后按照标准来处理收发消息就可以。但这种模式需要开发人员多维护一个中间件,就是Tomcat。 我在工作中偶然看到一篇文章,是讲解如何自己构建和解析http请求和响应消息,于是笔者决定把http消息的构建和解析搭建在自己的NIO Server上,这样就可以完成一个基于Java NIO的http Server. 可以自己掌握通讯模块实现细节,并简化业务模块开发,只需要处理http请求和响应,同时可以实现main方法启动服务,减少对Tomcat的依赖和维护。实际上就是要实现一个简化版的Tomcat, 但这种服务的实用性和安全性和Tomcat当然是无法比的,所以这种模式只是用来给有兴趣的同学学习交流。 首先封装Http请求报文头信息,并解析存储在对象中 package niosrv; import java.io.BufferedReader; import java.io.IOException; /** * Parse and save HttpHead details */ public class NioHttpHead { public static final String ACCEPT = "Accept"; public static final String ACCEPT_LANGUAGE = "Accept-Language"; public static final String USER_AGENT = "User-Agent"; public static final String ACCEPT_ENCODING = "Accept-Encoding"; public static final String HOST = "Host"; public static final String DNT = "DNT"; public static final String CONNECTION = "Connection"; public static final String COOKIE = "Cookie"; private BufferedReader br; private String method; private String requestURL; private String protocol; private String agent; private String host; private int port; private String encoding; private String language; private String accept; private String dnt; private String connection; private String cookie; private String headStr; private String contentStr = ""; public NioHttpHead(BufferedReader br) { this.br = br; } public void parseHead() { String s = null; String content = ""; try { s = br.readLine(); String[] firstLine = s.split(" "); if (firstLine.length == 3) { this.method = firstLine[0].trim(); this.requestURL = firstLine[1].trim(); this.protocol = firstLine[2].trim(); } StringBuffer headBuffer = new StringBuffer(s + "\r\n"); s = br.readLine(); while (s != null && !s.equals("")) { String[] split = s.split(":"); switch (split[0].trim()) { case ACCEPT: { this.accept = split[1].trim(); } case ACCEPT_LANGUAGE: { this.language = split[1].trim(); break; } case USER_AGENT: { this.agent = split[1].trim(); break; } case ACCEPT_ENCODING: { this.encoding = split[1].trim(); break; } case HOST: { this.host = split[1].trim(); break; } case DNT: { this.dnt = split[1].trim(); break; } case CONNECTION: { this.connection = split[1].trim(); break; } case COOKIE: { this.cookie = split[1].trim(); break; } } s = br.readLine(); headBuffer.append(s + "\r\n"); } this.headStr = headBuffer.toString(); //content parse, put request content into content attribute content = br.readLine(); if (content != null && !content.equals("")) { StringBuffer contentBuffer = new StringBuffer(content); while (content != null && !content.equals("")) { content = br.readLine(); if (content != null && !content.equals("")) { contentBuffer.append(content); } } this.contentStr = contentBuffer.toString(); } System.out.println("Request HEAD message format:" + h eadBuffer.toString()); System.out.println("Request BODY message format:" + contentStr.toString()); } catch (IOException e) { e.printStackTrace(); System.out.println("Request HEAD message parse error:", e); System.out.println("Request BODY message parse error:", e); } } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getRequestURL() { return requestURL; } public void setRequestURL(String requestURL) { this.requestURL = requestURL; } public String getProtocol() { return protocol; } public void setProtocol(String protocol) { this.protocol = protocol; } public String getAgent() { return agent; } public void setAgent(String agent) { this.agent = agent; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getEncoding() { return encoding; } public void setEncoding(String encoding) { this.encoding = encoding; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public String getAccept() { return accept; } public void setAccept(String accept) { this.accept = accept; } public String getDnt() { return dnt; } public void setDnt(String dnt) { this.dnt = dnt; } public String getConnection() { return connection; } public void setConnection(String connection) { this.connection = connection; } public String getCookie() { return cookie; } public void setCookie(String cookie) { this.cookie = cookie; } public String getHeadStr() { return headStr; } public void setHeadStr(String headStr) { this.headStr = headStr; } public String getContentStr() { return contentStr; } public void setContentStr(String contentStr) { this.contentStr = contentStr; } } 重写Java 中的Writer类,用来将响应信息通过NIO Socket发回给客户端: package niosrv; import java.io.IOException; import java.io.Writer; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; public class NioHttpOutWriter extends Writer { //because of the pictures size are always too big, so the buffer size here is big ByteBuffer buffer = ByteBuffer.allocate(1024000); private SocketChannel channel; private Selector selector; private SelectionKey key; private String successHead; public NioHttpOutWriter(String successHeader, SelectionKey key, SocketChannel channel, Selector selector) { this.successHead = successHeader; this.key = key; this.channel = channel; this.selector = selector; } @Override public void write(char[] contentChar, int off, int len) throws IOException { System.out.println("Write Back By write(1,2,3)!"); String content = new String(contentChar); StringBuffer respStr = new StringBuffer(this.successHead.toString()); respStr.append(content); byte[] bytesBody = respStr.toString().getBytes(); buffer.put(bytesBody); buffer.flip(); channel.write(buffer); channel.register(selector, SelectionKey.OP_WRITE, this); key.cancel(); channel.shutdownOutput(); channel.close(); } @Override public void flush() throws IOException { } @Override public void close() throws IOException { } public ByteBuffer getBuffer() { return buffer; } public void setBuffer(ByteBuffer buffer) { this.buffer = buffer; } } 重写Java 中的PrintWriter类,用来将响应信息以文本的形式发回给客户端,比如JSON或者加密后的密文文本 package niosrv; import java.io.PrintWriter; import java.io.Writer; public class NioHttpPrtWriter extends PrintWriter { public NioHttpPrtWriter(Writer write) { super(write); } public void writeAsString(String content) throws Exception { super.write(content.toCharArray(), 0, content.length()); } @Override public void print(String s) { super.print(s); } } 实现Servlet中的HttpServletRequest接口,实现自己的NIO Http Request请求处理类, 在这个实现类中一共有三个动作需要做: 1,通过NIO Socket 收取Http报文。 2, 解析报文头,将报文头和报文体分别存储。 3,解析报文体,获得报文中的请求参数,请求参数支持三种模式,分别是JSON模式,URL拼装,和无参模式。 package niosrv; import com.alibaba.fastjson.JSONObject; import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.security.Principal; import java.util.*; public class NioHttpRequest implements HttpServletRequest { private SocketChannel channel; private NioHttpHead head; private Map<String, String> params = new HashMap<>(); private String context; public byte[] reqContent; private SelectionKey key; public NioHttpRequest(SelectionKey key) { this.key = key; this.channel = (SocketChannel) this.key.channel(); } public boolean parseHead(String projectName) throws Exception { String receive = receive(channel); if (null == receive || ("").equals(receive)) { return false; } //parse the request head BufferedReader br = new BufferedReader(new StringReader(receive)); this.head = new NioHttpHead(br); this.head.parseHead(); br.close(); //parse the request content String content = this.head.getContentStr(); boolean flag = parseParams(projectName, content); return true && flag; } private boolean parseParams(String projectName, String content) { String url = this.head.getRequestURL(); //Block illegal request URLs if (!url.substring(1).startsWith(projectName)) { return false; } //The parameters are concatenated with the original URL if (url.indexOf("?") > 0) { this.context = url.substring(0, url.indexOf("?")); String params = url.substring(url.indexOf("?") + 1, url.length()); String[] split = params.split("&"); for (String param : split) { String[] split2 = param.split("="); this.params.put(split2[0], split2[1]); } reqContent = params.getBytes(); //The parameter is stream mode, the structure is JSON or original mode x=1&y=2 } else if (content != null && !("").equals(content)) { this.context = url.substring(url.indexOf("/"), url.length()); try { if ((!content.contains("{") && !content.contains("}")) || content.indexOf("&") > 0) { String[] split = content.split("&"); for (String param : split) { String[] split2 = param.split("="); this.params.put(split2[0], split2[1]); } } else { JSONObject json = JSONObject.parseObject(content); for (String key : json.keySet()) { this.params.put(key.toString(), json.get(key).toString()); } } reqContent = content.getBytes(); } catch (Exception e) { e.printStackTrace(); return false; } } //no parameters this.context = url.substring(url.indexOf("/"), url.length()); return true; } private String receive(SocketChannel socketChannel) throws Exception { byte[] bytes = null; ByteBuffer receiveBuffer = null; ByteBuffer buffer = ByteBuffer.allocate(1024000); if (socketChannel.isOpen() && socketChannel.isConnected()) { int nBytes = 0; buffer.clear(); try { nBytes = socketChannel.read(buffer); } catch (IOException e) { System.out.println("Socket Channel Can Not Read Buffer From Request,Socket Closed By Client In Exception!"); //If the client is closed abnormally, it will also be closed here key.cancel(); if (channel.isOpen()) { channel.shutdownOutput(); channel.close(); } return null; } buffer.flip();//open state if (nBytes > 0) { receiveBuffer = ByteBuffer.allocate(nBytes); receiveBuffer.clear(); buffer.get(receiveBuffer.array()); receiveBuffer.flip(); } if (receiveBuffer != null) { bytes = receiveBuffer.array(); return new String(bytes); } else { System.out.println("Socket Channel Can Not Read Buffer From Request,Socket Channel Closed By Client Unexpected!"); //If the client is closed abnormally, it will also be closed here key.channel().close(); key.cancel(); if (channel.isOpen()) { channel.shutdownOutput(); channel.close(); } return null; } } else { System.out.println("Socket Channel Closed By Client Unexpected,Interrupt The Thread immidiately,!"); return null; } } public byte[] getReqContent() { return reqContent; } @Override public String getParameter(String arg0) { return this.params.get(arg0); } @Override public String getContextPath() { this.context = context.substring(1, context.length()); if (this.context.indexOf("/") > 0) { return this.context.substring(0, context.indexOf("/")); } else { return this.context; } } @Override public String getServletPath() { if (this.context.indexOf("/") == 2) { return this.context.substring(context.indexOf("/") + 1, context.lastIndexOf("/")); } else { return ""; } } public String getNioHead() { if (null == head) { return "[error http head]"; } else if (null == head.getHeadStr()) { return "[error http head]"; } else { return head.getHeadStr(); } } @Override public String getAuthType() { return null; } @Override public Cookie[] getCookies() { return new Cookie[0]; } @Override public long getDateHeader(String s) { return 0; } @Override public String getHeader(String s) { return null; } @Override public Enumeration<String> getHeaders(String s) { return null; } @Override public Enumeration<String> getHeaderNames() { return null; } @Override public int getIntHeader(String s) { return 0; } @Override public String getMethod() { return this.head.getMethod(); } @Override public String getPathInfo() { return null; } @Override public String getPathTranslated() { return null; } @Override public String getQueryString() { return null; } @Override public String getRemoteUser() { return null; } @Override public boolean isUserInRole(String s) { return false; } @Override public Principal getUserPrincipal() { return null; } @Override public String getRequestedSessionId() { return null; } @Override public String getRequestURI() { String wholeUrl = this.head.getRequestURL(); if (wholeUrl.contains("?")) { return wholeUrl.substring(wholeUrl.lastIndexOf("/") + 1, wholeUrl.indexOf("?")); } else { return wholeUrl.substring(wholeUrl.lastIndexOf("/") + 1); } } @Override public StringBuffer getRequestURL() { return new StringBuffer(this.head.getRequestURL()); } @Override public HttpSession getSession(boolean b) { return null; } @Override public HttpSession getSession() { return null; } @Override public String changeSessionId() { return null; } @Override public boolean isRequestedSessionIdValid() { return false; } @Override public boolean isRequestedSessionIdFromCookie() { return false; } @Override public boolean isRequestedSessionIdFromURL() { return false; } @Override public boolean isRequestedSessionIdFromUrl() { return false; } @Override public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { return false; } @Override public void login(String s, String s2) throws ServletException { } @Override public void logout() throws ServletException { } @Override public Collection<Part> getParts() throws IOException, ServletException { return null; } @Override public Part getPart(String s) throws IOException, ServletException { return null; } @Override public <T extends HttpUpgradeHandler> T upgrade(Class<T> tClass) throws IOException, ServletException { return null; } @Override public Object getAttribute(String s) { return null; } @Override public Enumeration<String> getAttributeNames() { return null; } @Override public String getCharacterEncoding() { return null; } @Override public void setCharacterEncoding(String s) throws UnsupportedEncodingException { } @Override public int getContentLength() { if (reqContent == null) { return 0; } else { String content = new String(reqContent); return content.length(); } } @Override public long getContentLengthLong() { return 0; } @Override public String getContentType() { return null; } @Override public ServletInputStream getInputStream() throws IOException { return null; } @Override public Enumeration<String> getParameterNames() { return null; } @Override public String[] getParameterValues(String s) { return new String[0]; } @Override public Map<String, String[]> getParameterMap() { return null; } @Override public String getProtocol() { return this.head.getProtocol(); } @Override public String getScheme() { return null; } @Override public String getServerName() { return null; } @Override public int getServerPort() { return 0; } @Override public BufferedReader getReader() throws IOException { return null; } @Override public String getRemoteAddr() { return channel.socket().getInetAddress().getHostAddress(); } @Override public String getRemoteHost() { return channel.socket().getInetAddress().getHostAddress(); } @Override public void setAttribute(String s, Object o) { } @Override public void removeAttribute(String s) { } @Override public Locale getLocale() { return null; } @Override public Enumeration<Locale> getLocales() { return null; } @Override public boolean isSecure() { return false; } @Override public RequestDispatcher getRequestDispatcher(String s) { return null; } @Override public String getRealPath(String s) { return null; } @Override public int getRemotePort() { return 0; } @Override public String getLocalName() { return null; } @Override public String getLocalAddr() { return null; } @Override public int getLocalPort() { return 0; } @Override public ServletContext getServletContext() { return null; } @Override public AsyncContext startAsync() throws IllegalStateException { return null; } @Override public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { return null; } @Override public boolean isAsyncStarted() { return false; } @Override public boolean isAsyncSupported() { return false; } @Override public AsyncContext getAsyncContext() { return null; } @Override public DispatcherType getDispatcherType() { return null; } } 实现Servlet中的HttpServletResponse接口,实现自己的NIO Http Response响应处理类, 在Http相应类中主要构建响应的Http head, 并等待和响应内容一起写回给客户端: package niosrv; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.text.SimpleDateFormat; import java.util.*; public class NioHttpResponse implements HttpServletResponse { private SocketChannel channel; private SelectionKey key; private Selector selector; public static final int HTTP_STATUS_SUCCESS = 200; public static final int HTTP_STATUS_NOT_FOUND = 404; //head message private final Map<String, String> headers = new HashMap<String, String>(); private int responseCode = HTTP_STATUS_SUCCESS; private String responseInfo = "OK"; private int responseError = HTTP_STATUS_NOT_FOUND; private String responseErrInfo = "SYS_ERROR"; private String successHeader; public NioHttpResponse(SelectionKey key, Selector selector) { this.key = key; this.channel = (SocketChannel) this.key.channel(); this.selector = selector; setUpHeader(); } private void setUpHeader() { headers.put("Server", "HttpServer (Nio HttpServer 1.0)"); headers.put("User-Agent", "*/*"); headers.put("Content-Type", getContentType()); headers.put("Connection", "keep-alive"); String date = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US).format(new Date()); headers.put("Date", date); } private String header2String(int resCode) { StringBuffer head = new StringBuffer("HTTP/1.1"); head.append(" ").append(resCode == 0 ? responseCode : responseError).append(" ").append(resCode == 0 ? responseInfo : responseErrInfo).append("\r\n"); head.append("Date:").append(" ").append(headers.get("Date")).append("\r\n"); head.append("Server:").append(" ").append(headers.get("Server")).append("\r\n"); head.append("User-Agent:").append(" ").append(headers.get("User-Agent")).append("\r\n"); head.append("Content-Type:").append(" ").append(headers.get("Content-Type")).append("\r\n"); head.append("Connection:").append(" ").append(headers.get("Connection")).append("\r\n"); head.append("\r\n"); return head.toString(); } public void setContentType(String contentType) { headers.put("Content-Type", contentType); } public PrintWriter getWriter(int respCode) throws IOException { this.successHeader = header2String(respCode); return getWriter(); } @Override public PrintWriter getWriter() throws IOException { NioHttpPrtWriter printWrite = new NioHttpPrtWriter(new NioHttpOutWriter( this.successHeader, key, channel, selector)); return printWrite; } @Override public void addCookie(Cookie cookie) { } @Override public boolean containsHeader(String s) { return false; } @Override public String encodeURL(String s) { return null; } @Override public String encodeRedirectURL(String s) { return null; } @Override public String encodeUrl(String s) { return null; } @Override public String encodeRedirectUrl(String s) { return null; } @Override public void sendError(int i, String s) throws IOException { } @Override public void sendError(int i) throws IOException { } @Override public void sendRedirect(String s) throws IOException { } @Override public void setDateHeader(String s, long l) { } @Override public void addDateHeader(String s, long l) { } @Override public void setHeader(String s, String s2) { } @Override public void addHeader(String s, String s2) { } @Override public void setIntHeader(String s, int i) { } @Override public void addIntHeader(String s, int i) { } @Override public void setStatus(int i) { } @Override public void setStatus(int i, String s) { } @Override public int getStatus() { return 0; } @Override public String getHeader(String s) { return null; } @Override public Collection<String> getHeaders(String s) { return null; } @Override public Collection<String> getHeaderNames() { return null; } @Override public String getCharacterEncoding() { return null; } @Override public String getContentType() { return headers.get("Content-Type"); } @Override public ServletOutputStream getOutputStream() throws IOException { return null; } @Override public void setCharacterEncoding(String s) { } @Override public void setContentLength(int i) { } @Override public void setContentLengthLong(long l) { } @Override public void setBufferSize(int i) { } @Override public int getBufferSize() { return 0; } @Override public void flushBuffer() throws IOException { } @Override public void resetBuffer() { } @Override public boolean isCommitted() { return false; } @Override public void reset() { } @Override public void setLocale(Locale locale) { } @Override public Locale getLocale() { return null; } } 以上就是第一节的内容,完成了以Nio为底层实现的Http请求和响应的解析和构建, 下一节继续后面的内容。 文章导航 How to initialize auto-increment ID when Mysql table has foreign key association Java NIO Socketに基づいて、HTTP リクエストとレスポンスを手動で解析し、Tomcatと同様のカスタマイズされたhttpサーバーを実装します(1)