夜雪天狼
学习笔记
技术博文
转载备份
心灵鸡汤
目录
Go Web基础
发布者:caijw
阅读量:60862
发布时间:2018-11-06 17:26:40
# Web工作方式 我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢 1. 浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP 2. 通过IP地址找到IP对应的服务器后,要求建立TCP连接 3. 浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包 4. 客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接 -vertical- ## URL 我们浏览网页都是通过URL访问的,那么URL到底是怎么样的呢? 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 锚 ``` -vertical- ## DNS解析 DNS(Domain Name System)是“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于TCP/IP网络,它是一个从事将主机名或域名转换为实际IP地址的工作的一位“翻译官” -vertical- ## DNS工作原理 1. 在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析 2. 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析 3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性 4. 如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性 5. 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至 “根DNS服务器”,“根DNS服务器”收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机 6. 如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。 -vertical-  Comment: 所谓 递归查询过程 就是 “查询的递交者” 更替, 而 迭代查询过程 则是 “查询的递交者”不变 -separator- # Go搭建Web服务器 上面介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作 -vertical- ## http包建立Web服务器 ```go package main import ( "fmt" "net/http" "strings" "log" ) func helloWorld(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 world!") //这个写入到w的是输出到客户端的 } func main() { http.HandleFunc("/", helloWorld) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil { log.Fatal("ListenAndServe: ", err) } } ``` Comment: 并不需要apache等服务器,因为他直接就监听tcp端口了,做了服务器做的事情,然后helloWorld这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似 -separator- # 小结 介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server -separator- # go如何使得Web工作 前面介绍了如何通过Go搭建一个Web服务,我们可以看到简单应用一个net/http包就方便的搭建起来了 那么Go在底层到底是怎么做的呢?万变不离其宗,Go的Web服务工作也离不开我们开始所介绍的Web工作方式 -vertical- ## web工作方式的几个概念 以下均是服务器端的几个概念 * Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息 * Response:服务器需要反馈给客户端的信息 * Conn:用户的每次请求链接 * Handler:处理请求和生成返回信息的处理逻辑 -vertical- ## 分析http包运行机制 如下图所示,是Go实现Web服务的工作模式的流程图  Comment: 1. 创建Listen Socket, 监听指定的端口, 等待客户端请求到来 2. Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信 3. 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端 -separator- # 问题分析 这整个的过程里面我们只要了解清楚下面三个问题,也就知道Go是如何让Web运行起来了 1. 如何监听端口? 2. 如何接收客户端请求? 3. 如何分配handler? -vertical- ## 如何监听端口 前面小节的代码里面我们可以看到,Go是通过一个函数ListenAndServe来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了net.Listen("tcp", addr),也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口 下面代码来自Go的http包的源码,通过下面的代码我们可以看到整个的http处理过程 ```go func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c, err := srv.newConn(rw) if err != nil { continue } go c.serve() } } ``` -vertical- ## 如何接收客户端请求 上面代码执行监控端口之后,调用了srv.Serve(net.Listener)函数,这个函数就是处理接收客户端的请求信息 这个函数里面写了一个for{},首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务 go c.serve()。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响 -vertical- ## 如何分配handler conn首先会解析`request:c.readRequest()` 然后获取相应的`handler:handler := c.server.Handler`,也就是我们刚才在调用函数ListenAndServe时候的第二个参数 我们前面例子传递的是nil,也就是为空,那么默认获取handler = DefaultServeMux,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,上面也有设置过:调用的代码里面第一句即`http.HandleFunc("/", sayhelloName)` 这个作用就是注册了请求/的路由规则,当请求uri为"/",路由就会转到函数sayhelloName DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端 -vertical- ## 流程示意图  -separator-