web运作原理简单解析

引子

在java web的学习中,JSP具有承上启下的作用。基本上包含了前面我们学习的所有内容,所以也是学习的难点。在JSP的学习中,我们还用到了一个新的软件应用服务器Tomcat,但是在教学过程中发现,很多同学基本就不是太理解Tomcat的基本原理,就只是在学习的过程中机械的把myeclipse里面的内容导入到Tomcat中,再输入网址运行。甚至有些同学把myeclipse换成eclipse就不知道如何去建工程写程序了。出现这个问题的原因还是同学们把知识学的太死,平时只是按照老师写的程序依葫芦画瓢,不知道程序或者软件底层的一些意义,所以今天这篇文章主要是探讨一下Tomcat的一些基本的运作原理.

web运行原理

要理解Tomcat其实首先就是要理解Web的运行原理,基本上每个同学都上网,但是既然我们自己在学习在做动态网页,有没有真正考虑过我们在浏览网页时底层的一些基本运行原理。
当我们输入一个网址,如HTTP://www.yingside.com/JAVA/index.html
这中间其实是你的客户端浏览器与服务器端的通信过程,具体如下:

  1. 浏览器与网络上的域名为www.yingside.com 的 Web服务器建立TCP连接
  2. 浏览器发出要求访问JAVA/index.html的HTTP请求
  3. Web服务器在接收到HTTP请求后,解析HTTP请求,然后发回包含index.html文件数据的HTTP响应
  4. 浏览器接受到HTTP响应后,解析HTTP响应,并在其窗口中展示index.html文件
  5. 浏览器与Web服务器之间的TCP连接关闭

就是这样的一个简单过程,有些同学可能会说,这个过程书上也有,还有图。但是,就是这个样子的一个过程,中间就有很多值得探讨的地方。

我们来解析一下,从上面这个过程中分析出

浏览器应该有一下的功能

  1. 请求与Web服务器建立TCP连接
  2. 创建并发送HTTP请求
  3. 接受并解析HTTP响应
  4. 展示html文档

Web服务器应该具有的功能

  1. 接受来自浏览器的TCP的请求
  2. 接收并解析HTTP请求
  3. 创建并发送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请求由三部分组成

  1. 请求方法,URI和HTTP协议的版本
  2. 请求头(Request Header)
  3. 请求正文(Request Content)

看一个HTTP请求的列子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//请求方法,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
  1. 请求方法,URI和HTTP协议版本

这三个都在HTTP请求的第一行,以空格分开,以上代码中”post”为请求方式,”/servlet/default.JSP”为URI, ”HTTP/1.1”为HTTP协议版本

  1. 请求头

请求头包含许多有关客户端环境和请求正文的有用信息。比如包含浏览器类型,所用语言,请求正文类型以及请求正文长度等。

  1. 请求正文

HTTP协议规定,请求头和请求正文之间必须以空行分割(\r\n),这个空行很重要,它表示请求头已经结束,接下来是请求正文。在请求正文中可以包含客户以Post方式提交的数据表单
LastName=Franks&FirstName=Michael

HTTP响应格式

和请求相似,HTTP响应也是由3部分组成

  1. HTTP协议的版本,状态代码和描述
  2. 响应头(Response Header)
  3. 响应正文(Response Content)
    看一个HTTP响应列子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>
  1. HTTP协议版本,状态代码和描述

HTTP响应第一行也是3个内容,同样以空格分隔,依次是HTTP协议版本,状态代码以及对状态代码的描述。状态代码200表示服务器已经成功处理了客户端发送的请求。状态代码是三位整数,以1,2,3,4,5开头,具体有哪些常见的状态代码这里不再多做描述。

  1. 响应头

响应头主要是一些描述信息,如服务器类型,正文类型和正文长度等

  1. 响应正文

响应正文就是服务器返回的具体数据,它是浏览器真正请求访问的信息,最常见的当然就是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服务器程序

首先做好准备工作,注意整个测试工程的路径是下面这样子的,如图:

这个html文件在工程中我放在了test文件夹下面,接下来上代码

html中的代码很简单
index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
Hello!!
</body>
</html>

HTTPServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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("\r\n");
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 OK\r\n";
String responseHeader = "Content-Tyep:" + contentType +
"\r\n\r\n";
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();
}
}

大家可以看到上面的代码其实就是操作了一些HTTP请求与响应的协议字符串而已。写好上面的代码后,我们启动浏览器,输入HTTP://127.0.0.1:8080/index.html 大家会看到下面的效果:

浏览器自动帮我们输出了index.html下面的文字,

1
2
3
4
5
6
7
8
9
10
服务器与一个客户端建立了新的连接,该客户端的地址为:/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

这其实就是一个简单自制的HTTP远程访问,但是上面的代码就只是能根据原始的html返回内容,不能和客户端发生交互,那么现在做一个简单交互

和服务器进行交互

比如我们输入如下网址:
HTTP://127.0.0.1:8080/servlet/HelloServlet?userName=yingside
那么久应该能出现下面这样的效果

输入:
HTTP://127.0.0.1:8080/servlet/HelloServlet?userName=lovo
就会是这样的效果:

其实这里我们只要对之前的代码做一下简单的修改,让代码能够分析出后面的值就行了。

首先,先来看一下,我们修改之后工程的路径,因为代码中一些路径都是写死了的,为了避免出错,大家先按照我工程的路径搭建就行了.

这里把分析后面参数值的内容专门放在了一个类中,为了让这个类具有通用性,定义了一个接口

Servlet.java

1
2
3
4
5
6
7
8
9
package com.ying.http;
import java.io.OutputStream;
public interface Servlet {
void init() throws Exception;
void service(byte[] requestBuffer,OutputStream out)
throws Exception;
}

init()方法:为初始化方法,当HTTPServer创建了实现该接口的类的一个实例后,就会立即调用该实例的init()方法
service()方法:用于响应HTTP请求,产生具体的HTTP响应结果。

HelloServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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("\r\n"));
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("\r\n\r\n");//提取出两行空行的位置
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 OK\r\n".getBytes());
//发送响应头
out.write("Content-Type:text/html\r\n\r\n".getBytes());
//发送HTTP响应正文
out.write("<html><head><title>HelloWord</title></head>".getBytes());
out.write(new String("<body><h1>hello:"+userName+"</h1></body></html>").getBytes());
}
}

说的简单点,其实就是把解析HTTP请求和响应协议字符串放在了这个HelloServlet.JAVA的类里面。最后把HTTPServer做一下修改,干脆重新新建一个类HTTPServerParam.java,大家可以下去自行比较一下两个的区别

HTTPServerParam.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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 {
// 存放servlet实例的map缓存
private static Map servletCache = new HashMap();
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("\r\n");
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 OK\r\n";
String responseHeader = "Content-Tyep:" + contentType + "\r\n\r\n";
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();
}
}

修改之后的HelloServerParam的基本逻辑就是如果客户端请求的URI位于servlet子目录下,就按照Serlvet来处理,否则就按照普通的静态文件来处理。当客户端请求访问特定的Servlet时,服务器端代码先从自己的servletCache缓存中寻找特定的Servlet实例,如果存在就调用它的service()方法;否则就先创建Servlet实例,把它放入servletCache缓存中,再调用它的service()方法。
如果学习过servlet的同学就会发现,这其实就是实现了一个j2ee的servlet,现在相当于我们就自己建立一个非常简单的Tomcat服务器…当然这里只能说是一个转换器而已…不过基本的Tomcat基本的原理就是这些,希望能够帮助大家理解.