ウェブサービス設計基礎

Last renew: April 7, 2022 pm

How the Web Works?

注:本文仅为个人学习笔记,无任何版权。

平时在上网的时候,打开浏览器,输入网址,按下回车,显示想要的内容。这个过程电脑端与服务器端是怎样实现的呢?

对于普通的上网过程,系统是这样做的:浏览器是一个客户端(client-site),当你输入URL时,首先浏览器会请求DNS服务器,通过DNS服务器获取域名所对应的IP,然后通过IP地址找到Ip对应的服务器,要求建立TCP链接,等浏览器发送完HTTP Request(请求)包之后,服务器(server-site)接收到请求包,处理请求包,调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包中的主体(body),等收到全部的内容后端开与服务器之间的TCP链接。3.1.web2

一个web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端一般指的是web浏览器。

web服务器端的功能可以被总结为以下几点

  1. 客户端通过TCP/IP协议建立到服务器的TCP连接。
  2. 客户端向服务器发送HTTP协议请求包(HTTP Request),请求服务器里的资源文档。
  3. 服务器向客户端发送HTTP协议应答包(HTTP Response),如果请求的资源包含动态语言,服务器会调用动态语言的解释引擎处理“动态内容”,并将处理得到的数据返回给客户端。
  4. 客户端与服务器断开,由客户端解释HTML文档,在客户端屏幕上渲染图形结果。

注:客户端与服务器之间的通讯是非持久性的,服务器发送了应答之后就会与客户端断开连接,等待下一次请求。

URLとDNS

略,Computer Science:an Overview讲过

注:DNS使用的是递归查询过程+迭代查询过程,最后获取IP地址

HTTP Protocol

HTTP协议是web工作的核心。想要了解web的工作原理,就要了解HTTP是怎么工作的。

HTTP(Hyper Text Transfer Protocol) 超文本传输协议:客户端可能发送给服务器什么样的消息以及得到什么样的响应。

HTTP是一种让web服务器与浏览器(客户端)通过互联网(Internet)发送与接收数据的协议,它建立在TCP协议之上,一般采用TCP的80端口。它是一个请求与响应的协议:客户端发出请求,服务器响应请求。服务器不能主动联系客户端,也不能给客户端发出回调链接。客户端与服务器端都可以提前中断一个连接。

HTTP协议是无状态的,同一个客户端的这次请求和上次请求没有任何对应关系。服务器并不知道这两个请求是否来自同一个客户端。为了解决这个问题,web程序引入了cookie机制来维护连接的可持续状态

HTTP Requests (Client Site)

对于客户端发出的HTTP请求包,其结构分为三部分。第一部分为Request line(请求行),第二部分叫request header(请求头),第三部分是body(主体)。header 和 body之间有个空行,如下所示。

1
2
3
4
5
6
7
8
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 //客户端能接收的MIME
Accept-Encoding:gzip,deflate,sdch //是否支持流压缩
Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集
//空行,用于分割请求头和消息体
//消息体,请求资源参数,例如POST传递的参数

HTTP协议定义了许多与服务器交互的请求方法,最基本的有4种,GET/ POST/ PUT/ DELETE。一个URL地址用于描述一个网络上的资源,而HTTP的四个基本操作对应着对这个资源的查,增,改,删。

GET一般用于获取/查询资源信息,POST一般用于更新资源信息。

GET与POST的区别如下:

  1. GET的请求body为空,而POST的body不为空
  2. GET提交的数据会放到URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx? name=test1&id=123456POST方法则是把提交的数据放到HTTP包的body中。
  3. GET提交的数据大小有限制,POST无限制
  4. GET提交数据会带来安全问题(URL会显示账号密码,类似zoom的登陆URL)

HTTP Response (Server site)

HTTP的response包结构如下

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK						//状态行
Server: nginx/1.0.8 //服务器使用的WEB软件名及版本
Date: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"... //消息体

其第一行叫做状态行,由HTTP协议版本号,状态码,状态消息三部分组成

状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response。对于HTTP/1.1版本来说,状态码情况如下:

  • 1XX 提示信息 - 表示请求已被成功接收,继续处理
  • 2XX 成功 - 表示请求已被成功接收,理解,接受
  • 3XX 重定向 - 要完成请求必须进行更进一步的处理
  • 4XX 客户端错误 - 请求有语法错误或请求无法实现
  • 5XX 服务器端错误 - 服务器未能实现合法的请求

本质上还是被分为Header(包含状态行)与Body。

HTTP と Connection: keep-alive

从HTTP/1.1起,默认开启了Keep-Alive保持连接特性,其含义是当一个网页完成打开后,客户端与服务器之间用于传输的TCP连接不会被关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的TCP连接。

Keep-Alive不会永久保持,有一个保持时间,可以设定。

Goを使ってウェブサーバーをつくよう!

对于Go语言来说,其提供给了我们一个完整的net/http包,这样可以很方便的搭建起一个可以运行的web服务。这个使用包也能很简单的对于web的router、静态文件、模版、cookie等数据进行操作和设置。

Use net/http Package to Create a Web Server

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
package main

import (
"fmt"
"log"
"net/http"
"strings"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // 解析参数,default是不解析的。
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息。
fmt.Println("path", r.URL.Scheme)
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 XiaoGeAmadeus!") // 将你想说的话写入w,之后会在客户端输出。
}

func main() {
http.HandleFunc("/", sayhelloName) //设置访问的路由
err := http.ListenAndServe(":9090", nil) //设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

上面的代码,当我们build之后,就已经在9090端口监听http链接请求了。

可以在浏览器输入http://localhost:9090进行实验。

Goがウェブを動かすの方法

我们可以看见简单应用net/http包就可以很轻松的用Go搭建起web服务。那么go在底层是怎么做的呢?

Several concepts of how the web works

Request:用户请求的信息,用来解析用户的请求信息,包括POST、GET、cookie、url等信息。

Response:服务器需要反馈给客户端的信息

Conn:用户的每次请求链接

Handler:处理请求和生成返回信息的处理逻辑

Analyze the http package operation mechanism

  1. 创建Listen Socket,监听指定的端口,等待客户端请求到来。(Socket是什么)
  2. Listen Socket接受客户端的请求,得到Client Socket,接下来通过Client Socket与客户端通信。
  3. 处理客户端的请求,首先从Client Socket读取HTTP请求的协议头,如果是POST方法,还需要读取客户端提交的数据,然后交给相应的handler处理请求,handler处理完毕准备好客户端需要的数据,通过Client Socket写给客户端。

在整个过程中,我们只需要了解三个问题,就可以明白Go是如何让Web运行起来的。

  1. 如何监听端口?
  2. 如何接收客户端请求?
  3. 如何分配handler?
How to Listen the Part

从前面小节的代码中我们可以看出,对于监听来说,net/http包中有一个专用的APIListenAndServe来进行处理。

1
2
3
4
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

ListenAndServe会初始化一个sever对象,然后调用了Server对象的方法ListenAndServe。其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (srv *Server) ListenAndServe() error{
//对于Server对象的ListenAndServe方法。
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
//若err值不为空(出现了错误值)
return err
//返回错误值
}
return srv.Serve(ln)
}

ListenAndServe调用了net.Listen("tcp, addr"),也就是底层用TCP协议搭建了一个服务,最后调用src.Serve监控我们设置的端口。监控之后如何接受客户端的请求呢?

How to Accept the Request from client-side

Serve的具体实现如下(仅包括关键代码),通过下面的分析源码我们可以看到客户端请求的具体处理过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (srv *Server) Serve(l net.Listener) error {
...

ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
...
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, stateNew, runHooks) //before Serve can return
go c.serve(connCtx)
}
}

这个函数利用了for循环,首先通过Listener接受请求l.Accept(),其次创建一个Conn:c := srv.newConn(rw) ,最后单独开了一个goroutine,把这个请求的数据当成参数扔给这个conn去服务:go c.serve(connCtx)。这个就是对于高并发的体现。用户的每一次请求都是在一个新的goroutine去服务,相互不影响。

How to distribute the function to solve request.

那么如何分配具体的函数来处理请求呢?我们继续分析conn的serve方法,其源码如下(仅展示关键代码):

1
2
3
4
5
func (c *conn) serve(ctx context.Context) {
...

ctx, cancelCtx := context.WithCancel(ctx)
}