这篇文章用一个简单的示例回味一下Web程序的运行原理。
在开始Web框架设计之前,有必要先讲一讲Web软件的运行原理。
Web软件和一般软件最大不同,就是Web软件是运行在一台服务器上,大家通过浏览器访问服务器来工作学习。而一般软件则是运行在用户手中的计算机设备里。
从部署角度来讲,Web软件是一处部署服务各方。而一般软件则需要一台一台的去安装部署。
照此分析,如今流行的App其实说白了也算是一般软件喽?其实从运行本质上来看是这样的。不同的是App运行在端上,这个端一般指的是Pad、手机或者智能设备。
现如今一些智能路由器也支持App了,搞不好那天我们的插座上也能跑两个App。但是无论如何一对一的部署方式和Web一次部署服务各方这种方式。是有着本质的区别的,好了言归正传回到我们的话题上来。
一个简单的Web应用程序
所谓Web开发其实就是编写一个程序运行在一台机器上,然后通过浏览器访问这个应用。下面就展示一个非常简单的Web应用程序:
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.io.Reader;import java.net.ServerSocket;import java.net.Socket;public class SocketServer { public static void main(String[] args) throws IOException { int messageID = 0; ServerSocket server = new ServerSocket(80); // 服务器端Socket对象 while (true) { try { dialog(server.accept(), messageID++); } catch (Throwable e) { System.out.println("Error. " + e); } } } public static void dialog(Socket socket, int messageID) throws Throwable { // //Request Reader reader = new InputStreamReader(socket.getInputStream()); BufferedReader ioRead = new BufferedReader(reader); String readLine = null; while ((readLine = ioRead.readLine()) != null) { System.out.println("[Message " + messageID + "] - " + readLine); if ("".equals(readLine.trim())) { break; } } // //Response String body = "Hello ..."; // PrintWriter outWriter = new PrintWriter(socket.getOutputStream()); outWriter.println("HTTP/1.1 200 OK"); outWriter.println("Content-Type:text/html; charset=utf-8"); outWriter.println("\n"); outWriter.println(body); // outWriter.close(); ioRead.close(); socket.close(); }}
运行上面这个程序,它会在你的计算机上打开“80”端口,然后等待浏览器发来的请求。
接着你只需要打开浏览器输入你自己的机器IP地址或者输入“localhost”敲一下回车就可以看到页面上打印出“Hello...”
现在我们来仔细分析一下这个Web程序都做了哪些事?
首先,我们使用JDK自带的类“ServerSocket”创建了一个网络套接字并打开了80端口进行侦听。这件事如果不是很理解,可以这样比喻一下。
假设你在一个大房子里,而这个房子有好多好多的窗户,外面的人和你沟通的唯一方式就是向窗户里仍纸条。那么你要做的就是和某个人约定好,用具体的窗户。
为什么要约定窗户呢?
因为考虑到这个房子里不一定就只有你一个人,假定另外一个人也想和外界沟通,那他也要通过窗户传小纸条。为了避免你和他争抢同一个窗户,所以大家要分开各自用各自的窗户。并且要高速和你传纸条的人,你用的是左边第一个窗户,另外一个人用的是右边第一个窗户。
结合到我们的例子里,80端口就是你在使用的“左边第一个窗户”而和你传纸条的那个同学就是浏览器。
现在我们通过“ServerSocket server = new ServerSocket(80);”这段代码打开了一扇窗准备和浏览器传小纸条。下面就是等着对方来跟你对暗号了。
当浏览器发来第一个“小纸条”时“server.accept()”方法就得到了返回值。接着我们就可以通过“server.accept()”得到的“Socket”对象在“dialog”方法中做我们想做的事情。记得在最后别忘了在写一个小纸条回递给浏览器。
与浏览器进行外交的“礼仪”
浏览器就好比是一个人,当和浏览器进行交流的时候Web程序必须要“说”浏览器能听懂的语言。所以这就需要用到“HTML”了。
但是大家不要忘记,浏览器是能听懂很多语言的。像“xml、json、png、gif、css”这些东西浏览器都能听懂。所以当我们跟浏览器交流的时候,一定要先告诉浏览器你说的是哪国语言,不然浏览器也会像人一样傻傻的呆在那里不知所云。
这就好比潜水员在水下通过手势,交流一些基本信息。
好了现在我们知道了,要想让浏览器听懂我们程序说的话,就要必须告诉浏览器Web程序说的是哪国语言,而这个信息就要通过HTTP协议来告诉浏览器。
当Web程序说“HTML”国的语言时,浏览器好做好准备,用“HTML”国家的思维方式来理解我们告诉它的内容。
这个沟通和协定说什么语言的过程有个专有名词叫做“HTTP”协议,这个就是Web程序的社交礼仪。
下面在讲一讲浏览器和Web程序之间是怎样交流的?
首先第一个地方是,交谈的方式是“一问一答”式。率先发问题的是浏览器,回答问题的是服务器。通常一次交谈,只有一个问题和一个答案。
现在回头看看我们刚刚写的Web程序,虽然很粗糙但是已经可以和浏览器进行对话了。
我们的例子程序很简单,以致于不管浏览器问什么问题回答的答案都一样,因此在下面这个代码中我们只是将浏览器的问题打印到控制台不做处理。
在代码中,浏览器当问完问题等待服务器回答时,会额外送出一个“空行 + 回车”表示发问结束等待回答。而这在我们的程序看来就是多了一个换行而已。
如下:服务器一旦读到这里就表示浏览器的问题问完了,接着程序跳出循环不在处理浏览器的问题,转而进行回答。
Reader reader = new InputStreamReader(socket.getInputStream());BufferedReader ioRead = new BufferedReader(reader);String readLine = null;while ((readLine = ioRead.readLine()) != null) { System.out.println("[Message " + messageID + "] - " + readLine); if ("".equals(readLine.trim())) { break; }}
那么我们的Web程序怎么回答的呢?
下面这段代码就是我们的服务器Web程序回答问题的代码。
首先它会先告诉浏览器你的问题我都听到了“HTTP/1.1 200 OK”。然后紧接着又告诉浏览器,接下来我说的是HTML语言,并且用了一个方言是“UTF-8”。随后浏览器沉了口气“输出一个空行”。开始说我们事先准备好的事“一段HTML代码”。
outWriter.println("HTTP/1.1 200 OK");outWriter.println("Content-Type:text/html; charset=utf-8");outWriter.println("\n");outWriter.println(body);
当服务器都说完了之后,就随手把小纸条丢出去。最后还不忘记把用过的“笔墨纸砚”都收拾了一遍。
outWriter.flush();outWriter.close();ioRead.close();socket.close();
最后小纸条回到了浏览器的手中,剩下的事就是浏览器自己去理解并展示给我们了。
Web程序和浏览器的交互原理
现在我们已经掌握了Web程序和浏览器之间的外交礼仪,那么下面我们在深入一下,让两个世界的人民做一些贸易往来吧。
我们来假设一个场景,中国打算进口一批商品。于是打开了国门准备进行贸易。
闻讯赶来的美国正好手里有一批多余的石油,想出售给中国。于是美国的商人们向中国运送了这批石油,中国收下了。并给了一个交易回执。后来欧洲的国家也过来,不过卖的却是羊毛。
假定Web程序就是上面例子中我们的国家中国,不同的浏览器分别代表美国和欧洲。下面改造一下我们的程序,让外国朋友们可以吧货物送过来。
我们吧上面代码中,下面这个代码换成另外一个
//ResponseString body = "Hello ...";
换成
//ResponseString body = "";
重新启动我们的Web程序,你会发现浏览器上出现了一个让我们填写的表单。我们只要在浏览器上输入货物的名称,点击“发货”就可以了。
浏览器会以这种方式把我们的货物 -> “表单内容”,发送给服务器。剩下的就是服务器如何接受这批货物了。
接收货物,下面我们吧Request的部分改成下面这个代码用来收货:
Reader reader = new InputStreamReader(socket.getInputStream());BufferedReader ioRead = new BufferedReader(reader);String readLine = ioRead.readLine();//String requestURI = readLine.split(" ")[1];String params = null;if (requestURI.split("\\?").length == 2) { params = requestURI.split("\\?")[1]; System.out.println("param:" + params);}//while ((readLine = ioRead.readLine()) != null) { System.out.println("[Message " + messageID + "] - " + readLine); if ("".equals(readLine.trim())) { break; }}
首先上面这段代码中,我们加入了“ioRead.readLine();”这么一行,这行代码的意义是读取浏览器送来的第一行内容。当浏览器递交表单的时候,会把表单内容放在这里。而表单的内容就是我们浏览器送来的货物。
因为这个内容是有特殊格式的,我们需要将它进行拆解,这样才能真正拿到浏览器送来的货物。这就像远洋运输一定是把货物放在集装箱里通过船舶运输过来一样。
后面的if判断和字符串拆解目的就是把集装箱里的货物拿出来。最后通过“System.out.println("param:" + params);”的方式把这个货物打印出来。完成我们例子中提到的商贸合作。
下面这个是最终的Web程序代码。
public class WebServer { public static void main(String[] args) throws IOException { int messageID = 0; ServerSocket server = new ServerSocket(80); while (true) { try { dialog(server.accept(), messageID++); } catch (Throwable e) { System.out.println("Error. " + e); } } } public static void dialog(Socket socket, int messageID) throws Throwable { // //Request Reader reader = new InputStreamReader(socket.getInputStream()); BufferedReader ioRead = new BufferedReader(reader); String readLine = ioRead.readLine(); // String requestURI = readLine.split(" ")[1]; String params = null; if (requestURI.split("\\?").length == 2) { params = requestURI.split("\\?")[1]; System.out.println("param:" + params); } // while ((readLine = ioRead.readLine()) != null) { System.out.println("[Message " + messageID + "] - " + readLine); if ("".equals(readLine.trim())) { break; } } // //Response String body = ""; // PrintWriter outWriter = new PrintWriter(socket.getOutputStream()); outWriter.println("HTTP/1.1 200 OK"); outWriter.println("Content-Type:text/html; charset=utf-8"); outWriter.println("\n"); outWriter.println(body); // outWriter.flush(); outWriter.close(); ioRead.close(); socket.close(); }}