本文仅是介绍 golang web 应用与服务的 hello world 的工作原理,开发工具等。
本文要点:
1. 分布式计算 “C/S架构”知识
2. HTTP 协议基础知识;协议分析工具 curl
3. Golang web 应用搭建、工作原理、源代码阅读、程序设计技巧
4. Web 应用框架,压力测试工具 ab前提条件:
1. OO 思想在 Golang 应用
2. OS 并发基础知识
3. 了解 socket 通讯与 stream
4. 了解 python tornado 框架、Java web 应用服务器框架最好
本文档代码位置:https://github.com/pmlpml/golang-learning/tree/master/web
在分布式计算中,计算机之间协作最基础、最简单的结构就是 C/S 架构 。一个 进程 扮演 Server 提供服务,一个或多个 进程 扮演 Client 发起服务请求。C/S架构( Client–Server model )应用及其广泛,从关系数据库服务、到web应用、电子邮局、FTP服务、以及我们现在见到的大多数 云服务 都是C/S架构应用。
C/S架构通讯模型如图所示:
+----------+ Request -> +----------+
| |--------------------------------->| |
| Client | | Server |
| |<---------------------------------| |
+----------+ <- Response +----------+
这里,我们看出C/S架构通讯非常简单。如同 client 问“你吃过了吗?”,server 回答“吃了红烧肉”,但是,server 不能主动反问“你吃了吗?”。 问题是为什么要做这样的限制……
当然,在许多应用中也需要突破这样的限制,于是就有了一些辅助技术手段… 例如 websocket
C/S架构编程手段很多,用socket直接编程不是很方便吗?如果仅编写一个没有任何工业用途的 “toy” 程序,可以这样做。 但是,一个好的服务程序,必须满足 高可靠、高可用(7*24) 、高性能 、可伸缩 、高开发效率 、安全 、可扩展 等特性。例如,做一个服务全球的订票服务系统开发,就必须支持这些特性。 基于 HTTP 协议的 web service,不仅开发简单,而且能满足产业界的要求,随着“云服务”技术的发展,近几年大规模普及。
The Hypertext Transfer Protocol ( HTTP ) is an application protocol for distributed, collaborative, hypermedia information systems.
HTTP 协议是一个复杂的协议, 支持虚拟主机、消息路由(负载均衡)、分段下载、缓存服务、安全认证等等。 HTTP 也是非常简单文本协议。 客户端与服务器建立 TCP 连接后,客户端发出 Request 文本, 服务器端返回 Response 文本。
HTTP 是应用层协议,传输建立在传输层 TCP 协议基础之上。 例如:用户在浏览器中输入 http://www.sysu.edu.cn/
浏览器与服务器之间发生了什么呢?
www.sysu.edu.cn
得到 121.46.26.52
121.46.26.52:80
发起 TCP
连接请求Accept
客户端请求,建立该连接其中: Request 与 Response 的约定,就是 HTTP 协议。 HTTP/1.1 标准就是 RFC 2616。
Request 文本格式
它是三段式的文本(命令行、header、body)
GET / HTTP/1.1 #第一行:第一个单词 - 方法;第二个单词 - uri; 协议与版本
Host: www.example.com #第2-n行,都是 key:value 格式,称为 headers
...
CRLF #header 结束标识
message-body #按heahder指令处理
Request methods
Request第一行第一个单词( 大写 ), 例如 GET、HEAD、POST、PUT、DELETE等。
URI & URL
统一资源标识(URI),即要访问对象(文件)的路径和参数
统一资源定位符(URL)。例如:
https://www.baidu.com/s?ie=utf-8&wd=toy
协议://主机/文件路径#标签?查询字符串
Header
那就不是几句话能搞定的问题,HTTP的服务功能约定几乎全部由头的语义定义。例如:
Host: www.example.com
表示这台服务器上,叫 www.example.com 域名的 web 服务提供服务,这样实现了一台服务器,一个 IP 地址支持多个域名的 web 应用。当 Apache, Nginx 等服务器收到这个指令,应用把请求交给指定域的服务。
更多Head的信息参考:List of HTTP header fields
又例如,服务器怎么知道的用的手机的型号、操作系统、浏览器类型?
User-Agent: ...
然后,你又问,如何多线程下载呢? 那需要知道 Resopnse 的 headers
Response 文本格式
也是同样的三段式(状态行、headers、body),例如:
HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
ETag: "3f80f-1b6-3e1cb03b"
Content-Type: text/html; charset=UTF-8
Content-Length: 138
Accept-Ranges: bytes
Connection: close
<html>
<head>
<title>An Example Pagetitle>
head>
<body>
Hello World, this is a very simple HTML document.
body>
html>
返回状态
第一行:协议,状态编码,状态名称。
更多参见:List of HTTP status codes
Header
更多Head的信息参考:List of HTTP header fields
Query String
URL的查询字符串是“?”后面以“&”分割的若干 K = V 对。或者 POST 请求正文部分内容。
https://www.baidu.com/s?ie=utf-8&wd=toy
协议://主机/文件路径#标签?查询字符串
浏览器
几乎所有现代浏览器都自带开发者工具,Network 标签就是!
实验: 访问 http://www.sysu.edu.cn/
, 请问:
/2012/cn/index.htm
的 Request 和 response 的 Headers 是?curl
curl 才是 web 开发者最常用的利器。它是一个控制台程序,可以精确控制 HTTP 请求的每一个细节。实战中,配合 shell 程序,我们可以简单,重复给服务器发送不同的请求序列,调试程序或分析输出。curl 是 linux 系统自带的命令行工具。
实验: 用 curl 访问 http://www.sysu.edu.cn/
$curl -v http://www.sysu.edu.cn/
* About to connect() to www.sysu.edu.cn port 80 (#0)
* Trying 121.46.26.52...
* Connected to www.sysu.edu.cn (121.46.26.52) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.sysu.edu.cn
> Accept: */*
>
< HTTP/1.1 200 OK
< Vary: Accept-Encoding
< Content-Type: text/html
< Accept-Ranges: bytes
< ETag: "974272193"
< Last-Modified: Thu, 11 Apr 2013 06:43:52 GMT
< Content-Length: 357
< Date: Sun, 29 Oct 2017 13:53:00 GMT
< Server: lighttpd/1.4.35
<
"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
"http://www.w3.org/1999/xhtml">
"refresh" content="0;url=/2012">
"Content-Type" content="text/html; charset=utf-8" />
中山大学 SUN YAT-SEN UNIVERSITY
* Connection #0 to host www.sysu.edu.cn left intact
curl 才是你未来的工具,浏览器工具是给前端大神玩的,系统工程师表示不屑任何复杂的玩意。这多简单:
第一个符号
*
表示 curl 任务;>
发送的信息;<
返回的信息请问:
本部分任务是通过 golang 的 http 包编写简单程序,了解 web 服务器处理 http 协议的过程。
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)
}
}
用 go run
运行它! 这个时候其实已经在9090端口监听http链接请求了。
打开另一个控制台,用 curl -v http://localhost/
看结果。
$ curl -v http://localhost:9090/
* About to connect() to localhost port 9090 (#0)
* Trying ::1...
* Connected to localhost (::1) port 9090 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:9090
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 29 Oct 2017 23:39:11 GMT
< Content-Length: 14
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
可以换一个地址试试:http://localhost:9090/?url_long=111&url_long=222
上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。
如果你以前是PHP程序员,那你也许就会问,我们的nginx、apache服务器不需要吗?Go就是不需要这些,因为他直接就监听tcp端口了,做了nginx做的事情,然后sayhelloName这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似。
如果你以前是Python程序员,那么你一定听说过tornado,这个代码和他是不是很像,对,没错,Go就是拥有类似Python这样动态语言的特性,写Web应用很方便。
如果你以前是Ruby程序员,会发现和ROR的/script/server启动有点类似。
我们看到Go通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性。
难道有了 go, Nginx、Apache,Lighttpd 就和我们 bye-bye 了?
– “Too Young, too naive”
Web服务的工作模式的流程 图:
web工作方式的几个概念
你暂时可以不用了解分布式技术的细节,以下是服务器端的几个重要概念:
本部分是学习 http 包的一些代码,同时学习面向对象的一些技巧。在应用中学习…
接口回调技术
按 ctrl
键点函数名ListenAndServe
:
这个函数和 Cobra 的 Execcute 类似,创建默认的 Server 数据,调用它的同名方法。
其中,Handler
是一个接口
// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// ...
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
为了解耦(分离) 调用者 Caller 和 执行者 Callee 之间的逻辑,最常用的手段就是面向接口的编程(OO 思想的核心之一)。
这样,用户可以自定义处理逻辑,而服务者只需要知道接口抽象的接口。
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
逻辑分离原理图:
+----------+ use interface +-----------+
| Caller |------------------------>| Interface |
+----------+ +-----------+
^
|
+------------------+
| unknown Callee |
+------------------+
这里,由于 handler = nil 则会调用默认的处理逻辑 DefaultServeMux 的实现。
// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
代码第一句, http.HandleFunc
函数则会构造一个 muxEntry 把对应 URL 处理程序注入 DefaultServeMux
思考: 函数回调 与 接口回调 有什么区别? 使用场景?
golang 的 web 服务流程
ListenAndServe(addr string, handler Handler)
+ server.ListenAndServe()
| net.Listen("tcp", addr)
+ srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
| srv.setupHTTP2_Serve()
| baseCtx := context.Background()
+ for {}
| l.Accept()
| + select ... //为什么
| c := srv.newConn(rw)
| c.setState(c.rwc, StateNew) // before Serve can return
+ go c.serve(ctx) // 新的链接 goroutine
| ... // 构建 w , r
| serverHandler{c.server}.ServeHTTP(w, w.req)
| ... // after Serve
详细文字描述见:Go的http包详解
到 serverHandler{c.server}.ServeHTTP(w, w.req)
实现每个 conn 对应一个 serverHandler 的处理函数。(见前面)
拦截 DefaultServeMux
每个人都有一颗做框架的心,当然你可以重写库,这里研究如何拦截 DefaultServeMux 做一些身份认证工作。
package main
import (
"fmt"
"log"
"net/http"
"strings"
)
type MyMux struct {
defaultMux *http.ServeMux
}
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("do some extension here!")
if p.defaultMux == nil {
p.defaultMux = http.DefaultServeMux
}
p.defaultMux.ServeHTTP(w, r)
return
}
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", &MyMux{}) //设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
运行结果是什么?
为什么要拦截,给一些理由 … …
func pathMatch
实现)由于 DefaultServeMux 仅是一个 demo ,当你需要使用正则表达式、或提取 path 中参数(如: /user/your-name)时,就搞不定了。 Go语言选择了让程序员自由发挥!
(go 库设计原则 - 简单,简单,简单,可扩展!)
经过程序员多年自由发挥,你现在不得不面临海量的选择,有轻量级(做小部件,模仿 tonardo)、中重量(模仿 python Flask, 只提供 MVC 基础服务)、重量级(Java MVC,ORM 等等)。以下仅是建议:
net/http
如果你考虑高性能,请自己测试不同的框架。如何测试?
程序非常简单,它发布了一个 hello user 的 web 服务。请自己阅读代码,它用了哪些库:
main.go
package main
import (
"os"
"github.com/pmlpml/golang-learning/web/cloudgo/service"
flag "github.com/spf13/pflag"
)
const (
PORT string = "8080"
)
func main() {
port := os.Getenv("PORT")
if len(port) == 0 {
port = PORT
}
pPort := flag.StringP("port", "p", PORT, "PORT for httpd listening")
flag.Parse()
if len(*pPort) != 0 {
port = *pPort
}
server := service.NewServer()
server.Run(":" + port)
}
service/server.go
package service
import (
"net/http"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/unrolled/render"
)
// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {
formatter := render.New(render.Options{
IndentJSON: true,
})
n := negroni.Classic()
mx := mux.NewRouter()
initRoutes(mx, formatter)
n.UseHandler(mx)
return n
}
func initRoutes(mx *mux.Router, formatter *render.Render) {
mx.HandleFunc("/hello/{id}", testHandler(formatter)).Methods("GET")
}
func testHandler(formatter *render.Render) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id := vars["id"]
formatter.JSON(w, http.StatusOK, struct{ Test string }{"Hello " + id})
}
}
运行程序:
$ go run ./web/cloudgo/main.go -p9090
[negroni] listening on :9090
[negroni] 2017-10-31T14:37:42+08:00 | 200 | 277.524µs | localhost:9090 | GET /hello/testuser
测试用命令:
$ curl -v http://localhost:9090/hello/testuser
* About to connect() to localhost port 9090 (#0)
* Trying ::1...
* Connected to localhost (::1) port 9090 (#0)
> GET /hello/testuser HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:9090
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=UTF-8
< Date: Tue, 31 Oct 2017 06:37:42 GMT
< Content-Length: 31
<
{
"Test": "Hello testuser"
}
* Connection #0 to host localhost left intact
很酷的程序,程序扩展自然,几乎没有任何累赘。用户程序可读性也好,下一步就是处理输入、输出。
压力测试
安装 Apache web 压力测试程序(以 centos 为例):
yum -y install httpd-tools
执行压力测试:
$ ab -n 1000 -c 100 http://localhost:9090/hello/your
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
...
Server Software:
Server Hostname: localhost
Server Port: 9090
Document Path: /hello/your
Document Length: 27 bytes
Concurrency Level: 100
Time taken for tests: 0.200 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 150000 bytes
HTML transferred: 27000 bytes
Requests per second: 4996.33 [#/sec] (mean)
Time per request: 20.015 [ms] (mean)
Time per request: 0.200 [ms] (mean, across all concurrent requests)
Transfer rate: 731.88 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 4 3.0 3 12
Processing: 1 15 6.7 14 53
Waiting: 1 12 6.5 11 51
Total: 2 19 7.1 18 55
Percentage of the requests served within a certain time (ms)
50% 18
66% 21
75% 22
80% 24
90% 28
95% 32
98% 39
99% 40
100% 55 (longest request)
具体安装、命令参数与结果解释参考:CentOS服务器Http压力测试之ab
通过对 net/http
包解析,你应该对 http 协议的工作原理和实现技术有初步了解,了解一些常用的 web 开发组件。也许你对 golang 的“简单、简单、简单”理念有了更好的理解!简单、高效是有代价的,需要你具备高超的程序设计技能,学习这些技能最好的老师就是 golang 的源代码与优秀框架的设计。