01月11, 2019

web运作原理简单解析

web运作原理简单解析

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请求的列子:

//请求方法,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部分组成

  1. HTTP协议的版本,状态代码和描述
  2. 响应头(Response Header)
  3. 响应正文(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基本的原理就是这些,希望能够帮助大家理解.

本文链接:http://www.yanhongzhi.com/post/web-theory.html

-- EOF --

Comments