基于Mina的Http Server以及简单的Http请求客户端
目的:
Java平台下的内部组件之间的通信。
1.WebService 由于感觉本身Java平台下的Web Service标准就不够统一,相互之间的调用就会有一些问题,更不用说与.net等其他平台了。而且WebService也是对HTTP请求的一次封装,效率上肯定会有损失,所以就不考虑用WebService了。
2.Socket,包括Java原生的Socket API和nio,本身都很好,效率也会不错,它们之间的区别大概就是资源占用上了。但是使用Socket的通信,有几个比较复杂的地方是:1)协议解析,要订协议,解析及序列化2)粘包分包的处理(这个在长连接的情况下才会出现,可以不在考虑范围内)3)资源的管理,弄不好的话会导致CPU占用较高或者内存不知不觉泄露。
3.HTTP通信。由于应用是独立的,不能依托于Web容器。Java原生的HttpServer API好像不推荐使用(藏在好深的一个包里com.sun.net.httpserver.*)。
4.话说Mina的效率很高,是基于nio的异步通信,封装简化了好多。通过比较简单的包装就可以组成一个HTTP Server(下面例子中就是按照Mina官方提供的demo,自己改动了几点形成的)。然后HTTP的Client端也随便封装下就是了。
步骤
1.封装HTTP请求消息类和响应消息类
package com.ajita.httpserver; import java.util.Map; import java.util.Map.Entry; /** * 使用Mina解析出的HTTP请求对象 * * @author Ajita * */ public class HttpRequestMessage { /** * HTTP请求的主要属性及内容 */ private Map<String, String[]> headers = null; public Map<String, String[]> getHeaders() { return headers; } public void setHeaders(Map<String, String[]> headers) { this.headers = headers; } /** * 获取HTTP请求的Context信息 */ public String getContext() { String[] context = headers.get("Context"); return context == null ? "" : context[0]; } /** * 根据属性名称获得属性值数组第一个值,用于在url中传递的参数 */ public String getParameter(String name) { String[] param = headers.get("@".concat(name)); return param == null ? "" : param[0]; } /** * 根据属性名称获得属性值,用于在url中传递的参数 */ public String[] getParameters(String name) { String[] param = headers.get("@".concat(name)); return param == null ? new String[] {} : param; } /** * 根据属性名称获得属性值,用于请求的特征参数 */ public String[] getHeader(String name) { return headers.get(name); } @Override public String toString() { StringBuilder str = new StringBuilder(); for (Entry<String, String[]> e : headers.entrySet()) { str.append(e.getKey() + " : " + arrayToString(e.getValue(), ',') + "\n"); } return str.toString(); } /** * 静态方法,用来把一个字符串数组拼接成一个字符串 * * @param s要拼接的字符串数组 * @param sep数据元素之间的烦恼歌负 * @return 拼接成的字符串 */ public static String arrayToString(String[] s, char sep) { if (s == null || s.length == 0) { return ""; } StringBuffer buf = new StringBuffer(); if (s != null) { for (int i = 0; i < s.length; i++) { if (i > 0) { buf.append(sep); } buf.append(s[i]); } } return buf.toString(); } } package com.ajita.httpserver; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.apache.mina.core.buffer.IoBuffer; public class HttpResponseMessage { /** HTTP response codes */ public static final int HTTP_STATUS_SUCCESS = 200; public static final int HTTP_STATUS_NOT_FOUND = 404; /** Map<String, String> */ private final Map<String, String> headers = new HashMap<String, String>(); /** Storage for body of HTTP response. */ private final ByteArrayOutputStream body = new ByteArrayOutputStream(1024); private int responseCode = HTTP_STATUS_SUCCESS; public HttpResponseMessage() { // headers.put("Server", "HttpServer (" + Server.VERSION_STRING + ')'); headers.put("Server", "HttpServer (" + "Mina 2.0" + ')'); headers.put("Cache-Control", "private"); headers.put("Content-Type", "text/html; charset=iso-8859-1"); headers.put("Connection", "keep-alive"); headers.put("Keep-Alive", "200"); headers.put("Date", new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date())); headers.put("Last-Modified", new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date())); } public Map<String, String> getHeaders() { return headers; } public void setContentType(String contentType) { headers.put("Content-Type", contentType); } public void setResponseCode(int responseCode) { this.responseCode = responseCode; } public int getResponseCode() { return this.responseCode; } public void appendBody(byte[] b) { try { body.write(b); } catch (IOException ex) { ex.printStackTrace(); } } public void appendBody(String s) { try { body.write(s.getBytes()); } catch (IOException ex) { ex.printStackTrace(); } } public IoBuffer getBody() { return IoBuffer.wrap(body.toByteArray()); } public int getBodyLength() { return body.size(); } }
2.封装Mina的解析HTTP请求和发送HTTP响应的编码类和解码类
package com.ajita.httpserver; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.HashMap; import java.util.Map; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.filter.codec.demux.MessageDecoderAdapter; import org.apache.mina.filter.codec.demux.MessageDecoderResult; public class HttpRequestDecoder extends MessageDecoderAdapter { private static final byte[] CONTENT_LENGTH = new String("Content-Length:") .getBytes(); static String defaultEncoding; private CharsetDecoder decoder; public CharsetDecoder getDecoder() { return decoder; } public void setEncoder(CharsetDecoder decoder) { this.decoder = decoder; } private HttpRequestMessage request = null; public HttpRequestDecoder() { decoder = Charset.forName(defaultEncoding).newDecoder(); } public MessageDecoderResult decodable(IoSession session, IoBuffer in) { try { return messageComplete(in) ? MessageDecoderResult.OK : MessageDecoderResult.NEED_DATA; } catch (Exception ex) { ex.printStackTrace(); } return MessageDecoderResult.NOT_OK; } public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { HttpRequestMessage m = decodeBody(in); // Return NEED_DATA if the body is not fully read. if (m == null) { return MessageDecoderResult.NEED_DATA; } out.write(m); return MessageDecoderResult.OK; } /* * 判断HTTP请求是否完整,若格式有错误直接抛出异常 */ private boolean messageComplete(IoBuffer in) { int last = in.remaining() - 1; if (in.remaining() < 4) { return false; } // to speed up things we check if the Http request is a GET or POST if (in.get(0) == (byte) 'G' && in.get(1) == (byte) 'E' && in.get(2) == (byte) 'T') { // Http GET request therefore the last 4 bytes should be 0x0D 0x0A // 0x0D 0x0A return in.get(last) == (byte) 0x0A && in.get(last - 1) == (byte) 0x0D && in.get(last - 2) == (byte) 0x0A && in.get(last - 3) == (byte) 0x0D; } else if (in.get(0) == (byte) 'P' && in.get(1) == (byte) 'O' && in.get(2) == (byte) 'S' && in.get(3) == (byte) 'T') { // Http POST request // first the position of the 0x0D 0x0A 0x0D 0x0A bytes int eoh = -1; for (int i = last; i > 2; i--) { if (in.get(i) == (byte) 0x0A && in.get(i - 1) == (byte) 0x0D && in.get(i - 2) == (byte) 0x0A && in.get(i - 3) == (byte) 0x0D) { eoh = i + 1; break; } } if (eoh == -1) { return false; } for (int i = 0; i < last; i++) { boolean found = false; for (int j = 0; j < CONTENT_LENGTH.length; j++) { if (in.get(i + j) != CONTENT_LENGTH[j]) { found = false; break; } found = true; } if (found) { // retrieve value from this position till next 0x0D 0x0A StringBuilder contentLength = new StringBuilder(); for (int j = i + CONTENT_LENGTH.length; j < last; j++) { if (in.get(j) == 0x0D) { break; } contentLength.append(new String( new byte[] { in.get(j) })); } // if content-length worth of data has been received then // the message is complete return Integer.parseInt(contentLength.toString().trim()) + eoh == in.remaining(); } } } // the message is not complete and we need more data return false; } private HttpRequestMessage decodeBody(IoBuffer in) { request = new HttpRequestMessage(); try { request.setHeaders(parseRequest(new StringReader(in .getString(decoder)))); return request; } catch (CharacterCodingException ex) { ex.printStackTrace(); } return null; } private Map<String, String[]> parseRequest(StringReader is) { Map<String, String[]> map = new HashMap<String, String[]>(); BufferedReader rdr = new BufferedReader(is); try { // Get request URL. String line = rdr.readLine(); String[] url = line.split(" "); if (url.length < 3) { return map; } map.put("URI", new String[] { line }); map.put("Method", new String[] { url[0].toUpperCase() }); map.put("Context", new String[] { url[1].substring(1) }); map.put("Protocol", new String[] { url[2] }); // Read header while ((line = rdr.readLine()) != null && line.length() > 0) { String[] tokens = line.split(": "); map.put(tokens[0], new String[] { tokens[1] }); } // If method 'POST' then read Content-Length worth of data if (url[0].equalsIgnoreCase("POST")) { int len = Integer.parseInt(map.get("Content-Length")[0]); char[] buf = new char[len]; if (rdr.read(buf) == len) { line = String.copyValueOf(buf); } } else if (url[0].equalsIgnoreCase("GET")) { int idx = url[1].indexOf('?'); if (idx != -1) { map.put("Context", new String[] { url[1].substring(1, idx) }); line = url[1].substring(idx + 1); } else { line = null; } } if (line != null) { String[] match = line.split("\\&"); for (String element : match) { String[] params = new String[1]; String[] tokens = element.split("="); switch (tokens.length) { case 0: map.put("@".concat(element), new String[] {}); break; case 1: map.put("@".concat(tokens[0]), new String[] {}); break; default: String name = "@".concat(tokens[0]); if (map.containsKey(name)) { params = map.get(name); String[] tmp = new String[params.length + 1]; for (int j = 0; j < params.length; j++) { tmp[j] = params[j]; } params = null; params = tmp; } params[params.length - 1] = tokens[1].trim(); map.put(name, params); } } } } catch (IOException ex) { ex.printStackTrace(); } return map; } } package com.ajita.httpserver; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.apache.mina.core.buffer.IoBuffer; public class HttpResponseMessage { /** HTTP response codes */ public static final int HTTP_STATUS_SUCCESS = 200; public static final int HTTP_STATUS_NOT_FOUND = 404; /** Map<String, String> */ private final Map<String, String> headers = new HashMap<String, String>(); /** Storage for body of HTTP response. */ private final ByteArrayOutputStream body = new ByteArrayOutputStream(1024); private int responseCode = HTTP_STATUS_SUCCESS; public HttpResponseMessage() { // headers.put("Server", "HttpServer (" + Server.VERSION_STRING + ')'); headers.put("Server", "HttpServer (" + "Mina 2.0" + ')'); headers.put("Cache-Control", "private"); headers.put("Content-Type", "text/html; charset=iso-8859-1"); headers.put("Connection", "keep-alive"); headers.put("Keep-Alive", "200"); headers.put("Date", new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date())); headers.put("Last-Modified", new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date())); } public Map<String, String> getHeaders() { return headers; } public void setContentType(String contentType) { headers.put("Content-Type", contentType); } public void setResponseCode(int responseCode) { this.responseCode = responseCode; } public int getResponseCode() { return this.responseCode; } public void appendBody(byte[] b) { try { body.write(b); } catch (IOException ex) { ex.printStackTrace(); } } public void appendBody(String s) { try { body.write(s.getBytes()); } catch (IOException ex) { ex.printStackTrace(); } } public IoBuffer getBody() { return IoBuffer.wrap(body.toByteArray()); } public int getBodyLength() { return body.size(); } }
3.封装HTTP的Server类及HTTP的Handler处理接口,其中HttpHandler接口是要暴露给外部就行自定义处理的。
package com.ajita.httpserver; import java.io.IOException; import java.net.InetSocketAddress; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.transport.socket.nio.NioSocketAcceptor; public class HttpServer { /** Default HTTP port */ private static final int DEFAULT_PORT = 8080; private NioSocketAcceptor acceptor; private boolean isRunning; private String encoding; private HttpHandler httpHandler; public String getEncoding() { return encoding; } public void setEncoding(String encoding) { this.encoding = encoding; HttpRequestDecoder.defaultEncoding = encoding; HttpResponseEncoder.defaultEncoding = encoding; } public HttpHandler getHttpHandler() { return httpHandler; } public void setHttpHandler(HttpHandler httpHandler) { this.httpHandler = httpHandler; } /** * 启动HTTP服务端箭筒HTTP请求 * * @param port要监听的端口号 * @throws IOException */ public void run(int port) throws IOException { synchronized (this) { if (isRunning) { System.out.println("Server is already running."); return; } acceptor = new NioSocketAcceptor(); acceptor.getFilterChain().addLast( "protocolFilter", new ProtocolCodecFilter( new HttpServerProtocolCodecFactory())); // acceptor.getFilterChain().addLast("logger", new LoggingFilter()); ServerHandler handler = new ServerHandler(); handler.setHandler(httpHandler); acceptor.setHandler(handler); acceptor.bind(new InetSocketAddress(port)); isRunning = true; System.out.println("Server now listening on port " + port); } } /** * 使用默认端口8080 * * @throws IOException */ public void run() throws IOException { run(DEFAULT_PORT); } /** * 停止监听HTTP服务 */ public void stop() { synchronized (this) { if (!isRunning) { System.out.println("Server is already stoped."); return; } isRunning = false; try { acceptor.unbind(); acceptor.dispose(); System.out.println("Server is stoped."); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { int port = DEFAULT_PORT; for (int i = 0; i < args.length; i++) { if (args[i].equals("-port")) { port = Integer.parseInt(args[i + 1]); } } try { // Create an acceptor NioSocketAcceptor acceptor = new NioSocketAcceptor(); // Create a service configuration acceptor.getFilterChain().addLast( "protocolFilter", new ProtocolCodecFilter( new HttpServerProtocolCodecFactory())); acceptor.getFilterChain().addLast("logger", new LoggingFilter()); acceptor.setHandler(new ServerHandler()); acceptor.bind(new InetSocketAddress(port)); System.out.println("Server now listening on port " + port); } catch (Exception ex) { ex.printStackTrace(); } } } package com.ajita.httpserver; import org.apache.mina.filter.codec.demux.DemuxingProtocolCodecFactory; public class HttpServerProtocolCodecFactory extends DemuxingProtocolCodecFactory { public HttpServerProtocolCodecFactory() { super.addMessageDecoder(HttpRequestDecoder.class); super.addMessageEncoder(HttpResponseMessage.class, HttpResponseEncoder.class); } } package com.ajita.httpserver; import org.apache.mina.core.future.IoFutureListener; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; public class ServerHandler extends IoHandlerAdapter { private HttpHandler handler; public HttpHandler getHandler() { return handler; } public void setHandler(HttpHandler handler) { this.handler = handler; } @Override public void sessionOpened(IoSession session) { // set idle time to 60 seconds session.getConfig().setIdleTime(IdleStatus.BOTH_IDLE, 60); } @Override public void messageReceived(IoSession session, Object message) { // Check that we can service the request context HttpRequestMessage request = (HttpRequestMessage) message; HttpResponseMessage response = handler.handle(request); // HttpResponseMessage response = new HttpResponseMessage(); // response.setContentType("text/plain"); // response.setResponseCode(HttpResponseMessage.HTTP_STATUS_SUCCESS); // response.appendBody("CONNECTED"); // msg.setResponseCode(HttpResponseMessage.HTTP_STATUS_SUCCESS); // byte[] b = new byte[ta.buffer.limit()]; // ta.buffer.rewind().get(b); // msg.appendBody(b); // System.out.println("####################"); // System.out.println(" GET_TILE RESPONSE SENT - ATTACHMENT GOOD DIAMOND.SI="+d.si+ // ", "+new // java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss.SSS").format(new // java.util.Date())); // System.out.println("#################### - status="+ta.state+", index="+message.getIndex()); // // Unknown request // response = new HttpResponseMessage(); // response.setResponseCode(HttpResponseMessage.HTTP_STATUS_NOT_FOUND); // response.appendBody(String.format( // "<html><body><h1>UNKNOWN REQUEST %d</h1></body></html>", // HttpResponseMessage.HTTP_STATUS_NOT_FOUND)); if (response != null) { session.write(response).addListener(IoFutureListener.CLOSE); } } @Override public void sessionIdle(IoSession session, IdleStatus status) { session.close(false); } @Override public void exceptionCaught(IoSession session, Throwable cause) { session.close(false); } } package com.ajita.httpserver; /** * HTTP请求的处理接口 * * @author Ajita * */ public interface HttpHandler { /** * 自定义HTTP请求处理需要实现的方法 * @param request 一个HTTP请求对象 * @return HTTP请求处理后的返回结果 */ HttpResponseMessage handle(HttpRequestMessage request); }
4.HTTP Client端;
5.测试,建立测试类如下
package com.jita; import java.io.IOException; import com.ajita.httpserver.HttpHandler; import com.ajita.httpserver.HttpRequestMessage; import com.ajita.httpserver.HttpResponseMessage; import com.ajita.httpserver.HttpServer; public class TestHttpServer { public static void main(String[] args) throws IOException, InterruptedException { HttpServer server = new HttpServer(); server.setEncoding("GB2312"); server.setHttpHandler(new HttpHandler() { public HttpResponseMessage handle(HttpRequestMessage request) { String level = request.getParameter("level"); System.out.println(request.getParameter("level")); System.out.println(request.getContext()); HttpResponseMessage response = new HttpResponseMessage(); response.setContentType("text/plain"); response.setResponseCode(HttpResponseMessage.HTTP_STATUS_SUCCESS); response.appendBody("CONNECTED\n"); response.appendBody(level); return response; } }); server.run(); //Thread.sleep(10000); // server.stop(); } }
启动,在浏览器中输入HTTP请求如:http://192.168.1.111:8080/test.do?level=1