做过java web的同学都对tomcat非常熟悉。我们在使用tomcat带来的便利的同时,是否想过tomcat是如何工作的呢?tomcat本质是一个http服务器,本篇文章将搭建一个简单的http服务器。
1 Catalina模型
首先我们先了解一下tomcat的大致工作原理。tomcat的核心是servlet容器,我们称它为Catalina(为什么叫这个名字?我也不知道 ̄へ ̄)。模型图如1.1
图1.1
Connector是用来“连接”容器里边的请求的。它的工作是为接收到每一个 HTTP 请求构造一个 request 和 response 对象。然后它把流程传递给容器。容器从连接器接收到 requset 和 response 对象之后调用 servlet 的 service 方法用于响应。谨记,这个描述仅仅是冰山一角而已。这里容器做了相当多事情。例如,在它调用 servlet 的 service 方法之前,它必须加载这个 servlet,验证用户(假如需要的话),更新用户会话等等。以此为思路,我们就开始我们的构造http服务器之旅吧。
2 服务器搭建
首先我们明确一下我们的服务器的功能点。
1. 需要有一个类去接收http请求;
2. 需要一个自定义Request类和Response类,把接收到的请求构造成这两个类;
3. 根据请求的格式来确定处理方式:返回静态资源 or 进入Servlet ?
4. 需要一个Servlet类执行业务逻辑
UML图如下2.1
图2.1
2.1 HttpServer
首先构造HttpServer类
public class HttpServer { private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
private static boolean shutdown = false; public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
} public static void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// Loop waiting for a request
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
// create Request object and parse
Request request = new Request(input);
request.parseUrl();
// create Response object
Response response = new Response(output);
response.setRequest(request); if (request.getUri().startsWith("/v2/")) {
ServletProcessor processor = new ServletProcessor();
processor.process(request, response);
}
else {
StaticResourceProcessor processor =
new StaticResourceProcessor();
processor.process(request, response);
}
// Close the socket
socket.close();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
}
我们的服务器启动入口放在了HttpServer里面。await()方法负责接收Socket连接,只有当用户输入了代表shutdown的URL时,服务器才会停止运行。Request类提供了解析请求的功能,根据请求的url,来决定是返回静态资源,或者进入对应的servlet类执行service逻辑。
这里我们需要关注一下ServerSocket这个类的用法。Socket 类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候,你都会第一时间想到这个类。而ServerSocket 和 Socket 不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服 务器套接字获得一个连接请求,它创建一个 Socket 实例来与客户端进行通信。 ServletSocket套接字的其中一个构造函数为
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
port代表端口号,backLog代表这个套接字可支持的最大连接数量,bindingAddress代表服务器绑定的地址。一旦你有一个 ServerSocket 实例,你可以通过调用 ServerSocket 类的 accept 方法j。这个监听当前地址的当前端口上的请求,方法只会在有连接请求时才会返回,并且返回值是一个 Socket 类的实例。
2.2 Request Response
servlet 的 service 方法从 servlet 容器中接收一个 javax.servlet.ServletRequest 实例 和一个 javax.servlet.ServletResponse 实例。这就是说对于每一个 HTTP 请求,servlet 容器 必须构造一个 ServletRequest 对象和一个 ServletResponse 对象并把它们传递给正在服务的 servlet 的 service 方法。
public class Request implements ServletRequest { private InputStream input;
private String uri; public Request(InputStream input) {
this.input = input;
} public String getUri(){
return uri;
} public void parseUrl() {
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[2048]; try {
i = input.read(buffer);
} catch (IOException e) {
e.printStackTrace();
i = -1;
} for (int j = 0; j < i; j++) {
request.append((char) buffer[j]);
} System.out.print(request.toString());
uri = parseUri(request.toString());
} private static String parseUri(String requestString) {
int index1, index2;
index1 = requestString.indexOf(' ');
if (index1 != -1) {
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
return requestString.substring(index1 + 1, index2);
}
return null;
} @Override
public Object getAttribute(String name) {
return null;
} @Override
public Enumeration getAttributeNames() {
return null;
} @Override
public String getCharacterEncoding() {
return null;
} @Override
public void setCharacterEncoding(String env) throws UnsupportedEncodingException { } @Override
public int getContentLength() {
return 0;
} @Override
public String getContentType() {
return null;
} @Override
public ServletInputStream getInputStream() throws IOException {
return null;
} @Override
public String getParameter(String name) {
return null;
} @Override
public Enumeration getParameterNames() {
return null;
} @Override
public String[] getParameterValues(String name) {
return new String[0];
} @Override
public Map getParameterMap() {
return null;
} @Override
public String getProtocol() {
return null;
} @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 null;
} @Override
public String getRemoteHost() {
return null;
} @Override
public void setAttribute(String name, Object o) { } @Override
public void removeAttribute(String name) { } @Override
public Locale getLocale() {
return null;
} @Override
public Enumeration getLocales() {
return null;
} @Override
public boolean isSecure() {
return false;
} @Override
public RequestDispatcher getRequestDispatcher(String path) {
return null;
} @Override
public String getRealPath(String path) {
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;
}
}
Request类代表一个 request 对象并被传递给 servlet 的 service 方法。就本身而言,它必须实现 javax.servlet.ServletRequest 接口。这个类必须提供这个接口所有方法的实现。不过,我们想要让它非常简单并且仅仅提供实现其中一些方法,比如解析url的功能。在Request初始化时初始化成员变量inputStream,并且用parseUrl()方法创建了一个字节数组来读入输入流,并转化为成一个StringBuffer对象,进而解析url。
public class Response implements ServletResponse { private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
PrintWriter writer; public Response(OutputStream output) {
this.output = output;
} public void setRequest(Request request) {
this.request = request;
} public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch != -1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
} else {
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" +
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
}
} catch (Exception e) {
System.out.println(e.toString());
} finally {
if (fis != null)
fis.close();
}
} @Override
public String getCharacterEncoding() {
return null;
} @Override
public String getContentType() {
return null;
} @Override
public ServletOutputStream getOutputStream() throws IOException {
return null;
} @Override
public PrintWriter getWriter() throws IOException {
writer = new PrintWriter(output, true);
return writer; } @Override
public void setCharacterEncoding(String charset) { } @Override
public void setContentLength(int len) { } @Override
public void setContentType(String type) { } @Override
public void setBufferSize(int size) { } @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 loc) { } @Override
public Locale getLocale() {
return null;
}
}
Response类则提供了发送静态资源的功能。sendStaticResource()方法根据request内解析过的url,在本地寻找指定文件。如果找得到,把文件内容读出到浏览器,如果找不到,那么返回404的错误码。
2.3 PrimitiveServlet类
public class PrimitiveServlet implements Servlet { @Override
public void init(ServletConfig config) throws ServletException {
System.out.println("init");
} @Override
public ServletConfig getServletConfig() {
return null;
} @Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("from service");
PrintWriter out = res.getWriter();
out.println("Hello. Roses are red.");
out.print("Violets are blue.");
} @Override
public String getServletInfo() {
return "this is v2 info";
} @Override
public void destroy() {
System.out.println("destroy");
}
}
PrimitiveServlet实现了标准的Servlet接口。我们简单的实现了Servlet生命周期的其他方法,并在service()方法我们做了最简单的向浏览器吐文字的操作。
2.4 ServletProcessor 和 StaticResourceProcessor
public class ServletProcessor { public void process(Request request, Response response) {
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
Class myClass = null;
try {
myClass = Class.forName("tomcat.v2." + servletName);
} catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
ServletProcessor是处理serlvet请求的类。它的作用在于,根据请求的路径实例化对应的Servlet,并且执行该Servlet的service()方法。
public class StaticResourceProcessor { public void process(Request request, Response response){
try{
response.sendStaticResource();
}catch (IOException e){
e.printStackTrace();
}
}
}
StaticResourceProcessor则简单的调用response的sendStaticResource()方法来返回静态资源。
最后我们还需要建一个辅助类Constants指定静态资源的存放路径
public class Constants {
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
}
3 启动服务器
启动main函数来启动我们的http服务器。在浏览器输入http://localhost:8080/test,得到的结果如图3.1
图3.1
这个url访问的是我的电脑中的一个文件test,它的存储路径为 /Users/wangyu/Documents/workspace/Tomcat/webroot/test,即当前项目的webroot目录下。
我们还可以输入一个servlet地址查看效果。在浏览器输入http://localhost:8080/v2/PrimitiveServlet,得到的结果如图3.2
图3.2
这和我们的PrimitiveServlet的输出是一致的。
4 结语
当然,tomcat的功能比我们的简易http服务器强大的多。但是通过这个最简单的http服务器,是否让你对http服务器有更深的一点了解了呢?
参考资料:
1 深入剖析Tomcat