目录
HTTP项目(MyTomcat)
1.项目总览
流程图:
流程:
- 初始化工作
- 扫描所有的Context
- 读取并解析各子的web配置文件
- 加载需要的ServletClass,表现为Class<?>
- 实例化需要的Servlet对象
- 执行Servlet对象的初始化工作
- 处理Http请求-响应(单次的请求响应处理逻辑)
- 读取解析HTTP请求->Request对象,实现标准种定义的HttpServletRequest接口
- 解析请求行
- 解析方法
- 解析路径
- contextPath
- servletPath
- queryString->parameters
- 解析版本(这里我们不使用)
- 解析请求头(核心是解析cookie,根据cookie-name是session的找出session-id(也可能不存在))
- 理论上也需要解析请求体,但是这里我们只支持Get方法
- 解析请求行
- 构建Response对象
- 根据请求的contextPath找到,交给哪个Context处理
- 根据servletPath找到,交给哪个Servlet处理
- 调用servlet.service(请求,响应)
- 发送Response对象->响应
- 读取解析HTTP请求->Request对象,实现标准种定义的HttpServletRequest接口
- 销毁工作
2.Servlet容器
作为Servlet容器,所以满足Servlet标准,定义了满足Servlet标准的抽象类和接口
3.HTTP服务器
<1>TCP连接的理解
<2>正式项目
(1)初始化工作
找到所有Servlet对象,进行初始化
-
通过webapps目录下不同的Web项目,找到他们的项目路径Context
本质上是文件操作
扫描固定的目录(webapps)下有哪些子目录
目录名称作为context的name
public static final String WEBAPPS_BASE = "D:\\javaCode\\HTTP\\http-project\\webapps";
//管理所有的Context对象
public static final List<Context> contextList = new ArrayList<>();
private static final ConfigReader configReader = new ConfigReader();
public static final DefaultContext defaultContext=new DefaultContext(configReader);
private static void scanContexts() {
//扫描目录,获取context
File webappsRoot = new File(WEBAPPS_BASE);
File[] files = webappsRoot.listFiles();
if (files == null) {
throw new RuntimeException();
}
for (File file : files) {
//不是目录,说明不是web应用,直接跳过
if (!file.isDirectory()) {
continue;
}
//获取各个web应用对应的应用上下文路径(context)
String contextName = file.getName();
Context context = new Context(configReader, contextName);
contextList.add(context);
}
}
-
读取并解析webapps/WEB-INF目录下的web.conf文件,获取Servlet类的名称
web.conf:例如
servlets:
# ServletName = ServletClassName
TranslateServlet = org.example.webapps.dictionary.TranslateServlet
LoginActionServlet= org.example.webapps.dictionary.LoginActionServlet
ProfileActionServlet = org.example.webapps.dictionary.ProfileActionServlet
servlet-mappings:
# URLPattern = ServletName
/translate = TranslateServlet
/login-action = LoginActionServlet
/profile-action = ProfileActionServlet
利用有限状态机已经对字符串的切割等处理,得到两个map,找到了url和需要处理的Servlet类对象之间的映射
//ServletName和ServletClassName对应关系:上文servlets:后的对应关系
Map<String,String > servletNameToServletClassNameMap=new HashMap<>();
//URI和Servlet类名称的对应关系:上文servlet-mappings:后的对应关系
LinkedHashMap<String,String> urlToServletNameMap=new LinkedHashMap<>();
代码:
public Config read(String name) throws IOException {
Map<String,String > servletNameToServletClassNameMap=new HashMap<>();
LinkedHashMap<String,String> urlToServletNameMap=new LinkedHashMap<>();
//进行web.conf文件的读取+解析
//规范:web.conf放哪里,必须符合规范,否则就会读不到
String fileName=String.format("%s/%s/WEB-INF/web.conf", HttpServer.WEBAPPS_BASE,name);//两个参数分别表示webapps的绝对路径即Context项目地址
String stage="start";//"servlets"/"mappings" 下面switch的三种状态,分别表示三个解析的步骤
//进行文本文件的读取
try (InputStream is=new FileInputStream(fileName)){
Scanner scanner=new Scanner(is,"UTF-8");
while (scanner.hasNextLine()){
String line=scanner.nextLine().trim();
if(line.isEmpty() || line.startsWith("#")){
//如果是空行或者是以#开头的注释不做处理
continue;
}
switch (stage){
case "start":
if(line.equals("servlets:")){
stage="servlets";
}
break;
case "servlets":
if(line.equals("servlet-mappings:")){
stage="mappings";
}else {
// 进行Servlet解析 ServletName=>ServletClassName的解析
String []parts=line.split("=");
String servletName=parts[0].trim();
String servletClassName=parts[1].trim();
servletNameToServletClassNameMap.put(servletName,servletClassName);
}
break;
case "mappings":
//进行URL => ServletName的解析
String []parts=line.split("=");
String url=parts[0].trim();
String servletName=parts[1].trim();
urlToServletNameMap.put(url,servletName);
break;
}
}
}
return new Config(servletNameToServletClassNameMap,urlToServletNameMap);
}
-
加载需要的ServletClass,表现为Class<?>
通过步骤2,得到了URI对应需要处理的Servlet的全类名,通过反射,可以得到需要的类
进行Servlet类加载
List<Class<?>> servletClassList=new ArrayList<>();
public void loadServletClasses() throws ClassNotFoundException {
Set<String> servletClassNames = new HashSet<>(config.servletNameToServletClassNameMap.values());
for(String servletClassName : servletClassNames){
Class<?> clazz=webappsClassLoader.loadClass(servletClassName);
servletClassList.add(clazz);
}
}
这里我们每个Context项目使用各自的类加载器classLoader,将项目之间进行隔离,防止数据库等版本的不同,而进行类加载时发生错误
-
实例化需要的Servlet对象
List<Servlet> servletList = new ArrayList<>();
public void instantiateServletObjects() throws IllegalAccessException, InstantiationException {
for(Class<?> servletClass : servletClassList){
//利用反射,默认调用该类的无参构造方法,进行实例化对象
Servlet servlet = (Servlet) servletClass.newInstance();
servletList.add(servlet);
}
}
利用反射,实例化Servlet对象
-
调用Servlet的init(),执行类的初始化工作
涉及到“Servlet生命周期”的概念
调用每个Servlet对象的init()方法,这样子类可以通过重写自己的init()方法,进行不同的初始化
private static void initializeServletObjects() throws ServletException {
for (Context context : contextList) {
context.initServletObjects();
}
defaultServlet.init();
notFoundServlet.init();
}
(2)处理HTTP请求-响应(单次的请求响应处理逻辑)
服务器逻辑:使用简单的线程池,使用多线程对每次的响应进行处理,每次响应间不存在共享变量,所以无序考虑线程安全问题,将单次响应任务放入RequestResponseTask任务中处理
private static void startServer() throws IOException {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
ServerSocket serverSocket = new ServerSocket(8080);
//2.每次循环处理一个请求
while (true) {
Socket socket = serverSocket.accept();
Runnable task = new RequestResponseTask(socket);
threadPool.execute(task);
}
}
RequestResponseTask任务处理逻辑:
-
读取解析HTTP请求->Request对象,实现标准中定义的HttpServletRequest接口
我们定义一个专门解析请求Request的类,对HttpRequest的请求行和请求头进行解析(简易版HTTP服务器,只支持GET方法,所以我们不解析请求体).保存请求中的Cookie信息
public class HttpRequestParser {
public Request parse(InputStream socketInputStream) throws IOException, ClassNotFoundException {
//1.读取请求行
Scanner scanner=new Scanner(socketInputStream);
String method=scanner.next().toUpperCase();//读取请求方法
String path=scanner.next();//读取请求的全路径
//解析parameters,请求行传来的参数
Map<String,String> parameters=new HashMap<>();
String requestURI=path;
int i=requestURI.indexOf("?");
if(i != -1){
requestURI=path.substring(0,i);
String queryString = path.substring(i+1);
for(String kv : queryString.split("&")){
String[] partsKV = kv.split("=");
String name= URLDecoder.decode(partsKV[0],"UTF-8");
String value= URLDecoder.decode(partsKV[1],"UTF-8");
parameters.put(name,value);
}
}
//解析contextPath和servletPath
int j=requestURI.indexOf('/',1);//找到第二个"/"
String contextPath="/";
String servletPath=requestURI;
if(j != -1){
//例如:requestURI=/blog/add
contextPath=requestURI.substring(1,j);// blog(好比较)
servletPath=requestURI.substring(j); // /add
}
String version=scanner.nextLine();//读取版本信息,没用
//2.读取请求头,将请求头种的Cookie信息保存
String headerLine;
Map<String,String> headers=new HashMap<>();
List<Cookie> cookieList=new ArrayList<>();
while (scanner.hasNextLine() && !(headerLine=scanner.nextLine().trim()).isEmpty()){
String[] parts=headerLine.split(":");
String name=parts[0].toLowerCase();
String value=parts[1];
headers.put(name,value);
//判断是否是cookie
if(name.equals("cookie")){
String [] kvcookies=value.split(";");
for(String kvcookie : kvcookies){
if(kvcookie.trim().isEmpty()){
continue;
}
String[] split = kvcookie.split("=");
String cookieName=split[0].trim();
String cookieValue=split[1].trim();
Cookie cookie=new Cookie(cookieName,cookieValue);
cookieList.add(cookie);
}
}
}
return new Request(method,requestURI,contextPath,servletPath,parameters,headers,cookieList);
}
}
将请求方法,请求的全路径,项目路径ContextPath,Servlet路径servletPath,请求的参数,请求头以及Cookie信息放入到Request对象中
对于Request对象,我们需要遍历Cookie信息,当Cookie存在Session-id时,需要构建Session对象,这里我们专门建一个session文件夹,将Session数据按文件形式保存本地,持久化
for(Cookie cookie : cookieList){
if(cookie.getName().equals("session-id")){
String sessionId=cookie.getValue();
session = new HttpSessionImpl(sessionId);
break;
}
}
Session对象:
通过loadSessionData()方法,将Session数据保存在本地文件
public class HttpSessionImpl implements HttpSession {
public final Map<String,Object> sessionData;
public final String sessionId;
//没有从cookie中拿到sessionId时使用
public HttpSessionImpl(){
sessionId= UUID.randomUUID().toString();//没有传入,随机生成一个
sessionData=new HashMap<>();
}
//从cookie中拿到了sessionId时使用
public HttpSessionImpl(String sessionId) throws IOException, ClassNotFoundException {
this.sessionId=sessionId;
sessionData=loadSessionData(sessionId);//加载Session数据
}
private static final String SESSION_BASE="D:\\javaCode\\HTTP\\http-project\\sessions";
//加载Session里面的数据
//文件名 : <session-id>.session
private Map<String, Object> loadSessionData(String sessionId) throws IOException, ClassNotFoundException {
String sessionFileName=String.format("%s\\%s.session",SESSION_BASE,sessionId);
File sessionFile=new File(sessionFileName);
if(!(sessionFile.exists())){
return new HashMap<>();//session不存在,返回一个空的map
}
try(InputStream is=new FileInputStream(sessionFile) {
}){
//使用ObjectInputStream进行对象读取
ObjectInputStream objectInputStream=new ObjectInputStream(is);
return (Map<String, Object>) objectInputStream.readObject();
}
}
//保存Session里面的数据
public void saveSessionData() throws IOException {
if(sessionData.isEmpty()){
return;
}
String sessionDataFile=String.format("%s\\%s.session",SESSION_BASE,sessionId);
try(OutputStream os=new FileOutputStream(sessionDataFile)){
ObjectOutputStream objectOutputStream=new ObjectOutputStream(os);
objectOutputStream.writeObject(sessionData);
objectOutputStream.flush();
}
}
@Override
public Object getAttribute(String name) {
return sessionData.get(name);
}
@Override
public void removeAttribute(String name) {
sessionData.remove(name);
}
@Override
public void setAttribute(String name, Object value) {
sessionData.put(name, value);
}
-
构建Response响应对象
将响应的状态码,响应体及Cookie等信息放入Response对象中,并使用IO输出到页面
部分Response对象代码:
public class Response implements HttpServletResponse {
public int status = 200;
public final List<Cookie> cookieList;
public final Map<String, String> headers;
public final ByteArrayOutputStream bodyOutputStream;
public final PrintWriter bodyPrintWriter;
public Response() throws UnsupportedEncodingException {
cookieList = new ArrayList<>();
headers = new HashMap<>();
bodyOutputStream = new ByteArrayOutputStream(1024);
Writer writer = new OutputStreamWriter(bodyOutputStream, "UTF-8");
bodyPrintWriter = new PrintWriter(writer);
}
}
-
根据请求的contextPath找到,交给哪个Context处理
如果没找到Context,即交给默认Context处理(404)
Context handleContext = HttpServer.defaultContext;
for (Context context : HttpServer.contextList) {
if (context.getName().equals(request.getContextPath())) {
handleContext = context;
break;
}
}
-
根据servletPath找到,交给哪个Servlet处理
如果没找到ServletPath,交给默认的Servlet处理
Servlet servlet = handleContext.getServlet(request.getServletPath());
if (servlet == null) {
servlet = HttpServer.defaultServlet;
}
这里默认的Servlet:
首先先寻找这个ServletPath是否是静态资源,如果是静态资源,按照静态资源的Servlet->DefaultServlet处理
如果也不是静态资源,交给NotFoundServlet处理->404处理
public class DefaultServlet extends HttpServlet {
private final String welcomeFile="index.html";
private final Map<String,String> mime=new HashMap<>();
private final String defaultContentType="text/plain";
//静态资源后缀名所对应的输出格式
@Override
public void init() throws ServletException {
mime.put("htm","text/html");
mime.put("html","text/html");
mime.put("jpg","image/jepg");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String contextPath=req.getContextPath();
String servletPath=req.getServletPath();
//静态资源处理
if(servletPath.equals("/")){
servletPath=welcomeFile;//欢迎页面
}
String fileName=String.format("%s\\%s\\%s", HttpServer.WEBAPPS_BASE,contextPath,servletPath);
System.out.println(fileName);
File file=new File(fileName);
if(!file.exists()){
//按照404的方式进行处理
HttpServer.notFoundServlet.service(req,resp);
System.out.println("not");
return;
}
String contentType=getContentType(servletPath);
System.out.println(contentType);
resp.setContentType(contentType);
OutputStream outputStream=resp.getOutputStream();
try(InputStream is=new FileInputStream(file)){
byte[] buffer=new byte[1024];
int len=-1;
while ((len = is.read(buffer)) != -1){
outputStream.write(buffer,0,len);
}
outputStream.flush();
}
}
private String getContentType(String servletPath) {
String contentType=defaultContentType;
int i=servletPath.lastIndexOf(".");
if(i != -1){
String extension=servletPath.substring(i+1);
contentType=mime.getOrDefault(extension,defaultContentType);
}
return contentType;
}
}
-
调用servlet.service()方法,交给业务处理
自己实现的Servlet方法继承Servlet类,重写service()方法,实现自己的业务
servlet.service(request, response);
-
业务处理完之后,根据Response对象中的数据,发送HTTP响应
在发送HTTP响应时,需要将Cookie的数据放入响应体中,并将Session数据保存在本地文件中
private void sendResponse(OutputStream outputStream, Request request, Response response) throws IOException {
// 保存 session
// 1. 种 cookie
// 2. 保存成文件
if (request.session != null) {
Cookie cookie = new Cookie("session-id", request.session.sessionId);
response.addCookie(cookie);
request.session.saveSessionData();
}
Writer writer = new OutputStreamWriter(outputStream, "UTF-8");
PrintWriter printWriter = new PrintWriter(writer);
for (Cookie cookie : response.cookieList) {
response.setHeader("Set-Cookie", String.format("%s=%s", cookie.getName(), cookie.getValue()));
}
}
(3)销毁所有的Servlet对象,结束生命周期
private static void destroyAllServletClass () {
for (Context context : contextList) {
context.destroy();
}
defaultServlet.destroy();
notFoundServlet.destroy();
}
<3>自定义登录逻辑进行测试
public class LoginActionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username=req.getParameter("username");
String password=req.getParameter("password");
if(username.equals("zyf") && password.equals("123")){
User user=new User(username,password);
HttpSession session = req.getSession();
session.setAttribute("user",user);
resp.sendRedirect("profile-action");
}else {
resp.sendRedirect("login.html");
}
}
}
public class ProfileActionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
User user=(User)session.getAttribute("user");
if(user == null){
resp.sendRedirect("login.html");
}else {
resp.setContentType("text/plain");
resp.getWriter().println(user.toString());
}
}
}
4.项目总结
HTTP服务器部分的总结:
仿Tomcat的设计:
管理多个Web项目的Servlet容器,通过TCP建立连接,解析Http的请求和响应。对Servlet的生命周期进行管理(通过不同项目的类加载器,通过反射,每个项目对各自的Servlet进行管理)
5.项目的不足
- 只支持了HTTP1.0协议(短链接),一条TCP只能处理一次的请求-响应周期
- 只支持了GET方法
- 字符集的编码,固定成了"UTF-8"
对于TCP Server
- 项目使用的时BIO的形式,真实的Tomcat使用的NIO
- 线程池只是使用的最简单的,不高效
- 没有涉及日志功能,调试只能使用System.out.printIn()打印观察