第一部分:概述
这一节基于《深度剖析Tomcat》第一章:一个简单的Web服务器 总结而成。
总的来说,一个简单的Web服务器的流程是这样的:
Server创建一个serverSocket对象,等待Client发送请求;
Client发送请求后,Server获取用户socket,从而得到请求的输入输出流;
从输入输出流中创建request和response对象;
解析request,同时response设置静态资源;
关闭用户socket;
从request中获取URI,判断文件是否存在,如果不存在就响应404,如果是就关闭服务器,
否则将对应的文件内容响应给浏览器写入页面;
判断URI是否为/SHUTDOWN,如果不是则重新进入等待请求状态,回到第2步,否则关闭服务器。
第二部分:代码讲解
HttpServer.java
package ex01.pyrmont;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
public class HttpServer {
// 获取项目webroot目录的实际物理路径,判断目标文件是否存在
public static final String WEB_ROOT = System.getProperty("user.dir")
+ File.separator + "webroot";
// 用于判断是否需要关闭服务器,默认是false
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
//创建服务器端的ServerSocket
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
//进入死循环,直到shutdown为SHUTDOWN
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept(); //Server一直等待直到Client发送请求
input = socket.getInputStream(); //接收请求后获取输入输出流
output = socket.getOutputStream();
//创建request对象,传入input参数用于获取输入流的参数
Request request = new Request(input);
request.parse(); //解析request对象,这一节只是获取请求中请求行的URI
//创建response对象,传入output对象用于获取Writer对象将响应内容写到浏览器
Response response = new Response(output);
//设置request,用于sendStaticResource()方法获取URI判断WEB_ROOT下是否存在目标文件
response.setRequest(request);
response.sendStaticResource(); //如果请求不存在就发送404错误,否则写入文件内容
//关闭用户socket
socket.close();
//如果URI是/SHUTDOWN说明需要关闭服务器
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
详细说明:
- await 方法首先创建一个 ServerSocket 实例然后进入一个 while 循环;
- while循环里边的代码运行到 ServletSocket 的accept()方法停了下来,只会在8080端口接收到一个HTTP请求的时候才返回;
- 接收到请求之后,await方法从accept方法返回的Socket实例中取得java.io.InputStream 和 java.io.OutputStream 对象;
- await 方法接下去创建一个 Request 对象并且调用它的 parse()方法去解析HTTP请求的原始数据。
- 在这之后,创建一个 Response 对象,把 Request 对象设置给它,并调用它的 sendStaticResource 方法。
- 最后,await 关闭套接字并调用 Request 的 getUri 来检测 HTTP 请求的 URI 是不是一个 shutdown 命令。假如是的话,shutdown 变量将被设置为 true 且程序会退出 while 循环。
如何发送请求:
- 为了请求一个静态资源,在浏览器的地址栏或者网址框里边敲入以下的 URL: http://machineName:port/staticResource
- 如果你要从一个不同的机器上发送请求到你的应用程序正在运行的机器上,machineName 应 该是正在运行应用程序的机器的名称或者 IP地址。假如你的浏览器在同一台机器上,你可以使 用localhost作为machineName。端口是8080,staticResource 是你需要请求的文件的名称, 且必须位于WEB_ROOT 里边。举例来说,假如你正在使用同一台计算机上测试应用程序,并且你想要调用 HttpServer 对 象去发送一个 index.html文件,你可以使用一下的 URL: http://localhost:8080/index.html
- 要停止服务器,你可以在 web 浏览器的地址栏或者网址框里边敲入预定义字符串,就在 URL 的 host:port 的后面,发送一个 shutdown 命令。 http://localhost:8080/SHUTDOWN
Request.java
解析请求流中的内容:本节仅仅获取URI
package ex01.pyrmont;
import java.io.InputStream;
import java.io.IOException;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
// 解析input输入流,这里只是获取请求行的URI,实际的解析过程远不止这些
public void parse() {
//下面是用最常见的read()方法获取输入流的内容,也是为什么要传入输入流的原因
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[2048];
try {
i = input.read(buffer);//将输入流读取,放在buffer中,并返回已读字节数
} catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j = 0; j index1)
return requestString.substring(index1 + 1, index2);
}
return null;
}
public String getUri() {
return uri;
}
}
Request.java详细说明:
- Request类代表一个HTTP请求。从负责与客户端通信的 Socket 中传递过来 InputStream对象中,来构造这个类的一个实例。调用 InputStream 对象其中一个 read方法来获 取HTTP请求的原始数据。
- parse方法解析 HTTP 请求里边的原始数据。这个方法没有做很多事情。它唯一可用的信息是通过调用HTTP请求的私有方法parseUri获得的URI。parseUri方法在uri变量里边存储URI。公共方法 getUri被调用并返回 HTTP 请求的 URI。 - GET /index.html HTTP/1.1
- parse 方法从传递给 Requst 对象的套接字的 InputStream 中读取整个字节流并在一个缓冲区中存储字节数组。然后它使用缓冲区字节数据的字节来填入一个 StringBuffer 对象,并且把 代表 StringBuffer的字符串传递给 parseUri 方法。
- 然后 parseUri 方法从请求行里边获得 URI。
Response.java
对目标文件存在与否,进行两种不同的处理。如果存在就将文件的内容写入浏览器,否则返回404页面到浏览器。从这个类可以看出,这个类只是简单的文件作为静态资源,将文件的内容写到浏览器中,没有加载servlet的代码
package ex01.pyrmont;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;
/*
HTTP Response = Status-Line
*(( general-header | response-header | entity-header ) CRLF)
CRLF
[ message-body ]
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/
public class CopyOfResponse {
private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
public CopyOfResponse(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 {
//获取URI对应磁盘下的文件对象,因为需要用到URI,所以传入request参数
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 {
//文件不存在,返回404页面
String errorMessage = "HTTP/1.1 404 File Not Found\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: 23\r\n" + "\r\n"
+ "File Not Found";
output.write(errorMessage.getBytes());
}
} catch (Exception e) {
System.out.println(e.toString());
} finally {
if (fis != null)
fis.close();
}
}
}
Response.java详细说明
- Response对象是通过传递由套接字获得的OutputStream对象给HttpServer类的await方法来构造的。
- Response 类有两个公共方法:setRequest 和 sendStaticResource。setRequest 方法用来传递一个 Request 对象给 Response 对象,sendStaticResource 方法是用来发送一个静态资源,例如一个 HTML 文件。它首先通过传递上一级目录的路径和子路径给 File 累的构造方法来实例化 java.io.File 类。 File file = new File(HttpServer.WEB_ROOT, request.getUri());
- 然后它检查该文件是否存在。假如存在的话,通过传递 File 对象让 sendStaticResource 构造一个 java.io.FileInputStream 对象。然后,它调用 FileInputStream 的 read 方法并把字节数组写入 OutputStream 对象。请注意,这种情况下,静态资源是作为原始数据发送给浏览器的。
- 假如文件并不存在,sendStaticResource 方法发送一个错误信息到浏览器。
我们大体知道了一个Web服务器的大致的整体流程,虽然其中有很多问题没有考虑到,但是这里提供了一个很好的学习工具。