当前位置:   article > 正文

使用java自制简易web服务器_纯手工java web server

纯手工java web server

微信公众号:进击的蛋糕(dangao123coding)

什么是web服务器

记得好多年前,刚刚开始学javaweb的时候,老师教的第一件事是安装jdk,第二件事就是安装tomcat了。

当时老师的操作是,下载完压缩包后解压,然后把tomcat的bin目录添加到环境变量里面,然后打开黑乎乎的cmd,输入catalina就可以运行tomcat了。当时还不知道为什么只要添加了环境变量,就可以在cmd里面启动tomcat,更不要说为什么我们什么都没有设置,输入一个命令就能启动web服务器了。

这个问题其实困惑了我好久好久,不过当时由于水平有限,以及网上的课程大多都是教你怎么搭建web服务器,怎么编写servlet,很少有人会去探究web服务器究竟是怎么运行起来的,在输入命令背后又进行了那些操作,以及如何加载我们的servlet进行服务等等。

这些东西对于找工作来说也许一点用也没有,但是我真的好奇,于是去网络上寻找答案,最终理顺了一个简易版的web服务器的运行流程。

简单来说(我们就说最最简单的情况),web服务器就是一个可以用socket接收客户端连接,然后进行HTTP协议解析和相应软件。没错,就是一个软件而已,当然,像tomcat这样非常流行,并且可以用于生产环境的web服务器的内部逻辑是非常非常复杂的,因为要应对生产环境中的各种问题。

从上图中可以看出来,一个完整的web服务一般可以分为客户端和服务端,客户端就是各种可以连接网络的终端,比如浏览器,安卓手机,苹果手机等等;服务端值得就是我们编写的业务代码,这个根据业务的不同,编写的代码也不同。web服务器实际上可以看成是我们javaweb应用的容器,我们编好了代码就放到web服务器里运行,可以简单理解成web服务器+业务代码=完整的web服务

web服务器起到了连接客户端和服务端的目的,不管公司的业务是什么,这部分的需求基本都是一致的,要求高并发,高可用等等。有了tomcat这样的开源web框架,大家就可以不用自己去编写web服务器的代码了,而是专注于自己的具体业务,这就是软件开源的意义。

HTTP协议

上图中,我们认为客户端和服务端是使用HTTP协议进行通信的,事实上也是如此,不过这不是固定的,你也可以定义一个通信协议,只要有人愿意使用你定义的通信协议进行通信就行。

不过,现如今互联网用的最多的通信协议还是HTTP。我们知道HTTP是应用层的协议,所谓应用层的协议,我的理解就是,操作系统底层不提供,需要你自己编写代码解析的协议。类似TCP/UDP这种通信协议操作系统都帮你写好了,你只需要进行系统调用就行了。当然,如果你用的是java这种更加高级的编程语言,那么你需要调用的api就更少了,因为很多细节已经帮你封装好了。

我们要写web服务器的话,就要能相应客户端发过来的HTTP请求信息。下面,我们用一个简单程序来看一下HTTP相应头都有哪些信息。

  1. public class HttpRequest {
  2. public static void main(String[] args) {
  3. try {
  4. URL url = new URL("https://backdata.net/");
  5. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  6. Map<String, List<String>> map = conn.getHeaderFields();
  7. List<String> keylist = new ArrayList<>(map.keySet());
  8. for(int i=0; i<keylist.size(); i++) {
  9. System.out.println(keylist.get(i) + ": " + map.get(keylist.get(i)));
  10. }
  11. } catch (MalformedURLException e) {
  12. e.printStackTrace();
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }

运行后可以得到下面的结果

上图中后面三个框的信息是必须的,我们在写建议的web服务器的时候,只需要相应三个响应头信息就行了。

什么是Servlet

不知道你们是不是和我一样,刚刚开始学javaweb的时候就听老师说写servlet,然后注册,然后就可以映射到url请求了,但是整个流程是怎么运转起来的却一头雾水。

其实servlet就是一个javaweb定义的标准而已,servlet是一个接口,里面定义了几个方法,所有的servlet都需要实现接口里面的那些方法。

我们现在也定义一个简单的servlet。

首先定义一个接口,命名为BaseServlet,这个接口是所有Servlet的基础

  1. public interface BaseServlet {
  2. public void doService(ServletRequest servletRequest, ServletResponse servletResponse);
  3. }

可以看到,接口只有一个doService()方法,其中有两个参数,分别是ServletRequest 和ServletResponse ,这两个类就是我们在处理请求和相应时用到的类,他们的定义分别如下所示

  1. public class ServletRequest {
  2. private String path;
  3. public ServletRequest() {
  4. }
  5. public ServletRequest(String path) {
  6. this.path = path;
  7. }
  8. public String getPath() {
  9. return path;
  10. }
  11. public void setPath(String path) {
  12. this.path = path;
  13. }
  14. }
  1. public class ServletResponse {
  2. private int code;
  3. private String msg;
  4. private String content_type;
  5. public ServletResponse() {
  6. }
  7. public ServletResponse(int code, String msg, String content_type) {
  8. this.code = code;
  9. this.msg = msg;
  10. this.content_type = content_type;
  11. }
  12. public int getCode() {
  13. return code;
  14. }
  15. public void setCode(int code) {
  16. this.code = code;
  17. }
  18. public String getMsg() {
  19. return msg;
  20. }
  21. public void setMsg(String msg) {
  22. this.msg = msg;
  23. }
  24. public String getContent_type() {
  25. return content_type;
  26. }
  27. public void setContent_type(String content_type) {
  28. this.content_type = content_type;
  29. }
  30. }

当然,我们这里只是进行简单定义,随着需求的复杂化,这两个类肯定也是要变得更加复杂的。总的来说,这两个类就是在进行http请求处理的时候传递信息用的。

有了上述的基础,我们就可以定义自己的具体业务类,基础BaseServlet,重写doService()方法,怎么样,是不是有javaweb那个味道了,逻辑是以上的,不过tomcat肯定进行了更加复杂的处理。

我们就举一个简单的例子

  1. public class HelloServlet implements BaseServlet{
  2. @Override
  3. public void doService(ServletRequest servletRequest, ServletResponse servletResponse) {
  4. String msg = "<html><head><title>HelloServlet</title></head>" +
  5. "<body><div><h1>hello servlet</h1></div></body>" +
  6. "</html>";
  7. servletResponse.setCode(200);
  8. servletResponse.setContent_type("application/xml");
  9. servletResponse.setMsg(msg);
  10. }
  11. }

上面的代码相信不难理解,实际上就是构造HTTP响应体,实际业务中可能会与数据库交互等等复杂逻辑。

Simplecat的生命周期

由于是简化版的web服务器,所以我暂时命名为simplecat。我们定义一个非常非常简单的生命周期,那就是

初始化->开启服务,开启客户端请求->关闭服务

我们将生命周期写在启动代码里

  1. public class SimplecatBootstrap {
  2. public static void main(String[] args) {
  3. Server server = new Server();
  4. // 1. 初始化环境:加载boot.properties,然后将servlet实例化
  5. server.init();
  6. // 2. 开启服务端监听,接收客户端请求
  7. server.start();
  8. // 3. 关闭服务
  9. server.shutdown();
  10. }
  11. }

启动代码实例化了Server类,这个类里面编写了服务端的三个生命周期

  1. public class Server {
  2. private final int PORT = 8082;
  3. // 用来存放socket连接
  4. private ThreadPoolExecutor threadpool;
  5. // 用来存放servlet
  6. public static Map<String, BaseServlet> servletMap;
  7. public Server() {
  8. }
  9. public void init() {
  10. System.out.println("初始化环境...");
  11. threadpool = new ThreadPoolExecutor(16, 128,
  12. 1000, TimeUnit.MICROSECONDS,
  13. new ArrayBlockingQueue<Runnable>(16));
  14. servletMap = new HashMap<>();
  15. // 读取配置文件,将servlet注册进去
  16. Properties properties = new Properties();
  17. File file = new File("res/boot.properties");
  18. try {
  19. properties.load(new FileInputStream(file));
  20. List<String> strList = new ArrayList<>(properties.stringPropertyNames());
  21. for(int i=0; i<strList.size(); i++) {
  22. Class<?> clazz = Class.forName(strList.get(i));
  23. BaseServlet servlet = (BaseServlet) clazz.newInstance();
  24. servletMap.put((String)properties.get(strList.get(i)), servlet);
  25. }
  26. } catch (IOException e) {
  27. e.printStackTrace();
  28. } catch (ClassNotFoundException e) {
  29. e.printStackTrace();
  30. } catch (IllegalAccessException e) {
  31. e.printStackTrace();
  32. } catch (InstantiationException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. public void start() {
  37. System.out.println("服务端启动...");
  38. try {
  39. ServerSocket serverSocket = new ServerSocket(PORT);
  40. while(true) {
  41. Socket socket = serverSocket.accept();
  42. System.out.println("接收一个socket连接请求:" + socket.getInetAddress().toString());
  43. ClientThread thread = new ClientThread(socket);
  44. threadpool.execute(thread);
  45. }
  46. } catch (IOException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. public void shutdown() {
  51. System.out.println("服务端停止...");
  52. threadpool.shutdown();
  53. servletMap.clear();
  54. }
  55. }

其中我们定义了一个Map<String, BaseServlet>来存放我们的servlet。下面我们主要介绍一下初始化环境的操作

  1. public void init() {
  2. System.out.println("初始化环境...");
  3. threadpool = new ThreadPoolExecutor(16, 128,
  4. 1000, TimeUnit.MICROSECONDS,
  5. new ArrayBlockingQueue<Runnable>(16));
  6. servletMap = new HashMap<>();
  7. // 读取配置文件,将servlet注册进去
  8. Properties properties = new Properties();
  9. File file = new File("res/boot.properties");
  10. try {
  11. properties.load(new FileInputStream(file));
  12. List<String> strList = new ArrayList<>(properties.stringPropertyNames());
  13. for(int i=0; i<strList.size(); i++) {
  14. Class<?> clazz = Class.forName(strList.get(i));
  15. BaseServlet servlet = (BaseServlet) clazz.newInstance();
  16. servletMap.put((String)properties.get(strList.get(i)), servlet);
  17. }
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. } catch (ClassNotFoundException e) {
  21. e.printStackTrace();
  22. } catch (IllegalAccessException e) {
  23. e.printStackTrace();
  24. } catch (InstantiationException e) {
  25. e.printStackTrace();
  26. }
  27. }

这个操作的逻辑是

1. 加载 boot.properties 文件

2. 根据加载的文件内容实例化servlet,然后存放到map中

boot.properties文件中记录了我们需要被实例化的servlet

比如上图中,当客户端访问book这个路径的时候,就是交给BookServlet这个servlet进行处理的。

HTTP响应处理逻辑

我们将每一个客户端连接独立出来写成ClientThread,这个类继承Thread类,并且重写run方法,这样就可以丢到线程池里面进行处理了。

  1. public class ClientThread extends Thread{
  2. private Socket socket;
  3. public ClientThread(Socket socket) {
  4. this.socket = socket;
  5. }
  6. @Override
  7. public void run() {
  8. super.run();
  9. try {
  10. // 建立连接,监听客户端请求
  11. InputStream inputStream = socket.getInputStream();
  12. OutputStream outputStream = socket.getOutputStream();
  13. // 1. 接收详细的请求头
  14. String requestHead = SocketUtil.parseRequestHead(inputStream);
  15. // 2. 拿到请求路径
  16. String path = SocketUtil.getPath(requestHead);
  17. // 3. 根据请求路径进行分发
  18. ServletResponse servletResponse = ServletHandler.dispatch(path);
  19. // 4. 根据servletResponse的信息构造相应信息
  20. String response = SocketUtil.getResponse(servletResponse);
  21. // 5. 发送请求
  22. SocketUtil.sendResponse(outputStream, response);
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }

可以看到具体的处理流程一共有五步

1. 接收详细的请求头

2. 从请求头中拿到路径

3. 根据请求路径进行请求的分发

4. 根据servletResponse的信息构建响应信息

5. 发送HTTP响应给客户端

以上五个步骤逻辑比较复杂的是请求路径的分发,我们的具体分发逻辑写在了ServletHandler这个类中

  1. public class ServletHandler {
  2. private static final String PATH_INDEX = "res/html/index.html";
  3. private static final String PATH_404 = "res/html/404.html";
  4. // 根据请求路径进行详细的分发
  5. public static ServletResponse dispatch(String path) {
  6. // 构建ServletRequest和ServletResponse对象
  7. ServletRequest servletRequest = new ServletRequest(path);
  8. ServletResponse servletResponse = new ServletResponse();
  9. System.out.println(path);
  10. // 如果是根目录,那么直接返回index.html即可
  11. if(path.equals("/")) {
  12. servletResponse.setCode(200);
  13. servletResponse.setMsg(SocketUtil.getFile(new File(PATH_INDEX)));
  14. servletResponse.setContent_type("text/html");
  15. }
  16. // 看一下是否存在servlet可以响应请求
  17. else if(Server.servletMap.containsKey(path)) {
  18. Server.servletMap.get(path).doService(servletRequest, servletResponse);
  19. }
  20. // 返回静态文件
  21. else {
  22. File file = new File("res/html/" + path);
  23. // 文件存在,则code200,msg就是文件内容
  24. if(file.exists()) {
  25. servletResponse.setCode(200);
  26. servletResponse.setMsg(SocketUtil.getFile(file));
  27. }
  28. // 若文件不存在,则code404,msg就是404.html文件的内容
  29. else {
  30. servletResponse.setCode(404);
  31. servletResponse.setMsg(SocketUtil.getFile(new File(PATH_404)));
  32. }
  33. servletResponse.setContent_type("text/html");
  34. }
  35. return servletResponse;
  36. }
  37. }

分发的逻辑如下

1. 如果是根目录,那么就返回index.html

2. 如果有对应的servlet,那么就调用具体的servlet的doService()方法进行处理

3. 否则寻找相应的静态资源,如果找到了就返回相应的页面,否则返回404.html页面

最后,我们将一些工具类都封装到了SocketUtil这个类中了,就是一些简单的处理逻辑

  1. public class SocketUtil {
  2. // 一次发送的字节数
  3. private static final int CHUNK = 100;
  4. public static String parseRequestHead(InputStream inputStream) {
  5. StringBuilder sb = new StringBuilder();
  6. byte[] bytebuf = new byte[1024];
  7. int len = 0;
  8. while(true) {
  9. try {
  10. len = inputStream.read(bytebuf);
  11. if(len == -1){
  12. continue;
  13. }
  14. sb.append(new String(bytebuf, 0, len));
  15. // 如果已经接收完毕请求头就结束
  16. if(check(sb.toString())) {
  17. return sb.toString();
  18. }
  19. } catch (IOException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }
  24. // 检验是否已经将请求头接收完毕
  25. // 这里的判断逻辑就是最后四个字符是否是\r\n\r\n
  26. private static boolean check(String str) {
  27. if(str.length()>=4 && str.substring(str.length()-4).equals("\r\n\r\n")) {
  28. return true;
  29. }
  30. return false;
  31. }
  32. // 获取请求路径
  33. // 我们这里进行简化处理,只取最后一个路径作为请求路径
  34. // 有两种请求资源,第一种是servlet请求,比如http://localhost:8082/hello
  35. // 第二种是对html资源的请求,比如http://localhost:8082/hello.html
  36. public static String getPath(String requestHead) {
  37. String line = requestHead.split("\n")[0];
  38. String path = line.split(" ")[1];
  39. // 如果是根目录直接返回即可
  40. if(path.equals("/")){
  41. return path;
  42. }
  43. path = path.split("/")[path.split("/").length-1];
  44. return path;
  45. }
  46. // 将文件读成String对象
  47. public static String getFile(File file) {
  48. StringBuilder sb = new StringBuilder();
  49. try {
  50. FileInputStream fis = new FileInputStream(file);
  51. byte[] bytebuf = new byte[1024];
  52. int c;
  53. while((c=fis.read(bytebuf))!=-1) {
  54. sb.append(new String(bytebuf, 0, c));
  55. }
  56. return sb.toString();
  57. } catch (FileNotFoundException e) {
  58. e.printStackTrace();
  59. } catch (IOException e) {
  60. e.printStackTrace();
  61. }
  62. return sb.toString();
  63. }
  64. // 根据servletResponse构造响应体
  65. public static String getResponse(ServletResponse servletResponse) {
  66. StringBuilder sb = new StringBuilder();
  67. sb.append("HTTP/1.1 " + servletResponse.getCode() + " OK\r\n");
  68. sb.append("Content-Length: " + servletResponse.getMsg().getBytes().length + "\r\n");
  69. sb.append("Content-Type: " + servletResponse.getContent_type() +"\r\n\r\n");
  70. sb.append(servletResponse.getMsg());
  71. return sb.toString();
  72. }
  73. // 发送请求
  74. public static void sendResponse(OutputStream outputStream, String response) {
  75. System.out.println(response);
  76. try {
  77. for (int i = 0; i < response.length() / CHUNK; i++) {
  78. // 超过CHUNK字节数的部分
  79. if (i != 0 && i != response.length() / CHUNK - 1) {
  80. outputStream.write(response.substring(i * CHUNK, i * CHUNK + CHUNK).getBytes());
  81. outputStream.flush();
  82. }
  83. // 不满CHUNK字节数,全部发送
  84. else if(i==0 && response.length()/CHUNK==0) {
  85. outputStream.write(response.getBytes());
  86. outputStream.flush();
  87. }
  88. // 发送剩余部分
  89. else {
  90. outputStream.write(response.substring(i*CHUNK).getBytes());
  91. outputStream.flush();
  92. }
  93. }
  94. } catch (IOException e) {
  95. e.printStackTrace();
  96. }
  97. }
  98. }

运行效果

在看运行效果前,先来看一下项目的目录结构

从目录结构中可以看出来,我们将静态资源和配置文件都放到和src文件同级的res文件夹中。

接着我们就来看一下用浏览器进行各种请求的结果

 

 

可以发现基本实现了目标哈哈,既可以响应静态资源,可以调用servlet进行逻辑处理,当找不到响应servlet和静态资源的时候,响应一个404页面。

结语

我们只是做了一个非常非常简单的web服务器,可以响应浏览器的简单请求,这篇文章的目的只是介绍一下web服务器的基本原理。 

我把代码都放到gitee上了,如果有需要的话自行下载就行。

simplecat: 使用java编写的一个极简web服务器

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/191513
推荐阅读
相关标签
  

闽ICP备14008679号