https://www.leiphone.com/news/201606/UEiart497WUzS62u.html
https://lavas.baidu.com/
http://ai.baidu.com/
http://unit.baidu.com/
https://www.leiphone.com/news/201606/UEiart497WUzS62u.html
https://lavas.baidu.com/
http://ai.baidu.com/
http://unit.baidu.com/
这个仓库试图回答一个古老的面试问题:当你在浏览器中输入 google.com 并且按下回车之后发生了什么?
不过我们不再局限于平常的回答,而是想办法回答地尽可能具体,不遗漏任何细节。
这将是一个协作的过程,所以深入挖掘吧,并且帮助我们一起完善它。仍然有大量的细节等待着你来添加,欢迎向我们发送 Pull Requset!
这些内容使用 Creative Commons Zero 协议发布。
WM_KEYDOWN
消息被发往应用程序KeyDown
NSEvent被发往应用程序接下来的内容介绍了物理键盘和系统中断的工作原理,但是有一部分内容却没有涉及。当你按下“g”键,浏览器接收到这个消息之后,会触发自动完成机制。浏览器根据自己的算法,以及你是否处于隐私浏览模式,会在浏览器的地址框下方给出输入建议。大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。你打算输入 “google.com”,因此给出的建议并不匹配。但是输入过程中仍然有大量的代码在后台运行,你的每一次按键都会使得给出的建议更加准确。甚至有可能在你输入之前,浏览器就将 “google.com” 建议给你。
为了从零开始,我们选择键盘上的回车键被按到最低处作为起点。在这个时刻,一个专用于回车键的电流回路被直接地或者通过电容器间接地闭合了,使得少量的电流进入了键盘的逻辑电路系统。这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除(debounce),并将其转化为键盘码值。在这里,回车的码值是13。键盘控制器在得到码值之后,将其编码,用于之后的传输。现在这个传输过程几乎都是通过通用串行总线(USB)或者蓝牙(Bluetooth)来进行的,以前是通过PS/2或者ADB连接进行。
USB键盘:
虚拟键盘(触屏设备):
键盘在它的中断请求线(IRQ)上发送信号,信号会被中断控制器映射到一个中断向量,实际上就是一个整型数 。CPU使用中断描述符表(IDT)把中断向量映射到对应函数,这些函数被称为中断处理器,它们由操作系统内核提供。当一个中断到达时,CPU根据IDT和中断向量索引到对应的中断处理器,然后操作系统内核出场了。
WM_KEYDOWN
消息被发往应用程序HID把键盘按下的事件传送给 KBDHID.sys
驱动,把HID的信号转换成一个扫描码(Scancode),这里回车的扫描码是 VK_RETURN(0x0d)
。 KBDHID.sys
驱动和 KBDCLASS.sys
(键盘类驱动,keyboard class driver)进行交互,这个驱动负责安全地处理所有键盘和小键盘的输入事件。之后它又去调用 Win32K.sys
,在这之前有可能把消息传递给安装的第三方键盘过滤器。这些都是发生在内核模式。
Win32K.sys
通过 GetForegroundWindow()
API函数找到当前哪个窗口是活跃的。这个API函数提供了当前浏览器的地址栏的句柄。Windows系统的”message pump”机制调用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam)
函数, lParam
是一个用来指示这个按键的更多信息的掩码,这些信息包括按键重复次数(这里是0),实际扫描码(可能依赖于OEM厂商,不过通常不会是 VK_RETURN
),功能键(alt, shift, ctrl)是否被按下(在这里没有),以及一些其他状态。
Windows的 SendMessage
API直接将消息添加到特定窗口句柄 hWnd
的消息队列中,之后赋给 hWnd
的主要消息处理函数 WindowProc
将会被调用,用于处理队列中的消息。
当前活跃的句柄 hWnd
实际上是一个edit control控件,这种情况下,WindowProc
有一个用于处理 WM_KEYDOWN
消息的处理器,这段代码会查看 SendMessage
传入的第三个参数 wParam
,因为这个参数是 VK_RETURN
,于是它知道用户按下了回车键。
KeyDown
NSEvent被发往应用程序中断信号引发了I/O Kit Kext键盘驱动的中断处理事件,驱动把信号翻译成键码值,然后传给OS X的 WindowServer
进程。然后, WindowServer
将这个事件通过Mach端口分发给合适的(活跃的,或者正在监听的)应用程序,这个信号会被放到应用程序的消息队列里。队列中的消息可以被拥有足够高权限的线程使用 mach_ipc_dispatch
函数读取到。这个过程通常是由 NSApplication
主事件循环产生并且处理的,通过 NSEventType
为 KeyDown
的 NSEvent
。
当使用图形化的 X Server 时,X Server 会按照特定的规则把键码值再一次映射,映射成扫描码。当这个映射过程完成之后, X Server 把这个按键字符发送给窗口管理器(DWM,metacity, i3等等),窗口管理器再把字符发送给当前窗口。当前窗口使用有关图形API把文字打印在输入框内。
Protocol
“http”- 使用HTTP协议
Resource
“/”- 请求的资源是主页(index)
当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。
a-z
, A-Z
,0-9
, -
或者 .
的字符google.com
,所以没有非ASCII的字符;如果有的话,浏览器会对主机名部分使用 Punycode 编码gethostbyname
库函数(操作系统不同函数也不同)进行查询。gethostbyname
函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系统有所不同gethostbyname
没有这个域名的缓存记录,也没有在 hosts
里找到,它将会向 DNS 服务器发送一条 DNS 查询请求。DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。要想发送 ARP(地址解析协议)广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。
如果缓存没有命中:
ARP Request
:
Sender MAC: interface:mac:address:here Sender IP: interface.ip.goes.here Target MAC: FF:FF:FF:FF:FF:FF (Broadcast) Target IP: target.ip.goes.here
根据连接主机和路由器的硬件类型不同,可以分为以下几种情况:
直连:
ARP Reply
(见下面)。集线器:
ARP Reply
。交换机:
ARP Reply
ARP Reply
:
Sender MAC: target:mac:address:here Sender IP: target.ip.goes.here Target MAC: interface:mac:address:here Target IP: interface.ip.goes.here
现在我们有了 DNS 服务器或者默认网关的 IP 地址,我们可以继续 DNS 请求了:
当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket
,请求一个 TCP流套接字,对应的参数是 AF_INET/AF_INET6
和 SOCK_STREAM
。
到了现在,TCP 封包已经准备好了,可以使用下面的方式进行传输:
对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点 处理。节点的目标地址和源地址将在后面讨论。
大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理。
最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP数据报头部TTL域的值每经过一个路由器就减1,如果封包的TTL变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。
上面的发送和接受过程在 TCP 连接期间会发生很多次:
Client hello
消息到服务器端,消息中同时包含了它的TLS版本,可用的加密算法和压缩算法。Server hello
消息,消息中包含了服务器端的TLS版本,服务器选择了哪个加密和压缩算法,以及服务器的公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥Finished
消息给服务器端,使用对称密钥加密这次通讯的一个散列值Finished
消息,也使用协商好的对称密钥加密如果浏览器是 Google 出品的,它不会使用 HTTP 协议来获取页面信息,而是会与服务器端发送请求,商讨使用 SPDY 协议。
如果浏览器使用 HTTP 协议,它会向服务器发送这样的一个请求:
GET / HTTP/1.1 Host: google.com [其他头部]
“其他头部”包含了一系列的由冒号分割开的键值对,它们的格式符合HTTP协议标准,它们之间由一个换行符分割开来。这里我们假设浏览器没有违反HTTP协议标准的bug,同时浏览器使用 HTTP/1.1
协议,不然的话头部可能不包含 Host
字段,同时 GET
请求中的版本号会变成 HTTP/1.0
或者 HTTP/0.9
。
HTTP/1.1 定义了“关闭连接”的选项 “close”,发送者使用这个选项指示这次连接在响应结束之后会断开:
Connection:close
不支持持久连接的 HTTP/1.1 必须在每条消息中都包含 “close” 选项。
在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了。
服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的:
200 OK [响应头部]
然后是一个换行,接下来有效载荷(payload),也就是 www.google.com
的HTML内容。服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供以后的请求重用。
如果浏览器发送的HTTP头部包含了足够多的信息(例如包含了 Etag 头部,以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应:
304 Not Modified [响应头部]
这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。
在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到HTML页面引入的所有资源(图片,CSS,favicon.ico等等)全部都获取完毕,区别只是头部的 GET / HTTP/1.1
会变成 GET /$(相对www.google.com的URL) HTTP/1.1
。
如果HTML引入了 www.google.com
域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host
头部会变成另外的域名。
HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。
GET
, POST
, HEAD
, PUT
, DELETE
, CONNECT
, OPTIONS
, 或者 TRACE
)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法当服务器提供了资源之后(HTML,CSS,JS,图片等),浏览器会执行下面的操作:
浏览器的功能是从服务器上取回你想要的资源,然后展示在浏览器窗口当中。资源通常是 HTML 文件,也可能是 PDF,图片,或者其他类型的内容。资源的位置通过用户提供的 URI(Uniform Resource Identifier) 来确定。
浏览器解释和展示 HTML 文件的方法,在 HTML 和 CSS 的标准中有详细介绍。这些标准由 Web 标准组织 W3C(World Wide Web Consortium) 维护。
不同浏览器的用户界面大都十分接近,有很多共同的 UI 元素:
浏览器高层架构
组成浏览器的组件有:
浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成8kB大小的分块传输。
HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树。
解析树是以 DOM 元素以及属性为节点的树。DOM是文档对象模型(Document Object Model)的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如Javascript)的接口。树的根部是”Document”对象。整个 DOM 和 HTML 文档几乎是一对一的关系。
解析算法
HTML不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:
由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。
解析结束之后
浏览器开始加载网页的外部资源(CSS,图像,Javascript 文件等)。
此时浏览器把文档标记为“可交互的”,浏览器开始解析处于“推迟”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成”,浏览器会进行“加载”事件。
注意解析 HTML 网页时永远不会出现“语法错误”,浏览器会修复所有错误,然后继续解析。
执行同步 Javascript 代码。
标签包含的内容floated
,位置有 absolutely
或 relatively
属性的时候,会有更多复杂的计算,详见http://dev.w3.org/csswg/css2/ 和 http://www.w3.org/Style/CSS/current-work渲染结束后,浏览器根据某些时间机制运行JavaScript代码(比如Google Doodle动画)或与用户交互(在搜索栏输入关键字获得搜索建议)。类似Flash和Java的插件也会运行,尽管Google主页里没有。这些脚本可以触发网络请求,也可能改变网页的内容和布局,产生又一轮渲染与绘制。
from:https://github.com/skyline75489/what-happens-when-zh_CN
本文以HTTP请求和响应的过程来讲解涉及到的相关知识点。
图片来自: 理解Http请求与响应
以上完整表示了HTTP请求和响应的7个步骤,下面从TCP/IP协议模型的角度来理解HTTP请求和响应如何传递的。
TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议,通过20多年的发展已日渐成熟,并被广泛应用于局域网和广域网中,目前已成为事实上的国际标准。TCP/IP协议簇是一组不同层次上的多个协议的组合,通常被认为是一个四层协议系统,与OSI的七层模型相对应。
HTTP协议就是基于TCP/IP协议模型来传输信息的。
(1). 链路层
也称作数据链路层或网络接口层(在第一个图中为网络接口层和硬件层),通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。ARP(地址解析协议)和RARP(逆地址解析协议)是某些网络接口(如以太网和令牌环网)使用的特殊协议,用来转换IP层和网络接口层使用的地址。
(2). 网络层
也称作互联网层(在第一个图中为网际层),处理分组在网络中的活动,例如分组的选路。在TCP/IP协议族中,网络层协议包括IP协议(网际协议),ICMP协议(Internet互联网控制报文协议),以及IGMP协议(Internet组管理协议)。
IP是一种网络层协议,提供的是一种不可靠的服务,它只是尽可能快地把分组从源结点送到目的结点,但是并不提供任何可靠性保证。同时被TCP和UDP使用。TCP和UDP的每组数据都通过端系统和每个中间路由器中的IP层在互联网中进行传输。
ICMP是IP协议的附属协议。IP层用它来与其他主机或路由器交换错误报文和其他重要信息。
IGMP是Internet组管理协议。它用来把一个UDP数据报多播到多个主机。
(3). 传输层
主要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个互不相同的传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。
TCP为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。为了提供可靠的服务,TCP采用了超时重传、发送和接收端到端的确认分组等机制。
UDP则为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。一个数据报是指从发送方传输到接收方的一个信息单元(例如,发送方指定的一定字节数的信息)。UDP协议任何必需的可靠性必须由应用层来提供。
(4). 应用层
应用层决定了向用户提供应用服务时通信的活动。TCP/IP 协议族内预存了各类通用的应用服务。包括 HTTP,FTP(File Transfer Protocol,文件传输协议),DNS(Domain Name System,域名系统)服务。
当应用程序用TCP传送数据时,数据被送入协议栈中,然后逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息),该过程如图所示。
当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议盒都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称作分用(Demultiplexing)。协议是通过目的端口号、源I P地址和源端口号进行解包的。
通过以上步骤我们从TCP/IP模型的角度来理解了一次HTTP请求与响应的过程。
下面这张图更清楚明白:
下面具体来看如何进行一步步操作的。
TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换 TCP窗口大小信息。
第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
具体例子:“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
通俗来讲,他就是计算机通过网络进行通信的规则,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据。目前任何终端(手机,笔记本电脑。。)之间进行任何一种通信都必须按照Http协议进行,否则无法连接。
四个基于:
请求与响应:客户端发送请求,服务器端响应数据
无状态的:协议对于事务处理没有记忆能力,客户端第一次与服务器建立连接发送请求时需要进行一系列的安全认证匹配等,因此增加页面等待时间,当客户端向服务器端发送请求,服务器端响应完毕后,两者断开连接,也不保存连接状态,一刀两断!恩断义绝!从此路人!下一次客户端向同样的服务器发送请求时,由于他们之前已经遗忘了彼此,所以需要重新建立连接。
应用层:Http是属于应用层的协议,配合TCP/IP使用。
TCP/IP:Http使用TCP作为它的支撑运输协议。HTTP客户机发起一个与服务器的TCP连接,一旦连接建立,浏览器(客户机)和服务器进程就可以通过套接字接口访问TCP。
针对无状态的一些解决策略:
有时需要对用户之前的HTTP通信状态进行保存,比如执行一次登陆操作,在30分钟内所有的请求都不需要再次登陆。于是引入了Cookie技术。
HTTP/1.1想出了持久连接(HTTP keep-alive)方法。其特点是,只要任意一端没有明确提出断开连接,则保持TCP连接状态,在请求首部字段中的Connection: keep-alive即为表明使用了持久连接。
等等还有很多。。。。。。
下面开始讲解重头戏:HTTP请求报文,响应报文,对应于上述步骤的2,3,4,5,6。
HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的。HTTP有两类报文:请求报文和响应报文。
一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下图给出了请求报文的一般格式。
请求行分为三个部分:请求方法、请求地址和协议版本
请求方法
HTTP/1.1 定义的请求方法有8种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。
最常的两种GET和POST,如果是RESTful接口的话一般会用到GET、POST、DELETE、PUT。
请求地址
URL:统一资源定位符,是一种自愿位置的抽象唯一识别方法。
组成:<协议>://<主机>:<端口>/<路径>
端口和路径有时可以省略(HTTP默认端口号是80)
如下例:
有时会带参数,GET请求
协议版本
协议版本的格式为:HTTP/主版本号.次版本号,常用的有HTTP/1.0和HTTP/1.1
请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。
常见请求头如下:
请求头部的最后会有一个空行,表示请求头部结束,接下来为请求数据,这一行非常重要,必不可少。
可选部分,比如GET请求就没有请求数据。
下面是一个POST方法的请求报文:
POST /index.php HTTP/1.1 请求行
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2 请求头
Accept: text/html,application/xhtml+xml,application/xml;q=0.9, /;q=0.8
Accept-Language: zh-cn,zh;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://localhost/
Content-Length:25
Content-Type:application/x-www-form-urlencoded
空行
username=aa&password=1234 请求数据
HTTP响应报文主要由状态行、响应头部、空行以及响应数据组成。
由3部分组成,分别为:协议版本,状态码,状态码描述。
其中协议版本与请求报文一致,状态码描述是对状态码的简单描述,所以这里就只介绍状态码。
状态码
状态代码为3位数字。
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。
下面列举几个常见的:
与请求头部类似,为响应报文添加了一些附加信息
常见响应头部如下:
用于存放需要返回给客户端的数据信息。
下面是一个响应报文的实例:
HTTP/1.1 200 OK 状态行
Date: Sun, 17 Mar 2013 08:12:54 GMT 响应头部
Server: Apache/2.2.8 (Win32) PHP/5.2.5
X-Powered-By: PHP/5.2.5
Set-Cookie: PHPSESSID=c0huq7pdkmm5gg6osoe3mgjmm3; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4393
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8
空行<html> 响应数据
<head>
<title>HTTP响应示例<title>
</head>
<body>
Hello HTTP!
</body>
</html>
关于请求头部和响应头部的知识点很多,这里只是简单介绍。
通过以上步骤,数据已经传递完毕,HTTP/1.1会维持持久连接,但持续一段时间总会有关闭连接的时候,这时候据需要断开TCP连接。
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次分手”。
第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
通过以上步骤便完成了HTTP的请求和响应,进行了数据传递,这其中涉及到需要知识点,都进行了逐一了解。
参考文章:
你需要了解的HTTP知识都在这里了!
HTTP知识点总结
理解Http请求与响应
HTTP-请求、响应、缓存
你应该知道的HTTP基础知识
整理Http知识点
简析TCP的三次握手与四次分手
HTTP请求报文和HTTP响应报文
TCP/IP协议簇分层详解
HTTP请求报文和HTTP响应报文
from:http://www.shellsec.com/news/37745.html
由于具有实现更高性能的浏览器和智能电话应用程序的潜力,单页应用程序 (SPA) 技术在软件行业引起了广泛的兴趣。在过去 5 年多的时间里,开发人员对 Angular(一个开源 SPA 框架)的兴趣远超他们对其他 Web 框架(React、Backbone、Meteor 和 Ember)的兴趣,这从 StackOverflow 网站上针对每种 Web 框架的问题数量上可以判断出:
(图像来源:tag-trends)
Web 和移动开发人员非常喜欢 Angular 2(2016 年 9 月发布)。Angular 2 不是 Angular 1 的一次升级,而是一个全新的、不同的、更高级的框架。精通 Angular 2 已成为构建高性能、可扩展、稳健、现代的跨平台应用程序的一种很吃香的技能。
本教程将介绍 Angular 2 中的重要构建块,演示如何在开发环境和沙箱服务器中使用 Angular 2 编写和运行 SPA。要充分掌握本教程,您需要具有一定的 Web 编程经验,包括 JavaScript、TypeScript 和 HTML 的各方面知识。无需提前拥有 Angular 经验。练习完示例项目后,您就会为使用 Angular 2 创建自己的 SPA 做好充足的准备。
可以从教程末尾的 “可下载资源” 部分下载针对沙箱应用程序的完整示例代码。
在用户启动 SPA 时,该应用程序仅呈现来自服务器的一个 HTML 页面。除了这个 HTML 页面,服务器还会向客户端发送一个应用程序引擎。该引擎控制整个应用程序,包括HTML 页面的处理、输入、输出、绘制和加载。通常,90–95% 的应用程序代码是在浏览器中运行;当用户需要新数据时或必须执行身份验证等安全操作时,就会在服务器中运行剩余代码。由于几乎消除了对服务器的依赖,所以 SPA 能在 Angular 2 环境中自动扩展:无论有多少用户同时访问服务器,在 90–95% 的时间里,应用程序的性能绝不会受到影响。
另外,因为大部分负载都在客户端上运行,所以服务器在大部分时间里都处于空闲状态。对服务器资源的低需求显著减少了服务器上的压力,也潜在地降低了服务器成本。
Angular 2 的另一个优势是,它可以帮助 SPA 有效地使用微服务。
“ 通常,90–95% 的 SPA 代码在浏览器中运行;当用户需要新数据或必须执行身份验证等安全操作时,就会在服务器中运行剩余代码。”
Angular 2 中的关键概念包括:
从现在开始,我将 Angular 2 简称为 Angular。
Angular 应用程序是模块化的。每个 Angular 应用程序拥有至少一个模块,即根模块,传统上将其命名为 AppModule
。根模块可以是小型应用程序中唯一的模块,但大多数应用程序都拥有更多的模块。作为开发人员,您需要决定如何使用模块概念。通常,会将主要功能或特性映射到一个模块。假设您的系统中有 5 个主要区域。除了根模块之外,每个区域都将拥有自己的模块,总计 6 个模块。
组件 控制页面的一片区域,该区域被称为视图。您定义组件的应用程序逻辑 — 它如何支持类中的视图。类通过属性和方法的 API 与视图交互。一个组件包含一个类、一个模板和元数据。模板是一个 HTML 表单,用于告诉 Angular 如何呈现组件。一个组件只能属于一个模块。
服务 提供应用程序所需的任何值、功能或特性。服务通常是一个具有明确定义的较小用途的类;它应高效地执行具体的操作。组件大量使用服务。服务大量使用微服务。
路由 支持在用户执行应用程序任务时,从一个视图导航到下一个视图。路由等效于用于控制菜单和子菜单的机制。
了解 SPA 的优势并掌握 Angular 概念后,是时候在示例项目上开始实践了。
要完成示例项目,您需要在开发 PC 上安装 Node.js 和 Angular CLI(一个用于 Angular 的命令行接口):
node -v
来验证版本号 — 在我的情况下,版本为 v6.9.1
。Node Package Manager (NPM) 会随 Node 自动安装。键入 npm -v
来查看它的版本号。当您安装来自公共 NPM 存储库的包时,会在后台运行 NPM。一个常见的 NPM 命令是 npm install
,它用于下载您的 Angular 项目的 package.json 文件中列出的包版本。
npm install -g angular-cli@1.0.0-beta.21
来安装我用于示例应用程序的版本(在编写本文时,仍处于公测阶段)。(如果您想尝试一个不同的编译版,可访问 CLI 站点。)需要花约 10 分钟才能完成安装。ng -v
,以查看您的 CLI 版本号 — 在我的操作系统下:
angular-cli: 1.0.0-beta.21 node: 6.9.1 os: win32 x64 |
如果您需要获得有关 ng
语法的帮助,可在操作系统命令行上键入 ng help
。
package.json 文件(Angular 应用程序中一个关键的元数据文件)包含应用程序和它的依赖包的细节。此文件是 Angular 应用程序中最重要的文件,尤其是在将代码从一台计算机迁移到另一台时,或者是在升级期间。package.json 文件包含需要安装的包版本。
您无需为适合本教程练习的 package.json 感到担心。但在开发生产级应用程序时,必须多多留意此文件中列出的版本号。
以下是来自一个 package.json 文件的一些有效语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{ "dependencies" : { "foo" : "1.0.0 - 2.9999.9999" , "bar" : ">=1.0.2 < 2.1.2 " , "baz" : ">1.0.2 <=2.3.4" , "boo" : "2.0.1" , "qux" : "< 1.0.0 || >=2.3.1 < 2.4.5 || >=2.5.2 <3.0.0" , "til" : "^1.2" , "elf" : "~1.2.3" , "two" : "2.x" , "thr" : "3.3.x" , "lat" : "latest" } } |
npm install
命令对冒号右侧的部分语句的解释如下(其中版本 表示所使用的版本号):
*
相同要进一步了解 package.json,可在操作系统命令行上键入 npm help package.json
,以便在浏览器中查看帮助内容。
示例项目包含一个开箱即用的 Angular 应用程序,以及您将在开箱即用的应用程序上开发的一个自定义应用程序。当您完成上述操作后,您将拥有一个包含 3 个微型应用程序的 Angular 应用程序,每个微型应用程序中的特性使用了 3 个 Web 服务 API:
所有应用程序逻辑都将在您的浏览器中运行。仅在浏览器需要新数据时,才需要服务器。事实上,您可以关闭服务器进程,它仍会在您的应用程序中工作,因为它是一个 SPA。
下图显示了应用程序拓扑结构:
您将使用 Angular CLI 创建您的项目(该项目默认情况下会包含 AppModule
和 AppComponent
)和 4 个自定义组件:
您将创建用于菜单导航的路由,并将以下服务注入到天气、电影和货币组件中:
准备好开始了吗?首先在操作系统命令行上转到您想放置项目目录的位置。
运行下面的命令来生成一个新 Angular 项目(其中 dw_ng2_app
是项目名称):
ng new dw_ng2_app --skip-git |
安装所有需要的包和 Angular 基础应用程序(这将花费大约 10 分钟时间)后,您将返回到操作系统命令提示符上。如果您随后列出 /dw_ng2_app 目录的内容,就可以看到项目结构:
|— e2e |— node_modules |— src angular-cli.json karma.conf.js package.json protractor.conf.js README.md tslint.json |
../dw_ng2_app/src 目录的内容包括:
|— app |— assets |— environments favicon.ico index.html main.ts polyfills.ts styles.css test.ts tsconfig.json typings.d.ts |
../dw_ng2_app/src/app 目录(根模块文件夹)包含以下文件:
app.component.css app.component.html app.component.spec.ts app.component.ts app.module.ts index.ts |
更改到项目目录,运行 ng serve
来启动开箱即用的 Angular 应用程序。
默认情况下,该进程在端口 4200 上启动。如果您的 port
系统环境变量的值不是 4200,该进程将在此端口上启动。您可以运行 ng serve --port 4200
命令来覆盖默认端口号,这是一项可选操作。
打开您的浏览器并输入 URL http://localhost:4200/
。您的 Angular 应用程序会显示 app works!,这表明应用程序已启动、运行并准备就绪:
如果在应用程序运行过程中更改代码,Angular 会非常智能地监视并自动重新启动应用程序。尝试编辑 app.component.ts 文件,更改 title
的值。您可以看到,您的浏览器页面反映了这一更改:
在清单 1 中,第 20 行显示了 AppModule
模块的声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } |
此模块仅包含一个组件 —AppComponent
— 如上第 10 行所示。第 18 行表明,在引导进程下启动的第一个组件是 AppComponent
。
清单 2 给出了 app.component.ts 文件中的主要应用程序组件的内容。
1
2
3
4
5
6
7
8
9
10
|
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'My Angular 2 app works!'; } |
这是 app.component.html 文件中的主要应用程序组件的内容:
1
2
3
|
< h1 > {{title}} </ h1 > |
元数据告诉 Angular 如何处理类。事实上,在您通过向 AppComponent
附加元数据给类时,告诉 Angular 它还不是组件。
可以使用 @Component
修饰器附加元数据,这会将类标识为组件。@Component
修饰器接受一个必需的配置对象,该对象包含 Angular 创建和呈现该组件和它的视图所需的信息。
清单 2 中的代码使用可用的 @Component
配置选项中的 3 个选项:
selector
:一个 CSS 选择器,告诉 Angular 在父 HTML 文件中找到 selector
标记的地方创建和插入此组件的一个实例templateUrl
:组件的 HTML 文件styleUrls
:组件的样式表,比如 .css 文件模板是一个 HTML 表单,用于告诉 Angular 如何呈现组件。在 清单 2 的第 5 行上,templateUrl
指向一个名为 app.component.html
的视图。
数据绑定是一个 HTML 表单,用于告诉 Angular 如何呈现组件。在 app.component.ts 文件中,title
的值在类内设置,在 app.component.html 文件中使用。数据绑定可以是单向或双向的。在本例中,如果您在双花括号 {{ }}
内提及该变量,则该映射是单向的。值从类传递到 HTML 文件。
此刻,您的 Angular 应用程序已准备就绪并能工作正常。这个基础应用程序有一个模块、一个组件、一个类、一个模板、元数据和数据绑定 — 但它仍缺少 4 个其他的重要部分:
接下来,您将创建这些自定义组件。
按 Ctrl-C 停止 Angular 进程(确保您在 Angular 项目的目录中,在本例中为 dw_ng2_app)。在命令提示符下,运行以下命令:
ng g c Menu -is --spec false --flat
:在 AppModule
根模块(同一个文件夹中)内创建 Menu
组件。ng g c Weather -is --spec false
:在 AppModule
根模块内名为 weather 的子文件夹中创建 Weather
组件。ng g c Currency -is --spec false
:在 AppModule
根模块内名为 currency 的子文件夹中创建 Currency
组件。ng g c Movie -is --spec false
:在 AppModule
根模块内名为 movie 的子文件夹中创建 Movie
组件。现在,有了创建的新组件 — 包括类、元数据和模板 — 您可以看到 AppModule
如何链接到这些组件。在清单 3 中,第 28 行包含 AppModule
模块的声明。此模块包含 5 个组件,包括根组件和其他 4 个组件,如第 14–18 行所示。
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
|
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { MenuComponent } from './menu.component'; import { WeatherComponent } from './weather/weather.component'; import { CurrencyComponent } from './currency/currency.component'; import { MovieComponent } from './movie/movie.component'; @NgModule({ declarations: [ AppComponent, MenuComponent, WeatherComponent, CurrencyComponent, MovieComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } |
要让 Angular 能在组件之间导航,需要创建路由。使用清单 4 的内容覆盖 menu.component.html 文件,以便 HTML 包含所有组件的正确菜单。
1
2
3
4
5
6
7
8
9
|
< div class = "row" > < div class = "col-xs-12" > < ul class = "nav nav-pills" > < li routerLinkActive = "active" > < a [routerLink]="['/weather']" >Weather</ a ></ li > < li routerLinkActive = "active" > < a [routerLink]="['/movie']" >Movie Details</ a ></ li > < li routerLinkActive = "active" > < a [routerLink]="['/currency']" >Currency Rates</ a ></ li > </ ul > </ div > </ div > |
清单 4 中的代码提供了 GUI 与 URL 路径之间的映射。例如,当用户单击 GUI 中的 Movie Details 按钮时,Angular 知道它需要像 URL 路径为 http://localhost:4200/movie
一样运行。
接下来,将 URL 路径映射到组件。在根模块的相同文件夹中,创建一个名为 app.routing.ts 的配置文件,使用清单 5 中的代码作为其内容。
1
2
3
4
5
6
7
8
9
10
11
12
|
import { Routes, RouterModule } from '@angular/router'; import { CurrencyComponent } from "./currency/currency.component"; import { WeatherComponent } from "./weather/weather.component"; import { MovieComponent } from "./movie/movie.component"; const MAINMENU_ROUTES: Routes = [ //full : makes sure the path is absolute path { path: '', redirectTo: '/weather', pathMatch: 'full' }, { path: 'weather', component: WeatherComponent }, { path: 'movie', component: MovieComponent }, { path: 'currency', component: CurrencyComponent } ]; export const CONST_ROUTING = RouterModule.forRoot(MAINMENU_ROUTES); |
在本例中,如果您的 URL 相对路径是 movie
,则会告诉 Angular 调用 MovieComponent
组件。换句话说,相对路径 movie
映射到 URL http://localhost:4200/movie
。
现在,您需要将此视图链接到它的父组件。使用以下代码覆盖 app.component.html 文件内容:
1
2
3
4
5
|
< div class = "container" > < app-menu ></ app-menu > < hr > < router-outlet ></ router-outlet > </ div > |
<app-menu></app-menu>
选择器会包含菜单。<router-outlet></router-outlet>
选择器是当前组件的占位符。根据 URL 路径,该值可以是以下 3 个组件中的任意一个:天气、电影或货币。
您还必须向该模块告知此路由。在 app.module.ts 文件中添加两项,如清单 6 中的第 11 和 25 行所示。
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
|
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { MenuComponent } from './menu.component'; import { WeatherComponent } from './weather/weather.component'; import { CurrencyComponent } from './currency/currency.component'; import { MovieComponent } from './movie/movie.component'; import { CONST_ROUTING } from './app.routing'; @NgModule({ declarations: [ AppComponent, MenuComponent, WeatherComponent, CurrencyComponent, MovieComponent ], imports: [ BrowserModule, FormsModule, HttpModule, CONST_ROUTING ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } |
现在,如果您运行应用程序并单击 Weather 链接,应用程序将会显示 weather works!:
如果单击 Movie Details 链接,应用程序会显示 movie works!:
如果单击 Currency Rates 链接,应用程序将会显示 currency works!:
您已成功修改了您的 Angular 应用程序,以便包含多个自定义组件和路由。现在您已经准备好执行最后两个重要操作:
按下 Ctrl-C 停止 Angular 进程。运行下面的命令:
ng g service Shared --spec false |
此命令在根模块文件夹中的 shared.service.ts 文件中创建该服务:
将 shared.service.ts 的内容替换为清单 7 中的代码。
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
|
import { Injectable } from '@angular/core'; import { Http, Headers, Response } from "@angular/http"; import 'rxjs/Rx'; import { Observable } from "rxjs"; @Injectable() export class SharedService { weatherURL1 = "https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22"; weatherURL2 = "%2C%20"; weatherURL3 = "%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys"; findMovieURL1 = "http://www.omdbapi.com/?t="; findMovieURL2 = "&y=&plot=short&r=json"; currencyURL = "http://api.fixer.io/latest?symbols="; totReqsMade: number = 0; constructor(private _http: Http) { } findWeather(city, state) { this.totReqsMade = this.totReqsMade + 1; return this._http.get(this.weatherURL1 + city + this.weatherURL2+ state + this.weatherURL3) .map(response => { { return response.json() }; }) .catch(error => Observable.throw(error.json())); } findMovie(movie) { this.totReqsMade = this.totReqsMade + 1; return this._http.get(this.findMovieURL1 + movie + this.findMovieURL2) .map(response => { { return response.json() }; }) .catch(error => Observable.throw(error.json().error)); } getCurrencyExchRate(currency) { this.totReqsMade = this.totReqsMade + 1; return this._http.get(this.currencyURL + currency) .map(response => { { return response.json() }; }) .catch(error => Observable.throw(error.json())); } } |
清单 7 中的 import ...
语句是任何服务正常运行所必不可少的。@Injectable()
语句特别重要;它表明此 service
可注入到其他组件中 — 该技术通常被称为依赖注入。
totReqsMade
变量在这里声明,将用于在 3 个组件之间传递它的值。这将跟踪为获得微服务结果而发出的服务请求总数。
您有 3 个方法,它们的名称表明了自己的功能:findWeather()
、findMovie()
和 getCurrencyExchRate()
。在方法执行期间,您的 Angular 应用程序将让浏览器访问网络来使用微服务。现在您将把组件链接到所创建的服务。
将 movie.component.ts 文件内容替换为清单 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
|
import { Component, OnInit } from '@angular/core'; import { SharedService } from "./../shared.service"; @Component({ selector: 'app-movie', templateUrl: './movie.component.html', styles: [] }) export class MovieComponent implements OnInit { id_movie: string = ""; mv_Title: string = ""; mv_Rated: string = ""; mv_Released: string = ""; mv_Director: string = ""; mv_Actors: string = ""; mv_Plot: string = ""; constructor(private _sharedService: SharedService) { } ngOnInit() { } callMovieService() { this._sharedService.findMovie(this.id_movie) .subscribe( lstresult => { this.mv_Title = lstresult["Title"]; this.mv_Rated = lstresult["Rated"]; this.mv_Released = lstresult["Released"]; this.mv_Director = lstresult["Director"]; this.mv_Actors = lstresult["Actors"]; this.mv_Plot = lstresult["Plot"]; }, error => { console.log("Error. The findMovie result JSON value is as follows:"); console.log(error); } ); } } |
这个重要的代码端调用服务方法来获取新数据。在本例中,它调用 callMovieService()
,然后调用 this._sharedService.findMovie()
方法。
类似地,将 currency.component.ts 文件内容替换为清单 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
|
import { Component, OnInit } from '@angular/core'; import { SharedService } from "./../shared.service"; @Component({ selector: 'app-currency', templateUrl: './currency.component.html', styles: [] }) export class CurrencyComponent implements OnInit { id_currency: string = ""; my_result: any; constructor(private _sharedService: SharedService) { } ngOnInit() { } callCurrencyService() { this._sharedService.getCurrencyExchRate(this.id_currency.toUpperCase()) .subscribe( lstresult => { this.my_result = JSON.stringify(lstresult); }, error => { console.log("Error. The callCurrencyService result JSON value is as follows:"); console.log(error); } ); } } |
将 weather.component.ts 文件内容替换为清单 10 中的代码。
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
|
import { Component, OnInit } from '@angular/core'; import { SharedService } from "./../shared.service"; @Component({ selector: 'app-weather', templateUrl: './weather.component.html', styles: [] }) export class WeatherComponent implements OnInit { id_city: string = ""; id_state: string = ""; op_city: string = ""; op_region: string = ""; op_country: string = ""; op_date: string = ""; op_text: string = ""; op_temp: string = ""; constructor(private _sharedService: SharedService) { } ngOnInit() { } callWeatherService() { this._sharedService.findWeather(this.id_city, this.id_state) .subscribe( lstresult => { this.op_city = lstresult["query"]["results"]["channel"]["location"]["city"]; this.op_region = lstresult["query"]["results"]["channel"]["location"]["region"]; this.op_country = lstresult["query"]["results"]["channel"]["location"]["country"]; this.op_date = lstresult["query"]["results"]["channel"]["item"]["condition"]["date"]; this.op_text = lstresult["query"]["results"]["channel"]["item"]["condition"]["text"]; this.op_temp = lstresult["query"]["results"]["channel"]["item"]["condition"]["temp"]; }, error => { console.log("Error. The findWeather result JSON value is as follows:"); console.log(error); } ); } } |
现在,更新该模块以包含这些服务。编辑 app.module.ts 文件,以包含清单 11 的第 12 和 28 行的两条语句。
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
|
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { MenuComponent } from './menu.component'; import { WeatherComponent } from './weather/weather.component'; import { CurrencyComponent } from './currency/currency.component'; import { MovieComponent } from './movie/movie.component'; import { CONST_ROUTING } from './app.routing'; import { SharedService } from "./shared.service"; @NgModule({ declarations: [ AppComponent, MenuComponent, WeatherComponent, CurrencyComponent, MovieComponent ], imports: [ BrowserModule, FormsModule, HttpModule, CONST_ROUTING ], providers: [SharedService], bootstrap: [AppComponent] }) export class AppModule { } |
现在还剩难题的最后一部分。您需要告诉 HTML 文件调用正确的服务方法。为此,请将 movie.component.html 文件的内容替换为清单 12 中的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
< h2 >Open Movie Database</ h2 > < div class = "col-md-8 col-md-offset-2" > < div class = "form-group" > < input type = "text" required [(ngModel)]="id_movie" (change)="callMovieService()" class = "form-control" placeholder = "Enter Movie name ..." > < br >< br > < h3 >Movie Details</ h3 > < br > < p class = "well lead" > < i > Title :</ i > {{ this.mv_Title }} < br > < i > Plot :</ i > {{ this.mv_Plot }} < br > < i > Actors :</ i > {{ this.mv_Actors }} < br > < i > Directed by :</ i > {{ this.mv_Director }} < br > < i > Rated :</ i > {{ this.mv_Rated }} < br > < i > Release Date :</ i > {{ this.mv_Released }} < br > </ p > < p class = "text-info" >Total # of all the service requests including Weather, Movie, and Currency is : < span class = "badge" >{{this._sharedService.totReqsMade}}</ span > </ p > </ div > </ div > |
movie.component.html 中编码了一些重要的信息:
{{ this._sharedService.totReqsMade }}
:这是在服务级别上跟踪的值,它会在所有 3 个应用程序组件之间共享。[(ngModel)]="id_movie"
:用户输入的 GUI 输入被传递到调用此 HTML 的类。在本例中,该类为 MovieComponent
。(change)="callMovieService()
“:当此字段值更改时,就会告诉系统调用 movie.component.ts 文件中包含的 callMovieService()
方法。{{ this.mv_Title }}, {{ this.mv_Plot }}, {{ this.mv_Actors }}, {{ this.mv_Director }}, {{ this.mv_Rated }}, {{ this.mv_Released }}
:显示从 callMovieService() -> this._sharedService.findMovie(this.id_movie)
执行的服务调用的结果。将 weather.component.html 文件的内容替换为清单 13 中的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
< h2 >Yahoo! Weather </ h2 > < div class = "col-md-8 col-md-offset-2" > < div class = "form-group" > < input type = "text" [(ngModel)]="id_city" class = "form-control" placeholder = "Enter City name ..." >< br > < input type = "text" [(ngModel)]="id_state" class = "form-control" placeholder = "Enter State. Example CA for California ..." >< br > < button type = "button" class = "btn btn-primary" (click)="callWeatherService()">Submit</ button > < br >< br >< br > < br > < p class = "well lead" > < i >City, State, Country :</ i > {{ this.op_city }} {{ this.op_region }} {{ this.op_country }} < br > < i >Current Condition :</ i > {{ this.op_text }} < br > < i >Current Temperature :</ i > {{ this.op_temp }} < br > </ p > < p class = "text-info" >Total # of all the service requests including Weather, Movie, and Currency is : < span class = "badge" >{{this._sharedService.totReqsMade}}</ span > </ p > </ div > </ div > |
最后,将 currency.component.html 文件的内容替换为清单 14 中的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
< h2 >Currency Exchange Rates</ h2 > < div class = "col-md-8 col-md-offset-2" > < div class = "form-group" > < input type = "text" [(ngModel)]="id_currency" (change)="callCurrencyService()" class = "form-control" placeholder = "Enter Currency Symbol. Example: GBP(,AUD,INR)..." > < br >< br > < h3 >Rate Details</ h3 > < br > < p class = "well lead" >Exchange rate relative to Euro in a JSON format: : {{ this.my_result }} </ p > < p class = "text-info" >Total # of all the service requests including Weather, Movie, and Currency is : < span class = "badge" >{{this._sharedService.totReqsMade}}</ span > </ p > </ div > </ div > |
现在,如果各部分均按预期运行,应用程序可接受浏览器中的用户输入。
现在运行应用程序,输入一些值,然后查看结果。例如,单击 Weather 链接,输入 San Francisco
来查看该城市的天气条件:
一切正常,但还可以让 UI 更具吸引力。改进 GUI 的一种方法是使用 Bootstrap。(Angular 2 资料 是理想的选择,但截至编写本文时,它尚未正式发布)。
转到 Bootstrap 入门页面,将该页的以下两行复制到剪贴板:
1
2
|
<!-- Latest compiled and minified CSS --> < link rel = "stylesheet" href = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity = "sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin = "anonymous" > |
打开 index.html 文件,将刚复制的语句粘贴到第 8 行下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<!doctype html> < html > < head > < meta charset = "utf-8" > < title >DwNg2App</ title > < base href = "/" > <!-- Latest compiled and minified CSS --> < link rel = "stylesheet" href = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity = "sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin = "anonymous" > < meta name = "viewport" content = "width=device-width, initial-scale=1" > < link rel = "icon" type = "image/x-icon" href = "favicon.ico" > </ head > < body > < app-root >Loading...</ app-root > </ body > </ html > |
现在该应用程序在浏览器中看起来更加美观,拥有更容易理解的样式和菜单按钮,而不是 Weather、Movie Details 和 Currency Rates 链接:
现在花一分钟时间体会一下您刚完成的应用程序为什么被称为 SPA。
当 Angular 应用程序启动时,服务器将 index.html 文件发送到浏览器,而且 index.html 是浏览器显示的唯一文件。Angular 对该页面执行的任何操作都会插入到此视图中:
index.html 末尾的 <app-root>
选择器被 app.component.html 的内容所取代。app.component.html 包含两个选择器:<app-menu>
和 <router-outlet>
。<app-menu>
选择器中填入 menu.component.html 的内容,<router-outlet>
依据菜单选项而动态填充 — 也就是说,填入 weather.component.html、currency.component.html 或 movie.component.html 的内容。
除了 Angular 保留选择器 <router-outlet></router-outlet>
外,所有选择器都是静态的。此选择器在运行时期间依据路由器值而填充。仅显示 index.html,您编码的其他所有 HTML 文件都嵌套在 index.html 文件内。
您的 Angular 项目已成功在开发计算机中运行。如果您能访问远程沙箱服务器,那么可以将代码迁移到那里,看看应用程序在被用户运行时的行为。(否则,可以跳到教程的 结束语 部分。)
确保 Node.js 和 Angular CLI 已安装在远程沙箱中。压缩本地项目文件夹中的所有内容,node_modules 目录及其内容除外。将压缩后的项目文件复制到沙箱服务器并解压。转到包含 package.json 的服务器目录并运行 npm install
命令。package.json 文件使 npm install
命令能转到 NPM 公共存储库,安装所有需要的包版本。运行此命令也会自动在服务器上创建 node_modules 目录和它的内容。
运行 ng serve
命令,以便在沙箱服务器中启动该应用程序,就像在开发计算机中所做的一样。按下 Ctrl-C 停止该进程。同样地,如果您想了解 ng serve
的其他选项,可以运行 ng help
命令。
使用 ng serve --port sandbox-port# --host sandbox-hostname
命令运行该应用程序。
现在 Angular 应用程序可在 URL http://sandbox-hostname:sandbox-port# 上访问。在开发计算机浏览器中运行该 URL 上的应用程序时,在沙箱服务器上按下 Ctrl-C 停止服务器 Angular 进程。请注意,整个应用程序都在开发计算机的浏览器中运行,尽管服务器 Angular 进程已关闭。这会告诉您,SPA 技术正在运行。该应用程序加载到浏览器中后,除非应用程序需要新数据,否则控制权绝不会转交给服务器。
在 SPA 领域,浏览器就是新型的服务器。如果 10 个用户在运行该应用程序,就会有 10 个浏览器处理该负载。如果 1,000 个用户在运行该应用程序,则有 1,000 个浏览器处理该负载。整个 Angular 应用程序都在浏览器的控制之下,除了在服务器中运行的任何逻辑,比如身份验证和数据库操作。
更高的性能和服务器压力的减轻,最终会提高用户体验和用户满意度 — 这是任何企业获得成功的关键。
“ 在 SPA 领域,浏览器就是新型的服务器。”
您学习了如何使用 Angular 2 编写一个 SPA,并在开发计算机和沙箱服务器中运行它。对于生产需求,请与您的 IT 部门协商。大多数生产应用程序拥有的一项主要功能是身份验证和授权,应在服务器中运行该功能(主要出于安全原因)。您可能需要一个专用服务器来处理这些操作。对于该服务器,可以使用 Node.js,在 Angular 2 在前端运行时,它可以充当在后端运行的服务器。(Node 和 Angular 都起源于 Google,所以它们能有效地协同运行。)
您可以考虑的用来提高应用程序性能的其他技术包括:
感谢 IBM 的优秀员工 Guy Huinen、Dean P Cummings、Don Turner 和 Mark Shade 的评审工作和支持。
from:http://www.ibm.com/developerworks/cn/web/wa-implement-a-single-page-application-with-angular2/index.html
篇文章写得很好,介绍了三种实现web api版本化的三种方式。我从评论里又收集到两种方式,所以一共是5种:
方式一:利用URL
HTTP GET: https://haveibeenpwned.com/api/v2/breachedaccount/foo
方式二:利用用户自定义的request header
HTTP GET: https://haveibeenpwned.com/api/breachedaccount/foo api-version: 2
方式三:利用content type
HTTP GET: https://haveibeenpwned.com/api/breachedaccount/foo Accept: application/vnd.haveibeenpwned.v2+json
方式四:利用content type
HTTP GET: https://haveibeenpwned.com/api/breachedaccount/foo Accept: application/vnd.haveibeenpwned+json; version=2.0
这个方式和方式三的小不同的地方是,把版本号分离出来了。
方式五:利用URL里的parameter
HTTP GET: https://haveibeenpwned.com/api/breachedaccount/foo?v=2
作者说他最喜欢第三种方式,因为
但作者很蛋疼地在他的网站上把前面三种方式都实现了,而且都支持。
https://haveibeenpwned.com/API/v2
我个人最喜欢的是第二种方式,因为这个用spring mvc实现最容易,也最简洁。
因为只要在Controler上用@RequestMapping标明版本即可。不用再去各种匹配,各种识别。
如果是自己写一个Annotation来识别的话,也要花些功夫,而且怎么无缝地转发到原有的Spring mvc的配置也是个问题。
@Controller@RequestMapping(headers=”apt-version=2″)public class TestControllerV2 {}
另外这个网站列举了很多国外的有名网站是如何实现web api版本控制的。
http://www.lexicalscope.com/blog/2012/03/12/how-are-rest-apis-versioned/