《JSP和Servlet那些事儿 》系列文章旨在阐述Servlet(Struts和Spring的MVC架构基础)和JSP内部原理以及一些比较容易混淆的概念(比如forward和redirect区别、静态include和<jsp:include标签区别等)和使用,本文为系列文章之启蒙篇--初探HTTP服务器,基本能从本文中折射出Tomcat和Apache HTTPD等处理静态文件的原理。敬请关注连载!
在学习Servlet和JSP的过程中,如果对HTTP协议本身以及HTTP服务器运行原理有初步的认识的话,这会使得后边的学习更加容易。HTTP服务器本身的内部原理对于Java而言是比较简单的,就是一个Socket处理;请求的解析就是Socket InputStream的读取和分析,所谓的响应仅仅是按照HTTP协议规定的顺序把字节流写入到Socket OutputStream里面。以下是一个简单的HTTP服务器,希望读者在阅读代码的过程中能够想起RFC2616的一些相关术语、或者能够很容易的理解代码。
一个简单的HTTP服务器需要注意以下几点 :
1,HTTP服务器监听主机和端口:也就是Java Socket的监听主机和端口
2,DocRoot:也就是文档根路径,就是http服务器查找客户端请求资源的根路径。
3,处理线程:需要有至少一个处理线程用于解析Socket输入流及回写请求到Socket输出流,Socket输出流就是发给客户端的通道
4,以上配置最好提供配置文件(类似具有HTTP服务功能的tomcat配置文件conf/server.xml,Apache HTTPD的httpd.conf文件)
根据以上几点,Java实现基本的HTTP服务器的思路如下 :
1,需要写一个类,代码全局的HTTP服务器配置(端口,线程数等);对应本文中Configure类
2,需要一个Main类,实质就是主线程,主线程需要绑定ServerSocket用于监听客户端请求,并启动多个处理线程处理客户端Socket请求; 对应本文中的HttpServer类
3,需要多个处理线程,用于处理主线程分配的Socket处理任务; 对应本文中的ProcessThread类
4,需要一个专门用于解析HTTP请求的类,该类从Socket中获取到输入流,然后读取输入流中的字节,从而解析出客户端希望请求的资源; 对应本文中的HttpRequest类
5,需要一个专门回写请求的类,把客户端请求的资源对应的文件流输出到Socket的输出流,如果资源找不到的话,就返回404给客户端; 对应本文中的HttpResponse类
以下是全部的代码:
常量类:
主要定义了一些常量,比如HTTP服务器默认的监听主机和端口、默认的文档根路径、默认处理线程数、默认配置文件等。
package lesson1.server;
import java.util.HashMap;
public final class Constants {
/**
* Listener's default values.
*/
public final static String DEFAULT_HOST = "localhost";
public final static int DEFAULT_PORT = 8080;
public final static String DEFAULT_DOC_ROOT = "./webapps";
/**
* Default work thread count.
*/
public final static int DEFAULT_WORKER_COUNT = 10;
public static final byte CR = (byte) '\r';
public static final byte LF = (byte) '\n';
public static final byte SP = (byte) ' ';
public static final byte HT = (byte) '\t';
public static final String CRLF = "\r\n";
public static final byte COLON = (byte) ':';
public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1";
public static final int HTTP_CODE_200 = 200;
public static final int HTTP_CODE_403 = 403;
public static final int HTTP_CODE_404 = 404;
public static final int HTTP_CODE_500 = 500;
public static final int HTTP_CODE_503 = 503;
/**
* 定义HTTP Response Code对应的Message
*/
public static HashMap<Integer, String> CODE2MESSAGE = new HashMap<Integer, String>();
static{
CODE2MESSAGE.put(HTTP_CODE_200, "OK");
CODE2MESSAGE.put(HTTP_CODE_403, "Forbidden");
CODE2MESSAGE.put(HTTP_CODE_404, "Not Found");
CODE2MESSAGE.put(HTTP_CODE_500, "Internal Server Error");
CODE2MESSAGE.put(HTTP_CODE_503, "Service Unavailable");
}
/**
* 定义MIME Type
*/
public static HashMap<String, String> MIMETYPES = new HashMap<String, String>();
static{
MIMETYPES.put("html", "text/html");
MIMETYPES.put("htm", "text/html");
MIMETYPES.put("txt", "text/plain");
MIMETYPES.put("xml", "application/xml");
MIMETYPES.put("js", "text/javascript");
MIMETYPES.put("css", "text/css");
MIMETYPES.put("jpe", "image/jpeg");
MIMETYPES.put("jpeg", "image/jpeg");
MIMETYPES.put("jpg", "image/jpeg");
}
public static final String DEFAULE_CONFIG_FILE ="server.properties";
public static final String CONFIG_HOST = "host";
public static final String CONFIG_PORT ="port";
public static final String CONFIG_DOCROOT ="docRoot";
public static final String CONFIG_THREAD_COUNT ="threadCount";
}
全局配置类:
该类主要负责从配置文件中读取到HTTP服务器的监听主机、端口、DocRoot等重要信息。HTTP服务器的其他代码全部都会应用这个类的属性。
package lesson1.server;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
/**
* HTTP服务器全局配置项
*
* @author sta
*
*/
public class Configure {
// listening host
private String host = Constants.DEFAULT_HOST;
// listening port
private int port = Constants.DEFAULT_PORT;
// Document Root which locate the static resource
private String docRoot = Constants.DEFAULT_DOC_ROOT;
/**
* Http Server config file path
*/
private String configFile = Constants.DEFAULE_CONFIG_FILE;
/**
* Worker thread count.
*/
private int workerCount = Constants.DEFAULT_WORKER_COUNT;
// 发送HTTP响应的缓冲区大小
private static final int DEFAULT_SEND_BUFFER_SIZE = 8 * 1024; // default 8k
private int sendBufferSize = DEFAULT_SEND_BUFFER_SIZE;
private static final Configure instance = new Configure();
// for singleton
private Configure() {
Properties properties = new Properties();
InputStream in = null;
try {
in = new FileInputStream(configFile);
properties.load(in);
setHost(properties.getProperty(Constants.CONFIG_HOST));
setPort(Integer.parseInt(properties.getProperty(Constants.CONFIG_PORT)));
setDocRoot(properties.getProperty(Constants.CONFIG_DOCROOT));
setWorkerCount(Integer.parseInt(properties
.getProperty(Constants.CONFIG_THREAD_COUNT)));
} catch (Exception e) {
System.out.println("Failed to load the config from file["
+ configFile
+ "], Http Server will use the default config.");
e.printStackTrace();
} finally {
try {
in.close();
} catch (Exception e) {
}
}
}
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 getDocRoot() {
return docRoot;
}
public void setDocRoot(String docRoot) {
this.docRoot = docRoot;
}
public static Configure getInstance() {
return instance;
}
public int getWorkerCount() {
return workerCount;
}
public void setWorkerCount(int workerCount) {
this.workerCount = workerCount;
}
public int getSendBufferSize() {
return sendBufferSize;
}
public void setSendBufferSize(int sendBufferSize) {
this.sendBufferSize = sendBufferSize;
}
}
Main类:
该类中定义了一个队列(taskQueue)用于保存客户端请求对应的Socket;
初始化方法中启动多个处理线程,并同时把taskQueue的引用传递给处理线程;
run方法主要是绑定ServerSocket监听,然后while循环不断接受客户端请求,客户端请求对应的socket全部保存到taskQueue队列中,然后被处理线程取走进行处理。
package lesson1.server;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This class is the mock HTTP Server which can only process static
* resource(*.html,*.js,*.css etc.).
*
* @author sta
*
*/
public class HttpServer {
/**
* HttpServer will add socket to this queue, ProcessThread will get task
* from this queue.
*/
private LinkedBlockingQueue<Socket> taskQueue = new LinkedBlockingQueue<Socket>();
/**
* @param args
*/
public static void main(String[] args) {
HttpServer instance = new HttpServer();
try {
instance.init();
instance.run();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Init method mainly start the worker thread.
*/
private void init() {
// use fixed thread to processing message
for (int i = 0; i < Configure.getInstance().getWorkerCount(); i++) {
// Use "taskQueue" as the constructor parameter, ProcessThread will
// block at getting task util server get task.
Thread processThread = new ProcessThread(taskQueue);
processThread.setName("HttpServer-ProcessThread" + i);
processThread.start();
}
System.out.println(Configure.getInstance().getWorkerCount()
+ " work thread had been started.");
}
/**
* Bind the socket to specified host and port, waiting the connection from
* client.
*
* @throws Exception
*/
private void run() throws Exception {
InetSocketAddress address = new InetSocketAddress(Configure
.getInstance().getHost(), Configure.getInstance().getPort());
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(address);
System.out.println("Server is listening on Host["
+ Configure.getInstance().getHost() + "],Port["
+ Configure.getInstance().getPort() + "]");
System.out.println("Server is waiting the connection from Client.");
while (true) {
Socket s = serverSocket.accept();
// just only add the Socket into taskQueue,the worker threads will get
// this socket and process it.
taskQueue.add(s);
}
}
}
处理线程类:
处理线程持有以上主线程的taskQueue引用,然后不断从该队列中获取socket,获得socket之后便实例化HttpRequest和HttpResponse对象,并调用HttpRequest的parse方法进行请求解析,根据解析结果查找本地资源,如果请求的资源存在,那么就把本地资源对应的流传递给HttpResponse,由HttpResponse进行读取,HttpResponse读取到的流全部回写到客户端,从而完成请求。
package lesson1.server;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This is the main thread which will read resource from server machine, then
* return the byte to client.
*
* @author sta
*
*/
public class ProcessThread extends Thread {
LinkedBlockingQueue<Socket> queue = null;
public ProcessThread(LinkedBlockingQueue<Socket> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
HttpRequest request = null;
HttpResponse response = null;
InputStream inStream = null;
OutputStream outStream = null;
Socket socket = null;
try {
socket = queue.take();
inStream = socket.getInputStream();
request = new HttpRequest(inStream);
outStream = socket.getOutputStream();
response = new HttpResponse(outStream);
// 解析请求消息
try {
request.parse();
response.setHttpRequest(request);
} catch (IOException e) {
}
/*
* HTTP服务器真正处理逻辑,主要是: 1,根据URI查找响应的流 2,把流输出给客户端
*/
String uri = request.getUri();
File resourceFile = new File(Configure.getInstance()
.getDocRoot()
+ uri);
if (!resourceFile.exists() || !resourceFile.canRead()) {
response.setStatus(Constants.HTTP_CODE_404);
} else {
response.setStatus(Constants.HTTP_CODE_200);
response.setResource(new FileInputStream(resourceFile));
}
response.send();
} catch (Exception e) {
response.setStatus(Constants.HTTP_CODE_500);
try {
response.send();
} catch (IOException e1) {
e1.printStackTrace();
}
} finally {
try {
inStream.close();
} catch (Exception e2) {
}
try {
outStream.close();
} catch (Exception e2) {
}
// 默认对socket进行关闭
try {
socket.close();
} catch (Exception e2) {
}
}
}
}
}
请求解析类:
HTTP请求类的职责很简单,就是从输入流中读取字节,解析出请求的资源名称。其中parseRequestLine方法就是最重要的处理逻辑,这个基本是参照tomcat来实现的。
package lesson1.server;
import java.io.IOException;
import java.io.InputStream;
/**
* 代表HTTP请求,主要包含: 1,HTTP方法 2,请求URI:也就是请求的资源 3,协议:区分HTTP协议版本
*
* @author sta
*
*/
public class HttpRequest {
String method = "GET";
String uri;
String protocol;
InputStream in = null;
public HttpRequest(InputStream in) {
this.in = in;
}
public void parse() throws IOException {
parseRequestLine();
}
private void parseRequestLine() throws IOException {
int start = 0;
int pos = 0;
byte chr = 0;
// 1024 byte is enough for test.
byte[] buf = new byte[1024];
in.read(buf);
// ignore blank line
do {
chr = buf[pos++];
} while (chr == Constants.CR || chr == Constants.LF);
pos--;
start = pos;
// parse HTTP Method
boolean space = false;
while (!space) {
if (buf[pos] == Constants.SP) {
space = true;
method = new String(buf, start, pos - start);
}
pos++;
}
start = pos;
// parse URI
space = false;
while (!space) {
if (buf[pos] == Constants.SP) {
space = true;
uri = new String(buf, start, pos - start);
}
pos++;
}
start = pos;
// parse protocol
space = false;
while (!space) {
if (buf[pos] == Constants.SP || buf[pos] == Constants.CR
|| (buf[pos] == Constants.LF)) {
space = true;
protocol = new String(buf, start, pos - start);
}
pos++;
}
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
}
响应处理类:
相应处理类主要是把HTTP状态码,和服务端找到的资源流回写到输出流中。此外,HTTP响应头中也包含了Content-Length属性.
package lesson1.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class HttpResponse {
int status;
OutputStream toClientStream = null;
HttpRequest httpRequest = null;
private InputStream resource = null;
private String resourceType = "text/plain";
public void setHttpRequest(HttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
public HttpResponse(OutputStream out) {
this.toClientStream = out;
}
public void send() throws IOException {
writeStatusLine();
writeHeaderAndResponseBody();
}
private void writeStatusLine() throws IOException {
toClientStream.write(httpRequest.getProtocol().getBytes());
toClientStream.write(Constants.SP);
toClientStream.write(String.valueOf(status).getBytes());
toClientStream.write(Constants.SP);
toClientStream.write(Constants.CODE2MESSAGE.get(status).getBytes());
toClientStream.write(Constants.CRLF.getBytes());
// 没写HTTP响应消息(比如200对应的OK)
}
private void writeHeaderAndResponseBody() throws IOException {
if (resource != null) {
try {
// Content-Length和Content-Type头
String contentLengthLine = "Content-Length: "
+ resource.available();
toClientStream.write(contentLengthLine.getBytes());
toClientStream.write(Constants.CRLF.getBytes());
String contentType = "Content-Type: "
+ Constants.MIMETYPES.get(resourceType);
toClientStream.write(contentType.getBytes());
toClientStream.write(Constants.CRLF.getBytes());
// 头和消息体之间是两个回车换行符
toClientStream.write(Constants.CRLF.getBytes());
//HTTP响应消息体数据
byte[] bytePerTime = new byte[Configure.getInstance()
.getSendBufferSize()];
int count = -1;
while ((count = resource.read(bytePerTime)) > 0) {
toClientStream.write(bytePerTime,0,count);
toClientStream.flush();
}
} catch (IOException e) {
throw e;
} finally {
try {
resource.close();
} catch (Exception e2) {
}
}
}else{
toClientStream.flush();
}
}
public void setResource(InputStream resource) {
this.resource = resource;
}
public void setResourceType(String fileSuffix) {
this.resourceType = fileSuffix;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
以上代码基本具备了HTTP请求处理能力,为了尽可能的简化和描述出HTTP服务器最本质的东西,省略了很多处理(比如静态资源缓存,HTTP头处理等)。验证HTTP服务器步骤:
1,新建一个server.properties文件,并正确配置主机、端口等
---
host=localhost
port=8080
docRoot=./webapps
threadCount=10
2,需要保证server.properties文件中配置的docRoot目录存在,拷贝一些静态文件(html...)到docRoot目录下
3,java lesson1.server.HttpServer启动
4,通过浏览器输入http://$host:$port/$resourceName便可,其中resourceName为相对于server.properties中配置的docRoot目录的相对路径。可以使用firefox的httpwatch查看请求和响应的细节。
注:本文于2013年4月3号进行了一次修改维护,主要是解决一下问题:
1,HTTP响应格式不正确,特别是响应的状态码不正确、以及未设置content-type导致图片数据在浏览器可能不见的问题。
2,本次附上所有打包好的源代码,包括测试用静态文件