目前市面上大多数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请求和响应的解析和构建, 下一节继续后面的内容。


发表回复

Thanks for your support to bet365fans!