本文(也有可能是本系列文章)是笔者自己对 Tomcat 的思考,肯定有很多不当之处,仅作为自己学习记录之用。
1. Socket
An Internet socket is characterized by at least the following:
以上是维基百科对 Socket
的定义。
从我的角度看,Tomcat 是和 Socket 有很多相似之处的:都拥有一个 IP 地址、监听一个端口、接收请求并返回响应,所以我觉得从 Socket 入手比较合适。
定义中提到 Socket 可以由多种协议实现,以下我的思考全是基于 TCP 协议及其 Java 实现:TCP 有服务端,并且客户端需要和服务端建立连接,提供的是可靠服务(UDP 则不需要建立连接,提供的服务是不可靠的)。
ServerSocket
ServerSocket 是 TCP 协议的服务端,服务器端只有一个。构造函数的签名(选了参数最多的一个构造函数):
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
三个参数:
- port:端口号;
- backlog:请求最大的排队个数;
- bindAddr:服务器端绑定的 IP 地址。
ServerSocket 建立之后,调用 accept()
方法接收客户端连接,这个返回在客户端连接之前会一直阻塞:
public Socket accept() throws IOException
accept()
方法返回 Socket 对象,ServerSocket 会对所有连接到服务器端的客户端建立一个 Socket。
Socket
Socket 是 TCP 协议的客户端,客户端可以有多个。构造函数签名(选了本文关心的一个):
public Socket(String host, int port) throws IOException
两个参数:
- host:远程服务端地址;
- port:远程端口。
Socket 有两个重要的方法 getInputStream()
和 getOutputStream()
,Socket 的 InputStream
可以理解为 Request
,而 OutputStream
可以理解为 Response
。
描述了上面两个类,实际上我们已经可以开发出自己的一个服务器,因为我们已经有了监听端口、绑定地址的能力,而且我们能接收请求,并返回响应——和平时接触到的 Tomcat 的功能几乎一模一样了!
2. HTTP 协议
在本文我们要了解 HTTP 协议的一些基本内容。HTTP 请求包含三部分:
- 请求方法、URI、协议/版本
- 请求头:HTTP 的请求头的参数是用换行符(CRLF)分隔的。
- 请求体
如:
POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Content-Length: 33
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
3. Java 实现的简易服务器
下面我们用 ServerSocket 和 Socket 来开发一个服务器。用 Socket 模拟请求、响应,ServerSocket 模拟服务器。
例子来自《How Tomcat Works》,稍作改动。
各个类的作用:
-
Request:读取请求头信息;解析 URI,作为文件名。
-
Response:从 URI 的解析结果查找本地文件目录的对应文件,如果没找到返回 404 页面。
-
HttpServer:监听本地 8080 端口,如果 URI 是
/SHUTDOWN
则关闭服务器。
Request
Request 作为请求,读取 HTTP 请求头并解析。
public class Request {
private static final int BUFFER_SIZE = 2048;
private InputStream inputStream;
private String uri;
public Request(InputStream inputStream) {
this.inputStream = inputStream;
}
/**
* 获取请求头
*/
public void parse() {
StringBuffer request = new StringBuffer(BUFFER_SIZE);
byte[] buffer = new byte[BUFFER_SIZE];
int i;
try {
i = inputStream.read(buffer);
} catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j = 0; j < i; j++) {
request.append((char) buffer[j]);
}
uri = parseUri(request.toString());
}
/**
* 获取 URI
* @param requestString
* @return
*/
private 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;
}
public String getUri() {
return uri;
}
}
Response
查找本地目录中对应的文件,如果没有返回 404 页面:
public class Response {
private static final int BUFFER_SIZE = 1024;
private Request request;
private OutputStream outputStream;
public Response(OutputStream output) {
this.outputStream = 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) {
outputStream.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>";
outputStream.write(errorMessage.getBytes());
}
} catch (Exception e) {
System.out.println(e.toString());
} finally {
if (fis != null)
fis.close();
}
}
}
Server
启动之后等待客户端请求,并监听停止服务器命令。
public class HttpServer {
// 文件目录:用户的 home 目录 + webroot
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
// 关闭服务器命令
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
// 关闭服务器命令是否收到
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
private 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);
}
// 等待请求
while (!shutdown) {
Socket socket;
InputStream input;
OutputStream output;
try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
// 创建 Request 并解析
Request request = new Request(input);
request.parse();
// 创建 Response
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
// 关闭 Socket
socket.close();
// 检查 URI 是否关闭命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
启动 HttpServer,在浏览器输入访问 127.0.0.1:8080
,网址后面加上任意 URI,Response 就会到指定目录下去找同名文件。