使用java基础实现一个简陋的web服务器软件

1、写在前面

大学已经过了一年半了,从接触各种web服务器软件已经有一年多了,从大一上最开始折腾Windows电脑自带的IIS开始,上手了自己的第一个静态网站,从此开启了web方向学习的兴趣。到现在,从陪伴了javaweb阶段的Tomcat走来,也陆续接触了jetty,Nginx等web服务器软件。但是,这些web服务器的软件也一直都是开箱即用,从未探究过其背后的原理。今天,尽量用最简单的java代码,实现一个最简陋的web服务器软件,揭开web服务器软件的神秘面纱。

2、Tomcat的架构模式

由上图可以看出,Tomcat作为如今相对成熟的web服务器软件,有着相对较为复杂的架构,有着Server、Service、Engine、Connerctor、Host、Context等诸多组件。对于Tomcat的源码分析将在以后的博文中分篇讲解

,在此不在叙述。本节主要是实现一个自己的web服务器软件,其架构也超级简单。

3、编写一个简单的web服务器类

3.1、web服务器软件面向的浏览器客户,因此在同一时间肯定不止有一个http请求,因此肯定需要开启多线程来进行服务,对类上实现Runnable接口,并重写其中的run方法。

  1. public class ServerThread implements Runnable {
  2. @Override
  3. public void run() {}
  4. }

3.2、在本类中只有两个方法,其中构造方法用来初始化该web服务器需要的资源,run方法用来处理请求,开启服务。

3.3、首先,我们先需要定义一堆类级别的变量,如:

  • 浏览器发送Http请求时,需要有一个Socket来接受,并且需要或等输入、输出流。

    1. private Socket client;
    2. private InputStream in;
    3. private OutputStream out;
  • 在Tomcat中,有一个webapp文件夹用来存放静态资源,在此,我们也在D盘根路径下定义一个webroot文件夹,用来存储静态的资源。(该路径也可以通过获取当前j软件的相对路径来动态生成,但是为了简单起见,更好的揭示web服务器的工作流程,在此采用的是绝对路径)

    1. private static final String WEBROOT = "D:\\webroot\\";

3.4、通过构造函数来初始化全局变量

  1. /**
  2. * 构造函数初始化客户端
  3. */
  4. public ServerThread(Socket client) {
  5. this.client = client;
  6. //其他初始化信息
  7. try {
  8. //获取客户端连接的流对象
  9. in = client.getInputStream();
  10. out = client.getOutputStream();
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. }

该构造函数相当的简单,就是获取浏览器发来的Socket,并拿到其中的输入、输出流,然后赋值给全局变量。

3.5、run()方法方法体的编写

  1. 通过输入流获得请求的内容
  1. //读取请求的内容
  2. reader = new BufferedReader(new InputStreamReader(in));
  1. 解析获取的内容,并且放回网站得首页(index.html)

    1. //取得:后面得内容
    2. String line = reader.readLine().split(" ")[1].replace("/","\\");
    3. if("\\".equals(line)) {
    4. line += "index.html";
    5. }
    6. System.out.println(line);
    7. //获取文件的后缀名
    8. String strType = line.substring(line.lastIndexOf(".")+1, line.length());
    9. System.out.println("strType = " + strType);
  2. 给浏览器进行响应(用浏览器打开任意一个网站,调出控制台观查其响应头,因此我们的web服务器也应该把响应头给浏览器写出)

所以我们的代码应该为:

  1. //给用户响应
  2. pw = new PrintWriter(out);
  3. input = new FileInputStream(WEBROOT + line);
  4. //BufferedReader buffer = new BufferedReader(new InputStreamReader(input));
  5. //写响应头
  6. pw.println("HTTP/1.1 200 ok");
  7. pw.println("Content-Type: "+ contentMap.get(strType) +";charset=utf-8");
  8. pw.println("Content-Length: " + input.available());
  9. pw.println("Server: hello");
  10. pw.println("Date: " + new Date());
  11. pw.println();
  12. pw.flush();

因为放返回数据的类型有多样,所以我们可以用一个map集合来存储,并在类加载前将数据存入。

  1. /**
  2. * 静态资源的集合(对应的文本类型)
  3. */
  4. private static Map<String,String> contentMap = new HashMap<>();
  5. //初始化静态资源的集合
  6. static {
  7. contentMap.put("html", "text/html");
  8. contentMap.put("htm", "text/html");
  9. contentMap.put("jpg", "image/jpeg");
  10. contentMap.put("jpeg", "image/jpeg");
  11. contentMap.put("gif", "image/gif");
  12. contentMap.put("js", "application/javascript");
  13. contentMap.put("css", "text/css");
  14. contentMap.put("json", "application/json");
  15. contentMap.put("mp3", "audio/mpeg");
  16. contentMap.put("mp4", "video/mp4");
  17. }

3.6、向浏览器写回数据,并写完后进行刷新

  1. //向浏览器写数据
  2. byte[] bytes = new byte[1024];
  3. int len = 0;
  4. while ((len = input.read(bytes)) != -1){
  5. out.write(bytes, 0, len);
  6. }
  7. pw.flush();

3.7、关闭流、释放资源

  1. if(input != null) {
  2. input.close();
  3. }
  4. if(pw != null) {
  5. pw.close();
  6. }
  7. if(reader != null) {
  8. reader.close();
  9. }
  10. if(out != null) {
  11. out.close();
  12. }
  13. if(client != null) {
  14. client.close();
  15. }

3.8、该类完整的代码为:

  1. package com.xgp.company;
  2. import java.io.*;
  3. import java.net.Socket;
  4. import java.util.Date;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. import java.util.concurrent.ConcurrentHashMap;
  8. /**
  9. * 服务线程
  10. * @author 薛国鹏
  11. */
  12. public class ServerThread implements Runnable {
  13. /**
  14. * 静态资源的集合(对应的文本类型)
  15. */
  16. private static Map<String,String> contentMap = new HashMap<>();
  17. //初始化静态资源的集合
  18. static {
  19. contentMap.put("html", "text/html");
  20. contentMap.put("htm", "text/html");
  21. contentMap.put("jpg", "image/jpeg");
  22. contentMap.put("jpeg", "image/jpeg");
  23. contentMap.put("gif", "image/gif");
  24. contentMap.put("js", "application/javascript");
  25. contentMap.put("css", "text/css");
  26. contentMap.put("json", "application/json");
  27. contentMap.put("mp3", "audio/mpeg");
  28. contentMap.put("mp4", "video/mp4");
  29. }
  30. private Socket client;
  31. private InputStream in;
  32. private OutputStream out;
  33. private static final String WEBROOT = "D:\\webroot\\";
  34. /**
  35. * 构造函数初始化客户端
  36. */
  37. public ServerThread(Socket client) {
  38. this.client = client;
  39. //其他初始化信息
  40. try {
  41. //获取客户端连接的流对象
  42. in = client.getInputStream();
  43. out = client.getOutputStream();
  44. } catch (IOException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. /**
  49. * 解析信息,给用户响应
  50. */
  51. @Override
  52. public void run() {
  53. PrintWriter pw = null;
  54. BufferedReader reader = null;
  55. FileInputStream input = null;
  56. try {
  57. //读取请求的内容
  58. reader = new BufferedReader(new InputStreamReader(in));
  59. /**
  60. * //请求的资源
  61. * //解析请求头
  62. * Host: static.zhihu.com
  63. * User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0
  64. * Accept: text/css,
  65. */
  66. //取得:后面得内容
  67. String line = reader.readLine().split(" ")[1].replace("/","\\");
  68. if("\\".equals(line)) {
  69. line += "index.html";
  70. }
  71. System.out.println(line);
  72. //获取文件的后缀名
  73. String strType = line.substring(line.lastIndexOf(".")+1, line.length());
  74. System.out.println("strType = " + strType);
  75. //给用户响应
  76. pw = new PrintWriter(out);
  77. input = new FileInputStream(WEBROOT + line);
  78. //BufferedReader buffer = new BufferedReader(new InputStreamReader(input));
  79. //写响应头
  80. pw.println("HTTP/1.1 200 ok");
  81. pw.println("Content-Type: "+ contentMap.get(strType) +";charset=utf-8");
  82. pw.println("Content-Length: " + input.available());
  83. pw.println("Server: hello");
  84. pw.println("Date: " + new Date());
  85. pw.println();
  86. pw.flush();
  87. //向浏览器写数据
  88. byte[] bytes = new byte[1024];
  89. int len = 0;
  90. while ((len = input.read(bytes)) != -1){
  91. out.write(bytes, 0, len);
  92. }
  93. pw.flush();
  94. }catch (Exception e) {
  95. throw new RuntimeException(e.getMessage() + "服务端的run方法出错");
  96. }finally {
  97. try {
  98. if(input != null) {
  99. input.close();
  100. }
  101. if(pw != null) {
  102. pw.close();
  103. }
  104. if(reader != null) {
  105. reader.close();
  106. }
  107. if(out != null) {
  108. out.close();
  109. }
  110. if(client != null) {
  111. client.close();
  112. }
  113. } catch (IOException e) {
  114. e.printStackTrace();
  115. }
  116. }
  117. }
  118. }

4、编写启动类

4.1、一般连接性行为会采用池化技术,这里使用一个可以弹性伸缩的线程池。(如果想要跟为专业化,最好是使用一个默认的线程数量的线程池,并且可以让开发者自行设定)

  1. //创建一个可伸缩的连接池
  2. pool = Executors.newCachedThreadPool();

4.2、监听端口。(这里监听的是80端口,其实监听端口的权力应该交给使用者指定)

  1. //启动服务器,监听8080端口
  2. server = new ServerSocket(80);
  3. System.out.println("服务器启动,当前端口为80");

4.3、启动服务器,处理来自于浏览器的请求

  1. while (!Thread.interrupted()){
  2. //不停接收客户端请求
  3. Socket client = server.accept();
  4. //向线程池中提交任务
  5. pool.execute(new ServerThread(client));
  6. }

4.4、关闭连接,释放资源

  1. if(server != null) {
  2. server.close();
  3. }
  4. if(pool != null) {
  5. pool.shutdown();
  6. }

4.5、本类完整的代码为:

  1. package com.xgp.company;
  2. import java.io.IOException;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. /**
  8. * 服务端
  9. * @author 薛国鹏
  10. */
  11. public class MyHttpServer {
  12. public static void main(String[] args) {
  13. ServerSocket server = null;
  14. ExecutorService pool = null;
  15. try {
  16. //创建一个可伸缩的连接池
  17. pool = Executors.newCachedThreadPool();
  18. //启动服务器,监听8080端口
  19. server = new ServerSocket(80);
  20. System.out.println("服务器启动,当前端口为80");
  21. while (!Thread.interrupted()){
  22. //不停接收客户端请求
  23. Socket client = server.accept();
  24. //向线程池中提交任务
  25. pool.execute(new ServerThread(client));
  26. }
  27. }catch (Exception e) {
  28. throw new RuntimeException(e.getMessage() + "服务端异常");
  29. }finally {
  30. try {
  31. if(server != null) {
  32. server.close();
  33. }
  34. if(pool != null) {
  35. pool.shutdown();
  36. }
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }
  42. }

5、进行测试

5.1、将测试的静态文件放在D:\webroot目录下,如图是一个使用Vue编写的一个静态的前端项目

5.2、启动自己编写的web服务器软件,看到控制台出现了"服务器启动,当前端口为80"则服务启动成功

5.3、输入域名,进行访问

调出浏览器控制台,看请求的资源是否正常解析:

可以看到,页面正确渲染了,请求的资源也没有发生问题,因此我们自己编写的简陋版本的web服务器软件编写成功。

使用java基础实现一个简陋的web服务器软件的更多相关文章

  1. 探秘Tomcat——从一个简陋的Web服务器开始

    前言: 无论是之前所在实习单位小到一个三五个人做的项目,还是如今一个在做的百人以上的产品,一直都能看到tomcat的身影.工作中经常遇到的操作就是启动和关闭tomcat服务,或者修改了摸个java文件 ...

  2. 自己动手模拟开发一个简单的Web服务器

    开篇:每当我们将开发好的ASP.NET网站部署到IIS服务器中,在浏览器正常浏览页面时,可曾想过Web服务器是怎么工作的,其原理是什么?“纸上得来终觉浅,绝知此事要躬行”,于是我们自己模拟一个简单的W ...

  3. (一)一个简单的Web服务器

    万丈高楼平地起,首先我们必须了解 超文本传输协议(HTTP) 以后才能够比较清晰的明白web服务器是怎么回事. 1. 浅析Http协议 HTTP是一种协议,允许web服务器和浏览器通过互联网进行来发送 ...

  4. Tomcat剖析(一):一个简单的Web服务器

    Tomcat剖析(一):一个简单的Web服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器 ...

  5. Java中常见的5种WEB服务器介绍

    这篇文章主要介绍了Java中常见的5种WEB服务器介绍,它们分别是Tomcat.Resin.JBoss.WebSphere.WebLogic,需要的朋友可以参考下 Web服务器是运行及发布Web应用的 ...

  6. 自己模拟的一个简单的web服务器

    首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...

  7. 《深度解析Tomcat》 第一章 一个简单的Web服务器

    本章介绍Java Web服务器是如何运行的.从中可以知道Tomcat是如何工作的. 基于Java的Web服务器会使用java.net.Socket类和java.net.ServerSocket类这两个 ...

  8. java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 使用过程 和servlet差不多

    java nio 写一个完整的http服务器  支持文件上传   chunk传输    gzip 压缩      也仿照着 netty处理了NIO的空轮询BUG        本项目并不复杂 代码不多 ...

  9. 一个简单的web服务器

    写在前面 新的一年了,新的开始,打算重新看一遍asp.net本质论这本书,再重新认识一下,查漏补缺,认认真真的过一遍. 一个简单的web服务器 首先需要引入命名空间: System.Net,关于网络编 ...

随机推荐

  1. Oracle索引大全

    文档结构如下: 前言: Oracle 官方文档对索引的描述真是弱透了,对索引的说明就是一坨……,support也没有很好的资料,下面还是用的官方上的内容经过自己的整理加上网上的资料. 索引类型: 索引 ...

  2. pinpoint实现告警推送至钉钉和微信群

    前言 在前面的文章中,我们学习了如何通过java实现将消息发送到钉钉.和将消息发送到微信群聊. 基于上述基础,我们今天来接入pinpoint的告警,发送到钉钉群. 实操前准备 开始之前,推荐阅读一下, ...

  3. [UWP]用画中画模式(CompactOverlay Mode)让用总在最前端显示

    1. 什么是,以及怎么用画中画 Windows 10 Creators Update以后UWP提供了一个新的视图模式CompactOverlay,中文翻译成 紧凑的覆盖层?反正大部分时间我们都会称它为 ...

  4. Spring Boot2 系列教程 (三) | 使用 LomBok 提高开发效率

    微信公众号:一个优秀的废人 如有问题或建议,请后台留言,我会尽力解决你的问题. 前言 上周去了开年会,去的地方是温泉度假村.老实说,我是无感的,90% 是因为没中奖(老板太抠,两百人只抽三个奖),10 ...

  5. Java入门 - 语言基础 - 21.Scanner类

    原文地址:http://www.work100.net/training/java-scanner.html 更多教程:光束云 - 免费课程 Scanner类 序号 文内章节 视频 1 概述 2 使用 ...

  6. 2015年3月26日 - Javascript MVC 框架DerbyJS DerbyJS 是一个 MVC 框架,帮助编写实时,交互的应用。

    2015年3月26日 -  Javascript MVC 框架DerbyJS DerbyJS 是一个 MVC 框架,帮助编写实时,交互的应用.

  7. 认识JPA以及如何使用JPA(1)

    一:JDBC是什么? JDBC统一了Java应用程序访问数据库的标准. 二:什么是JPA? JPA统一了Java应用程序使用使用ORM框架的方式. 配置文件说明: 三:使用JPA的第一个实例. 1.创 ...

  8. SpringBoot简单实现登录功能

    登陆 开发期间模板引擎页面修改以后,要实时生效 1).禁用模板引擎的缓存 # 禁用缓存 spring.thymeleaf.cache=false 2).页面修改完成以后ctrl+f9:重新编译: 登陆 ...

  9. Java8新特性一点通 | 回顾功能接口Functional Interface

    Functional Interface Functional Interface是什么? 功能接口是java 8中的新增功能,它们只允许一个抽象方法.这些接口也称为单抽象方法接口(SAM接口).这些 ...

  10. Java:谈谈控制线程的几种办法

    目录 Java:谈谈控制线程的几种办法 join() sleep() 守护线程 主要方法 需要注意 优先级 弃用三兄弟 stop() resume suspend 中断三兄弟 interrupt() ...