web运作原理简单解析
web运行原理
要理解Tomcat其实首先就是要理解Web的运行原理,基本上每个人都上网,但是既然我们自己在学习在做动态网页,有没有真正考虑过我们在浏览网页时底层的一些基本运行原理。 当我们输入一个网址,如HTTP://www.yingside.com/JAVA/index.html 这中间其实是你的客户端浏览器与服务器端的通信过程,具体如下:
- 浏览器与网络上的域名为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
1.请求方法,URI和HTTP协议版本 这三个都在HTTP请求的第一行,以空格分开,以上代码中”post”为请求方式,”/servlet/default.JSP”为URI, ”HTTP/1.1”为HTTP协议版本
2.请求头 请求头包含许多有关客户端环境和请求正文的有用信息。比如包含浏览器类型,所用语言,请求正文类型以及请求正文长度等。
3.请求正文 HTTP协议规定,请求头和请求正文之间必须以空行分割(\r\n),这个空行很重要,它表示请求头已经结束,接下来是请求正文。在请求正文中可以包含客户以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>
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服务器程序
首先做好准备工作,注意整个测试工程的路径是下面这样子的,如图:
这个html文件在工程中我放在了test文件夹下面,接下来上代码
html中的代码很简单 index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
Hello!!
</body>
</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("\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下面的文字,
服务器与一个客户端建立了新的连接,该客户端的地址为:/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
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
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
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("\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基本的原理就是这些,希望能够帮助大家理解.
Comments