【JAVA进化论】LV4-5:网络通信、协议、序列化、程序之间的联系

⚓️ 前排提示:

本篇第一部分的大部分内容来源:

https://www.bilibili.com/video/BV1EW411u7th?p=28

https://www.bilibili.com/video/BV1EW411u7th?p=29

https://www.bilibili.com/video/BV1EW411u7th?p=30

本篇第一部分仅做计算机网络的简单科普,旨在帮助你梳理你写的java业务程序在全局充当一个怎样的地位,另外一个目的是为了引入tomcatjava web程序以及http,常说的建连三次握手四次挥手又是指什么(如果你是计算机科班的话,对这些应该会比较熟悉,如果忘了也不要紧,这里会再重新帮你梳理一下)。看懂第一部分后,你会很容易的理解tomcat是什么,浏览器是什么,然后你甚至可以发散下思维,我们常说的RPC框架又是什么。

看懂第一部分后,第二部分的序列化以及为什么要序列化以及json是什么就更加容易理解了,之后利用SDK连接操作mysqlredis等服务端,你也可以很容易理解,看懂第一部分后,你可以尝试自学下java的套接字Socket部分的内容,当然没兴趣也可以不学,因为tomcat等web服务器程序已经帮你规避掉这部分的开发了,你要做的是写好业务代码

一、网络发展史

你在计算机上请求一个网站的数据,需要发起一个http请求,然后你传输的参数以及path信息漂洋过海跑到该网站的服务器上(request),服务器收到你的请求后,找到对应的程序,解析出来你传的参数和path,这个时候该程序内部对应path的逻辑将被触发,到此为止,你已经成功进入服务端程序的内部了,经过一系列内部逻辑的处理,服务端将处理结果再次通过http协议将数据包发送到你的电脑上(response)。这个过程在网络畅通和对方服务端程序没有瓶颈的条件下,可能在极短的时间内就会完成(以毫秒甚至纳秒计),即便真实的服务器可能离你几百公里。

这个过程里发生了什么呢?

1.1:局域网

1.1.1:以太网简介

毫无疑问,用户在全球网络中发送和接收信息的能力,永远改变了这个世界。150年前,发一封信件从伦敦到加州要花2~3周,而且还是特快邮件,如今,电子邮件只要几分之一秒,时延改变了上百万倍,振兴了全球经济,帮助现代世界在遍布全球的光纤中快速发展。你可能觉得计算机和网络密切相关,但事实上,1970年以前,大多数计算机是独立运行的,然而,因为大型计算机开始随处可见,廉价机器开始出现在书桌上,分享数据和资源渐渐变得有用起来,首个计算机网络出现了。

第一个计算机网络出现在1950~1960年代,通常在公司或研究室内部使用,为了方便信息交换,比把纸卡或磁带送到另一栋楼里更快速可靠,这叫“球鞋网络”,第二个好处是能共享物理资源,举个例子,与其每台电脑配一台打印机,大家可以共享一台联网的打印机,早期网络也会共享存储空间,因为每台电脑都配存储器太贵了,计算机近距离构成的小型网络叫做局域网,简称LAN,局域网能小到是同一个房间里的两台机器,或大到校园里的上千台机器,尽管开发和部署了很多不同的LAN技术,其中最著名和成功的是“以太网”(英文名称:Ethernet),开发于1970年代,在施乐的“帕洛阿尔托研究中心”诞生,今天仍被广泛使用。以太网的最简单形式是:一条以太网电线连接数台计算机:

图1

1.1.2:数据传输

当一台计算机要传数据给另一台计算机时,它以电信号形式,将数据传入电缆,当然,由于电缆是共享的,连在同一个网络里的其他计算机也能看得到数据:

图2

图2由A发出的数据会被BCDEF接到,正因如此,它们不知道A发出的这些数据是给它们中的哪一个的,为了解决这个问题,以太网需要每台计算机有唯一的媒体访问控制地址,简称MAC地址,这个地址放到数据的头部,作为数据的前缀发送到网络中,所以计算机只需要监听以太网电缆,只有匹配到自己的MAC地址才去处理数据,这个过程如下:

图3

这运作的很好,现在制造的每一台计算机都自带唯一MAC地址用于以太网无线网络

多台电脑共享一个传输媒介,这种方法叫做“载波侦听多路访问”,简称“CSMA”,这里的载体指的就是运输数据的共享媒介,以太网的载体是铜线,WIFI的载体是传播无线电波的空气。很多计算机同时侦听载体,所以叫“侦听”和多路访问,而载体传输数据的速度叫“带宽”。

1.1.3:冲突域

不幸的是,使用共享载体有个很大的弊端,当网络流量较小时,计算机可以等待载体清空,然后传送数据,但随着网络流量的上升,两台计算机想要同时写入数据的概率也会上升,比如A机和F机同时发出一个数据包:

图4

看,共享载体冲突了。就像两个人打电话,他们同时在电话里讲话一样。幸运的是,计算机能够监听电线中的信号检测这些冲突,最明显的解决办法是停止传输,等待网络空闲,然后再试一次,问题是,其他计算机也打算这样做,其他等待着的计算机可能在任何停顿间隙闯入,导致越来越多冲突,很快每个人都一个接一个的讲话,而且有一堆事要说,就像在家庭聚餐时和男朋友分手一样,这可不是个好主意。以太网有个超简单有效的方法,当计算机检测到冲突,就会在重传之前等待一小段时间,因为要举例,假设是一秒好了,当然,如果所有的计算机都用同样的等待时间是不行的,因为它们会在1s再次冲突,所以这个时间是随机的,这样就保证了每台机器下次重试的时间都不一样,然后大幅度降低冲突概率,这有用,但不能完全解决问题,所以要用另一个小技巧,正如我刚才说的,如果一台计算机在传输数据期间检测到冲突,会等1秒+随机时间,然而如果再次发生冲突,则表明有网络拥塞,这次不再等1s,而是2s,如果再次发生冲突,则等4s、8s、16s以此类推,直到成功传输为止。因为计算机的这种退避策略,冲突次数减少了,数据再次流动,网络再次畅通了,家庭晚餐有救了~这种指数级增长等待时间的方法叫做:指数退避。以太网和WiFi都用这种方法,很多其他传输协议也用。

但即便有了指数退避这种技巧,想用一根网线连通整个大学的计算机是不可能的,为了减少冲突+提升效率,我们需要减少同一载体中设备的数量,载体和设备统称“冲突域”(上面的例子就是一个冲突域,因为6个设备共享一个载体)

1.1.4:利用交换机降低冲突域大小

仍然以上面的例子为基础,例子中冲突域设备数量为6,我们利用交换机把它拆为两个冲突域,这样,单位冲突域内设备数量就会降低,冲突发生的概率也会大幅下降:

图5

交换机位于两个更小的网络之间,必要时才在两个网络间传数据,交换机会记录一个MAC地址列表,记录哪个MAC地址位于哪边的网络,所以,我们现在让A发送数据到C,交换机会控制不再让DEF接收到数据:

图6

如果同时E想要发送数据到F,网络就依然是空的,不会跟A→C这个网络传输冲突。那如果F想要传数据给A呢?这时仍然跟之前是一样的,整个通道都会被占用:

图7

大型的计算机网络也是这样构建的,包括大的网络:互联网,也是由多个连在一起的稍小的网络组成,使不同网络间信息互相传递。

这些大型网络比较有趣的一点是从一个地点到另一个地点通常有多条线路,这就带出了另一个话题:路由

1.1.5:路由

连接两个相隔遥远的计算机或者网络,最简单的办法就是分配一条专用的通信线路,早期电话系统就是这样运作的,比如如果在1910年的北京和上海,北京的小明想要打电话给上海的小红,而北京到上海的电话线只有5条:

图8

这叫电路交换,因为是要把电路连接到正确的目的地,能用是能用,但是不灵活,而且价格昂贵,因为总有闲置线路,好处是,如果有一条专属于自己的线路,你可以最大限度的随意使用,无需共享,因此军队、银行和其他一些机构依然会购买专用线路来连接数据中心。

传输数据的另一种方法是报文交换,报文交换就像邮政系统一样,不像之前A和B有一条专有线路,而是信息会经过好几个站点,比如从北京发一封信到上海可能会中转以下线路:

图9

每一个站点都知道下一个站点将会发往哪里,因为站点有表格,记录到各个目的地,信件该怎么传。报文交换的好处是可以使用不同的路由使信件传输更可靠更能容错,比如让图9中的济南站点断路,那么此时换另外一条传输线路仍然是ok的,虽然不是最优解,但确实解决了现在济南站点无法工作的问题:

图10

看,转郑州线路也是个不错的选择,至少你的信件最终还是顺利到达了上海。

在上面送邮件这个例子中,每个城市邮局站点就像路由器一样,消息沿着路由跳转的次数叫跳数,记录跳数很有用,因为可以分辨出路由问题。

现在假设北京传输信息到上海,假设转至郑州,郑州认为去往上海的最快线路应该是中转济南这条,但是济南认为去往上海的最佳路线应该是郑州,这就糟糕了,因为两个城市看到的目的地都是上海,它们互相认为对方是“最优解”,然后这个信息在济南和郑州之间转来转去:

图11

图中的问题不仅浪费带宽,而且这种路由错误需要被修复。这种错误会被检测到,因为跳数被记录在消息中,传输时会更新跳数,如果看到某条消息的跳数很高,就可以知道路由肯定哪里出错了,这叫做“跳数限制”。

报文交换的缺点之一是有时报文比较大,会堵塞网络,因为要把整个报文从一站转到下一站后,才能继续传递其他报文,传输一个大文件时,整条路都堵塞了,这时即便你有一个只有1kb的电子邮件要传输,也只能等大文件传完,或是选另一条效率稍低的线路,这就糟了,解决方法是将大报文分成很多小块,这些小数据块就是“数据包”,就像报文交换,每个数据包都有目标地址,因此路由器知道下一站该发往哪里,报文具体格式由“互联网协议”定义,简称IP,这个标准创建于1970年代,每台联网的计算机都需要有一个自己的IP地址,你可能见过,以点分割的4组数字,例120.92.174.135就是哔哩哔哩一个服务器的IP地址。

数百万计算机在网络上不断交换数据,瓶颈的出现和消失是毫秒级的,路由器会平衡和其他路由器之间的负载,以确保传输可以快速可靠,这叫做“阻塞控制”,有时同一个报文的不同数据包会经过不同的线路:

图12

最终上海收到了全部由北京发出的数据,但是四个数据包传过来时经历的路由路径可能不一样(图中线路颜色对应具体数据包颜色)。

但是拆包意味着有另外一个问题:到达顺序问题。这对于一些软件来说这是个大问题,比如图12里最终到达顺序变成了下面这样:

图13

这该怎么办呢?幸运的是,在IP协议之上还有其他的协议,例如TCP/IP,就可以解决乱序问题(TCP/IP下节会说)。

将数据拆分成多个小数据包,然后通过灵活的路由传递,非常高效且可容错,如今互联网就是这么运行的,这叫做”分组交换“,有个好处是,它是去中心化的,没有中心权威机构,没有单点失败问题,事实上,在冷战时期有核攻击威胁,所以创造了分组交换,如今,全球的路由器协同工作,找出高效的线路,用各种标准协议运输数据,比如因特网控制消息协议(ICMP)边界网关协议(BGP),世界上第一个分组交换网络以及现代互联网的祖先是”ARPANET“。

互联网非常快速的发展,如今不再只有几十台计算机联网,据估计,有接近100亿台联网设备,而且互联网会持续快速发展,特别是如今智能设备层出不穷,比如联网冰箱、恒温器,以及其他智能家电,它们组成了”物联网“。

1.2:广域网&高级协议

1.2.1:广域网

计算机想要从网络里获取一段信息,首先要连到局域网,也叫LAN,你家WiFi连接着的所有设备,组成了局域网,局域网再到广域网,广域网也叫WAN,WAN的路由器一般属于你的”互联网服务提供商“,简称ISP,中国这种提供商主要有:中国电信中国移动中国联通等,广域网里,先连接到一个区域性路由器,这个路由器可能覆盖了一个街区,然后再连到一个更大的WAN,可能覆盖了整个城市,可能会再跳几次,但最终到达互联网主干,互联网主干由一群超大型、带宽超高的路由器组成。

为了从一个网站获取到html信息或者接口信息,请求数据包要先到互联网主干,沿着主干到达有对应内容的目标网站服务器,请求数据包到从你的电脑跳到目标网站服务器,可能要跳个十几次,让后找到对应的程序,将程序内对应路径里目标html文件或json接口数据再按照类似的方式传送回你的电脑上.

上节讲到,大的数据会被拆分成一个个的小数据包,然后经过不同的路由线路,最终抵达目的地,数据包要想在互联网上传输就要符合”互联网协议“的标准,简称IP,就像邮快递一样,快件的打包、重量都是要符合快递公司标准的,比如每个邮包必须要有发件地址和目标地址,而且地址是独一无二的(就像中国地名中按照省/市/县/镇/村的规则,没有任何一个地名是完全重复的),并且要求邮包的大小和重量也符合标准,违反这些标准和规范,邮包将无法被邮寄,事实上,IP数据包也是如此,因为IP是一个非常底层的协议。

数据包的头部只有目标地址:

图14

仔细看上图的包结构,这意味着这个数据包抵达目标机器后,对方并不知道要把这个服务包给自己的哪个程序(是给tomcat这个http服务程序呢?还是给自己的文件处理程序呢?不知道,因为你只有一个IP头,它只能保证你的数据可以到达目标机器),因此需要在IP之上,开发更加高级的协议。

1.2.2:UDP协议

这些高级协议里最简单最常见的叫”用户数据报协议“,简称UDP,有了这个协议,我们的数据包进化成了下面这样:

图15

UDP协议头位于数据的前面,这个协议头里包含一些很重要的信息:

图16

看,我们熟悉的端口号出现了。每个想访问网络的程序都要向操作系统申请一个端口号,比如tomcat这个java开发的http程序默认端口号就是8080,对外提供http服务的nginx程序的默认端口号就是80,当一个数据包到达时,接收方的操作系统就会读取UDP头里的端口号,如果看到PORT=8080,那么就把数据包交给tomcat,如果是80,则交给nginx,看,到现在我们就可以利用这俩协议互相配合,完成一个客户端到一个服务端上某个程序的访问了。

⭐️ 总结:

  1. IP负责把数据包送到正确的计算机
  2. UDP负责把数据包送到正确的程序

我们看到图16里UDP头除了PORT,我还列出了一个叫CHECK_NUM的东西,它叫做”校验和“,用于检查数据是否正确,正如校验和这个名字所暗示的,检查方式是把数据求和来对比,以下是个简单的例子:

图17

值得注意的是,校验和只是一个16位的数(bit位),一旦累加和超过16位所能表达的最大范围,跟我们前面讲位运算时的位溢出一样,舍弃高位,保留低位。校验和用来判断数据的准确性,如果接收方收到的数据计算出的校验和跟发送方不一致,则说明数据被篡改过或由于电缆波动导致数据损坏过。不幸的是,UDP不提供数据修复或者数据重发的机制(划重点),接收到对方数据损坏后,一般只是扔掉(丢包),而且UDP无法得知数据包是否到达,发送方发送数据后无法感知对方是否已经接收到,这些特征听起来很糟糕,但有些程序其实并不在意这些问题,因为UDP又简单又快,对于那些有着高传输性能低完整性的系统(比如我们的日志系统和链路追踪系统),使用UDP协议开发服务端是个不错的选择,但是针对业务服务,对数据有着高精准性,不允许丢包的发生,对数据完整性要求极高,则不建议使用UDP,比如java开发的tomcat http服务程序,它就不是基于UDP协议完成通信传输的。

如果需要保证数据的完整性,即:所有数据必须到达,那么就可以使用”传输控制协议“,简称TCP

1.2.3:TCP/IP协议

TCP和UDP一样,头部也在存数据前面,就像UDP一样,TCP头部也有”端口号“和”校验和“,但TCP有更高级的功能,我们这里只介绍重要的几个:

  1. TCP数据包有序号,15号后是16号,16号后是17号,以此类推,发上百万个数据包也是有可能的(事实上,TCP在建连时会确定初始序列号,被称作Initial Sequence Number,简称ISN,这个序号是根据系统时间来的,下面建连三次握手会讲到),还记得上面图13里的拆包后的到达顺序错乱的问题吗?序号可以使接收方把数据包排列成正确的顺序,即使到达顺序不同,TCP协议也能把顺序排对。
  2. TCP要求接收方的电脑收到数据包并且”校验和“检查无误后(数据没有损坏)给发送方发一个确认码,代表收到了,确认码简称ACK,得知上一个数据包成功抵达后,发送方会发下一个数据包:

图18

数据包2在第一次发送时可能由于网络延迟,可能中途发送失败,这不重要,重要的是发送方确实没有收到接收方的ACK消息,此时发送发会重试。注意,这样有可能发送重复的数据包,因为没有收到ACK并不意味着接收方100%没有收到数据包2,好在我们有TCP序列号,如果收到重复序列号的数据包就会删掉,只保留一个。
图18里看到的是一个个的数据包发送,但事实上,TCP是支持多个数据包同时发送的然后收取多个ACK码的,这大大增加了效率,不用浪费时间等待确认码:

图19

有意思的是,确认码的成功率和来回时间可以推测网络的拥堵程度,TCP利用这个信息,调整同时发包的数量,解决拥堵问题(可以了解下TCP的窗口滑动协议),简单来说,TCP可以处理乱序和丢失数据包问题,丢了就重发,此外还可以根据拥堵情况自动调整传输率,相当厉害。你可能会奇怪,既然TCP那么厉害,为什么还有人用UDP呢?TCP最大的缺点是那些ACK确认码,交互的数据包的数量翻了一倍,但并没有传输更多信息,有时候这种代价是不值得的,特别是对时间要求很高的程序,比如前面提到的日志系统链路追踪系统,它们可能对数据完整性要求没有业务系统那么高,但是传输性能一定要保证高速,这时候就特别适合UDP传输协议。
图18图19都是在TCP建连后的数据传输阶段,在这之前,你要想在TCP/IP上传输数据首先要做的就是建连,建连需要三次握手,如果数据传输完毕,数据发送方不会再和数据接收方有任何交互,这是其中一方会断开这个连接,这时又需要通过四次挥手来断开连接:三次握手、四次挥手

1.2.4:DNS服务

这个大家应该很熟悉了,简单来说就是互联网的一个特殊服务,负责把域名和某个IP地址对应起来,我们并不希望在访问网站时输入一大串的数字,往往我们希望每个网站都有个自己的名字,毕竟bilibili.com记起来可比什么xxx.xxxx.xxx.xx容易多了。DNS就是做这个的,它可以将你记下来的域名解析出来它对应的IP地址,一般DNS服务器是互联网供应商提供的,DNS会查表,如果域名存在,就返回IP地址。你在浏览器输入bilibili.com就会解析到具体的一个IP地址,然后浏览器会给这个IP地址发请求,每个请求的数据传输都会基于一个或多个TCP连接完成,上层解析数据的工作则交给HTTP这个应用层协议去做。

1.2.5:网络协议分层

物理层:之前说过的电缆、无线电信号这些属于物理层

数据链路层:负责操控物理层,数据链路层有包含媒体访问控制地址(MAC地址),碰撞检测(之前说过的传输冲突),指数退避以及一些其他的底层协议,在往上一层是网络层。

网络层:负责各种报文交换和路由

传输层:上面刚讲的TCP和UDP都属于传输层协议,具体来说传输层负责在计算机之间进行点对点的传输(port to port),而且还会检测和修复错误。

会话层:利用TCP/UDP来创建连接,传递信息,然后关掉连接,这一整套叫”会话

表示层&应用程序层:HTML解码、浏览器等。

让我们来看看详细的分层结构图,目前有两种分层方式,我列举出一些具体的实现协议:

图20

这个概念性框架,把网络通信划分成多层,每一层处理各自的问题,如果不分层,直接从上到下捏在一起实现网络通信,是完全不可能的,看,这个过程也是抽象&分层,跟java里的抽象分层一样,软件分层也是同样的道理,通过抽象和分层让科学家和工程师们能各自分工同时改进多个层,不被整体复杂度难倒。

但我们目前会以TCP/IP协议为中心分的粒度更加粗,只有四层

应用层:最接近我们软件开发的一层,需要软件工程师亲自设计实现的一层,我们常见的有HTTP、HTTP2、FTP。

传输层:利用应用层协议打包好的数据,通过传输层传输,就跟我们前面解析TCP和UDP工作流程时是一样的,它们只用来发送数据包。

网络层:负责将数据包运送至互联网,传输到目标机器。

网际接口层:包含了数据链路层和物理层。

1.3:万维网

1.3.1:万维网简介

万维网不是互联网!万维网不是互联网!万维网不是互联网!尽管人们经常混用这两个词,万维网在互联网之上运行,互联网之上还有A站B站、知乎微博、王者荣耀等,互联网是传递数据的管道,各种程序都会用,其中传输最多数据的程序是万维网,分布在全球上百万个服务器上,你可以用浏览器访问万维网,本节会详细介绍万维网。

1.3.2:超链接

万维网的最基本单位,是单个页面,页面有内容,也有去往其他页面的链接,这些链接叫做”超链接“,大家都见过:可以点击的文字或图片,点击后把你送往另一个页面,这些超链接行成巨大的互联网络,这就是万维网名字的由来,现在说起来觉得很简单,但在超链接做出来之前,计算机上每次想看另一个信息时,你需要在文件系统中找到它,或是把地址输入搜索框,但有了超链接,你可以在相关主题间轻松切换,比如这样:(对,点一下我,你就可以离开我去百度了)。

超链接的价值早在1945年就被Vannevar Bush意识到了,他解释道:将两样东西联系到一起的过程十分重要,在任何时候,当其中一件东西进入视线,只需要点一下按钮,立马就能回忆起另一件,1945年的时候计算机连个显示屏都没有,所以这个想法在当时是非常超前的,因为文字超链接是如此的强大,它得到了一个同样厉害的名字:超文本

如今超文本最常指向的,是另一个网页,然后网页由浏览器渲染(下面会详细讲),为了使网页能相互连接,每个网页需要一个唯一的地址,这个地址叫”统一资源定位器“,简称URL(这东西很熟悉吧)。

1.3.3:定位一个超文本页面

结合我们前面介绍的,我们来看看浏览器如何定位到一个超文本信息:

假如你要访问:https://www.bilibili.com/anime

图21

这个时候会再次帮你巩固下TCP/IP的作用,我们常说的”建连“其实就是指TCP/IP连接的建立,没有TCP/IP连接,你的数据就无法和服务端进行准确的点对点的交互,而负责把你数据输送到对端又是通过基于IP协议的路由功能完成的,看,又串起来了。每一层网络协议都在我们上网时承载着不同的作用。

⚠️ 注意:图里虽然管TCP叫长连接,因为TCP在大部分应用层协议下都是不断开的,但是!但是!但是!由于HTTP协议的实现,TCP会很快断掉,你知道吗?TCP每次建连需要三次握手,下次访问时发现TCP已断开,访问前就会再次建连经历一遍三次握手,简单来说就是影响性能,这也是HTTP协议的一个弊端,所以后来HTTP协议通过keep-alive的方式进行优化,让长连接在断开前尽可能多的被复用。

依照这个流程,我们重点讲一下位于最上层的协议:HTTP超文本传输协议

1.3.4:HTML、HTTP协议

首先,它是一个应用层协议,位于网络通信的最上层,下层仍然通过TCP/IP协议完成网络传输,那么HTTP是什么样的一种协议呢?它在通信过程里起到的作用是什么呢?

HTTP的第一个标准,HTTP0.9,创建于1991年,当时只有一个指令:GET,幸运的是,这对当时来说也够用,如图21,因为我们想要的是b站的/anime页面,所以我们向服务器发送指令:GET /anime,该指令以ASCII码发送至Web服务器,服务器会返回该地址对应的网页,然后浏览器将其渲染到屏幕上,此时在该页面用户再次点了另一个链接,计算机会重新发送一个GET请求,你在浏览网站时,这个过程会不断重复。在之后的版本,HTTP添加了状态码,状态码放在服务端返回消息体的前面,举例,状态码200代表”网页找到了,给你“,状态码400~499代表客户端出错,比如网页不存在,也就是可怕的404错误(-_-||)。

超文本的存储和发送都是以普通文本形式进行,比如,编码有可能是ASCII码或者UTF-16,因为如果只有纯文本,就无法表明什么是链接,什么不是链接,所以有必要开发一种标记方法,于是超文本标记语言HTML就这样诞生了(看,我们甚至讲到了前端),HTML第一版的版本号是0.8,创建于1990年,有18种HTML指令,仅此而已,我们来做一个网页吧!

代码块1
1
2
<h1>这是标题</h1>
这是内容,<a href="http://www.bilibili.com">这是超链接</a>

h1就是一个HTML指令,它可以告诉浏览器,被它包裹的内容的字体需要变成1号标题大小,而a指令可以令被包裹的内容跳到其他页面。

这样,我们的HTML页面就完成了,把它保存到一个记事本里面,然后把记事本的后缀改成.html,它就可以运行在浏览器上了,说白了,你请求一个网站也是类似的道理,只不过需要走网络,将目标网站对应的HTML页面拿到。

最新版的HTML、HTML5,有100多种标签指令,图片标签、表格标签、表单标签、按钮标签等等,还有其他相关技术就不说了,比如层叠样式表(CSS)和JavaScript,这俩可以加进网页,嵌入进你的HTML代码里,做一些更加厉害的事情。

让我们回到浏览器,网页浏览器可以和网页服务器(HTTP服务器)通信,浏览器不仅获取网页和媒体,获取后还负责显示。

1.3.5:浏览器、web服务器的发展史

浏览器:基于HTTP协议完成和web服务器的网络通信,并将服务端返回的超文本内容进行渲染展示。

web服务器:支持HTTP协议的一个服务端程序,对,它只是一个服务器上运行的一个拥有端口号的程序而已,就像前面讲的网络通信一样,因为它支持HTTP协议的解析和封装传输,因此浏览器可以和它进行无障碍通信,这个流程简单画一下就是下面这样:

图22

上面省去了DNS这一步,对浏览器和web服务器程序通信的整个逻辑流程,仔细分析下,为什么web server会知道浏览器想要访问哪个文件呢?以及浏览器如何知道服务端的响应结果的?万一服务端找不到该咋办?这就是HTTP协议起到的作用,按照HTTP协议打包的数据,会告诉服务端当前请求资源的方式(GETPOST),还会告诉服务端请求的路径和参数,根据路径和参数找到对应的文件,然后包装成HTTP响应数据,返回给浏览器,如果没找到对应的路径,则返回404错误码,这时浏览器就知道了服务端找不到它要请求的界面。

看到这里,知道什么是应用层协议了吧?所谓应用层协议,其实就是软件内部处理对端数据时约定好的一种格式,便于符合同一种通信协议的两种软件相互通信(比如浏览器和web服务器程序都属于某种软件,或称应用,它们为什么可以顺利处理彼此通过TCP/IP传来的数据呢?因为它们都支持HTTP这种应用层协议,解析规则一致,所以它们都可以理解对方,比喻成人的话,就是他们俩虽然不是同一类人,但是他们俩是一个国家的人,能够听懂彼此的语言,而语言相对于人类来讲,也是一种”协议“性的东西)。

第一个浏览器和服务器,是Tim Berners-Lee在1990年写的,一共花了2个月,那时候他在瑞士的”欧洲核子研究所“工作,为了做出来,他同时建立了几个最基本的网络标准:URL(概念)HTML(语言)HTTP(协议),两个月。。就。。过于优秀!不过公平点说,他研究超文本系统已经有十几年了,和同事在CERN内部使用了一阵子后,在1991年发布了出去,万维网就此诞生!重要的是,万维网有开放标准,大家都可以开发新的服务器和新的浏览器,因此”伊利诺伊大学香槟分校“的一个小组,在1993年做了Mosaic浏览器,它是第一个可以在文字旁边显示图片的浏览器,之前的浏览器要单开一个页面专门展示图片,还引进了书签等新功能,界面友好,使它一度很受欢迎,它长这样:

图23

尽管以现在的眼光看上去很呆板,但和如今的浏览器长得也差不多。

1990年代末有许多浏览器面世:IEOperaOmniWebMozilla,当然也有很多基于HTTP协议的web服务器面世,比如Apache(重点)和微软互联网信息服务(IIS)。

基于这些成果,每天都有新网站冒出来,如今的网络巨头,比如亚马逊eBay,创建于1990年代中期,而在中国,1998年诞生了腾讯,1999年诞生了阿里巴巴,那是个黄金时代!

1.3.6:第一个爬虫程序&第一个搜索引擎

随着万维网日益繁荣,人们越来越需要搜索,如果你知道网站地址,比如bilibili.com,直接输入浏览器就行,但如果不知道地址呢?比如想找可爱猫咪的图片,现在就要!去哪里找呢?起初人们会维护一个目录,链接到其它网站,其中最有名的叫”Jerry和David的万维网指南“,1994年改名为Yahoo(没错,就是早期的雅虎搜索),随着网络越来越大,人工编辑的目录变得不便利,所以开发了搜索引擎。

长得最像现代搜索引擎的最早搜索引擎,名叫JumpStation,由[Jonathan Fletcher](https://baike.baidu.com/item/Jonathan Fletcher)于1993年在斯特林大学创建,它有3个部分:

  1. 第一个是爬虫,一个跟着链接到处跑的软件,每到看到新链接,就加进自己的列表里
  2. 第二部分是不断扩张的索引,记录访问过的网页上,出现过哪些词
  3. 最后一部分,是查询索引的搜索算法,比如我输入”猫“,那么每个有”猫“这个词的网页都会出现

早期搜索引擎的排名方式非常简单,取决于搜索词在页面上的出现次数,刚开始还行,直到有人开始钻空子,比如在页面上写几百个”猫“,把人们吸引过来,谷歌的成名的很大一个原因是创造了一个聪明的算法来规避这个问题。与其信任网页上的内容,搜索引擎会看其他网站,有没有链接到这个网站,如果只是写满”猫“的垃圾网站,没人会愿意指向它,如果有关于”猫“的有用内容,有网站会指向它,所以这些”反向链接“的数量,特别是有信誉的网站会特别多,反向链接的数量代表了网站的质量。

Google一开始时是1996年斯坦福大学一个叫BackRub的研究项目,两年后分离出来,演变成如今的谷歌

1.4:我自己的个人电脑联通网络后可以被别的电脑通过ip+port访问到吗?

我们的常识告诉我们,不行!

为什么呢?前面说到了局域网,或者说我们管它叫内网,我们的个人电脑只是接在了局域网里。一个公司里的电脑,一个学校里的电脑,都可以组成一个局域网,而一个局域网内的计算机,都会有个内网IP,它们通过NAT(网络地址转换)实现内网的IP地址与公网的IP地址之间的相互转换,将大量的内网IP地址转换为一个或少量的公网IP地址,减少对公网IP地址的占用,NAT的最典型的应用是:在一个局域网内,只需要一台计算机连接上Internet,就可以利用NAT共享Internet连接,使局域网内其他计算机也可以上网。使用NAT协议,局域网内的计算机可以访问Internet上的计算机,但Internet上的计算机无法访问局域网内的计算机,这就是你连上局域网的电脑为什么不可以被别人访问的原因。

那么如何让自己电脑上的某个程序让别人可以访问到呢?需要配置部署一个公网IP地址,公网IP是指以公网连接Internet上的非保留地址。

公网、内网是两种Internet的接入方式,公网的计算机和Internet上的其他计算机是可以随意互相访问的。

⚜️ 总结:

  1. 通过公网部署的计算机可以访问互联网里的任意计算机。
  2. 仅链接进局域网的计算机可以被公网计算机连接并访问,但是无法被别的计算机主动连接访问。

所以如果你想要建设一个个人站,首先要找到一个接入公网的计算机当做服务器,否则别人是无法访问你的,这种计算机在哪里买到呢?当然是阿里云啊~(不是广告,腾讯云也提供这种服务,这些服务按照职能不同还分了很多种类,相比阿里云这种提供全方位服务器的商户,七牛云和又拍云就只提供云存储和CDN服务)

如果你在阿里云购买了一台服务器,他会给你两个ip,一个是公网ip,一个是内网ip

阿里云控制台

你如果也注册了域名(在中国需要给域名完成备案,相当于给你的域名搞个“身份证”),就可以将公网ip绑定你的域名让别人访问了~

1.5:结束语

第一部分的内容到这里就结束了,最后我想讲一个词,你最近可能经常听到,网络中立性(Net Neutrality),现在的你对数据包路由万维网应该有了个大体的认识,足够你理解这个争论的核心点,至少从技术角度上来说。

简单说网络中立性是应该平等对待所有数据包,不论这个数据包是我的邮件,或者是你正在看的视频,速度和优先级应该是一样的,但很多公司会乐意让他们的数据优先到达,拿Comcast举例,他们不仅是大型互联网服务提供商,而且拥有很多家电视频道,比如NBC、The Weather Channel等,可以在线观看,我不是特意要找Comcast麻烦,但要是没有网络中立性,Comcast可以让自己的内容优先到达,节流其他的线上视频(节流是指故意给更少带宽和更低的优先级)。

支持网络中立性的人说,没有中立性后,服务商可以推出提速的”高级套餐“,给剥削性商业模式埋下种子,互联网服务供应商称为信息的”守门人“,他们有着强烈的动机去碾压对手,另外,网飞和谷歌这样的大公司可以花钱买特权,而小公司,比如刚成立的创业公司将会处于劣势,阻止了创新。

另一方面,从技术原因看,也许你会希望不同的数据传输速度不同,你希望b站看视频优先级更高,而企业微信发的邮件慢几秒没关系,而反对网络中立性的人则认为,市场竞争会阻碍不良行为,如果供应商把客户喜欢的网站降速,客户会选择离开供应商,这场争辩还会持续很久,你应该学会去独立思考,主动获取一些信息之后再发表自己的看法,因为网络中立性十分复杂和广泛。

二、序列化

2.1:序列化是做什么的?

回到我们之前讲过的TCP/IP传递的数据包的基本结构,它长下面这样:

图24

抛去带领我们数据路由的IP协议头和找到具体程序的TCP协议头,剩下的部分就是我们所要传输的具体数据流,我们假设现在浏览器发起一个请求,为了方便表述,这个请求只发一个数据包到我们的web server(端口号假设为80)程序上,它的过程是这样的:

图25

前面说过,浏览器和服务器都是支持解析HTTP协议的(这里是不是更加理解为什么HTTP是应用层协议了呢?你看看上图里HTTP的解析,都是发生在实际应用里解析的),但是实际参与两个应用做业务处理的这些业务数据才是我们最终最终需要关心的数据,因为它们关系着我们的业务代码的走向,那么业务DATA也是个二进制字节流啊,那可怎么办?这意味着浏览器和服务端都无法解析出最终的业务数据应该是什么,举个我们常用的例子(为了方便描述问题,我们省去了应用层以下的流程):

图26

我们抽象了两种常用的HTTP请求方式,一种是获取信息的GET请求,一种是提交表单信息的POST请求,但是你会发现,GET在根据id查到season信息后,需要返回给浏览器,而浏览器也必须得识别后端程序发过来这个字节流的数据结构,才能进一步做业务处理,同样的,POST请求这个问题则表现得更加彻底,首先浏览器表单填写完成后,传的参数也是一个结构体,服务端处理完成后也会返回一个应答结构体供浏览器做自己的业务处理。

好的,这个过程我们梳理了下,回想下,浏览器里负责发请求以及解析后端返回数据的语言叫啥?嗯,就是JavaScript,而我们前面说过,web server又是由别的语言来实现的,比如tomcat,它是由java开发的,那现在问题简化多了:

图27

上图的问题,被我们用json解决了,这就是json的意义,json是一个跨语言的序列化方式,js本身就支持json编解码,java也有自己的json编解码工具:fastjsongson等等,这样,JS可以解析JAVA传过来的字节流,JAVA也可以解析JS传来的字节流。

图28

根据上图,简单理解就是:

  • 序列化就是以某种算法方式让自己的数据结构对象变成一个二进制字节流
  • 反序列化就是以某种算法方式就是让一个以同样算法编码的字节流变成自己的一个数据结构对象
  • 说人话就是,java可以让浏览器传来的字节流变成自己的一个类的对象,类就是java的数据结构,而js也可以通过ajax获取到的服务端字节流变成自己可识别的对象结构。

这种算法就是我们常说的序列化算法,目前有java自己的序列化算法、json、protobuf等。

发散下思维,如果图27里浏览器通信的也是JAVA代码写的程序而不是JS,会不会也有序列化问题?当然有啦,说到底为什么要序列化反序列化,还不是因为网络传输只能是二进制字节流,只要跨进程了,这个问题就固然存在,我们刚刚列举了一些序列化方式,json做到了跨语言,比如js支持,java也支持,go也支持,php也支持,所以json很厉害,而java也有自己的序列化方式,这种方式解码程序仅适用于java程序自己,它做不到让js也理解。

2.2:JAVA是如何做序列化的?

通过上一节的理解,序列化就是让自己变成一串二进制字节流,反序列化就是让一串二进制字节流变成一个java对象,那我们现在用java的传统艺能来做下这个事情,还记得之前画的java io家族继承图吗?java用来序列化的内容也属于这一块的内容,它是由这俩类来完成的:

图29

我们不能因此做一个网络传输,太费劲了,我们可以利用文件io来做这个事情(本质上没什么不同,读网络传进来的io字节流跟读文件里面的字节流效果是一致的,只不过一个是从网关接收数据,一个是从文件接收数据),我们定义一个类:

代码块2
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
public class Season implements Serializable { //通过实现Serializable接口,来标记该对象是允许被序列化的

private int season_id;

private String title;

private transient String index; //被transient修饰的字段不参与序列化和反序列化

public int getSeason_id() {
return season_id;
}

public void setSeason_id(int season_id) {
this.season_id = season_id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getIndex() {
return index;
}

public void setIndex(String index) {
this.index = index;
}
}
代码块3

public class Test {

public static void main(String[] args) throws Exception {
//new一个Season对象
Season season = new Season();
season.setSeason_id(1);
season.setTitle("寒蝉鸣泣之时");
season.setIndex("索引1");
serializable(season); //把它序列化到一个文件里去

Season season2 = deSerializable(); //从文件读取被序列化好的season对象的字节流,然后反序列化成一个新的对象
System.out.println("season_id = " + season2.getSeason_id() + " title = " + season2.getTitle() + " index = " + season2.getIndex());
System.out.println(season == season2); //这是俩不同的对象,结果为false
}

//序列化
public static void serializable(Season season) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("/Users/sunqinwen/Documents/season.txt")));
//我们把这个对象写进对应的文件
oos.writeObject(season);
System.out.println("对象序列化成功!");
oos.close();
}

//反序列化
public static Season deSerializable() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("/Users/sunqinwen/Documents/season.txt")));
//我们把文件里的字节流读取进来进行反序列化
Season result = (Season) ois.readObject();
return result;
}

}

运行结果:

代码块4
1
2
3
对象序列化成功!
season_id = 1 title = 寒蝉鸣泣之时 index = null
false

可以看到,被transient修饰的index字段并没有参与序列化和反序列化,现在让我们看下season.txt这个文件里的内容:

图30

你没有必要读懂这段内容,你只需要知道,这块内容是java按照某种规则生成的内容,这段内容被以字节流的方式读进一个java程序里,再通过java反序列化类进行解码,它就可以得到一个你想要的对象。

2.3:FastJson与json序列化

我们在2.1里讲过json是一个跨语言的序列化方式,它有着人可以读懂的字符串结构,将这些符合json结构的字符直接录入文件,然后让java读取出来,通过java的fastjson工具包,就可以将这个json字节流变成你java程序内对应的数据结构(类)的对象,fastjson是一个第三方库,使用前需要在你的gradle文件里引入它:

代码块5
1
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.67'     //在你gradle文件的dependencies下加入这段代码,gradle就会帮你自动依赖进来它的jar包

然后我们将上面的例子来利用fastjson改编下:

代码块6
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
public class Test {

public static void main(String[] args) throws Exception {
//new一个Season对象
Season season = new Season();
season.setSeason_id(1);
season.setTitle("寒蝉鸣泣之时");
season.setIndex("索引1");
serializable(season); //把它序列化到一个文件里去

Season season2 = deSerializable(); //从文件读取被序列化好的season对象的字节流,然后反序列化成一个新的对象
System.out.println("season_id = " + season2.getSeason_id() + " title = " + season2.getTitle() + " index = " + season2.getIndex());
System.out.println(season == season2); //这是俩不同的对象,结果为false
}

//json序列化,我们利用fastjson将season对象序列化到文件里
public static void serializable(Season season) throws Exception {
byte[] seasonByte = JSONObject.toJSONBytes(season);
Path path = Paths.get("/Users", "sunqinwen", "Documents", "seasonJson.txt");
if (!Files.exists(path)) { //若文件不存在
Files.createFile(path); //创建文件
}
Files.write(path, seasonByte); //将序列化好的字节流存入文件
}

//json反序列化,我们利用fastjson将文件里的字节流重新转换成Season对象
public static Season deSerializable() throws Exception {
Path path = Paths.get("/Users", "sunqinwen", "Documents", "seasonJson.txt");
byte[] seasonByte = Files.readAllBytes(path); //读取出来所有的字节
return JSONObject.parseObject(seasonByte, Season.class); //反序列化成对应类型的对象
}

}

运行结果:

代码块7
1
2
season_id = 1    title = 寒蝉鸣泣之时     index = null
false

让我们来看下seasonJson.txt:

图31

我们终于可以看懂了,这就是json有魅力的地方,它实际的格式就是便于人类阅读的,实际上实现了json序列化的工具也是按照这种格式来解析这些json串的,json就不过多介绍了,实际上在测试当中,你已经见过太多复杂的结构体了。

但是,json串如何和我们的java对象映射起来呢?我们来写一个复杂一点的java类:

代码块8
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
public class Student {

private int id;
private String name;
private int age;
private int[] nos;
private List<Season> seasons;
private Map<String, Season> season_map;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public int[] getNos() {
return nos;
}

public void setNos(int[] nos) {
this.nos = nos;
}

public List<Season> getSeasons() {
return seasons;
}

public void setSeasons(List<Season> seasons) {
this.seasons = seasons;
}

public Map<String, Season> getSeason_map() {
return season_map;
}

public void setSeason_map(Map<String, Season> season_map) {
this.season_map = season_map;
}
}

然后我们来给它的一个对象赋值后再转成json串:

代码块9
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
public static void main(String[] args) {
Student student = new Student();
student.setId(1);
student.setName("蛤蛤");
student.setAge(1000000);
student.setNos(new int[]{1, 2, 3, 4, 5});

Map<String, Season> seasonMap = new HashMap<>();
Season season1 = new Season();
season1.setSeason_id(1);
season1.setTitle("寒蝉鸣泣之时 第一季");

Season season2 = new Season();
season2.setSeason_id(2);
season2.setTitle("寒蝉鸣泣之时 第二季");

seasonMap.put("第1季", season1);
seasonMap.put("第2季", season2);
student.setSeason_map(seasonMap);

List<Season> seasons = new ArrayList<>();
Season season3 = new Season();
season3.setSeason_id(3);
season3.setTitle("火影忍者 疾风传");

Season season4 = new Season();
season4.setSeason_id(4);
season4.setTitle("少女终末旅行");

seasons.add(season3);
seasons.add(season4);

student.setSeasons(seasons);

System.out.println(JSON.toJSONString(student)); //利用fastjson这个JSON类的静态方法toJSONString,可以把一个对象转成json串
}

运行结果,我们格式化一下,是下面的字符串:

代码块10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"age": 1000000,
"id": 1,
"name": "蛤蛤",
"nos": [1, 2, 3, 4, 5],
"season_map": {
"第1季": {
"season_id": 1,
"title": "寒蝉鸣泣之时 第一季"
},
"第2季": {
"season_id": 2,
"title": "寒蝉鸣泣之时 第二季"
}
},
"seasons": [{
"season_id": 3,
"title": "火影忍者 疾风传"
}, {
"season_id": 4,
"title": "少女终末旅行"
}]
}

现在,仔细观察这个json串的每一项,给我们的Student类里的属性一一对比,你就会发现它们之间的映射关系。

三、Tomcat

终于到了最令人兴奋的时刻了,之前了解了那么多网络知识,是我们写的业务代码发挥它意义的时刻了~

3.1:Tomcat简介

用维基百科里的解释:

Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持,并提供了作为Web服务器的一些特有功能,如Tomcat管理和控制平台、安全局管理和Tomcat阀等。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。

说人话就是:Tomcat是基于java语言实现的一个基于HTTP协议的web服务器程序,你写的业务代码可以放到这个服务程序下面,然后你的业务代码就可以被别人通过http://xxx.xxx.xxx/path的方式调用到,触发到了!而这节课所讲述的HTTP协议编解码、json序列化你完全不需要关心,tomcat已经支持了,你现在要做的是,编写自己的业务代码,把它们放进去,放到tomcat的身体里就好了!很开心吧?作为程序员的幸福感油然而生,正是因为前辈们不断的铺路,我们不用自己开发一个支持http协议的服务器程序,因为前辈们提供了Spring,我们不用自己深入理解IOC和AOP思想,因为前辈们提供了Mybatis,我们不用苦恼通过传统JDBC读出来的数据无法直接转成java对象。这里提到的Spring和Mybatis又是切实参与你业务代码编写的类库集,被我们称为”框架“,有了它们,我们写起来企业级业务代码会非常的开心,这些后面会通过一个程序带大家慢慢了解,正是因为前人不断的努力,我们其实已经很省力了,喜欢折腾的程序员还会重复造轮子,试图撼动这些传统框架的地位(个人观点:喜欢创新是好事,但要找对点,没有技术革新的造轮子唯一的意义就是帮助你更深理解前辈们的思想)。

3.2:Servlet

你以为你写的业务代码就那么容易放进Tomcat的身体里吗?不存在的,上面说了,Tomcat是一个Servlet容器,什么意思?就是说你写的代码必须要符合Servlet规范,至少你的程序入口得符合Servlet,同样的,我们摘一段维基百科里对它的描述:

Servlet(Server Applet),全称Java Servlet,未有中文译文。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。

还记得我们前面讲过的类的继承多态以及接口吗?你可以这样理解,你的业务代码类得是Servlet的实现类,并且实现了它的一些方法后才可以被Tomcat识别并进入,这就是java多态的好处之一,你只要符合某种规范,就可以被别的符合同样规范的程序(本例指tomcat)所识别,你重写了里面的方法,别的程序会触发被你重写的方法,看!你的业务代码就这样被触发了!

3.3:基于Servlet实现一个业务接口

前面说了一大堆废话,太虚了,我们来新建一个gradle项目,编写一个返回一个对象json结构体的例子(其实沃特玛自己都忘了咋写了,回头学会用Spring Boot写一个同样的接口,你会唾弃现在这种干法的)。

3.3.1:下一个tomcat先

tomcat下载地址:https://tomcat.apache.org/download-80.cgi

图32

找到属于你自己平台的那个即可,tomcat是不需要特别安装的,直接解压就能用。

它的目录长这样:

图33

圈出来的是最重要的几个包,bin存放的是tomcat的核心功能,conf下放的是tomcat本身的配置,webapps就是存放你业务代码的地方了~当然正常开发时,你是不需要手动将你编译好的业务代码放进去的,IDEA已经帮你做到了,你需要做的是给IDEA指定一个tomcat路径,让IDEA知道你的tomcat程序在哪里,这样你的web程序在启动时,IDEA就可以帮你把它编译好的代码放到webapps下面了。

3.3.2:添加依赖包

我们在构建好的gradle项目里的build.gradle的dependencies里引入以下配置,引入servlet的jar包和fastjson的jar包:

代码块11
1
2
compileOnly (group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1')
compile (group: 'com.alibaba', name: 'fastjson', version: '1.2.67')

这样,你就可以使用servlet的包编写你的业务程序,之前讲过,通信时以字节做交互的话需要序列化,因为我们是一个输出信息的接口,所以我们选择使用json方式完成序列化,所以我们还需要引入一个json序列化工具fastjson用来做数据的序列化。

3.3.3:编写业务代码

我们这个接口的目标是,将一个复杂的java对象,传输给浏览器,我们仍然使用之前那个复杂的Student做,只不过我们要利用分层思想来做:

图34

还记得我们之前在做一个简单的增删改查时那个例子里的业务分层模式吗?这里也是一样的,不过我们没有做service层,因为不需要,我们只是要将一个对象输送出去,没有任何特别的业务逻辑操作。

我们来看下Dao层的代码:

代码块12
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
public class StudentDao {

//之前例子里给Student赋值的main方法里的逻辑,被抽象进了Dao层的一个方法里
public Student getStudentInfo() {
Student student = new Student();
student.setId(1);
student.setName("蛤蛤");
student.setAge(1000000);
student.setNos(new int[]{1, 2, 3, 4, 5});

Map<String, Season> seasonMap = new HashMap<>();
Season season1 = new Season();
season1.setSeason_id(1);
season1.setTitle("寒蝉鸣泣之时 第一季");

Season season2 = new Season();
season2.setSeason_id(2);
season2.setTitle("寒蝉鸣泣之时 第二季");

seasonMap.put("第1季", season1);
seasonMap.put("第2季", season2);
student.setSeason_map(seasonMap);

List<Season> seasons = new ArrayList<>();
Season season3 = new Season();
season3.setSeason_id(3);
season3.setTitle("火影忍者 疾风传");

Season season4 = new Season();
season4.setSeason_id(4);
season4.setTitle("少女终末旅行");

seasons.add(season3);
seasons.add(season4);

student.setSeasons(seasons);

return student;
}
}

ok,截止目前,我们的程序已经写好了,但是,tomcat是如何知道这个Servlet类所在的地方呢?因此我们还需要给这个项目加一个叫web.xml的配置文件,它的位置在:

图35

需要注意的是,WEB-INF是没办法省略的,否则tomcat无法读取到这个配置文件,下面我们来看看这个文件里都放了什么东西吧:

代码块14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet> <!--指定一个Servlet块,并且告诉tomcat该Servlet的路径-->
<servlet-name>StudentInfoController</servlet-name>
<servlet-class>com.bilibili.controller.StudentInfoController</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>StudentInfoController</servlet-name>
<url-pattern>/get/student/info</url-pattern> <!-- http的url映射-->
</servlet-mapping>

</web-app>

这个文件是用来告诉tomcat,我自定义的Servlet类都在哪里,好让tomcat访问调用到,而下面的映射就更有意思了,你的浏览器在输入一个url时,除了域名,就是path了对吧?而这个映射的意思是说,当你在浏览器里输入“www.xx.com/get/student/info”时,这个url传到tomcat,tomcat可以根据path映射关系,找到这个叫StudentInfoController的Servlet,而这个Servlet对应哪个实现类呢?当然就是你自定义的子类StudentInfoController啦~

至此,你的这个简单的,只提供输出Student对象的接口,就完成了,下面让我们点击下tomcat的运行按钮,就可以启动了,假设我们现在已经启动了,在你的浏览器,输入:127.0.0.1:8080/get/student/info试试吧,它的运行结果如下(我自己改了端口号8081,所以图里是8081 -_-||,其次,localhost = 127.0.0.1,它表示的是你本机的ip地址):

图36

你可以打开下浏览器的检查功能,可以具体看到HTTP协议头信息:

图37

我们设置的Content-Type它生效了,它通过HTTP协议头来告诉浏览器:“我传给你的业务数据流是以json做的序列化,你也得用json序列化”。

你可能会好奇,这个过程中怎么没见main方法啊?因为这个过程中你的程序只是陪衬,真正运行的程序是包含了你的业务代码的tomcat,所以启动这个动作肯定是发生在了tomcat。

3.3.4:结束语

你写的普通的业务代码终于可以堂堂正正的让浏览器通过HTTP请求访问到了!!现在知道什么是java web了吧?其实一开始大家分的很细,像什么JAVA SE、JAVA EE,JAVA EE就是java web,也叫java企业级开发,其实你不用计较这么多,java就是java,因为你借助一个web程序tomcat完成了你让别人通过HTTP请求来触发你的业务程序,所以你写的程序也被叫成了java web,仅此而已,你需要学会从更宏观的角度来看待这些细微的差别,而不是死抠一些专有名词。

最后还是要说一下,你没发现写一个上面的程序,很特么复杂吗?还得下载配置tomcat,还得配置你的IDEA,还得指定各类序列化方式等等,让它可以结合tomcat运转你的程序,事实上,很多事情复杂到一定程度,就一定会有工程师去搞个更方便的东西出来,这也是接下来要让你们接触到的一个东西:spring boot。

别害怕,spring boot并不是单独一门技术,它就是spring,spring的一切特性它都具备,但是它是spring的进化态,它里面包含了一个tomcat!spring boot写好的程序,就自动包含了上面一堆有关tomcat的配置,你再也不用担心tomcat的任何事情,写个main方法就好~