程序基本流程如下:
代码组织结构如下:
HTTP重定向服务主线程:
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请求封装类:
HTTP请求报文格式和HTTP响应报文格式参照http://blog.csdn.net/a19881029/article/details/14002273
在解包时进行循环读取,以避免请求接收端没有接收到完整的HTTP请求报文信息
\n占一个字节,字节值为10,\r也占一个字节,字节值为13
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响应封装类:
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请求处理线程:
需要注意的是,这里并没有针对中文域名进行处理,也就是系统并不支持中文域名,如果需要支持中文域名,需要进行punycode转码,参见http://blog.csdn.net/a19881029/article/details/18262671
由于设置了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