程序基本流程以下:javascript
代碼組織結構以下:html
HTTP重定向服務主線程:java
package com.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.log4j.Logger; import com.conf.Config; public class HttpServer implements Runnable { private static ServerSocket server_socket = null; private static ExecutorService pool; private static int requestNum = 0; private static Logger serverLog = Logger.getLogger("HttpServerLog"); private static Logger requestNumLog = Logger.getLogger("RequestNumber"); public void run() { startServer(Config.serverListenPort); } private void startServer(int port){ try { pool = Executors.newFixedThreadPool(Config.threadPoolSize); server_socket = new ServerSocket(port,Config.serverQueueSize); serverLog.info("HTTP Server starts on port:" + server_socket.getLocalPort()); while (true) { try { if(Config.curThreadsNum.get() >= Config.maxThreadsNum.get()){ serverLog.info("HTTP Server sleep for 1 second!"); Thread.sleep(1000); continue; } } catch (Exception e) { serverLog.error(e); continue; } serverLog.debug("Get client request!"); Socket socket = server_socket.accept(); serverLog.debug("Create socket successfully!"); //socket.setReuseAddress(true); //某些HTTP客戶端創建鏈接後不發送數據 //若是這種鏈接過多,系統線程將被耗盡 //因此必須設置鏈接超時時間 socket.setSoTimeout(2*1000); socket.setSoLinger(true, 0); serverLog.debug("New connection:" + socket.getInetAddress() + ":" + socket.getPort()); serverLog.info("Max:" + Config.maxThreadsNum + ";Cur:" + Config.curThreadsNum); requestNum++; if(requestNum > 10000){ requestNumLog.info("10000 requests"); requestNum = 0; } try { DealThread dt = new DealThread(socket); serverLog.debug("Deal thread create successfully!"); pool.execute(dt); Config.curThreadsNum.incrementAndGet(); } catch (Exception e) { serverLog.error(e); } } } catch (IOException e) { serverLog.error(e); } } public static void main(String[] args){ HttpServer hs = new HttpServer(); Thread t = new Thread(hs); t.start(); } }
HTTP請求封裝類:apache
HTTP請求報文格式和HTTP響應報文格式參照http://blog.csdn.net/a19881029/article/details/14002273瀏覽器
在解包時進行循環讀取,以免請求接收端沒有接收到完整的HTTP請求報文信息服務器
\n佔一個字節,字節值爲10,\r也佔一個字節,字節值爲13app
package com.request; import java.io.InputStream; import java.net.Socket; import java.util.ArrayList; import com.server.DealThread; public class Request { private InputStream input; private String headerString = ""; private String bodyString = ""; public Request(Socket socket) throws Exception{ this.input = socket.getInputStream(); DealThread.threadLog.debug("Thread[" + Thread.currentThread().getId() + "]get input stream"); } public void resolvePackage(Socket socket) throws Exception{ DealThread.threadLog.debug("Thread[" + Thread.currentThread().getId() + "]analysis package begin"); String line = null; // HTTP request body length int contentLength = 0; // get HTTP request head do { line = readLine(input, 0); if (line.startsWith("Content-Length")) { contentLength = Integer.parseInt(line.split(":")[1].trim()); } headerString += line; //若是遇到了一個單獨的回車換行,則表示請求頭結束 } while (!line.equals("\r\n")); if(contentLength != 0){ bodyString = readLine(input,contentLength); } DealThread.threadLog.debug("HTTP request head:" + headerString); DealThread.threadLog.debug("HTTP request body:" + bodyString); DealThread.threadLog.debug("Thread[" + Thread.currentThread().getId() + "]analysis package end"); } private String readLine(InputStream is, int contentLe) throws Exception { ArrayList<Byte> lineByteList = new ArrayList<Byte>(); byte[] readByte; byte b; if (contentLe != 0 && contentLe > 0) { readByte = new byte[contentLe]; int num = 0; int totalnum = contentLe; int realreadnum = 0; while(num < totalnum){ realreadnum = is.read(readByte, num, totalnum-num); if(realreadnum > 0){ num += realreadnum; }else{ break; } } return new String(readByte); } else { //讀請求頭 do { b = (byte)is.read(); lineByteList.add(Byte.valueOf(b)); } while (b != 10); byte[] tmpByteArr = new byte[lineByteList.size()]; for (int i = 0; i < lineByteList.size(); i++) { tmpByteArr[i] = lineByteList.get(i).byteValue(); } lineByteList.clear(); return new String(tmpByteArr); } } public String getHeader(String name) { if (name == null || name.equals("")) return null; name = name + ": "; try { String[] item = headerString.split("\n"); String headerLine = null; for(int i=0;i<item.length;i++){ headerLine = item[i]; if (headerLine.indexOf(name) == 0) { return headerLine.substring(name.length()); } } } catch (Exception e) { DealThread.threadLog.error(e); } return null; } }
HTTP響應封裝類:dom
package com.response; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import com.server.DealThread; public class Response { private OutputStream output; private String ip; public Response(Socket socket) throws Exception{ this.output = socket.getOutputStream(); DealThread.threadLog.debug("Thread[" + Thread.currentThread().getId() + "]get output stream"); ip = socket.getInetAddress().toString(); } public void sendRedirect(String redirectUrl) { String head = "HTTP/1.1 200 OK\r\n" + "Content-Type:text/html\r\n"; String body = "<html><SCRIPT type=text/javascript>" + "window.location.href=\"" + redirectUrl + "\";</script></html>"; head += "Content-length:"+body.getBytes().length+"\r\n\r\n"; try { output.write(head.getBytes()); output.write(body.getBytes()); output.flush(); } catch (IOException e) { e.printStackTrace(); System.out.println("Thread[" + Thread.currentThread().getId() + "]["+ip+"]Redirect Send Error:"+redirectUrl); } } }
HTTP請求處理線程:socket
須要注意的是,這裏並無針對中文域名進行處理,也就是系統並不支持中文域名,若是須要支持中文域名,須要進行punycode轉碼,參見http://blog.csdn.net/a19881029/article/details/18262671ide
因爲設置了socket.setSoLinger(true, 0),當調用socket.close()方法時,底層socket鏈接會當即關閉,此時HTTP響應結果有可能還未所有發送完畢,故在關閉socket鏈接前,處理線程休眠200毫秒以便底層socket有一段時間用來發送HTTP響應信息
package com.server; import java.net.InetAddress; import java.net.Socket; import org.apache.log4j.Logger; import com.conf.Config; import com.request.Request; import com.response.Response; public class DealThread implements Runnable { private Socket socket; private Response response; private Request request; public static Logger threadLog = Logger.getLogger("ThreadLog"); public DealThread(Socket socket) throws Exception { this.socket = socket; this.request = new Request(this.socket); this.response = new Response(this.socket); } public void run() { try { threadLog.debug("thread " + Thread.currentThread().getName() + " open"); request.resolvePackage(socket); processRequest(); } catch (Exception e) { threadLog.error(e); }finally{ try { String identify = socket.getInetAddress() + ":" + socket.getLocalPort(); Thread.sleep(200); socket.shutdownInput(); socket.shutdownOutput(); socket.close(); if(socket.isClosed()){ threadLog.debug("socket [" + identify + "] closed"); } } catch (Exception e) { threadLog.error(e); } Config.curThreadsNum.decrementAndGet(); threadLog.debug("[Thread " + Thread.currentThread().getId() + "] closed"); } } private void processRequest() throws Exception { //沒有對中文域名進行轉碼 String host = request.getHeader("Host"); String user_agent = request.getHeader("User-Agent"); InetAddress netAddress = socket.getInetAddress(); String address = netAddress.getHostAddress(); threadLog.info("HOST:" + host + " User-Agent:" + user_agent + " IP:" + address); String redirectUrl = "http://www.baidu.com"; response.sendRedirect(new String(redirectUrl.getBytes("GBK"),"ISO-8859-1")); threadLog.info("redirectUrl:"+redirectUrl); } }
配置文件讀取類:
HTTP請求的默認監聽端口爲80
package com.conf; import java.io.File; import java.util.concurrent.atomic.AtomicInteger; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; public class Config { static{ SAXReader reader = new SAXReader(); Document document; try { String filePath = "./conf/config.xml"; document = reader.read(new File(filePath)); Element root = document.getRootElement(); int max_threads_num = Integer.valueOf( root.element("max_threads_num").getTextTrim()).intValue(); maxThreadsNum = new AtomicInteger(max_threads_num); int cur_threads_num = Integer.valueOf( root.element("cur_threads_num").getTextTrim()).intValue(); curThreadsNum = new AtomicInteger(cur_threads_num); serverListenPort = Integer.valueOf( root.element("server_listen_port").getTextTrim()).intValue(); serverQueueSize = Integer.valueOf( root.element("server_queue_size").getTextTrim()).intValue(); threadPoolSize = Integer.valueOf( root.element("thread_pool_size").getTextTrim()).intValue(); } catch (DocumentException e) { e.printStackTrace(); maxThreadsNum = new AtomicInteger(50); curThreadsNum = new AtomicInteger(0); serverListenPort = 80; serverQueueSize = 200; threadPoolSize = 60; } } public static AtomicInteger maxThreadsNum; public static AtomicInteger curThreadsNum; public static int serverListenPort; public static int serverQueueSize; public static int threadPoolSize; }
日誌配置文件log4j.properties:
HttpServerLog:記錄HTTP重定向服務主線程的運行情況
ThreadLog:記錄每個HTTP請求處理線程的運行情況
RequestNum:記錄系統負載狀況
log4j.rootLogger=INFO,console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d{MM-dd HH:mm:ss}->%m%n log4j.logger.HttpServerLog=debug,HttpServerLog log4j.appender.HttpServerLog=org.apache.log4j.RollingFileAppender log4j.additivity.HttpServerLog=false log4j.appender.HttpServerLog.File=./log/HttpServer.log log4j.appender.HttpServerLog.MaxFileSize=10MB log4j.appender.HttpServerLog.MaxBackupIndex=0 log4j.appender.HttpServerLog.layout=org.apache.log4j.PatternLayout log4j.appender.HttpServerLog.layout.ConversionPattern=%d{MM-dd HH:mm:ss}->%m%n log4j.logger.ThreadLog=debug,ThreadLog log4j.appender.ThreadLog=org.apache.log4j.RollingFileAppender log4j.additivity.ThreadLog=false log4j.appender.ThreadLog.File=./log/Thread.log log4j.appender.ThreadLog.MaxFileSize=10MB log4j.appender.ThreadLog.MaxBackupIndex=0 log4j.appender.ThreadLog.layout=org.apache.log4j.PatternLayout log4j.appender.ThreadLog.layout.ConversionPattern=%d{MM-dd HH:mm:ss}[%t]->%m%n log4j.logger.RequestNumber=info,RequestNumber log4j.appender.RequestNumber=org.apache.log4j.RollingFileAppender log4j.additivity.RequestNumber=false log4j.appender.RequestNumber.File=./log/RequestNum.log log4j.appender.RequestNumber.MaxFileSize=1MB log4j.appender.RequestNumber.MaxBackupIndex=0 log4j.appender.RequestNumber.layout=org.apache.log4j.PatternLayout log4j.appender.RequestNumber.layout.ConversionPattern=%d{MM-dd HH:mm:ss}->%m%n
系統參數配置文件config.xml:
這些參數須要根據服務器配置、系統負載進行配置,以便程序達到最佳性能
<?xml version="1.0" encoding="UTF-8"?> <server_config> <!-- 最大處理線程數 --> <max_threads_num>50</max_threads_num> <!-- 當前處理線程數 --> <cur_threads_num>0</cur_threads_num> <!-- 服務監聽端口 --> <server_listen_port>80</server_listen_port> <!-- 服務請求接收隊列長度 --> <server_queue_size>200</server_queue_size> <!-- 處理線程線程池大小 --> <thread_pool_size>60</thread_pool_size> </server_config>
經過HttpServer類啓動服務,此時在瀏覽器中輸入http://localhost,頁面最終將被重定向至http://www.baidu.com