赞
踩
微信公众号:进击的蛋糕(dangao123coding)
记得好多年前,刚刚开始学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是应用层的协议,所谓应用层的协议,我的理解就是,操作系统底层不提供,需要你自己编写代码解析的协议。类似TCP/UDP这种通信协议操作系统都帮你写好了,你只需要进行系统调用就行了。当然,如果你用的是java这种更加高级的编程语言,那么你需要调用的api就更少了,因为很多细节已经帮你封装好了。
我们要写web服务器的话,就要能相应客户端发过来的HTTP请求信息。下面,我们用一个简单程序来看一下HTTP相应头都有哪些信息。
- public class HttpRequest {
-
- public static void main(String[] args) {
- try {
- URL url = new URL("https://backdata.net/");
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- Map<String, List<String>> map = conn.getHeaderFields();
- List<String> keylist = new ArrayList<>(map.keySet());
- for(int i=0; i<keylist.size(); i++) {
- System.out.println(keylist.get(i) + ": " + map.get(keylist.get(i)));
- }
- } catch (MalformedURLException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- }
-
- }
运行后可以得到下面的结果
上图中后面三个框的信息是必须的,我们在写建议的web服务器的时候,只需要相应三个响应头信息就行了。
不知道你们是不是和我一样,刚刚开始学javaweb的时候就听老师说写servlet,然后注册,然后就可以映射到url请求了,但是整个流程是怎么运转起来的却一头雾水。
其实servlet就是一个javaweb定义的标准而已,servlet是一个接口,里面定义了几个方法,所有的servlet都需要实现接口里面的那些方法。
我们现在也定义一个简单的servlet。
首先定义一个接口,命名为BaseServlet,这个接口是所有Servlet的基础
- public interface BaseServlet {
-
- public void doService(ServletRequest servletRequest, ServletResponse servletResponse);
-
- }
可以看到,接口只有一个doService()方法,其中有两个参数,分别是ServletRequest 和ServletResponse ,这两个类就是我们在处理请求和相应时用到的类,他们的定义分别如下所示
- public class ServletRequest {
-
- private String path;
-
- public ServletRequest() {
- }
-
- public ServletRequest(String path) {
- this.path = path;
- }
-
- public String getPath() {
- return path;
- }
-
- public void setPath(String path) {
- this.path = path;
- }
- }
- public class ServletResponse {
-
- private int code;
- private String msg;
- private String content_type;
-
- public ServletResponse() {
- }
-
- public ServletResponse(int code, String msg, String content_type) {
- this.code = code;
- this.msg = msg;
- this.content_type = content_type;
- }
-
-
- public int getCode() {
- return code;
- }
-
- public void setCode(int code) {
- this.code = code;
- }
-
- public String getMsg() {
- return msg;
- }
-
- public void setMsg(String msg) {
- this.msg = msg;
- }
-
- public String getContent_type() {
- return content_type;
- }
-
- public void setContent_type(String content_type) {
- this.content_type = content_type;
- }
- }
当然,我们这里只是进行简单定义,随着需求的复杂化,这两个类肯定也是要变得更加复杂的。总的来说,这两个类就是在进行http请求处理的时候传递信息用的。
有了上述的基础,我们就可以定义自己的具体业务类,基础BaseServlet,重写doService()方法,怎么样,是不是有javaweb那个味道了,逻辑是以上的,不过tomcat肯定进行了更加复杂的处理。
我们就举一个简单的例子
- public class HelloServlet implements BaseServlet{
- @Override
- public void doService(ServletRequest servletRequest, ServletResponse servletResponse) {
- String msg = "<html><head><title>HelloServlet</title></head>" +
- "<body><div><h1>hello servlet</h1></div></body>" +
- "</html>";
- servletResponse.setCode(200);
- servletResponse.setContent_type("application/xml");
- servletResponse.setMsg(msg);
- }
- }
上面的代码相信不难理解,实际上就是构造HTTP响应体,实际业务中可能会与数据库交互等等复杂逻辑。
由于是简化版的web服务器,所以我暂时命名为simplecat。我们定义一个非常非常简单的生命周期,那就是
初始化->开启服务,开启客户端请求->关闭服务
我们将生命周期写在启动代码里
- public class SimplecatBootstrap {
-
- public static void main(String[] args) {
- Server server = new Server();
- // 1. 初始化环境:加载boot.properties,然后将servlet实例化
- server.init();
- // 2. 开启服务端监听,接收客户端请求
- server.start();
- // 3. 关闭服务
- server.shutdown();
- }
-
- }
启动代码实例化了Server类,这个类里面编写了服务端的三个生命周期
- public class Server {
-
- private final int PORT = 8082;
-
- // 用来存放socket连接
- private ThreadPoolExecutor threadpool;
- // 用来存放servlet
- public static Map<String, BaseServlet> servletMap;
-
- public Server() {
-
- }
-
- public void init() {
- System.out.println("初始化环境...");
- threadpool = new ThreadPoolExecutor(16, 128,
- 1000, TimeUnit.MICROSECONDS,
- new ArrayBlockingQueue<Runnable>(16));
- servletMap = new HashMap<>();
- // 读取配置文件,将servlet注册进去
- Properties properties = new Properties();
- File file = new File("res/boot.properties");
- try {
- properties.load(new FileInputStream(file));
- List<String> strList = new ArrayList<>(properties.stringPropertyNames());
- for(int i=0; i<strList.size(); i++) {
- Class<?> clazz = Class.forName(strList.get(i));
- BaseServlet servlet = (BaseServlet) clazz.newInstance();
- servletMap.put((String)properties.get(strList.get(i)), servlet);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (InstantiationException e) {
- e.printStackTrace();
- }
-
- }
-
- public void start() {
- System.out.println("服务端启动...");
- try {
- ServerSocket serverSocket = new ServerSocket(PORT);
- while(true) {
- Socket socket = serverSocket.accept();
- System.out.println("接收一个socket连接请求:" + socket.getInetAddress().toString());
- ClientThread thread = new ClientThread(socket);
- threadpool.execute(thread);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- }
-
-
- public void shutdown() {
- System.out.println("服务端停止...");
- threadpool.shutdown();
- servletMap.clear();
- }
-
- }
其中我们定义了一个Map<String, BaseServlet>来存放我们的servlet。下面我们主要介绍一下初始化环境的操作
- public void init() {
- System.out.println("初始化环境...");
- threadpool = new ThreadPoolExecutor(16, 128,
- 1000, TimeUnit.MICROSECONDS,
- new ArrayBlockingQueue<Runnable>(16));
- servletMap = new HashMap<>();
- // 读取配置文件,将servlet注册进去
- Properties properties = new Properties();
- File file = new File("res/boot.properties");
- try {
- properties.load(new FileInputStream(file));
- List<String> strList = new ArrayList<>(properties.stringPropertyNames());
- for(int i=0; i<strList.size(); i++) {
- Class<?> clazz = Class.forName(strList.get(i));
- BaseServlet servlet = (BaseServlet) clazz.newInstance();
- servletMap.put((String)properties.get(strList.get(i)), servlet);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (InstantiationException e) {
- e.printStackTrace();
- }
-
- }
这个操作的逻辑是
1. 加载 boot.properties 文件
2. 根据加载的文件内容实例化servlet,然后存放到map中
boot.properties文件中记录了我们需要被实例化的servlet
比如上图中,当客户端访问book这个路径的时候,就是交给BookServlet这个servlet进行处理的。
我们将每一个客户端连接独立出来写成ClientThread,这个类继承Thread类,并且重写run方法,这样就可以丢到线程池里面进行处理了。
- public class ClientThread extends Thread{
-
- private Socket socket;
-
- public ClientThread(Socket socket) {
- this.socket = socket;
- }
-
- @Override
- public void run() {
- super.run();
- try {
- // 建立连接,监听客户端请求
- InputStream inputStream = socket.getInputStream();
- OutputStream outputStream = socket.getOutputStream();
-
- // 1. 接收详细的请求头
- String requestHead = SocketUtil.parseRequestHead(inputStream);
- // 2. 拿到请求路径
- String path = SocketUtil.getPath(requestHead);
- // 3. 根据请求路径进行分发
- ServletResponse servletResponse = ServletHandler.dispatch(path);
- // 4. 根据servletResponse的信息构造相应信息
- String response = SocketUtil.getResponse(servletResponse);
- // 5. 发送请求
- SocketUtil.sendResponse(outputStream, response);
-
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
可以看到具体的处理流程一共有五步
1. 接收详细的请求头
2. 从请求头中拿到路径
3. 根据请求路径进行请求的分发
4. 根据servletResponse的信息构建响应信息
5. 发送HTTP响应给客户端
以上五个步骤逻辑比较复杂的是请求路径的分发,我们的具体分发逻辑写在了ServletHandler这个类中
- public class ServletHandler {
- private static final String PATH_INDEX = "res/html/index.html";
- private static final String PATH_404 = "res/html/404.html";
-
- // 根据请求路径进行详细的分发
- public static ServletResponse dispatch(String path) {
- // 构建ServletRequest和ServletResponse对象
- ServletRequest servletRequest = new ServletRequest(path);
- ServletResponse servletResponse = new ServletResponse();
- System.out.println(path);
- // 如果是根目录,那么直接返回index.html即可
- if(path.equals("/")) {
- servletResponse.setCode(200);
- servletResponse.setMsg(SocketUtil.getFile(new File(PATH_INDEX)));
- servletResponse.setContent_type("text/html");
- }
- // 看一下是否存在servlet可以响应请求
- else if(Server.servletMap.containsKey(path)) {
- Server.servletMap.get(path).doService(servletRequest, servletResponse);
- }
- // 返回静态文件
- else {
- File file = new File("res/html/" + path);
- // 文件存在,则code200,msg就是文件内容
- if(file.exists()) {
- servletResponse.setCode(200);
- servletResponse.setMsg(SocketUtil.getFile(file));
- }
- // 若文件不存在,则code404,msg就是404.html文件的内容
- else {
- servletResponse.setCode(404);
- servletResponse.setMsg(SocketUtil.getFile(new File(PATH_404)));
- }
- servletResponse.setContent_type("text/html");
- }
- return servletResponse;
- }
-
- }
分发的逻辑如下
1. 如果是根目录,那么就返回index.html
2. 如果有对应的servlet,那么就调用具体的servlet的doService()方法进行处理
3. 否则寻找相应的静态资源,如果找到了就返回相应的页面,否则返回404.html页面
最后,我们将一些工具类都封装到了SocketUtil这个类中了,就是一些简单的处理逻辑
- public class SocketUtil {
-
- // 一次发送的字节数
- private static final int CHUNK = 100;
-
- public static String parseRequestHead(InputStream inputStream) {
- StringBuilder sb = new StringBuilder();
- byte[] bytebuf = new byte[1024];
- int len = 0;
- while(true) {
- try {
- len = inputStream.read(bytebuf);
- if(len == -1){
- continue;
- }
- sb.append(new String(bytebuf, 0, len));
- // 如果已经接收完毕请求头就结束
- if(check(sb.toString())) {
- return sb.toString();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- // 检验是否已经将请求头接收完毕
- // 这里的判断逻辑就是最后四个字符是否是\r\n\r\n
- private static boolean check(String str) {
- if(str.length()>=4 && str.substring(str.length()-4).equals("\r\n\r\n")) {
- return true;
- }
- return false;
- }
-
- // 获取请求路径
- // 我们这里进行简化处理,只取最后一个路径作为请求路径
- // 有两种请求资源,第一种是servlet请求,比如http://localhost:8082/hello
- // 第二种是对html资源的请求,比如http://localhost:8082/hello.html
- public static String getPath(String requestHead) {
- String line = requestHead.split("\n")[0];
- String path = line.split(" ")[1];
- // 如果是根目录直接返回即可
- if(path.equals("/")){
- return path;
- }
- path = path.split("/")[path.split("/").length-1];
- return path;
- }
-
- // 将文件读成String对象
- public static String getFile(File file) {
- StringBuilder sb = new StringBuilder();
- try {
- FileInputStream fis = new FileInputStream(file);
- byte[] bytebuf = new byte[1024];
- int c;
- while((c=fis.read(bytebuf))!=-1) {
- sb.append(new String(bytebuf, 0, c));
- }
- return sb.toString();
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return sb.toString();
- }
-
- // 根据servletResponse构造响应体
- public static String getResponse(ServletResponse servletResponse) {
- StringBuilder sb = new StringBuilder();
- sb.append("HTTP/1.1 " + servletResponse.getCode() + " OK\r\n");
- sb.append("Content-Length: " + servletResponse.getMsg().getBytes().length + "\r\n");
- sb.append("Content-Type: " + servletResponse.getContent_type() +"\r\n\r\n");
- sb.append(servletResponse.getMsg());
- return sb.toString();
- }
-
- // 发送请求
- public static void sendResponse(OutputStream outputStream, String response) {
- System.out.println(response);
- try {
- for (int i = 0; i < response.length() / CHUNK; i++) {
- // 超过CHUNK字节数的部分
- if (i != 0 && i != response.length() / CHUNK - 1) {
- outputStream.write(response.substring(i * CHUNK, i * CHUNK + CHUNK).getBytes());
- outputStream.flush();
- }
- // 不满CHUNK字节数,全部发送
- else if(i==0 && response.length()/CHUNK==0) {
- outputStream.write(response.getBytes());
- outputStream.flush();
- }
- // 发送剩余部分
- else {
- outputStream.write(response.substring(i*CHUNK).getBytes());
- outputStream.flush();
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- }
在看运行效果前,先来看一下项目的目录结构
从目录结构中可以看出来,我们将静态资源和配置文件都放到和src文件同级的res文件夹中。
接着我们就来看一下用浏览器进行各种请求的结果
可以发现基本实现了目标哈哈,既可以响应静态资源,可以调用servlet进行逻辑处理,当找不到响应servlet和静态资源的时候,响应一个404页面。
我们只是做了一个非常非常简单的web服务器,可以响应浏览器的简单请求,这篇文章的目的只是介绍一下web服务器的基本原理。
我把代码都放到gitee上了,如果有需要的话自行下载就行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。