分享

HTTP 协议 与 golang web 应用服务

 菌心说 2022-02-13

本文仅是介绍 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

1、C/S 架构

在分布式计算中,计算机之间协作最基础、最简单的结构就是 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,不仅开发简单,而且能满足产业界的要求,随着“云服务”技术的发展,近几年大规模普及。

2、HTTP 协议基础

The Hypertext Transfer Protocol ( HTTP ) is an application protocol for distributed, collaborative, hypermedia information systems.

HTTP 协议是一个复杂的协议, 支持虚拟主机、消息路由(负载均衡)、分段下载、缓存服务、安全认证等等。 HTTP 也是非常简单文本协议。 客户端与服务器建立 TCP 连接后,客户端发出 Request 文本, 服务器端返回 Response 文本。

HTTP 是应用层协议,传输建立在传输层 TCP 协议基础之上。 例如:用户在浏览器中输入 http://www./ 浏览器与服务器之间发生了什么呢?

  1. 浏览器:请求 DNS 解析 www. 得到 121.46.26.52
  2. 浏览器:用 socket 与 服务器 121.46.26.52:80 发起 TCP 连接请求
  3. 服务器:Accept 客户端请求,建立该连接
  4. 浏览器:向服务器写信息(字符流),Request
  5. 服务器:按浏览器的请求,返回客户端信息(字符流)Response
  6. 浏览器:断开连接,让双方释放资源

其中: Request 与 Response 的约定,就是 HTTP 协议。 HTTP/1.1 标准就是 RFC 2616

2.1 HTTP 协议基本格式

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等。

  • GET 读取 uri 指向的信息
  • HEAD 查询 uri 指向的信息
  • PUT 写入 uri 指向的信息
  • DELETE 写入 uri 指向的信息
  • POST 提交表单

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 Page</title>
</head>
<body>
  Hello World, this is a very simple HTML document.
</body>
</html>

返回状态

第一行:协议,状态编码,状态名称。

  • 1xx Informational responses
  • 2xx Success
    • 200 OK
    • 204 No Content
  • 3xx Redirection
    • 301 Moved Permanently
  • 4xx Client errors
    • 403 Forbidden
    • 404 Not Found
  • 5xx Server errors
    • 500 Internal Server Error
    • 501 Not Implemented

更多参见: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

协议://主机/文件路径#标签?查询字符串

2.2 HTTP 协议工具

浏览器

几乎所有现代浏览器都自带开发者工具,Network 标签就是!

实验: 访问 http://www./ , 请问:

  • 这个网页总共发出多少次请求?
  • 访问 /2012/cn/index.htm 的 Request 和 response 的 Headers 是?
  • User-Agent 的内容是?

curl

curl 才是 web 开发者最常用的利器。它是一个控制台程序,可以精确控制 HTTP 请求的每一个细节。实战中,配合 shell 程序,我们可以简单,重复给服务器发送不同的请求序列,调试程序或分析输出。curl 是 linux 系统自带的命令行工具。

实验: 用 curl 访问 http://www./

$curl -v http://www./
* About to connect() to www. port 80 (#0)
*   Trying 121.46.26.52...
* Connected to www. (121.46.26.52) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.
> 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
<
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html xmlns='http://www./1999/xhtml'>
<head>
<meta http-equiv='refresh' content='0;url=/2012'>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
<title>中山大学 SUN YAT-SEN UNIVERSITY</title>


</html>
* Connection #0 to host www. left intact

curl 才是你未来的工具,浏览器工具是给前端大神玩的,系统工程师表示不屑任何复杂的玩意。这多简单:

第一个符号

  • * 表示 curl 任务;
  • > 发送的信息;
  • < 返回的信息

请问:

  • 中大 web 服务器是什么软件
  • 这是中大的首页吗?

3、编写简单的 web 应用

本部分任务是通过 golang 的 http 包编写简单程序,了解 web 服务器处理 http 协议的过程。

3.1 搭建简单 web 服务器

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”

3.2 HTTP 服务器运行机制

Web服务的工作模式的流程 图:

Web服务

  1. 创建一个 ServerSocket,绑定 地址和端口,侦听。等待客户端连接;
  2. 客户端创建 Socket, 连接
  3. 服务端 Accept,生成对应的 Socket
    • 如果你网络课编写过 Socket 通讯,认为简单那就错了!
    • 服务器要服务 100K+ 的连接,可以吗? 需要知道 epoll 或 iocp ,即 non-block 通讯!
    • 当然,你现在可以忽视它们,因为几乎所有 web 服务器都使用了它们。
  4. 与客户端通讯的 FD 有了,建立端对端通讯的方案有(以下名词可能让你迷惑):
    • 进程服务客户(典型 FastCGI)
    • 线程(典型 Java web 服务,如 Tomcat)
    • 协程 + 异步回调(典型 nodejs)
    • 或单进程阻塞应用(python tornado) + WSGI
  5. 端对端处理一个 Request 与 Response

web工作方式的几个概念

你暂时可以不用了解分布式技术的细节,以下是服务器端的几个重要概念:

  • Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
  • Response:服务器需要反馈给客户端的信息
  • Conn:用户的每次请求链接
  • Handler:处理请求和生成返回信息的处理逻辑

3.3 golang 处理 HTTP 请求的过程

本部分是学习 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)
    }
}

运行结果是什么?

为什么要拦截,给一些理由 … …

  1. DefaultServeMux 功能太有限 (why? 提示参见代码:func pathMatch 实现)

4、 golang web 开发框架

由于 DefaultServeMux 仅是一个 demo ,当你需要使用正则表达式、或提取 path 中参数(如: /user/your-name)时,就搞不定了。 Go语言选择了让程序员自由发挥!
(go 库设计原则 - 简单,简单,简单,可扩展!)

4.1 web 框架与选择综合症

经过程序员多年自由发挥,你现在不得不面临海量的选择,有轻量级(做小部件,模仿 tonardo)、中重量(模仿 python Flask, 只提供 MVC 基础服务)、重量级(Java MVC,ORM 等等)。以下仅是建议:

如果你考虑高性能,请自己测试不同的框架。如何测试?

4.2 开发产品级 cloudgo 应用

程序非常简单,它发布了一个 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

3、小结

通过对 net/http 包解析,你应该对 http 协议的工作原理和实现技术有初步了解,了解一些常用的 web 开发组件。也许你对 golang 的“简单、简单、简单”理念有了更好的理解!简单、高效是有代价的,需要你具备高超的程序设计技能,学习这些技能最好的老师就是 golang 的源代码与优秀框架的设计。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多