TOMCAT内核之旅–一个简单的WEB服务器–学习心得(一)
标签(空格分隔): web服务器
一、学习背景
本人是一名大三学生,开始以java学习为主,后来学习了javaWEB,了解到了TOMCAT服务器,很好奇其内部是如何实现的,其与浏览器是如何联系起来的,带着这一系列问题,我开始了TOMCAT的内核之旅。
二、知识支撑
本次学习借助HOW TOMCAT WORKS一书,跟随其思路,实现代码。这一节我们主要是了解一个WEB服务器是如何工作的?
三、基础知识
现在,我们就正式开始我们的学习。首先,我们知道,浏览器服务器这种B/S架构,其通信主要玩的就是协议!!!
什么是协议?我们举个简单的例子, 如果没有汉字,语法,文化这些限制,生活中我们还能正常沟通吗?
这些规范就是协议!而浏览器,服务器之间要通信,就必须有自己的协议。
Web服务器也成为超文本传输协议(HTTP)服务器,因为它使用HTTP来跟客户端进行通信的,这通常是个web浏览器。一个基于java的web服务器使用两个重要的类:java.net.Socket和java.net.ServerSocket,并通过HTTP消息进行通信。
3.1 超文本传输协议(HTTP)
HTTP是一种协议,允许web服务器和浏览器通过互联网进行来发送和接受数据。它是一种请求和响应协议。客户端请求一个文件而服务器响应请求。HTTP使用可靠的TCP连接--TCP默认使用80端口。第一个HTTP版是HTTP/0.9,然后被HTTP/1.0所替代。正在取代HTTP/1.0的是当前版本HTTP/1.1,它定义于征求意见文档(RFC) 2616,可以从http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf下载。
在HTTP中,始终都是客户端通过建立连接和发送一个HTTP请求从而开启一个事务。web服务器不需要联系客户端或者对客户端做一个回调连接。无论是客户端或者服务器都可以提前终止连接。举例来说,当你正在使用一个web浏览器的时候,可以通过点击浏览器上的停止按钮来停止一个文件的下载进程,从而有效的关闭与web服务器的HTTP连接。
3.2 HTTP请求
一个HTTP请求包括三个组成部分:
- 方法-统一资源标识符(URI)-协议/版本
- 请求的头部
主题内容
下面我们看一个例子: POST /examples/default.jsp HTTP/1.1 Accept: text/plain; text/html Accept-Language: en-gb Connection: Keep-Alive Host: localhost User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows98) Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate lastName=Franks&firstName=Michael 这里POST是请求方法,/examples/default.jsp是URI,而HTTP/1.1是协议/版本部分。每个HTTP请求可以使用HTTP标准里边提到的多种方法之一。HTTP 1.1支持7种类型的请求:GET, POST,HEAD,OPTIONS, PUT,DELETE和TRACE。GET和POST在互联网应用里边最普遍使用的。URI完全指明了一个互联网资源。URI通常是相对服务器的根目录解释的。因此,始终一斜线/开头。统一资源定位器(URL)其实是一种URI(查看http://www.ietf.org/rfc/rfc2396.txt)来的。该协议版本代表了正在使用的HTTP协议的版本。 请求的头部包含了关于客户端环境和请求的主体内容的有用信息。例如它可能包括浏览器设置的语言,主体内容的长度等等。每个头部通过一个回车换行符(CRLF)来分隔的。 对于HTTP请求格式来说,头部和主体内容之间有一个回车换行符(CRLF)是相当重要的。CRLF告诉HTTP服务器主体内容是在什么地方开始的。在一些互联网编程书籍中,CRLF还被认为是HTTP请求的第四部分。
3.3 HTTP响应
类似于HTTP请求,一个HTTP响应也包括三个组成部分:
- 方法—统一资源标识符(URI)—协议/版本
- 响应的头部
主体内容
下面我们看一个例子: HTTP/1.1 200 OK Server: Microsoft-IIS/4.0 Date: Mon, 5 Jan 2017 13:13:33 GMT Content-Type: text/html Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT Content-Length: 112 <html> <head> <title>HTTP Response Example</title> </head> <body> Welcome to My CSDN </body> </html> 响应头部的第一行类似于请求头部的第一行。第一行告诉你该协议使用HTTP 1.1,请求成功(200=成功),表示一切都运行良好。 响应头部和请求头部类似,也包括很多有用的信息。响应的主体内容是响应本身的HTML内容。头部和主体内容通过CRLF分隔开来。 现在你是不是对协议有了浏览器与服务器之间的通信有了一定的了解?那么我们继续我们的学习。
3.4 Socket类
套接字是网络连接的一个端点。套接字使得一个应用可以从网络中读取和写入数据。放在两个不同计算机上的两个应用可以通过连接发送和接受字节流。为了从你的应用发送一条信息到另一个应用,你需要知道另一个应用的IP地址和套接字端口。在Java里边,套接字指的是java.net.Socket类。
一旦你成功创建了一个Socket类的实例,你可以使用它来发送和接受字节流。要发送字节流,你首先必须调用Socket类的getOutputStream方法来获取一个java.io.OutputStream对象。要发送文本到一个远程应用,你经常要从返回的OutputStream对象中构造一个java.io.PrintWriter对象。要从连接的另一端接受字节流,你可以调用Socket类的getInputStream方法用来返回一个java.io.InputStream对象。
3.4 ServerSocket类
Socket类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字,现在,假如你想实施一个服务器应用,例如一个HTTP服务器或者FTP服务器,你需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用java.net.ServerSocket类。这是服务器套接字的实现。
ServerSocket和Socket不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个Socket实例来与客户端进行通信。
要创建一个服务器套接字,你需要使用ServerSocket类提供的四个构造方法中的一个。你需要指定IP地址和服务器套接字将要进行监听的端口号。通常,IP地址将会是127.0.0.1,也就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的IP地址被称为是绑定地址。服务器套接字的另一个重要的属性是backlog,这是服务器套接字开始拒绝传入的请求之前,传入的连接请求的最大队列长度。
有一定JAVA基础的同学,我相信,这些基础知识我们已经耳熟能详。接下来,我们就借助这些知识,完成一个最最最最最基础的WEB服务器。
四、“活尿泥版”的WEB服务器
4.1 源代码
我们的web服务器应用程序有三个类组成:
HttpServer类
package com.liu.tomcat.simpleTomcat; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import com.liu.tomcat.simpleTomcat.request.Request; import com.liu.tomcat.simpleTomcat.response.Response; public class HttpServer { /** WEB_ROOT is the directory where our HTML and other files reside. * For this package, WEB_ROOT is the "webroot" directory under the working * directory. * The working directory is the location in the file system * from where the java command was invoked. */ public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot" // shutdown command private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received private boolean shutdown = false; public static void main(String[] args) { HttpServer httpServer = new HttpServer(); System.out.println(httpServer.WEB_ROOT); httpServer.await(); } public void await() { ServerSocket serverSocket = null; int port = 8088; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } //Loop waiting for a request while(!shutdown) { Socket socket = null; InputStream inputStream = null; OutputStream outputStream = null; try { //这里会产生阻塞,等待客户端的请求 socket = serverSocket.accept(); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); // create Request object and parse Request request = new Request(inputStream); request.parse(); // create Response object Response response = new Response(outputStream); response.setRequest(request); response.sendStaticResource(); // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (IOException e) { e.printStackTrace(); continue; } } } }
Request类
package com.liu.tomcat.simpleTomcat.request; import java.io.IOException; import java.io.InputStream; public class Request { private InputStream inputStream; private String uri; public String getUri() { return uri; } public Request(InputStream inputStream) { this.inputStream = inputStream; } public void parse() { // Read a set of characters from the socket StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; try { i = inputStream.read(buffer); } catch (IOException e) { e.printStackTrace(); i = -1; } for(int j = 0; j < i; j++) { request.append((char)buffer[j]); } System.out.println("request:\n" + request.toString()); uri = parseUri(request.toString()); } private String parseUri(String requestString) { int index1, index2; index1 = requestString.indexOf(' '); if(index1 != -1) { index2 = requestString.indexOf(' ', index1 + 1); if(index2 > index1) { System.out.println("resquest文件:" + requestString.substring(index1+1, index2)); return requestString.substring(index1+1, index2); } } return null; } }
Response类
package com.liu.tomcat.simpleTomcat.response; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import com.liu.tomcat.simpleTomcat.HttpServer; import com.liu.tomcat.simpleTomcat.request.Request; /* HTTP Response = Status-Line *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF */ public class Response { private static final int BUFFER_SIZE = 1024; Request request; OutputStream outputStream; public Response(OutputStream outputStream) { super(); this.outputStream = outputStream; } public void setRequest(Request request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; FileInputStream fis = null; try { 连接用户请求的"文件" File file = new File(HttpServer.WEB_ROOT, request.getUri()); if(file.exists()) { String successMessage = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "\r\n"; fis = new FileInputStream(file); //把文件里的东西读出来放到bytes字符数组里 int ch = fis.read(bytes, 0, BUFFER_SIZE); outputStream.write(successMessage.getBytes()); //把bytes数组里的东西放到要给客户端回复的流里面 while(ch != -1) { outputStream.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } } else { //file not find String errorMassage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>"; outputStream.write(errorMassage.getBytes()); } } catch (Exception e){ // thrown if cannot instantiate a File object System.out.println(e.toString() ); } finally { if(fis != null) { fis.close(); } } } }
4.2 源码分析
这个应用程序的入口点(静态main方法)可以在HttpServer类里边找到。main方法创建了一个HttpServer的实例并调用了它的await方法。await方法,顾名思义就是在一个指定的端口上等待HTTP请求,处理它们并发送响应返回客户端。它一直等待直至接收到shutdown命令。
应用程序不能做什么,除了发送静态资源,例如放在一个特定目录的HTML文件和图像文件。它也在控制台上显示传入的HTTP请求的字节流。不过,它不给浏览器发送任何的头部例如日期或者cookies。
4.3 运行结果
首先我们必须将我们的WEB服务器打开,使其运行起来,这样子我们在浏览器中访问我们的WEB服务器,我们来看结果:
至此,本小结结束,累死了。目前已经凌晨1:25,不说了,去睡觉了;有问题请留言哈哈哈!!!