仿Tmocat的简易版HTTP服务器
Posted Serendipity sn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了仿Tmocat的简易版HTTP服务器相关的知识,希望对你有一定的参考价值。
目录
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以上是关于仿Tmocat的简易版HTTP服务器的主要内容,如果未能解决你的问题,请参考以下文章