一、Tomcat_v1
Tomcat服务器本质上就是一个 Server的程序,所以这里我们先写一个Server的程序和Socket程序,先完成最基本的通讯功能
Server.java:
package com.tomcat_v1.mySocket_v1;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
ServerSocket server;
try {
server = new ServerSocket(8888);
System.out.println("服务器已启动,初始化端口为:8888");
//这里不断接受服务端的请求
while(true){
//服务器端接收到客户端的Socket连接过来(在接收客户端请求之前会一直阻塞在这里)
Socket client = server.accept();
System.out.println("客户端["+client.getInetAddress().getHostAddress()+"]已连接到服务器");
/**
* 请求信息
*/
//获取客户端发送过来的信息
InputStream in = client.getInputStream(); //异步处理
byte[] buff = new byte[1024];
int length = in.read(buff);
if(length>0) {
String msg = new String(buff, 0, length);
System.out.println("服务端接收到客户端的信息:" + msg);
}
/**
* 响应信息
*/
//获取客户端输出流, 回复信息
OutputStream out = client.getOutputStream();
//构建响应信息
StringBuffer sb = new StringBuffer();
//响应正文
String html = "服务器已收到信息";
sb.append(html);
out.write(sb.toString().getBytes());
System.out.println("服务器响应结束\n");
out.flush();
out.close();
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server类中主要完成了一下功能:
- 初始化ServerSocket
- 等待接收客户端请求
- 解析请求信息
- 发出响应信息
Client.java:
package com.tomcat_v1.mySocket_v1;
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1",8888);
Thread.sleep(10000);
/**
* 发送请求信息
*/
OutputStream out = socket.getOutputStream();
out.write("客户端[2]发送信息".getBytes());
out.flush(); //刷新缓冲区,将数据发送出去
/**
* 接收响应信息
* 在接收到服务端返回的信息之前会一直阻塞在这里,直到服务器端发送流信息过来
*/
System.out.println("等待接收服务端信息......");
InputStream in = socket.getInputStream();
StringBuffer sb = new StringBuffer();
int len;
byte[] buff=new byte[1024];
while((len=in.read(buff)) != -1 ){
sb.append(new String(buff, 0, len));
}
System.out.println("服务器响应信息:"+sb.toString());
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Client类主要完成的功能:
- 初始化Socket, 连接到服务端
- 发送请求信息
- 接收服务器的响应信息
控制台输出:
服务器端:
客户端1:
客户端2:
由上可见, 服务端与客户端已完成通讯, 在tomcat中接收请求和响应分别由Request和Response来进行处理, 那么, 我们只需要把Server类中关于接收请求部分逻辑和响应分别封装到自定义的Request和Response中即可
二、Tomcat_v2
Server.java
package com.tomcat_v1.mySocket_v2;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
ServerSocket server;
try {
server = new ServerSocket(8888);
System.out.println("服务器已启动,初始化端口为:8888");
/**
* 接受客户端的请求
* 每次只能接收一个客户端的请求, 在前一个客户端请求未处理完成之前, 之后的请求会被阻塞(阻塞式IO)
*/
while(true){
//服务器端接收到客户端的Socket连接过来(在接收客户端请求之前会一直阻塞在这里)
Socket client = server.accept();
System.out.println("客户端["+client.getInetAddress().getHostAddress()+"]已连接到服务器");
/**
* 接收请求信息(由request进行处理)
* 如果没有获取到客户端发送过来的流信息,则程序会阻塞,直到客户端发送流信息过来
*/
//获取客户端发送过来的信息
System.out.println("等待接收客户端信息......");
Request request = new Request(client.getInputStream());
request.resolverRequest();
/* Thread.sleep(10000);*/
/**
* 发送响应信息(由response进行处理)
*/
//获取客户端输出流, 回复信息
Response response = new Response(client.getOutputStream());
response.resolverResponse();
System.out.println("服务端响应结束\n");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Request.java
package com.tomcat_v1.mySocket_v2;
import java.io.IOException;
import java.io.InputStream;
public class Request {
private InputStream in;
public Request(InputStream in) {
this.in = in;
}
public void resolverRequest() {
try {
byte[] buff = new byte[1024];
int length = in.read(buff);
if(length>0) {
String msg = new String(buff, 0, length);
System.out.println("服务端接收到客户端的信息:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Response.java
package com.tomcat_v1.mySocket_v2;
import java.io.IOException;
import java.io.OutputStream;
public class Response {
private OutputStream out;
public Response(OutputStream out) {
super();
this.out = out;
}
public void resolverResponse(){
try {
//构建响应信息
StringBuffer sb = new StringBuffer();
//响应正文
String content = "服务器已收到信息";
sb.append(content);
out.write(sb.toString().getBytes());
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
分别定义Request和Response, 将接收请求和响应的逻辑放在这两类中进行处理, 这里逻辑上并没有变化, 只是封装了一下;
而在tomcat中, 我们可以利用浏览器发送请求信息, 在服务端处理这些请求信息, 然后响应给浏览器; 而主要的处理逻辑是由我们编写的Servlet类来实现; 通常情况下是通过继承HttpServlet类重写doGet和doPost方法实现, 如下:
自定义Servlet:
package com.bookstore.constant;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
接下来实现自定义的tomcat
三、Tomcat_v3
项目结构:
第一步: 实现主启动类Server.java,在这个类里创建一个ServerSocket,等待客户端的连接,并且创建一个线程池, 把连接交由线程去处理; 在前一个连接处理完成后进入死循环, 继续等待下一个客户端的连接;
Server.java
package com.tomcat_v2.tomcat;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
private static ServerSocket serverSocket;
private static int port = 8080;
private final static int POOL_SIZE = 8;
private static ExecutorService executorService;
public static void start(){
try {
serverSocket = new ServerSocket(port);
Socket socket = null;
System.out.println("服务器已启动, 初始化端口为:"+port);
executorService = Executors.newFixedThreadPool(POOL_SIZE); //初始化线程池
while(true){
socket = serverSocket.accept(); //等待获取客户端请求
System.out.println("服务器已接收到请求");
executorService.execute(new Handler(socket)); //分配线程处理请求,响应
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
start();
}
}
第二步: 在接收到客户端请求后, 将请求和响应分开处理, 那么首先需要自定义Request和Response;
Request.java
package com.tomcat_v2.tomcat;
import com.tomcat_v2.servlet.HttpServlet;
import com.tomcat_v2.servlet.ServletContainer;
import java.util.Map;
public class Request {
private String path;
private String method;
private Map<String,Object> parameter;
private Map<String,String> attributes;
public HttpServlet getServlet(){
return ServletContainer.getHttpServlet(path);
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public Map<String, Object> getParameter() {
return parameter;
}
public void setParameter(Map<String, Object> parameter) {
this.parameter = parameter;
}
public Map<String, String> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}
}
Request类中包含请求的方式(GET,POST), 请求的路径, 属性,以及参数信息
Response.java
package com.tomcat_v2.tomcat;
import java.io.*;
public class Response {
private PrintWriter printWriter;
public Response(PrintWriter printWriter) {
this.printWriter = printWriter;
}
public void write(String msg){
if(msg != null){
printWriter.write(msg);
printWriter.flush();
}
}
public void forword(String dest){
InputStream in = this.getClass().getClassLoader().getResourceAsStream(dest);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
try {
String s =null;
do{
s = reader.readLine();
write(s);
}while (s != null);
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Reponse中主要通过对客户端Socket的输出流进行操作, 输出数据;
writer()方法: 通过输出流向浏览器输出信息
forword()方法: 根据指定的资源信息,将该资源输出到浏览器上; 比如请求的html页面
第三步: tomcat中对于业务逻辑的处理主要维护在Servlet中, 因此我们需要先定义一个抽象类HtppServlet
HttpServlet.java
package com.tomcat_v2.servlet;
import com.tomcat_v2.tomcat.Request;
import com.tomcat_v2.tomcat.Response;
import com.tomcat_v2.util.Constant;
public abstract class HttpServlet {
public void doGet(Request request, Response response){ }
public void doPost(Request request, Response response){ }
public void service(Request request, Response response){
if(Constant.REQUEST_METHOD_GET.equals(request.getMethod())){
doGet(request,response);
}else if(Constant.REQUEST_METHOD_POST.equals(request.getMethod())){
doPost(request,response);
}
}
}
Constant.java
package com.tomcat_v2.util;
public class Constant {
public final static String REQUEST_METHOD_GET ="GET";
public final static String REQUEST_METHOD_POST ="POST";
}
根据请求方式的不同进行不同的处理
第四步: 在步骤一中将获取到的客户端Socket交由线程池去处理, 那么需要实现一个线程类Handler; 在Handler中首先对请求信息解析, 对响应头进行设置; 然后根据请求路径信息获取到对应的Servlet, 进行处理
Handler.java
package com.tomcat_v2.tomcat;
import com.tomcat_v2.servlet.HttpServlet;
import com.tomcat_v2.util.Constant;
import java.io.*;
import java.net.Socket;
import java.util.HashMap;
public class Handler implements Runnable{
private Socket socket;
private PrintWriter writer;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8"));
writer.println("HTTP/1.1 200 OK");
writer.println("Content-Type: text/html;charset=utf-8");
writer.println();
Request request = new Request();
Response response = new Response(writer);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
/**
* 解析请求信息
*/
while(true){
String msg = reader.readLine();
if(msg == null || "".equals(msg.trim())){
break;
}
String[] msgs = msg.split(" ");
if(msgs.length == 3 && msgs[2].equalsIgnoreCase("HTTP/1.1")){
//设置请求方式
request.setMethod(msgs[0]);
//设置请求参数
String[] pathAndParams = msgs[1].split("\\?");
HashMap<String, Object> requestParams = new HashMap<String, Object>();
if(pathAndParams.length == 2){
String[] params = pathAndParams[1].split("&");
for (String param: params) {
String key = param.split("=")[0];
String value = param.split("=")[1];
requestParams.put(key,value);
}
}
request.setParameter(requestParams);
//设置请求路径
request.setPath(pathAndParams[0]);
break;
}
}
if(request.getPath().endsWith("ico")){
return;
}
/**
* 根据请求信息获取对应的Servlet
*/
HttpServlet httpServlet = request.getServlet();
/**
* 处理请求,以及响应信息
*/
dispatcher(httpServlet,request,response);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
writer.close();
socket.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
private void dispatcher(HttpServlet httpServlet, Request request, Response response) {
try{
if(httpServlet == null){
response.write("<h1>404 Not Found</h1>");
return;
}
httpServlet.service(request,response);
}catch (Exception e){
response.write("<h1>500 Server Error</h1>");
}
}
}
第五步: 在上一步中, 根据不同的请求路径映射不同的Servlet处理类, 那么, 需要将路径信息与对应的处理类Servlet类路径映射起来, 可以根据路径获取Servlet的类路径信息; 因此, 需要配置web.xml
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>servlet_1</servlet-name>
<servlet-class>com.tomcat_v2.myServlet.Servlet_1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servlet_1</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>servlet_2</servlet-name>
<servlet-class>com.tomcat_v2.myServlet.Servlet_2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servlet_2</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>servlet_3</servlet-name>
<servlet-class>com.tomcat_v2.myServlet.Servlet_3</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servlet_3</servlet-name>
<url-pattern>/test2</url-pattern>
</servlet-mapping>
</web-app>
第六步: 有了web.xml来将Servlet与请求路径映射起来, 那么首先要做的就是对web.xml进行解析; 在web.xml中维护着servlet与servlet-mapping信息, 这两个节点通过servlet-name进行关联, 而我们最终需要的是路径信息与servlet类路径进行关联, 因此, 需要定义实体类Servlet与ServletMapping
Servlet.java
package com.tomcat_v2.servlet;
public class Servlet {
private String name;
private String clazz;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
}
ServletMapping.java
package com.tomcat_v2.servlet;
public class ServletMapping {
private String name;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
第七步: 定义util类来解析web.xml
XMLUtil.java
package com.tomcat_v2.util;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import com.tomcat_v2.servlet.Servlet;
import com.tomcat_v2.servlet.ServletMapping;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class XMLUtil {
public static Map<String, Map<String, Object>> parseWebXML() throws Exception{
Map<String, Map<String, Object>> result = new HashMap<String, Map<String,Object>>();
//DOM解析器工厂实例
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//获取DOM解析器
DocumentBuilder db = dbf.newDocumentBuilder();
//把要解析的XML文档转化为输入流,让DOM解析器解析,从src根目录开始读取
InputStream in = XMLUtil.class.getClassLoader().getResourceAsStream("web.xml");
//解析输入流,获得document对象
Document document = db.parse(in);
//得到xml的根节点
Element root = document.getDocumentElement();
//得到根节点的子节点
NodeList xmlNodes = root.getChildNodes();
//循环读取
for(int i = 0; i < xmlNodes.getLength(); i++) {
Node config = xmlNodes.item(i);
//判断是否为元素节点
if(config != null && config.getNodeType()==Node.ELEMENT_NODE) {
String nodeName1 = config.getNodeName();
/**
* 解析<servlet>节点
*/
if("servlet".equals(nodeName1)) {
Map<String, Object> servletMaps = null;
if(!result.containsKey("servletMaps")) {
result.put("servletMaps", new HashMap<String, Object>());
}
servletMaps = result.get("servletMaps");
//获取元素节点的所有子节点
NodeList childNodes = config.getChildNodes();
//创建servlet实体类准备接受数据
Servlet servlet = new Servlet();
for (int j = 0; j < childNodes.getLength(); j++) {
Node node = childNodes.item(j);
//判断是否为元素节点
if(node != null && node.getNodeType()==Node.ELEMENT_NODE) {
//读取servlet-name和servlet-class
String nodeName2 = node.getNodeName();
//读取文本内容
String textContent = node.getTextContent();
if(nodeName2.equals("servlet-name")) {
servlet.setName(textContent);
}else if(nodeName2.equals("servlet-class")) {
servlet.setClazz(textContent);
}
}
}
//结果放到Map中
servletMaps.put(servlet.getName(), servlet);
}
/**
* 解析<servlet-mapping>节点
*/
if (nodeName1.equals("servlet-mapping")) {
Map<String, Object> servletMappingMaps = null;
if(!result.containsKey("servletMappingMaps")) {
result.put("servletMappingMaps", new HashMap<String, Object>());
}
servletMappingMaps = result.get("servletMappingMaps");
//获取元素节点的所有子节点
NodeList childNodes = config.getChildNodes();
//创建实体类
ServletMapping servletMapping = new ServletMapping();
for(int j = 0; j < childNodes.getLength(); j++) {
Node node = childNodes.item(j);
if(node!=null && node.getNodeType()==Node.ELEMENT_NODE) {
String nodeName2 = node.getNodeName();
String textContent = node.getTextContent();
if(nodeName2.equals("servlet-name")) {
servletMapping.setName(textContent);
}else if (nodeName2.equals("url-pattern")) {
servletMapping.setUrl(textContent);
}
}
}
servletMappingMaps.put(servletMapping.getUrl(), servletMapping);
}
}
}
return result;
}
public static void main(String[] args) throws Exception {
System.out.println(parseWebXML());
}
}
第八步: 在第四步中通过调用Request类的getServlet方法,根据路径获取对应的Servlet类; 而在getServlet()方法中调用ServletContainer.getHttpServlet(path)方法来实现
ServletContainer.java
package com.tomcat_v2.servlet;
;
import com.tomcat_v2.util.XMLUtil;
import java.util.HashMap;
import java.util.Map;
public class ServletContainer {
private static Map<String, Object> servletMaps = new HashMap();
private static Map<String, Object> servletMappingMaps = new HashMap();
private static Map<String, HttpServlet> servletContainer = new HashMap();
//静态代码块加载model实体类
static {
try {
Map<String, Map<String, Object>> maps = XMLUtil.parseWebXML();
if(maps != null) {
servletMaps = maps.get("servletMaps");
servletMappingMaps = maps.get("servletMappingMaps");
}
}catch(Exception e){
e.printStackTrace();
}
}
//获取servlet容器中对应的HttpServlet
public static HttpServlet getHttpServlet(String path) {
if(path == null || "".equals(path.trim()) || "/".equals(path)) {
path = "/index";
}
if(servletContainer.containsKey(path)) {
return servletContainer.get(path);
}
if(!servletMappingMaps.containsKey(path)) {
return null;
}
ServletMapping servletMapping = (ServletMapping)servletMappingMaps.get(path);
String name = servletMapping.getName();
if(!servletMaps.containsKey(name)) {
return null;
}
Servlet servlet = (Servlet)servletMaps.get(name);
String clazz = servlet.getClazz();
if(clazz==null || clazz.trim().equals("")) {
return null;
}
HttpServlet httpServlet = null;
try {
httpServlet = (HttpServlet)Class.forName(clazz).newInstance();
servletContainer.put(path, httpServlet);
} catch (Exception e) {
e.printStackTrace();
}
return httpServlet;
}
}
ServletContainer中,在静态代码块中调用XMLUtil.parseWebXML()方法来对web.xml进行解析; 然后根据Servlet对象中维护的class信息通过反射进行实例化, 并返回; 在第四步中获取到指定的Servlet后, 通过调用Servlet的service方法来处理请求,响应
第九步: 编写自定义Servlet
Servlet_1.java
package com.tomcat_v2.myServlet;
import com.tomcat_v2.servlet.HttpServlet;
import com.tomcat_v2.tomcat.Request;
import com.tomcat_v2.tomcat.Response;
public class Servlet_1 extends HttpServlet {
@Override
public void doGet(Request request, Response response) {
response.write("<h1>Servlet_1:Hello World</h1>");
}
}
Servlet_2.java
package com.tomcat_v2.myServlet;
import com.tomcat_v2.servlet.HttpServlet;
import com.tomcat_v2.tomcat.Request;
import com.tomcat_v2.tomcat.Response;
public class Servlet_2 extends HttpServlet {
@Override
public void doGet(Request request, Response response) {
response.write("<h1>Servlet_2:Hello World</h1>");
}
}
第十步: 启动Server, 在浏览器中输入localhost:8080/index, 向服务器发送信息
至此, 一个简单的自定义tomcat已完成
但是, 在实际应用场景中, 我们并不是让每一个Servlet只处理一个请求的逻辑而是将逻辑写在每一个方法中进行处理; 这里也进行简单的实现:
在servlet中, 逻辑是实现在doGet和doPost方法中, 这样做的局限性是每一个Servlet只能处理一个请求;效率太低; 因此, 我们可以对Servlet进行包装:
BaseServlet.java
package com.tomcat_v2.tomcat;
import com.tomcat_v2.servlet.HttpServlet;
import java.lang.reflect.Method;
import java.util.Map;
public class BaseServlet extends HttpServlet {
@Override
public void service(Request request, Response response) {
Map<String, Object> parameter = request.getParameter();
String methodName = (String) parameter.get("method");
if(methodName == null){
methodName = "index";
}
Class clazz = this.getClass();
try {
Method method = clazz.getMethod(methodName, request.getClass(), response.getClass());
String dest = (String) method.invoke(this,request,response);
if(dest != null){
response.forword(dest);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String index(Request request, Response response) {
return null;
}
}
在HttpServlet的service方法中只是根据请求方式的不同,将请求交由不同的方法进行处理, 局限性是从这里出现的; 因此, 对该方法逻辑进行修改, 通过获取request中请求参数可得到实际请求的方法, 因此可以通过反射执行该方法来完成请求的处理;
Servlet_v3.java
package com.tomcat_v2.myServlet;
import com.tomcat_v2.tomcat.BaseServlet;
import com.tomcat_v2.tomcat.Request;
import com.tomcat_v2.tomcat.Response;
public class Servlet_3 extends BaseServlet {
public String getAll(Request request, Response response){
response.write("{name:lic,age:16}");
return null;
}
public String getHtml(Request request, Response response){
return "index.html";
}
}
测试1:
不仅可以通过response返回数据, 也可以返回指定的html页面
测试2:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 style="color: red">Hello World</h1>
<h2 style="color: blue;">这是我的第一个网页</h2>
</body>
</html>
在使用tomcat时, 我们需要将自己的项目打成war包,放入tomcat下, 然后启动tomcat即可, 因此可以理解到, 在启动项目时, tomcat会将我们的项目加载到容器中, 通过将请求的路径映射到实体类来完成业务逻辑的处理, 这个后续播客会写出......