目次
4. サーバーとクライアント間のデータ チャネルを開きます。
1. Tomcat の基礎となる全体的なアーキテクチャ
1 はじめに:
Tomcat には 3 つの動作モード (BIO [ブロッキング]、NIO [ノンブロッキング]、APR) があり、ここでは BIO スレッド モデルを使用して実装をシミュレートします。
2. 分析チャート:
以下に示すように:
ブラウザがサーブレット リソースをリクエストすると、Tomcat の最下層はソケット ネットワーク プログラミングを通じてリクエストを受け取り、リクエストごとに新しいスレッドが作成され、対応する Web リソースを呼び出して戻ります。
3. Socket ベースのサーバー開発プロセス:
以下に示すように:
Socket オブジェクトを取得することで、Socket オブジェクトに対応するバイト入力ストリームとバイト出力ストリームを取得します。
オブジェクト変換ストリームを使用して、バイト ストリームを文字ストリーム オブジェクト (InputStreamReader は Reader 抽象クラスを実装します) に変換し、その後、BufferedReader のパッケージ化を通じてノード ストリームをパッケージ化ストリーム (処理ストリーム) に変換できます。
4. サーバーとクライアント間のデータ チャネルを開きます。
PS:
Maven は Web アプリケーションを構成し、実行時に jakarta.servlet.ServletException が発生します。 tomcat10 以降は javax.servlet ではなく、 jakarta.servlet である
ため、Web 依存関係を次のように変更する必要があります: (pom.xml 構成)ファイル)
<!--jar包的依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
<!--jsp的依赖-->
<dependency>
<groupId>jakarta.servlet.jsp</groupId>
<artifactId>jakarta.servlet.jsp-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
MyTomcat クラスのコードは次のとおりです: (サーバー; カスタマイズされた Tomcat)
package tomcat;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
服务器端
*/
public class MyTomcat {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Tomcat在8080端口进行监听...");
System.out.println("--------------------------------------");
while (!serverSocket.isClosed()) {
//获取Socket对象
Socket socket = serverSocket.accept();
//接收来自浏览器端的信息
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String content = null; //局部变量在使用前必须赋初值。
while ((content = bufferedReader.readLine()) != null) {
//判断content是否是空串儿
if (content.length() == 0) {
break;
}
System.out.println(content);
}
//服务器向浏览器回送消息
OutputStream outputStream = socket.getOutputStream();
//设置一个HTTP响应包的响应头
// "/r/n"表示换行
String respHeader =
"HTTP/1.1 200\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
//设置HTTP响应的响应体
String respBody = respHeader + "<h1>你好!</h1>";
/*
注意这里不能使用字符包装流来写数据!!!。
*/
outputStream.write(respBody.getBytes());
System.out.println("--------------------------------------");
System.out.println(respBody);
//释放资源
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
// serverSocket.close();
}
}
}
ブラウザのアドレス バーでローカル ポート 8080 にアクセスします。
login.html コードは次のとおりです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
<style>
table, tr, td {
border:2px cornflowerblue solid;
border-collapse: collapse;
padding: 10px;
background-color: lightcyan
}
#tr01 {
text-align: center
}
</style>
</head>
<body>
<form action="/Cyan_Tomcat/login" methods="get">
<table>
<tr>
<th colspan="2">User Logging</th>
</tr>
<tr>
<td>Username: </td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>Password: </td>
<td><input type="password" name="password"/></td>
</tr>
<tr id="tr01">
<td><input type="submit" value="Submit"/></td>
<td><input type="reset" value="Reset"/></td>
</tr>
</table>
</form>
</body>
</html>
LoginServlet クラスのコードは次のとおりです。
package servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
@WebServlet(urlPatterns = {"/login"})
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("LoginServlet's doPost is invoked~");
resp.setContentType("text/html; charset=utf-8");
PrintWriter writer = resp.getWriter();
req.setCharacterEncoding("utf-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
if ("Cyan".equals(username) && "123".equals(password)) {
writer.print("<h1>登录成功!</h1>");
} else {
writer.print("<h1>登录失败!请重新尝试!</h1>");
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
}
実行結果:
Web プロジェクトで構成された Tomcat を実行すると、次の図に示すように 、LoginServlet クラスのコード ロジックに従ってビジネスが処理されます。 (GIF)
カスタマイズされた Tomcat で実行すると、MyTomcat クラスのコード ロジックがビジネスの処理に使用されます。
以下に示すように:(GIF)
2. マルチスレッドモデルの実装
1. アイデア分析:
サーバーがブラウザから HTTP リクエストを受信すると、新しいスレッドを開始し、スレッドがブラウザに対応する Socket オブジェクトを保持して、スレッドとブラウザ間の接続を完了します。スレッド クラス HttpRequestHandler は、
Runnable インターフェイスを実装することで定義でき、ブラウザからの HTTP リクエストを処理するために使用されます。
2. HTTP リクエストを処理します。
スレッド クラスの HttpRequestHandler クラスのコードは次のとおりです。
package tomcat.handler;
import tomcat.http.CyanServletRequest;
import tomcat.http.CyanServletResponse;
import tomcat.servlet.CyanLoginServlet;
import java.io.*;
import java.net.Socket;
public class HttpRequestHandler implements Runnable {
private Socket socket;
public HttpRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//接收客户端的信息
InputStream inputStream = socket.getInputStream();
/* 以下代码已在CyanServletRequest类中实现
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//BIO,每次请求都对应一个新的线程
System.out.println("当前线程 = " + Thread.currentThread().getName());
String content = null; //局部变量在使用前必须赋初值。
while ((content = bufferedReader.readLine()) != null) {
//判断是否读到了空字符串
if (content.length() == 0) {
break;
}
System.out.println(content);
}
*/
//Die first and live second
//获取客户端的信息(利用了CyanServletRequest中封装好的方法)
//以下代码已在CyanLoginServlet中实现
/* CyanServletRequest cyanServletRequest = new CyanServletRequest(inputStream);
String username = cyanServletRequest.getParameter("username");
String password = cyanServletRequest.getParameter("password");
System.out.println("username = " + username);
System.out.println("password = " + password);
System.out.println(cyanServletRequest);*/
//给客户端回送信息
//以下代码已在CyanLoginServlet类中实现。
/* CyanServletResponse cyanServletResponse = new CyanServletResponse(socket.getOutputStream());
String resp = CyanServletResponse.respHeader + "<h1>CyanServletResponse!</h1>";
OutputStream outputStream = cyanServletResponse.getOutputStream();
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();*/
CyanServletRequest cyanServletRequest = new CyanServletRequest(inputStream);
CyanServletResponse cyanServletResponse = new CyanServletResponse(socket.getOutputStream());
CyanLoginServlet cyanLoginServlet = new CyanLoginServlet();
cyanLoginServlet.doPost(cyanServletRequest, cyanServletResponse);
//释放资源
inputStream.close();
socket.close();
/* 以下代码已在CyanServletResponse类中实现 :
String respHeader = "HTTP/1.1 200\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String respHttp = respHeader + "<h1>Cyan_RA9</h1>";
System.out.println("-----------------------------------------------");
System.out.println("回送的信息如下:(回显)");
System.out.println(respHttp);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(respHttp.getBytes());
//释放资源
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close(); */
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//确保Socket关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
3. Tomcat をカスタマイズします。
MyTomcat_EX クラスにスレッド分散を実装します。
MyTomcat_EX クラスのコードは次のとおりです。
package tomcat;
import tomcat.handler.HttpRequestHandler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class MyTomcat_EX {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("MyTomcat_EX在8080端口进行监听...");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
HttpRequestHandler httpRequestHandler = new HttpRequestHandler(socket);
Thread thread = new Thread(httpRequestHandler);
thread.start();
}
}
}
ランニングエフェクト:(GIF)
3. カスタムサーブレット仕様
1. HTTP リクエストとレスポンス:
1° CyanServletRequest
CyanServletRequest クラスの役割は、元の HttpServletRequest と同等です。このクラスは、メソッド、URI、フォーム データのパラメータ リストなど、HTTP リクエスト内のデータをカプセル化するために使用されます。
CyanServletRequest クラスのコードは次のとおりです。
package tomcat.http;
import java.io.*;
import java.util.HashMap;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : like the original HttpServletRequest.
*/
public class CyanServletRequest {
private String method;
private String URI;
private HashMap<String, String> parametersMapping = new HashMap<>();
private InputStream inputStream;
/*
此处传入的InputStream对象是和Socket关联的InputStream.
*/
public CyanServletRequest(InputStream inputStream) {
this.inputStream = inputStream;
//完成对HTTP请求数据的封装
this.init();
}
private void init() {
System.out.println("\nCyanServletRequest's init is invoked~");
try {
//注意转换流的形参列表
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//首先读取HTTP请求的请求行
/* eg : GET /Cyan/cyan.html HTTP/1.1 */
String requestLine = bufferedReader.readLine();
String[] requestLineArr = requestLine.split(" ");
//获取method
method = requestLineArr[0];
//获取URI
int index = requestLineArr[1].indexOf("?");
if (index == -1) { //if判断成立,说明请求行中没有参数列表
URI = requestLineArr[1];
} else {
URI = requestLineArr[1].substring(0, index);
//获取参数列表
String parameters = requestLineArr[1].substring(index + 1);
String[] parameterPairs = parameters.split("&");
//兼容性处理,防止?后啥都没有。
if (null != parameterPairs && !"".equals(parameterPairs)) {
for (String parameterPair : parameterPairs) {
String[] parameter = parameterPair.split("=");
if (parameter.length == 2) { //判断是否为一对完整的"name=value".
parametersMapping.put(parameter[0],parameter[1]);
}
}
}
}
//!!! 直接关闭Socket关联的InputStream,会引起Socket的关闭。
//inputStream.close();
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
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 getParameter(String name) {
if (parametersMapping.containsKey(name)) { //注意此处API的使用!
return parametersMapping.get(name);
} else {
return null;
}
}
@Override
public String toString() {
return "CyanServletRequest{" +
"method='" + method + '\'' +
", URI='" + URI + '\'' +
", parametersMapping=" + parametersMapping +
'}';
}
}
CyanServletRequest クラスのテスト、実行結果は次の GIF のようになります。
2° CyanServletResponse
CyanServletResponse クラスは、元の HttpServletResponse と同等であり、HTTP 応答関連の情報をカプセル化するために使用されます。
CyanServletResponse クラスのコードは次のとおりです。
package tomcat.http;
import java.io.OutputStream;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : like the original HttpServletResponse
*/
public class CyanServletResponse {
private OutputStream outputStream;
//设置一个HTTP响应头
public static final String respHeader = "HTTP/1.1 200\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
//传入与Socket关联的OutputStream对象
public CyanServletResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
public OutputStream getOutputStream() {
return outputStream;
}
}
テストを実行します (下の GIF):
2.サーブレット仕様:
1° シアンサーブレット
CyanServlet はネイティブ Servlet の init、destroy、service メソッドのみを保持しており、このうちservice メソッドは将来的に CyanServlet の抽象実装クラス CyanHttpServlet で書き換えることが可能です。サービス メソッドの正式なパラメーター リストでは、カスタマイズされたCyanServletRequest および CyanServletResponse.
CyanServlet インターフェイスを使用する必要があることに注意してください。コードは次のとおりです。
package tomcat.servlet;
import tomcat.http.CyanServletRequest;
import tomcat.http.CyanServletResponse;
import java.io.IOException;
public interface CyanServlet {
void init() throws Exception;
void service(CyanServletRequest req, CyanServletResponse resp) throws IOException;
void destroy();
}
2° CyanHttpサーブレット
CyanHttpServlet の機能はネイティブ HttpServlet と同様で、CyanServlet インターフェースのサービスメソッドは CyanHttpServlet に実装されており、サービスメソッドでは HTTP リクエストのメソッドタイプを判断する必要があります。
CyanHttpServlet 抽象クラス コードは次のとおりです。
package tomcat.servlet;
import tomcat.http.CyanServletRequest;
import tomcat.http.CyanServletResponse;
import java.io.IOException;
public abstract class CyanHttpServlet implements CyanServlet{
@Override
public void service(CyanServletRequest req, CyanServletResponse resp) throws IOException {
//忽略大小写
if ("GET".equalsIgnoreCase(req.getMethod())) {
this.deGet(req, resp);
} else if ("POST".equalsIgnoreCase(req.getMethod())) {
this.doPost(req, resp);
}
}
//模板设计模式
public abstract void deGet(CyanServletRequest req, CyanServletResponse resp);
public abstract void doPost(CyanServletRequest req, CyanServletResponse resp);
}
3° CyanLoginサーブレット
CyanLoginServlet は、CyanHttpServlet 抽象クラスを継承し、CyanHttpServlet クラスの doGet および doPost 抽象メソッドを実装する単純なサーブレット インスタンスです。その後、まず HttpRequestHandler スレッド クラスで CyanLoginServlet インスタンスを直接呼び出してみます。
CyanLoginServlet コードは次のとおりです。
package tomcat.servlet;
import tomcat.http.CyanServletRequest;
import tomcat.http.CyanServletResponse;
import java.io.IOException;
import java.io.OutputStream;
public class CyanLoginServlet extends CyanHttpServlet{
@Override
public void deGet(CyanServletRequest req, CyanServletResponse resp) {
doPost(req, resp);
}
@Override
public void doPost(CyanServletRequest req, CyanServletResponse resp) {
String username = req.getParameter("username");
String password = req.getParameter("password");
//获取与当前Socket相关联的OutputStream对象
OutputStream outputStream = resp.getOutputStream();
String respInfo = CyanServletResponse.respHeader + "<h1>username = " +
username + "</h1><br/> " + "<h1>password = " + password + "</h1>" + "<br/>" +
"<h3>Cyan_RA9</h3>";
try {
outputStream.write(respInfo.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
さらに、HttpServletHandler クラスの型を更新し、カプセル化されたコードをコメント アウトする必要もあります。上記の「マルチスレッド モデルの実装」の HTTP 要求処理モジュールで、HttpServletHandler クラスが更新されました。
ランニング効果は次の図に示されています。
3. コンテナの実装:
1° アイデア分析
Tomcat には少なくとも 2 つの大きな HashMap コンテナが維持されています。web.xml 設定ファイルを例にとると、ある HashMap コンテナでは、キーは <url-pattern> に保存され、値は <servlet-name> に保存され、別の HashMap コンテナでは、キーは <servlet に保存されます。 -name> と値は、生成されたサーブレット インスタンスを反映するために <servlet -class> に保存されます。
2° web.xml 設定ファイル
web.xml 構成ファイルは次のとおりです。
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<!--xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"-->
<!--IDEA报错——
因为这是我们自定义的servlet,IDEA无法识别;无所谓!继续用!-->
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>CyanLoginServlet</servlet-name>
<servlet-class>tomcat.servlet.CyanLoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CyanLoginServlet</servlet-name>
<url-pattern>/cyanLogin</url-pattern>
</servlet-mapping>
</web-app>
3° カスタマイズされた Tomcat の最終バージョン
MyTomcat_Pro クラスで 2 つの CurrentHashMap オブジェクトを定義し、2 つの CurrentHashMap オブジェクトの初期化を完了する init メソッドを定義します (Dom4J を使用して web.xml 構成ファイルを読み取ります)。まず、次の図に示すように、 Maven の pom.xml 構成ファイルに dom4j 依存関係を導入する
必要があります。
次に、次の図に示すように、 web.xml 構成ファイルを /target/classes/ ディレクトリにコピーします。
MyTomcat_Pro コードは次のとおりです。
package tomcat;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import tomcat.handler.HttpRequestHandler;
import tomcat.servlet.CyanHttpServlet;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* The final own custom Tomcat.
*/
public class MyTomcat_Pro {
//Tomcat维护的第一个容器
//String --> <servlet-name>
//CyanHttpServlet --> 可存放它的子类(即各种servlet实例)
public static final ConcurrentHashMap<String, CyanHttpServlet> servletMapping =
new ConcurrentHashMap<>();
//Tomcat维护的第二个容器
//String --> <url-patterns>
//String --> <servlet-name>
public static final ConcurrentHashMap<String, String> servletURLMapping =
new ConcurrentHashMap<>();
public static void main(String[] args) {
MyTomcat_Pro myTomcat_pro = new MyTomcat_Pro();
myTomcat_pro.init();
myTomcat_pro.run();
}
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("MyTomcat_Pro在8080端口进行监听...");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
Thread thread = new Thread(new HttpRequestHandler(socket));
thread.start();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void init() {
String path = MyTomcat_Pro.class.getResource("/").getPath();
//path = "/javaProject/Servlet/Cyan_Tomcat/target/classes/"
//使用Dom4J技术解析web.xml文件
SAXReader saxReader = new SAXReader();
try {
//注意文件名
Document document = saxReader.read(new File(path + "web.xml"));
System.out.println("document = " + document);
//获取根元素<web-app>
Element rootElement = document.getRootElement();
//得到根元素下面的所有子元素
List<Element> elements = rootElement.elements();
//遍历并判断
for (Element element : elements) {
if ("servlet".equals(element.getName())) {
Element servlet_name = element.element("servlet-name");
Element servlet_class = element.element("servlet-class");
//反射机制创建servlet实例 (注意getText()方法的使用!)
Class<?> clazz = Class.forName(servlet_class.getText().trim());
Constructor<?> constructor = clazz.getConstructor();
CyanHttpServlet o = (CyanHttpServlet) constructor.newInstance();
servletMapping.put(servlet_name.getText(), o);
} else if ("servlet-mapping".equals(element.getName())) {
Element url_pattern = element.element("url-pattern");
Element servlet_name = element.element("servlet-name");
servletURLMapping.put(url_pattern.getText(), servlet_name.getText());
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
4° カスタムスレッドクラスの最終バージョン
package tomcat.handler;
import tomcat.MyTomcat_Pro;
import tomcat.http.CyanServletRequest;
import tomcat.http.CyanServletResponse;
import tomcat.servlet.CyanHttpServlet;
import tomcat.servlet.CyanLoginServlet;
import java.io.*;
import java.net.Socket;
public class HttpRequestHandler implements Runnable {
private Socket socket;
public HttpRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//接收客户端的信息
CyanServletRequest cyanServletRequest = new CyanServletRequest(socket.getInputStream());
CyanServletResponse cyanServletResponse = new CyanServletResponse(socket.getOutputStream());
String uri = cyanServletRequest.getURI();
System.out.println("uri = " + uri);
String servlet_name = MyTomcat_Pro.servletURLMapping.get(uri);
/*
这里的servlet_name可能为空。
解决方式一 : 将CurrentHashMap替换为HashMap
解决方式二 : 增加一个是否为null的判断。
*/
if (servlet_name == null) {
servlet_name = "";
}
//多态 --> 动态绑定
CyanHttpServlet cyanHttpServlet = MyTomcat_Pro.servletMapping.get(servlet_name);
if (cyanHttpServlet != null) { //判断是否正常得到servlet实例
cyanHttpServlet.service(cyanServletRequest, cyanServletResponse);
} else {
//如果没有找到servlet,返回404
String resp = CyanServletResponse.respHeader + "<h1>404 Not Found!</h1>";
OutputStream outputStream = cyanServletResponse.getOutputStream();
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
}
//释放资源
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//确保Socket关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
5°コンテナ起動テスト
以下に示すように (GIF):