实现简易web服务器和web浏览器

web服务器和web浏览器的实现原理

前言

       预警:此篇篇幅很长,可通过菜单导航观看。最近在复习计算机网络的知识,因为想在Java web方面深入了解,所以在思考B/S结构模式的工作是怎样的过程呢,刚好找了好些资料和别人的总结,现在把这些都整理一下。在基础中已经学习了socket和http协议,我们也知道通过浏览器输入一个地址,访问一个网页的操作。实际对应的底层操作简单来说就是:客户端(浏览器)面向于WEB服务器的网络通信。

       那么,既然是网络通信。对应于Java当中来说,就自然离不开Socket与IO流。其实这也正是Web服务器与浏览器的基础实现原理。当然,想要开发一套完善的WEB服务器或浏览器,需要做的工作是很复杂的,要考虑多线程、并发、io流、网络等等。但我们想要了解的只是其原理。后面将对使用较多的servlet和tomcat在进行分析,达到一个比较深入的理解。本篇在此博客基础上进一步整理:浅析web服务器与浏览器的实现原理

原理概述

       我们知道,将开发的web项目部署到tomcat服务器之后,就可以通过浏览器对服务器上的资源进行访问。但重要的一点是,存在多种不同厂商开发的不同浏览器。但各个类型的WEB浏览器,都可以正常的访问tomcat服务器上的资源。对此,我们可以这样理解:我开发了一个WEB服务器,并且能够保证其他人开发的客户端都能够与我的服务器正常通信。

       能够实现这样的目的的前提自然就是,你要制定一个规范,并让想要与你开发的服务器正常进行通信的客户端都遵循这个规范来实现。这个规范,也就是所谓的协议。

       正如在网络通信中,数据的传输可以遵循TCP/IP或UDP协议一样。WEB服务器与WEB浏览器之间,也通过一种双方都熟悉的语言进行通信。这种协议即是:超文本传输协议,也就是HTTP协议。不同的是,TCP/IP与UDP议是传输层当中的通信协议,而HTTP协议是应用层当中的协议。

       当我们想要使用Java语言实现所谓的WEB通信,自然也应当遵循HTTP协议。Java中已经为我们提供了这样的一种实现规范,也就是广为人知的:Servlet接口。而我们开发web项目时,最常用到的HttpServlet类,就是基于此接口实现的具体子类。该类封装和提供了,针对基于Http协议通信的内容进行访问和操作的常用方法。

Servlet实例

       首先,我们通过一段简单的Servlet代码来看一下,基于HTTP协议进行WEB通信的请求信息。我的环境是os x+idea+tomcat7,项目工程我放在github上,链接传送门:wustzoujing/MyBrowerAndMyTomcat

       首先要了解servlet,这个基础没有的话,可以参考这个入门教程servlet教程。从helloworld开始做。这里我们基于HttpServlet+Tomcat,先建立一个web的项目,再写servlet测试类,然后配置web.xml,最后用tomcat发布。

       这里贴上一些代码:

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
import javax.servlet.http.*;
import javax.servlet.*;
import java.io.IOException;
import java.util.Enumeration;
/**
* Created by zoujing on 2017/5/10.
*/
public class ServeletTest extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
for (Enumeration e = request.getHeaderNames(); e.hasMoreElements();) {
String header = (String) e.nextElement();
if (header != null)
System.out.println((new StringBuilder(String.valueOf(header)))
.append(":").append(request.getHeader(header))
.toString());
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}

       接下来是web.xml的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd"
version="3.0">
<servlet>
<servlet-name>TomcatWeb</servlet-name>
<servlet-class>ServeletTest</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TomcatWeb</servlet-name>
<url-pattern>/servlet/HttpServletDemo</url-pattern>
</servlet-mapping>
</web-app>

       当我们在浏览器输入链接http://localhost:8080/servlet/HttpServletDemo的时候,会打印出下面信息:

1
2
3
4
5
6
7
8
host:localhost:8080
accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
upgrade-insecure-requests:1
cookie:JSESSIONID=92E7AE4F85499378342793886884D0C0; _ga=GA1.1.1073505442.1486722778
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.1 Safari/603.1.30
accept-language:zh-cn
accept-encoding:gzip, deflate
connection:keep-alive

       在计算机网络的学习中我们就知道了,这是http请求报文的内容,我们这里再回忆一下:一个HTTP协议的请求中,通常主要包含三个部分:

  1. 方法/统一资源标示符(URI)/协议/版本
  2. 请求标头
  3. 实体主体

       其中方法也就是所谓的get/post之类的请求方法,统一资源标示符也就是要访问的目标资源的路径,包括协议及协议版本,这些信息被放在请求的第一行。

       随后,紧接着的便是请求标头;请求标头通常包含了与客户端环境及请求实体主体相关的有用信息。

       最后,在标头与实体主体之间是一个空行。它对于HTTP请求格式是很重要的,空行告诉HTTP服务器,实体主体从这里开始。

实现最简易的Tomcat

       前面已经说过了,我们这里想要研究的,是WEB服务器的基本实现原理。那么我们自然想要自己来实现一下所谓的WEB服务器,我们已经知道了:所谓的B/S结构,实际上就是客户端与服务器之间基于HTTP协议的网络通信。那么,肯定是离不开socket与io的,所以我们可以简单的模拟一个最简易功能的web服务器:

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
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by zoujing on 2017/5/10.
*/
public class MyTomcat {
public static void main(String[] args) {
try {
ServerSocket tomcat = new ServerSocket(9090);
System.out.println("服务器启动");
//
Socket s = tomcat.accept();
//
byte[] buf = new byte[1024];
InputStream in = s.getInputStream();
//
int length = in.read(buf);
String request = new String(buf,0,length);
//
System.out.println(request);
} catch (IOException e) {
e.printStackTrace();
}
}
}

demo中所需理论

       demo中我们用到了ServerSocket和Socket,这也是Java网络编程中必须知道的,分别使用与服务器端和客户端。下面我们看下区别:

serverSocket类

       创建一个ServerSocket类,同时在运行该语句的计算机的指定端口处建立一个监听服务,如:上面的ServerSocket tomcat = new ServerSocket(9090);这里指定提供监听服务的端口是9090,一台计算机可以同时提供多个服务,这些不同的服务之间通过端口号来区别,不同的端口号上提供不同的服务。

       为了随时监听可能的Client请求,执行如下的语句:Socket s = tomcat.accept();该语句调用了ServerSocket对象的accept()方法,这个方法的执行将使Server端的程序处于等待状态,程序将一直阻塞直到捕捉到一个来自Client端的请求,并返回一个用于与该Client通信的Socket对象Link-Socket。此后Server程序只要向这个Socket对象读写数据,就可以实现向远端的Client读写数据。结束监听时,关闭ServerSocket对象:如tomcat.close();

Socket类

       当Client程序需要从Server端获取信息及其他服务时,应创建一个Socket对象:Socket MySocket=new Socket(“ServerComputerName”,600);

       Socket类的构造函数有两个参数,第一个参数是欲连接到的Server计算机的主机地址,第二个参数是该Server机上提供服务的端口号。Socket对象建立成功之后,就可以在Client和Server之间建立一个连接,并通过这个连接在两个端点之间传递数据。利用Socket类的方法getOutputStream()和getInputStream()分别获得向Socket读写数据的输入/输出流,最后将从Server端读取的数据重新返还到Server端。

       当Server和Client端的通信结束时,可以调用Socket类的close()方法关闭Socket,拆除连接。ServerSocket 一般仅用于设置端口号和监听,真正进行通信的是服务器端的Socket与客户端的Socket,在ServerSocket 进行accept之后,就将主动权转让了。

总结

       服务器端程序设计:

       在服务器端,利用ServerSocket类的构造函数ServerSocket(int port)创建一个ServerSocket类的对象,port参数传递端口,这个端口就是服务器监听连接请求的端口,如果在这时出现错误将抛出IOException异常对象,否则将创建ServerSocket对象并开始准备接收连接请求。

       服务程序从调用ServerSocket的accept()方法开始,直到连接建立。在建立连接后,accept()返回一个最近创建的Socket对象,该Socket对象绑定了客户程序的IP地址或端口号。

       客户端程序设计:

       当客户程序需要与服务器程序通信时,需在客户机创建一个Socket对象。Socket类有构造函数Socket(InetAddress addr,int port)和Socket(String host,intport),两个构造函数都创建了一个基于Socket的连接服务器端流套接字的流套接字。对于第一个InetAd-dress子类对象通过addr参数获得服务器主机的IP地址,对于第二个函数host参数包被分配到InetAddress对象中,如果没有IP地址与host参数相一致,那么将抛出UnknownHostException异常对象。两个函数都通过参数port获得服务器的端口号。假设已经建立连接了,网络API将在客户端基于Socket的流套接字中捆绑客户程序的IP地址和任意一个端口号,否则两个函数都会抛出一个IOException对象。

       如果创建了一个Socket对象,那么它可通过get-InputStream()方法从服务程序获得输入流读传送来的信息,也可通过调用getOutputStream()方法获得输出流来发送消息。在读写活动完成之后,客户程序调用close()方法关闭流和流套接字。

分析demo

       在demo中,我们开启一个服务器socket来监听9090端口,所以我们在通过在浏览器中输入http://localhost:9090,进行访问,经过我们demo可以得到的输出结果如下:

1
2
3
4
5
6
7
8
9
10
服务器启动
GET / HTTP/1.1
Host: localhost:9090
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Cookie: JSESSIONID=92E7AE4F85499378342793886884D0C0; _ga=GA1.1.1073505442.1486722778
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.1 Safari/603.1.30
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Connection: keep-alive

       通过成果我们看到,我们已经成功的实现了最简易的tomcat。不过这里需要注意的是,我们自己山寨的tomcat服务器当中,之所以也成功的输出了Http协议的请求体,是因为:我们是通过web浏览器进行访问的,如果通过普通的socket进行对serversocket的连接访问,是没有这些请求信息的。因为我们前面已经说过了,web浏览器与服务器之间的通信必须遵循Http协议。

实现简易浏览器

       通过前面的学习,我相信我们现在更想把浏览器功能也实现,剖析一下,接下来我们就试试:

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
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
* Created by zoujing on 2017/5/10.
*/
public class MyBrower {
public static void main(String[] args) {
try {
Socket browser = new Socket("127.0.0.1", 9090);
PrintWriter pw = new PrintWriter(browser.getOutputStream(),true);
// 封装请求第一行
pw.println("GET/ HTTP/1.1");
// 封装请求头
pw.println("User-Agent: Java/1.8.0_131");
pw.println("Host: 127.0.0.1:9090");
pw.println("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
pw.println("Connection: keep-alive");
// 空行
pw.println();
// 封装实体主体
pw.println("UserName=JessieZou&Age=22");
// 写入完毕
browser.shutdownOutput();
// 接受服务器返回信息,
InputStream in = browser.getInputStream();
//
int length = 0;
StringBuffer request = new StringBuffer();
byte[] buf = new byte[1024];
//
while ((length = in.read(buf)) != -1) {
String line = new String(buf, 0, length);
request.append(line);
}
System.out.println(request);
//browser.close();
} catch (IOException e) {
System.out.println("亲,出现了异常哦!");
}finally{
}
}
}

       同时改进我们的tomcat如下:

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
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by zoujing on 2017/5/10.
*/
public class MyTomcat {
public static void main(String[] args) {
try {
ServerSocket tomcat = new ServerSocket(9090);
System.out.println("服务器启动");
//
Socket s = tomcat.accept();
//
byte[] buf = new byte[1024];
InputStream in = s.getInputStream();
//
int length = 0;
StringBuffer request = new StringBuffer();
while ((length = in.read(buf)) != -1) {
String line = new String(buf, 0, length);
request.append(line);
}
//
System.out.println("request:"+request);
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
pw.println("<html>");
pw.println("<head>");
pw.println("<title>LiveSession List</title>");
pw.println("</head>");
pw.println("<body>");
pw.println("<p style=\"font-weight: bold;color: red;\">welcome to MyTomcat</p>");
pw.println("</body>");
s.close();
tomcat.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

URL

       响应行和响应标头当中,实际上是负责将相关的一些有用信息返回给我们,但这部分是不需要在浏览器中所展示的。也就是说,我们的浏览器除了应当具备获取一个完整的HTTP响应的能力之外,还应该具备解析HTTP协议响应的能力。事实上,Java也为我们提供了这样的对象,那就是URL及URLConnection对象。

如果我们在我们的浏览器中,植入这样的对象,来进行与服务器之间的HTTP通信,那么:

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
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.*;
/**
* Created by zoujing on 2017/5/10.
*/
public class MyBrowser2 {
public static void main(String[] args) {
try {
URL url = new URL("http://localhost:9090");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream in = conn.getInputStream();
byte[] buf = new byte[1024];
int length = 0;
StringBuffer text = new StringBuffer();
String line = null;
while ((length = in.read(buf)) != -1) {
line = new String(buf, 0, length);
text.append(line);
}
System.out.println(text);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

       这次当我们再运行程序,查看输出信息,发现我们从URLConnection对象获取到的输入流当中,读取的响应信息,就如我们所愿的,只剩下了需要被解析显示在页面的响应实体的内容。实际上这也就是Java为我们提供的对象,将对HTTP协议内容的解析功能进行了封装。而究其根本来说,我们基本可以想象到,URLConnection = Socket + HTTP协议解析器。也就是说,该对象的底层除了通过Socket连接到WEB服务器之外,还封装了对HTTP协议内容的解析功能。

总结

       我们先启动服务器,然后运行浏览器模拟网页浏览的过程,首先看到服务器端收到的请求信息,打印出来的信息和上面类似,我们这里就是把请求报文显示出来。

       紧接着,服务器收到请求进行处理后,返回资源给浏览器,于是得到输出信息,在改进后的tomcat服务器,我们封装了html,不过是静态的。如果在搭配html解析,就可以像我们浏览器一样,能先到解析的页面。

       现在我们是单线程单连接,而且没有前段操作界面的实现,我们真正使用的tomcat和浏览器当然在多线程并发、io、网络、人性化操作等方面也做了大量工作进行效率优化和简化操作,美化界面。我们通过例子是去了解基本底层原理。

说明

       文中出现的图片,文字描述有些来自互联网,但是出处无法考究,如果侵犯您的相关权益,请联系我,核实后我会马上加上转载说明。谢谢!!!

坚持原创技术分享,您的支持将鼓励我继续创作!