web工作方式
对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入 URL 的时候,首先浏览器会去请求 DNS 服务器,通过 DNS 获取相应的域名对应的 IP,然后通过 IP 地址找到 IP 对应的服务器后,要求建立 TCP 连接,等浏览器发送完 HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回 HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个 Response 包里的主体(body),等收到全部的内容随后断开与该服务器之间的 TCP 连接。
Web 服务器的工作原理可以简单地归纳为:
- 客户机通过 TCP/IP 协议建立到服务器的 TCP 连接
- 客户端向服务器发送 HTTP 协议请求包,请求服务器里的资源文档
- 服务器向客户机发送 HTTP 协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理 “动态内容”,并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释 HTML 文档,在客户端屏幕上渲染图形结果
URL 和 DNS 解析
URL (Uniform Resource Locator) 是 “统一资源定位符” 的英文缩写,用于描述一个网络上的资源,基本格式如下
scheme://host[:port#]/path/.../[?query-string][#anchor]
scheme 指定底层使用的协议(例如:http, https, ftp)
host HTTP 服务器的 IP 地址或者域名
port# HTTP 服务器的默认端口是 80,这种情况下端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/
path 访问资源的路径
query-string 发送给 http 服务器的数据
anchor 锚
DNS (Domain Name System) 是 “域名系统” 的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于 TCP/IP 网络,它从事将主机名或域名转换为实际 IP 地址的工作。DNS 就是这样的一位 “翻译官”,它的基本工作原理可用下图来表示。
更详细的 DNS 解析的过程如下,这个过程有助于我们理解 DNS 的工作模式
- 在浏览器中输入 www.qq.com 域名,操作系统会先检查自己本地的 hosts 文件是否有这个网址映射关系,如果有,就先调用这个 IP 地址映射,完成域名解析。
- 如果 hosts 里没有这个域名的映射,则查找本地 DNS 解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
- 如果 hosts 与本地 DNS 解析器缓存都没有相应的网址映射关系,首先会找 TCP/IP 参数中设置的首选 DNS 服务器,在此我们叫它本地 DNS 服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
- 如果要查询的域名,不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析,此解析不具有权威性。
- 如果本地 DNS 服务器本地区域文件与缓存解析都失效,则根据本地 DNS 服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地 DNS 就把请求发至 “根 DNS 服务器”,“根 DNS 服务器” 收到请求后会判断这个域名 (.com) 是谁来授权管理,并会返回一个负责该顶级域名服务器的一个 IP。本地 DNS 服务器收到 IP 信息后,将会联系负责 .com 域的这台服务器。这台负责 .com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理 .com 域的下一级 DNS 服务器地址 (qq.com) 给本地 DNS 服务器。当本地 DNS 服务器收到这个地址后,就会找 qq.com 域服务器,重复上面的动作,进行查询,直至找到 www.qq.com 主机。
- 如果用的是转发模式,此 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根 DNS 或把转请求转至上级,以此循环。不管是本地 DNS 服务器用是是转发,还是根提示,最后都是把结果返回给本地 DNS 服务器,由此 DNS 服务器再返回给客户机。
HTTP 协议详解
HTTP 是一种让 Web 服务器与浏览器 (客户端) 通过 Internet 发送与接收数据的协议,它建立在 TCP 协议之上,一般采用 TCP 的 80 端口。它是一个请求、响应协议 -- 客户端发出一个请求,服务器响应这个请求。在 HTTP 中,客户端总是通过建立一个连接与发送一个 HTTP 请求来发起一个事务。服务器不能主动去与客户端联系,也不能给客户端发出一个回调连接。客户端与服务器端都可以提前中断一个连接。例如,当浏览器下载一个文件时,你可以通过点击 “停止” 键来中断文件的下载,关闭与服务器的 HTTP 连接。
HTTP 请求包(浏览器信息
Request 包分为 3 部分,第一部分叫 Request line(请求行), 第二部分叫 Request header(请求头), 第三部分是 body(主体)。header 和 body 之间有个空行,请求包的例子所示:
GET /domains/example/ HTTP/1.1 // 请求行: 请求方法 请求 URI HTTP 协议/协议版本
Host:www.iana.org // 服务端的主机名
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 // 浏览器信息
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 // 客户端能接收的 mine
Accept-Encoding:gzip,deflate,sdch // 是否支持流压缩
Accept-Charset:UTF-8,*;q=0.5 // 客户端字符编码集
// 空行,用于分割请求头和消息体
// 消息体,请求资源参数,例如 POST 传递的参数
HTTP 协议定义了很多与服务器交互的请求方法,最基本的有 4 种,分别是 GET, POST, PUT, DELETE。一个 URL 地址用于描述一个网络上的资源,而 HTTP 中的 GET, POST, PUT, DELETE 就对应着对这个资源的查,增,改,删 4 个操作。我们最常见的就是 GET 和 POST 了。GET 一般用于获取 / 查询资源信息,而 POST 一般用于更新资源信息
HTTP 响应包(服务器信息
HTTP/1.1 200 OK // 状态行
Server: nginx/1.0.8 // 服务器使用的 WEB 软件名及版本
Date: Tue, 30 Oct 2012 04:14:25 GMT // 发送时间
Content-Type: text/html // 服务器发送信息的类型
Transfer-Encoding: chunked // 表示发送 HTTP 包是分段发的
Connection: keep-alive // 保持连接状态
Content-Length: 90 // 主体内容长度
// 空行 用来分割消息头和主体
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... // 消息体
Response 包中的第一行叫做状态行,由 HTTP 协议版本号, 状态码, 状态消息三部分组成。
状态码用来告诉 HTTP 客户端,HTTP 服务器是否产生了预期的 Response。HTTP/1.1 协议中定义了 5 类状态码, 状态码由三位数字组成,第一个数字定义了响应的类别
- 1XX 提示信息 - 表示请求已被成功接收,继续处理
- 2XX 成功 - 表示请求已被成功接收,理解,接受
- 3XX 重定向 - 要完成请求必须进行更进一步的处理
- 4XX 客户端错误 - 请求有语法错误或请求无法实现
- 5XX 服务器端错误 - 服务器未能实现合法的请求
HTTP 协议是无状态的和 Connection: keep-alive 的区别: 无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
HTTP 是一个无状态的面向连接的协议,无状态不代表 HTTP 不能保持 TCP 连接,更不能代表 HTTP 使用的是 UDP 协议(面对无连接)。
从 HTTP/1.1 起,默认都开启了 Keep-Alive 保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的 TCP 连接。
Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同服务器软件(如 Apache)中设置这个时间。
http 包建立 Web 服务器
Go 语言里面提供了一个完善的 net/http 包,通过 http 包可以很方便的就搭建起来一个可以运行的 Web 服务。同时使用这个包能很简单地对 Web 的路由,静态文件,模版,cookie 等数据进行设置和操作。
package main
import (
"fmt"
"net/http"
"strings"
"log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)
fmt.Println(r.Form["url_long"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "Hello astaxie!") // 这个写入到 w 的是输出到客户端的
}
func main() {
http.HandleFunc("/", sayhelloName) // 设置访问的路由
err := http.ListenAndServe(":9090", nil) // 设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
web 工作方式的几个概念
- Request:用户请求的信息,用来解析用户的请求信息,包括 post、get、cookie、url 等信息
- Response:服务器需要反馈给客户端的信息
- Conn:用户的每次请求链接
- Handler:处理请求和生成返回信息的处理逻辑
如下图所示,是 Go 实现 Web 服务的工作模式的流程图
- 创建 Listen Socket, 监听指定的端口,等待客户端请求到来。
- Listen Socket 接受客户端的请求,得到 Client Socket, 接下来通过 Client Socket 与客户端通信。
- 处理客户端的请求,首先从 Client Socket 读取 HTTP 请求的协议头,如果是 POST 方法,还可能要读取客户端提交的数据,然后交给相应的 handler 处理请求,handler 处理完毕准备好客户端需要的数据,通过 Client Socket 写给客户端
Go 是通过一个函数 ListenAndServe 来处理这些事情的,这个底层其实这样处理的:初始化一个 server 对象,然后调用了 net.Listen("tcp", addr),也就是底层用 TCP 协议搭建了一个服务,然后监控我们设置的端口。
详细的整个流程如下图所示
Go 的 http 包详解
Conn 的 goroutine
Go 在等待客户端请求里面是这样写的:
c, err := srv.newConn(rw)
if err != nil {
continue
}
go c.serve()
客户端的每次请求都会创建一个 Conn,这个 Conn 里面保存了该次请求的信息,然后再传递到对应的 handler,该 handler 中便可以读取到相应的 header 信息,这样保证了每个请求的独立性。
Go 代码的执行流程
首先调用 Http.HandleFunc
- 按顺序做了几件事:
1 调用了 DefaultServeMux 的 HandleFunc
2 调用了 DefaultServeMux 的 Handle
3 往 DefaultServeMux 的 map [string] muxEntry 中增加对应的 handler 和路由规则
- 其次调用 http.ListenAndServe (":9090", nil)
按顺序做了几件事情:
1 实例化 Server
2 调用 Server 的 ListenAndServe ()
3 调用 net.Listen ("tcp", addr) 监听端口
4 启动一个 for 循环,在循环体中 Accept 请求
5 对每个请求实例化一个 Conn,并且开启一个 goroutine 为这个请求进行服务 go c.serve ()
6 读取每个请求的内容 w, err := c.readRequest ()
7 判断 handler 是否为空,如果没有设置 handler(这个例子就没有设置 handler),handler 就设置为 DefaultServeMux
8 调用 handler 的 ServeHttp
9 在这个例子中,下面就进入到 DefaultServeMux.ServeHttp
10 根据 request 选择 handler,并且进入到这个 handler 的 ServeHTTP
11 选择 handler:
A 判断是否有路由能满足这个 request(循环遍历 ServeMux 的 muxEntry)
B 如果有路由满足,调用这个路由 handler 的 ServeHTTP
C 如果没有路由满足,调用 NotFoundHandler 的 ServeHTTP