web运行原理
要理解Tomcat其实首先就是要理解Web的运行原理,基本上每个人都上网,但是既然我们自己在学习在做动态网页,有没有真正考虑过我们在浏览网页时底层的一些基本运行原理。 当我们输入一个网址,如HTTPS://www.yingside.com/JAVA/index.html 这中间其实是你的客户端浏览器与服务器端的通信过程,具体如下:
- 浏览器与网络上的域名为http://www.yingside.com 的 Web服务器建立TCP连接
- 浏览器发出要求访问JAVA/index.html的HTTP请求
- Web服务器在接收到HTTP请求后,解析HTTP请求,然后发回包含index.html文件数据的HTTP响应
- 浏览器接受到HTTP响应后,解析HTTP响应,并在其窗口中展示index.html文件
- 浏览器与Web服务器之间的TCP连接关闭
就是这样的一个简单过程,这个过程很多书上也有,还有图。但是,就是这个样子的一个过程,中间就有很多值得探讨的地方。
我们来解析一下,从上面这个过程中分析出
浏览器应该有的功能:
- 请求与Web服务器建立TCP连接
- 创建并发送HTTP请求
- 接受并解析HTTP响应
- 展示html文档
Web服务器应该具有的功能:
- 接受来自浏览器的TCP的请求
- 接收并解析HTTP请求
- 创建并发送HTTP响应
HTTP客户程序(浏览器)和HTTP服务器分别由不同的软件开发商提供,目前 最流行的浏览器IE,Firefox,Google Chrome,Apple Safari等等,最常用的Web服务器有IIS,Tomcat,Weblogic,jboss等。不同的浏览器和Web服务器都是不同的编程语言编写的,那么用C++编写的HTTP客户端浏览器能否与用JAVA编写的Web服务进行通信呢?允许在苹果系统上的Safari浏览器能否与运行在Windows或者Linux平台上的Web服务器进行通信呢?
前面说了这么多,就是引出这一句话。
为什么不同语言编写,不同平台运行的软件双方能够看懂对方的数据呢?这主要归功于HTTP协议。
HTTP协议严格规定了HTTP请求和HTTP响应的数据格式,只要Web服务器与客户端浏览器之间的交换数据都遵守HTTP协议,双方就能看懂对方发送的数据从而进行交流。
HTTP请求格式
HTTP协议规定,HTTP请求由三部分组成
- 请求方法,URI和HTTP协议的版本
- 请求头(Request Header)
- 请求正文(Request Content)
看一个HTTP请求的列子:
- //请求方法,URI和HTTP协议的版本
- POST /servlet/default.JSP HTTP/1.1
- //========请求头==================//
- Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
- Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
- Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
- Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
- Connection: Keep-Alive
- Host: localhost
- Referer: HTTP://localhost/ch8/SendDetails.htm
- User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
- Content-Length: 33
- Content-Type: application/x-www-form-urlencoded
- //========请求头==================//
- //请求正文
- LastName=Franks&FirstName=Michael
groovy
1.请求方法,URI和HTTP协议版本 这三个都在HTTP请求的第一行,以空格分开,以上代码中”post”为请求方式,”/servlet/default.JSP”为URI, ”HTTP/1.1”为HTTP协议版本
2.请求头 请求头包含许多有关客户端环境和请求正文的有用信息。比如包含浏览器类型,所用语言,请求正文类型以及请求正文长度等。
3.请求正文 HTTP协议规定,请求头和请求正文之间必须以空行分割(rn),这个空行很重要,它表示请求头已经结束,接下来是请求正文。在请求正文中可以包含客户以Post方式提交的数据表单 LastName=Franks&FirstName=Michael
HTTP响应格式
和请求相似,HTTP响应也是由3部分组成
- HTTP协议的版本,状态代码和描述
- 响应头(Response Header)
- 响应正文(Response Content) 看一个HTTP响应列子:
- HTTP/1.1 200 OK
- Date: Tues, 07 May 2013 14:16:18 GMT
- Server: Apache/1.3.31 (Unix) mod_throttle/3.1.2
- Last-Modified: Tues, 07 May 2013 14:16:18
- ETag: "dd7b6e-d29-39cb69b2"
- Accept-Ranges: bytes
- Content-Length: 3369
- Connection: close
- Content-Type: text/html
- <html>
- <head>
- <title>hello</title>
- </head>
- <body>
- <h1>hello</h1>
- </body>
- </html>
http
1.HTTP协议版本,状态代码和描述 HTTP响应第一行也是3个内容,同样以空格分隔,依次是HTTP协议版本,状态代码以及对状态代码的描述。状态代码200表示服务器已经成功处理了客户端发送的请求。状态代码是三位整数,以1,2,3,4,5开头,具体有哪些常见的状态代码这里不再多做描述。
2.响应头 响应头主要是一些描述信息,如服务器类型,正文类型和正文长度等
3.响应正文 响应正文就是服务器返回的具体数据,它是浏览器真正请求访问的信息,最常见的当然就是HTML。同样,响应正文和响应头同样需要以空行分割
分析
前面说了这么多,描述的HTTP请求和响应的内容,主要是引出下面的内容,既然Tomcat可以作为Web服务器,那么我们自己能不能根据HTTP请求和响应搭建一个自己简单的Web服务器呢?
我们在启动好Tomcat后,访问的地址如HTTP://127.0.0.1:8080/index.html, 经过分析,前面的127.0.0.1无非就是主机IP,而8080就是Tomcat监听端口, index.html是我们需要访问的网址,其实也就是Tomcat帮我们读取之后,响应给我们的内容,这是在Tomcat上存在的一个网页。
根据上面的分析,我们自己要建一个简单的Web服务器,那就简单了,就是自己写一段JAVA代码,代替Tomcat监听在8080端口,然后打开网页输入8080端口后进入自己的代码程序,解析HTTP请求,然后在服务器本地读取html文档,最后再响应回去不就行了么?
用JAVA套接字创建HTTP服务器程序
首先做好准备工作,注意整个测试工程的路径是下面这样子的,如图:
![8e8c5ae4cf644dcb6c1c9f7662d9ee15.png](https://i-blog.csdnimg.cn/blog_migrate/4b9733d2ef1dc6c35821030dbb9355c9.jpeg)
这个html文件在工程中我放在了test文件夹下面,接下来上代码
html中的代码很简单 index.html
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>Test</title>
- </head>
- <body>
- Hello!!
- </body>
- </html>
html
HTTPServer.java
- package com.ying.http;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- public class HTTPServer {
- public static void main(String[] args) {
- int port;
- ServerSocket serverSocket;
- try {
- serverSocket = new ServerSocket(8080);
- System.out.println("服务器正在监听:" + serverSocket.getLocalPort());
- while(true){
- try {
- Socket socket = serverSocket.accept();
- System.out.println("服务器与一个客户端建立了新的连接,该客户端的地址为:"
- +socket.getInetAddress() + ":" + socket.getPort());
- service(socket);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public static void service(Socket socket) throws Exception{
- InputStream socketIn = socket.getInputStream();
- Thread.sleep(500);
- int size = socketIn.available();
- byte[] buffer = new byte[size];
- socketIn.read(buffer);
- String request = new String(buffer);
- if(request.equals("")) return;
- System.out.println(request);
- int l = request.indexOf("rn");
- String firstLineRequest = request.substring(0, l);
- String [] parts = firstLineRequest.split(" ");
- String uri = parts[1];
- //HTTP响应正文类型
- String contentType;
- if(uri.indexOf("html") != -1 || uri.indexOf("html") != -1){
- contentType = "text/html";
- }else if(uri.indexOf("jpg") != -1 || uri.indexOf("jpeg") != -1){
- contentType = "image/jpeg";
- }else if(uri.indexOf("gif") != -1){
- contentType = "image/gif";
- }else
- contentType = "application/octet-stream";
- /*创建HTTP响应结果*/
- String responseFirstLine = "HTTP/1.1 200 OKrn";
- String responseHeader = "Content-Tyep:"+contentType+"rnrn";
- InputStream in = HTTPServer.class.getResourceAsStream("test/" + uri);
- OutputStream socketOut = socket.getOutputStream();
- socketOut.write(responseFirstLine.getBytes());
- socketOut.write(responseHeader.getBytes());
- int len = 0;
- buffer = new byte[128];
- while((len=in.read(buffer)) != -1){
- socketOut.write(buffer,0,len);
- }
- Thread.sleep(1000);
- socket.close();
- }
- }
java
大家可以看到上面的代码其实就是操作了一些HTTP请求与响应的协议字符串而已。写好上面的代码后,我们启动浏览器,输入HTTP://127.0.0.1:8080/index.html 大家会看到下面的效果:
![bbc6a1bb834e547c05c786098308bfaf.png](https://i-blog.csdnimg.cn/blog_migrate/6e719e442c26efb536a6d75295258130.jpeg)
浏览器自动帮我们输出了index.html下面的文字,
- 服务器与一个客户端建立了新的连接,该客户端的地址为:/127.0.0.1:57891
- GET /index.html HTTP/1.1
- Host: 127.0.0.1:8080
- Connection: keep-alive
- Cache-Control: max-age=0
- Upgrade-Insecure-Requests: 1
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
- Accept-Encoding: gzip, deflate, sdch, br
- Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4
groovy
这其实就是一个简单自制的HTTP远程访问,但是上面的代码就只是能根据原始的html返回内容,不能和客户端发生交互,那么现在做一个简单交互。
和服务器进行交互
比如我们输入如下网址: HTTP://127.0.0.1:8080/servlet/HelloServlet?userName=yingside 那么久应该能出现下面这样的效果
![e57b46b5aa21933af7e5689c43cac3f0.png](https://i-blog.csdnimg.cn/blog_migrate/53016ddb420bb8de74f0ed8162baae96.jpeg)
输入: HTTP://127.0.0.1:8080/servlet/HelloServlet?userName=lovo 就会是这样的效果:
![00d34ee5e898bc5d8fee68d4baaf90c8.png](https://i-blog.csdnimg.cn/blog_migrate/ff45b0b3a7afdcee7f1345f29580ac76.jpeg)
其实这里我们只要对之前的代码做一下简单的修改,让代码能够分析出后面的值就行了。
首先,先来看一下,我们修改之后工程的路径,因为代码中一些路径都是写死了的,为了避免出错,大家先按照我工程的路径搭建就行了.
![ca4a4736d33668989fa7476c6a8d3ef7.png](https://i-blog.csdnimg.cn/blog_migrate/76ed782969b21b5990f8a663e15a6191.jpeg)
这里把分析后面参数值的内容专门放在了一个类中,为了让这个类具有通用性,定义了一个接口
Servlet.java
- package com.ying.http;
- import java.io.OutputStream;
- public interface Servlet {
- void init() throws Exception;
- void service(byte[] requestBuffer,OutputStream out) throws Exception;
- }
java
init()方法:为初始化方法,当HTTPServer创建了实现该接口的类的一个实例后,就会立即调用该实例的init()方法 service()方法:用于响应HTTP请求,产生具体的HTTP响应结果。
HelloServlet.java
- package com.ying.http;
- import java.io.OutputStream;
- public class HelloServlet implements Servlet {
- public void init() throws Exception {
- System.out.println("Hello Servlet is inited");
- }
- @Override
- public void service(byte[] requestBuffer, OutputStream out)
- throws Exception {
- String request = new String(requestBuffer);
- //获得请求的第一行
- String firstLineRequest = request.substring(0, request.indexOf("rn"));
- String [] parts = firstLineRequest.split(" ");
- String method = parts[0];//获得HTTP请求中的请求方式
- String uri = parts[1];//获得uri
- String userName = null;
- //如果请求方式为"GET",则请求参数紧跟在HTTP请求的第一行uri的后面
- if(method.equalsIgnoreCase("get")&&uri.indexOf("userName") != -1){
- /*假定uri="servlet/HelloServlet?userName=chenjie&password=accp"
- *那么参数="userName=chenjie&password=accp",所以这里截取参数字符串
- */
- String parameters = uri.substring(uri.indexOf("?"), uri.length());
- //通过"&"符号截取字符串
- //parts={"userName=chenjie","password=accp"}
- parts = parameters.split("&");
- //如果想截取出userName的值,再通过"="截取字符串
- parts = parts[0].split("=");
- userName = parts[1];
- }
- //如果请求方式为"post",则请求参数在HTTP请求的正文中
- //由于请求头和正文有两行空行,所以截取出两行空行,就能截取出正文
- if(method.equalsIgnoreCase("post")){
- int location = request.indexOf("rnrn");//提取出两行空行的位置
- String content = request.substring(location+4, request.length());
- //"post"提交正文里面只有参数,所以只需要
- //和"get"方式一样,分割字符串,提取出userName的值
- if(content.indexOf("userName") != -1){
- parts = content.split("&");
- parts = parts[0].split("=");
- userName = parts[1];
- }
- }
- /*创建并发送HTTP响应*/
- //发送HTTP响应第一行
- out.write("HTTP/1.1 200 OKrn".getBytes());
- //发送响应头
- out.write("Content-Type:text/htmlrnrn".getBytes());
- //发送HTTP响应正文
- out.write("<html><head><title>HelloWord</title></head>".getBytes());
- out.write(new String("<body><h1>hello:"+userName+"</h1></body></html>").getBytes());
- }
- }
processing
说的简单点,其实就是把解析HTTP请求和响应协议字符串放在了这个HelloServlet.JAVA的类里面。最后把HTTPServer做一下修改,干脆重新新建一个类HTTPServerParam.java,大家可以下去自行比较一下两个的区别
HTTPServerParam.java
- package com.ying.http;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.util.HashMap;
- import java.util.Map;
- public class HTTPServerParam {
- private static Map servletCache = new HashMap();// 存放servlet实例的map缓存
- public static void main(String[] args) {
- int port;
- ServerSocket serverSocket;
- try {
- serverSocket = new ServerSocket(8080);
- System.out.println("服务器正在监听:" + serverSocket.getLocalPort());
- while (true) {
- try {
- Socket socket = serverSocket.accept();
- System.out.println("服务器与一个客户端建立了新的连接,该客户端的地址为:" + socket.getInetAddress() + ":" + socket.getPort());
- service(socket);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public static void service(Socket socket) throws Exception {
- InputStream socketIn = socket.getInputStream();
- Thread.sleep(500);
- int size = socketIn.available();
- byte[] requestBuffer = new byte[size];
- socketIn.read(requestBuffer);
- String request = new String(requestBuffer);
- if (request.equals(""))
- return;
- System.out.println(request);
- /* 解析HTTP请求 */
- // 获得HTTP请求的第一行
- int l = request.indexOf("rn");
- String firstLineRequest = request.substring(0, l);
- // 解析HTTP请求的第一行,通过空格截取字符串数组
- String[] parts = firstLineRequest.split(" ");
- String uri = parts[1];
- /* 判断如果访问的是Servlet,则动态的调用Servlet对象的service()方法 */
- if (uri.indexOf("servlet") != -1) {
- String servletName = null;
- if (uri.indexOf("?") != -1)
- servletName = uri.substring(uri.indexOf("servlet/") + 8, uri.indexOf("?"));
- else
- servletName = uri.substring(uri.indexOf("servlet/") + 8, uri.length());
- // 首先从map里面获取有没有该Servlet
- Servlet servlet = (Servlet) servletCache.get(servletName);
- // 如果Servlet缓存中不存在Servlet对象,就创建它,并把它存到map缓存中
- if (servlet == null) {
- servlet = (Servlet) Class.forName("com.ying.http." + servletName).newInstance();
- servlet.init();
- servletCache.put(servletName, servlet);
- }
- // 调用Servlet的service()方法
- servlet.service(requestBuffer, socket.getOutputStream());
- Thread.sleep(1000);
- socket.close();
- return;
- }
- // HTTP响应正文类型
- String contentType;
- if (uri.indexOf("html") != -1 || uri.indexOf("html") != -1) {
- contentType = "text/html";
- } else if (uri.indexOf("jpg") != -1 || uri.indexOf("jpeg") != -1) {
- contentType = "image/jpeg";
- } else if (uri.indexOf("gif") != -1) {
- contentType = "image/gif";
- } else
- contentType = "application/octet-stream";
- /* 创建HTTP响应结果 */
- String responseFirstLine = "HTTP/1.1 200 OKrn";
- String responseHeader = "Content-Tyep:" + contentType + "rnrn";
- InputStream in = HTTPServerParam.class.getResourceAsStream("test/" + uri);
- OutputStream socketOut = socket.getOutputStream();
- socketOut.write(responseFirstLine.getBytes());
- socketOut.write(responseHeader.getBytes());
- int len = 0;
- requestBuffer = new byte[128];
- while ((len = in.read(requestBuffer)) != -1) {
- socketOut.write(requestBuffer, 0, len);
- }
- Thread.sleep(1000);
- socket.close();
- }
- }
java
修改之后的HelloServerParam的基本逻辑就是如果客户端请求的URI位于servlet子目录下,就按照Serlvet来处理,否则就按照普通的静态文件来处理。当客户端请求访问特定的Servlet时,服务器端代码先从自己的servletCache缓存中寻找特定的Servlet实例,如果存在就调用它的service()方法;否则就先创建Servlet实例,把它放入servletCache缓存中,再调用它的service()方法。 如果学习过servlet的人就会发现,这其实就是实现了一个j2ee的servlet,现在相当于我们就自己建立一个非常简单的Tomcat服务器...当然这里只能说是一个转换器而已...不过基本的Tomcat基本的原理就是这些,希望能够帮助大家理解.