BOBBAIcl

BOBBAIcl 查看完整档案

广州编辑中山大学  |  软件工程 编辑  |  填写所在公司/组织填写个人主网站
编辑

bobbai

个人动态

BOBBAIcl 发布了文章 · 2020-12-20

Kubernetes 1.19 国内镜像源部署,部署前后端服务,以及 Gitlab的helm部署

本篇博客将详细介绍如何在云服务器上搭建 Kubernetes 1.19 集群,以及在我们部署好的集群上使用 Helm 部署 gitlabminio 服务。

部署环境

  • 两台云服务器

    • 配置为:

      • 4 核 8GB 内存
      • 2 核 4GB 内存
    • 操作系统:ubuntu 18.04

目录

  • 搭建 Kubernetes 1.19 集群
  • 使用 helm 部署 gitlab 与 minio

1. 搭建 Kubernetes 1.19 集群

我的master节点设定为4核8gb的机器,这里称其为master,另一台机称为worker。

Step 1: 进入root用户的身份(master)

su

输入root的密码即可。

Step 2: 安装docker(master和worker都要操作)

关于 docker 的安装建议参考官方文档. 这里给出快速安装的脚本:

curl -fsSL https://get.docker.com | sudo sh -s -- --mirror Aliyun
sudo usermod -aG docker $USER
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
 "exec-opts": ["native.cgroupdriver=systemd"],
 "log-driver": "json-file",
 "log-opts": {
 "max-size": "100m"
 },
 "storage-driver": "overlay2",
 "registry-mirrors": ["https://t9ab0rkd.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

Step3: 安装 Kubernetes 三件套: kubelet kubeadm kubectl(仅在master)

# 添加并信任APT证书
curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -

# 添加源地址
add-apt-repository "deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main"

# 更新源并安装最新版 kubenetes
sudo apt update && apt install -y kubelet kubeadm kubectl

# 添加 completion,最好放入 .bashrc 中
source <(kubectl completion bash)
source <(kubeadm completion bash)

Step4: 关闭 swap(master和worker都要操作)

这里由于腾讯云主机是默认关闭 swap 的,如果是其他主机,则执行以下操作并重启机器:

$ vim /etc/fstab
// 注释掉带有swap的一行
# UUID=xxxxxxxxx none            swap    sw              0       0

Step5: 启动 master(仅在master操作)

kubeadm init --image-repository='registry.cn-hangzhou.aliyuncs.com/google_containers'

这里指定了镜像源为阿里云,所以能够避免从 k8s.gcr.io 这个官方默认源拉取时失败。执行成功之后,会生成一串类似于如下格式的信息:

kubeadm join 172.16.0.15:6443 --token qi9srp.7sudphpyhmkm8sw6 \
    --discovery-token-ca-cert-hash sha256:a30870d86f7d19aff4d57a2dd64d58736012dfa3b3d5b53c0e45413ba8f039bf 

这串信息稍后会在加入worker节点的时候用到。

Step6: 配置读取路径(仅在master操作)

export KUBECONFIG=/etc/kubernetes/admin.conf

Step7:安装网络插件(仅在master操作)

kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d 'n')" 

Step8:将 worker 节点加入到集群(仅在worker 操作)

如果有保存在master节点执行 kubeadm init 生成的信息,即:

kubeadm join 172.16.0.15:6443 --token qi9srp.7sudphpyhmkm8sw6 \
    --discovery-token-ca-cert-hash sha256:a30870d86f7d19aff4d57a2dd64d58736012dfa3b3d5b53c0e45413ba8f039bf 

可以直接复制过来使用. 没有保存的话,--token 可以通过 kubeadm token list 获取;--discovery-tolen-ca-cert-hash 可以通过下面的命令获取:

openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | sha256sum | awk '{print $1}'

Step9: 验证执行结果

2. 通过 yaml 文件部署前后端应用

由于服务计算的第九次作业开发了一个前后端分离的应用程序,并打包成了 docker 镜像,所以正好可以利用搭建好的集群部署前后端应用。查阅了 Kubernetes 官方文档后,我决定分别为前后端服务配置一个 Deployment 类型的资源和 Service 类型的资源。Deployment 类型资源负责管理 pod 的生命周期,调整动态伸缩的尺度,Service 负责暴露 Deployment 所对应的容器的端口到公网,使得服务可以在集群外部被访问。
下面介绍 yaml 文件的写法。

bbs_backend 的部署

Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bbs-backend
spec:
  selector:
    matchLabels:
      app: bbs-backend
      tier: backend
      track: stable
  replicas: 3       # 副本数
  template:
    metadata:
      labels:
        app: bbs-backend
        tier: backend
        track: stable
    spec:
      containers:
        - name: bbs-backend
          image: "docker.io/bobbai/bbs_backend"  # 这里pod要拉取的镜像 
          ports:
            - name: port
              containerPort: 5000   # 这里指定了基于镜像创建容器之后服务所在的端口
Service
apiVersion: v1
kind: Service
metadata:
  name: bbs-backend-service
spec:
  selector:
    app: bbs_backend
    tier: backend
  ports:
  - protocol: "TCP"        # 服务提供的协议
    port: 5000             # service暴露在cluster ip上的端口,<cluster ip>:port 是提供给集群内部客户访问service的入口。
    targetPort: 5000       # targetPort是pod上的端口,从port和nodePort上到来的数据最终经过kube-proxy流入到后端pod的targetPort上进入容器
    nodePort: 30086        # 暴露在公网的端口,服务可以通过 <master ip>:nodePort 访问
  type: NodePort

bbs_frontend 的部署

这里yaml文件与后端部署所用到的类似,不再重复,直接上文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: bbs-frontend
spec:
  selector:
    matchLabels:
      app: bbs-frontend
      tier: frontend
      track: stable
  replicas: 3
  template:
    metadata:
      labels:
        app: bbs-frontend
        tier: frontend
        track: stable
    spec:
      containers:
        - name: bbs-frontend
          image: "docker.io/bobbai/bbs_frontend:k8s"
          ports:
            - name: port
              containerPort: 3000

---

apiVersion: v1
kind: Service
metadata:
  name: bbs-frontend-service
spec:
  selector:
    app: bbs-frontend
    tier: frontend
  ports:
  - protocol: "TCP"
    port: 3000
    targetPort: 3000
    nodePort: 30087
  type: NodePort

执行:

kubectl apply -f bbs_backend -n bbs
kubectl apply -f bbs_frontend -n bbs

后,可以通过 dashboard 查看服务是否部署成功:
截屏2020-12-20 上午12.05.16.png

3. Gitlab 的 Helm 部署

Gitlab 是什么?Gitlab 是一个非常著名的开源项目管理工具。除了具有管理进度,项目版本等基本功能之外,gitlab还附带有一个强大的 CI/CD 系统,这使得项目的自动化测试,部署变得十分方便。

为什么要是用 Helm 进行部署呢?按照官方文档提供的,使用 yaml 文件部署一个服务的话,正常来说需要编写 yaml 文件来声明相应的 Deployment, Service, PersistentVolumeClaim, PersistentVolume 等等资源。如果后续要对于资源的一些属性,比如 PersistentVolume 的挂载目录,卷的大小等等就会比较麻烦。Helm 则提供了一种模版化的方式,将服务的属性提取到一个统一的文件中,然后只需要修改一次,就能在整个服务中生效,同时它也为Kubernetes 生态中的服务提供了包管理的功能,方便用户在自己的集群下快速部署提供了 helm chart 的服务。
关于 Helm 的安装,可以参考官方文档,这里不再详细说明.

(1) Gitlab 的 Helm 部署

step1: 添加 helm 源(在master执行)

helm repo add stable https://kubernetes-charts.storage.googleapis.com
helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
helm repo add apphub https://apphub.aliyuncs.com/

step2: 下载 gitlab-ce 的 charts

helm pull stable/gitlab-ce
// 目录下会生成一个 gitlab-ce-版本号的压缩包,通过tar命令解压即可
tar  -zxvf  gitlab-ce-xxx.tgz

解压后应当生成了一个名为 gitlab-ce 的目录

step3: 进入解压后的目录,并根据需求修改 values.yaml 文件

cd gitlab-ce
vim values.yaml

截屏2020-12-13 下午10.35.42.png
这几处需要我们根据需求来修改,externalUrl 可以随意写,后续可以修改; 由于我并没有购置额外的load balancer 服务,所以想要暴露服务,只能使用 NodePort 类型。

step4:适配 1.19 集群(如果您的集群是1.16版本之前的,则这一步可以忽略)

kubernetes 每个版本的yaml文件版本都有所不同,需要我们根据自己集群的版本进行适配,即修改 helm/template 中的一些 yaml 文件。

  • 修改1: vim gitlab-ce/templates/deployment.yaml

添加:
截屏2020-12-18 上午10.39.02.png

  • 修改2: vim gitlab-ce/charts/postgresql/templates/deployment.yaml

添加:
截屏2020-12-18 上午10.41.14.png

  • 修改3:vim gitlab-ce/charts/redis/templates/deployment.yaml

截屏2020-12-18 上午10.43.45.png

step5:创建 PersistentVolume

由于gitlab需要将 postgres, redis 的数据持久化,因此如果此时根据gitlab-ce的charts来创建服务,由于我们并没有声明对应到gitlab自带的 PersistentVolumeClaim 对应的挂载目录下的 PersistentVolume, 服务就无法正常启动。因此,我们首先创建四个文件夹:

mkdir -p /data/gitlab/pv{1..4}

然后创建4个声明pv的yaml文件,模版如下:

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: gitlab-pv1
  namespace: gitlab
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Gi
  hostPath:
    path: /data/gitlab/pv1
  persistentVolumeReclaimPolicy: Retain
  volumeMode: Filesystem

然后依次 apply 这些 yaml 文件:

kubectl apply -f pv1.yaml
kubectl apply -f pv2.yaml
kubectl apply -f pv3.yaml
kubectl apply -f pv4.yaml

随后查看 pv 的状态
截屏2020-12-18 上午10.54.05.png
证明创建成功

step6:创建服务

输入:

helm install gitlab gitlab-ce -n gitlab

这里,-n 参数指定了服务的命名空间,您可以根据需求自行更改。

查看服务状态:
截屏2020-12-18 上午11.04.55.png
证明服务创建成功,我们访问 http://master IP:32363
截屏2020-12-18 上午11.06.11.png
证明部署成功。

查看原文

赞 0 收藏 0 评论 0

BOBBAIcl 发布了文章 · 2020-11-18

Golang 源码阅读 --- net/http 与 mux

前言

在《服务计算》的第一堂课上,潘老师就强调:golang是为服务而生的语言。如今最流行的服务莫过于 http 服务,而golang官方也用其极其简洁的写法和优秀的服务特性(如高并发)向开发者们证明了这一点。这篇博客正是对于不使用第三方库,仅使用官方提供的程序包: net/http, 搭建http服务的原理,即背后的源码和逻辑的分析。同时,我也会简要的分析一个很常用的库 mux 的实现。

从简单的 Http Server 开始

golang 运行一个 http server 非常简单,需要这样几个部分:

  • 声明定义若干个 handler(w http.ResponseWriter, r *http.Request), 每个 handler 也就是服务端提供的每个服务的逻辑载体。
  • 调用 http.HandleFunc 来注册 handler 到对应的路由下。
  • 调用 http.ListenAndServe 来启动服务,监听某个指定的端口。

按照上面的三个步骤,我实现了一个最简单的 http server demo, 如下:

package main

import (
   "fmt"
   "net/http"
 )
func helloHandler(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello world")
}
func main(){
   http.HandleFunc("/", helloHandler)
   http.ListenAndServe(":3000", nil)
}

当我们运行这段代码, 并且用浏览器访问http://localhost:3000/ 时,就能如愿看到 helloHandler 中写入的 "Hello world". 每当一个请求到达我们搭建的http server后,客户端定义的请求体和请求参数是如何被解析的呢?解析之后又是如何找到helloHandler呢?我们来一步步探索 ListenAndServe 函数以及 HandleFunc 函数。

http.ListenAndServe 的工作机制

根据源码,ListenAndServe 要做两个工作:

  • 通过 Listen 函数建立对于本地网络端口的 Listener
  • 调用 Server 结构体的 Serve 函数来监听

关于 Listen 函数的实现和 Listener 的定义本篇博客并不讨论,我们重点来看 Serve 函数的实现。

// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
   // ...
   // 这里节选了比较关键的,与请求相关的实现
   for {
   // step1. 通过listener, 接受了一个请求
      rw, err := l.Accept()
      if err != nil {
         select {
         case <-srv.getDoneChan():
            return ErrServerClosed
         default:
         }
         if ne, ok := err.(net.Error); ok && ne.Temporary() {
            if tempDelay == 0 {
               tempDelay = 5 * time.Millisecond
 } else {
               tempDelay *= 2
 }
            if max := 1 * time.Second; tempDelay > max {
               tempDelay = max
            }
            srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
            time.Sleep(tempDelay)
            continue
 }
         return err
      }
      connCtx := ctx
      if cc := srv.ConnContext; cc != nil {
         connCtx = cc(connCtx, rw)
         if connCtx == nil {
            panic("ConnContext returned nil")
         }
      }
      tempDelay = 0
 // step2. 确认请求未超时之后,创建一个conn 对象
 c := srv.newConn(rw)
      c.setState(c.rwc, StateNew) // before Serve can return
      
 // step3. 单独创建一个gorouting, 负责处理这个请求
 go c.serve(connCtx)
   }
}

通过代码,Serve 的主要任务就是从listener中接收到请求,根据请求创建一个conn, 随后单独发起一个gorouting来进一步处理,解析,响应该请求。根据conn结构体的serve方法,负责解析 request 的函数是func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error), 这个函数将请求头和请求体中的字段放到 Request 结构体中并返回。
以上就是 ListenAndServe 的工作机理。

http.HandleFunc 的工作机制

每个http server都有一个ServerMux结构体类型的实例,该实例负责将请求根据定义好的pattern来将请求转发到对应的 handlerFunc 来处理。ServerMux 的结构体定义如下:

type ServeMux struct {
 mu    sync.RWMutex                 // 锁,负责处理并发
 m     map[string]muxEntry          // 由路由规则到handler的映射结构
 es    []muxEntry // slice of entries sorted from longest to shortest.
 hosts bool // whether any patterns contain hostnames
}

muxEntry的定义如下:

type muxEntry struct {
   h       Handler  // h 是用户定义的handler函数
   pattern string   // pattern 是路由匹配规则
}

Handler 类型是一个 interface 类型,只需要实现 func(w http.ResponseWriter, r *http.Request), 即:

type Handler interface {
   ServeHTTP(ResponseWriter, *Request)
}

当我们传入 pattern: "/" handlerFunc: helloHandler 后,DefaultMux 调用 HandleFunc, HandleFunc 调用 handle负责将我们定义的 pattern 和 handlerFunc 转换为 muxEntry, 代码实现如下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
   mux.mu.Lock()
   
   // 对于输入的 pattern 和 handler 进行校验
   defer mux.mu.Unlock()
   if pattern == "" {
      panic("http: invalid pattern")
   }
   if handler == nil {
      panic("http: nil handler")
   }
   if _, exist := mux.m[pattern]; exist {
      panic("http: multiple registrations for " + pattern)
   }
   
   // 初始化 muxEntry 和模式的映射
   if mux.m == nil {
      mux.m = make(map[string]muxEntry)
   }
   
   // 初始化 muxEntry
   e := muxEntry{h: handler, pattern: pattern}
   mux.m[pattern] = e
   if pattern[len(pattern)-1] == '/' {
      mux.es = appendSorted(mux.es, e)
   }
   if pattern[0] != '/' {
      mux.hosts = true
 }
}

可以看到,对于注册的handler, 传入 ServerMux 之后需要首先进行输入校验:pattern 和 handler 函数皆不能为空,同时不能重复注册同一个 pattern 对应的多个handler函数;完成校验以后,初始化 muxEntry 项,随后根据 pattern 传入 handler 即可。
以上就是注册路径与handler的过程。

http.ListenAndServe 与 http.HandleFunc 的耦合

介绍了请求的解析和handler的注册之后,解析后的 request 是怎样寻找到相应的 handler的呢?根据源码,这一过程通过 ServeMux 的方法:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
来实现。可以看到,这个函数根据解析后的请求 rmux 中寻找, 返回对应的 Handlerpattern. 这一机制的实现如下:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
   // CONNECT requests are not canonicalized.
   // 如果该请求未 CONNECT 方法的请求,则需要额外处理
 if r.Method == "CONNECT" {
      // If r.URL.Path is /tree and its handler is not registered,
 // the /tree -> /tree/ redirect applies to CONNECT requests // but the path canonicalization does not. 
 if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
         return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
      }
      return mux.handler(r.Host, r.URL.Path)
   }
   // All other requests have any port stripped and path cleaned
 // before passing to mux.handler. host := stripHostPort(r.Host)
 // 首先对于原有请求路径进行截取处理
   path := cleanPath(r.URL.Path)
   // If the given path is /tree and its handler is not registered,
 // redirect for /tree/. 
 if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
      return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
   }
   if path != r.URL.Path {
      _, pattern = mux.handler(host, path)
      url := *r.URL
      url.Path = path
      return RedirectHandler(url.String(), StatusMovedPermanently), pattern
   }
   return mux.handler(host, r.URL.Path)
}

可以看到,Handler 的作用是对于请求路径做处理,如果处理之后与请求中的路径不匹配则会直接返回状态
StatusMovedpermanently. 当通过上述验证后会进入 mux.handler 函数。匹配的主要逻辑都写在 handler 函数中,handler 函数的实现如下:

// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
   mux.mu.RLock()
   defer mux.mu.RUnlock()
   // Host-specific pattern takes precedence over generic ones
 if mux.hosts {
      h, pattern = mux.match(host + path)
   }
   if h == nil {
      h, pattern = mux.match(path)
   }
   if h == nil {
      h, pattern = NotFoundHandler(), ""
 }
   return
}

应用 ---- HTTP中间键

读过了源代码之后,我们可以利用golang中http server的工作特性开发更多现代服务端开发中常用的组件。在 Matrix 开发团队进行服务端开发的过程中,用到了nodejs的koa框架,这个框架的显著特点就是轻量,并且很方便的使用 中间件 的特性。这里我们也可以定义golang http开发中的中间件。
要实现中间键,我们需要满足以下两个特性:

  • 中间键需要定义和使用于 解析请求最终的handler之间
  • 中间键需要能够相互嵌套

给予我们上述对于golang http 服务的原理讨论,我们知道:http.HandleFunc 能够将特定的 handler 绑定在某个或者某一类 URL 上,它接受两个参数,一个参数是pattern, string 类型,另一个参数是一个函数,http.Handler 类型。不难想到,要实现中间键,我们只需要实现一个函数签名如下的函数作为中间键:

func (http.Handler) http.Handler

其中,任何实现了ServeHTTP(ResponseWriter, *Request) 这个函数的变量都是一个 http.Handler 类型的变量,所以我们的中间件既能够作为接收handler为参数从而在解析请求和传入的handler中实现,又能够嵌套多个中间键,形成调用链。
可是当我们定义了一个 func(ResponseWriter, *Request) 签名的函数,直接在中间件中返回会遇到类型不匹配的报错。这里就涉及到一个 golang 类型转换的技巧。net/http 包下的 HandlerFunc 是这样定义的:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

首先,Handlerfunc 是一个类型,每一个类型都可以被定义其方法,这里,HandlerFunc 类型下 ServeHTTP 方法已经被实现好。而当我们使用 golang 中的类型转换语法,即 Type(val), 我们可以通过 HandlerFunc(function_define_by_ourselves)来将我们定义的函数转换为 HandlerFunc 类型,又因为 HandlerFunc 实现了 Handler 这个 Interface 要求的 ServeHTTP 函数,因此我们只需要在中间件中返回 return HandlerFunc(func(ResponseWriter, *Request)),就可以巧妙的完成类型转换。

package main
import (
 "fmt"
 "log" 
 "net/http"
)

func middleware1(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      log.Println("middleware1")
      next.ServeHTTP(w, r)
   })
}
func middleware2(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      log.Println("middleware2")
      next.ServeHTTP(w, r)
   })
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello world")
}
func main(){
   mux := http.NewServeMux()
   finalHandler := http.HandlerFunc(helloHandler)
   mux.Handle("/", middleware1(middleware2(finalHandler)))
   err := http.ListenAndServe(":3000", mux)
   log.Println(err)
}

拓展 ---- github.com/gorilla/mux 包简要解析

基于我们上面的解析可以看到,使用net/http 注册handler, 搭建简单的 http server 并不复杂,只需要将每个特定的 pattern 映射到特定的 handler 即可。然而在设计api的过程中,pattern 并不是一个固定的字符串,而是需要匹配一系列具有相同模式的url(e.g. /api/users/:user_id, 所有符合这个模式的url, 比如 /api/users/1, /api/users/2, 都需要使用相同的控制器来处理)。第三方程序包 mux为这个特性提供了良好的支持。
相比于 net/http 包中的多路复用器的 HandlerFunc, mux 的 多路复用器HandlerFunc 进行了一些拓展,首先多路服务器的数据结构定义如下:

type Router struct {
   // Configurable Handler to be used when no route matches.
 NotFoundHandler http.Handler
 // Configurable Handler to be used when the request method does not match the route.
 MethodNotAllowedHandler http.Handler
 // Routes to be matched, in order.
 routes []*Route
 // Routes by name for URL building.
 namedRoutes map[string]*Route
 // If true, do not clear the request context after handling the request.
 // // Deprecated: No effect, since the context is stored on the request itself. KeepContext bool
 // Slice of middlewares to be called after a match is found
 middlewares []middleware
 // configuration shared with `Route`
 routeConf
}

相比 ServeMux, Router 结构增加了中间件成员 middleware 同时新定义了 Route 结构对于 muxEntry 做了拓展,如下:

type Route struct {
 // Request handler for the route.
 // 这里继承了 Handler 接口,所以Route可以作为 http.Handler 类型的参数
 handler http.Handler
 // If true, this route never matches: it is only used to build URLs.
 buildOnly bool
 // The name used to build URLs.
 name string
 // Error resulted from building a route.
 err error
 // "global" reference to all named routes
 // 增加了一个 namedRoutes 成员,从而能够支持嵌套路由
 namedRoutes map[string]*Route
 // config possibly passed in from `Router`
 routeConf
}

上面分析请求与handler的耦合时,我们提到了 ServeHTTP 函数,任何实现了这个函数的接口都是一个 http.Handler 类型变量。通过这种设计,第三方的库,比如mux可以很容易的与调用http.ListenAndServe 的服务端进行对接,处理请求。mux 最大的优势是支持 url 的模式匹配的逻辑实现在 Match 函数,在 ServeHTTP 函数中调用 Match 函数即可根据请求的url进行特定模式的handler寻找。通过阅读源码,net/http库中的 DefaultMux的 ServeHTTP 实现与 mux 中的 Router 的ServeHTTP 实现基本一致,仅更换了 Match 函数,这也进一步印证了这种设计模式。Match 的实现如下:

func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
   if r.buildOnly || r.err != nil {
      return false
 }
   var matchErr error
 // 扫描所遇的 handler, 封装在matchers中,查看是否有匹配
 for _, m := range r.matchers {
      if matched := m.Match(req, match); !matched {
         if _, ok := m.(methodMatcher); ok {
            matchErr = ErrMethodMismatch
            continue
 }
         // Ignore ErrNotFound errors. These errors arise from match call
 // to Subrouters. // // This prevents subsequent matching subrouters from failing to // run middleware. If not ignored, the middleware would see a // non-nil MatchErr and be skipped, even when there was a // matching route. 
 if match.MatchErr == ErrNotFound {
            match.MatchErr = nil
 }
         matchErr = nil
 return false }
   }
   if matchErr != nil {
      match.MatchErr = matchErr
      return false
 }
   if match.MatchErr == ErrMethodMismatch && r.handler != nil {
      // We found a route which matches request method, clear MatchErr
 match.MatchErr = nil
 // Then override the mis-matched handler
 match.Handler = r.handler
   }
   // Yay, we have a match. Let's collect some info about it.
 if match.Route == nil {
      match.Route = r
 }
   if match.Handler == nil {
      match.Handler = r.handler
   }
   if match.Vars == nil {
      match.Vars = make(map[string]string)
   }
   // Set variables.
 r.regexp.setMatch(req, match, r)
   return true
}

至于 mux 如何设计正则表达式来匹配模式,这里我们不深入讨论。

总结

通过阅读 net/httpgithub.com/gorilla/mux 的实现,我基本理解了 golang 下的 http server 建立,请求处理,和请求路由,一些要点总结如下:

  • http.ListenAndServe 完成以下几个任务:

    • 建立端口的监听
    • 解析发送来的请求
    • 保存 Mux/Router, 以便路由操作
  • http.ServeMux 完成以下几个任务:

    • 存放pattern和handler
    • 建立从pattern到handler的映射
    • 需要实现 handler 方法来寻找到每个请求 url 对应的handler
  • http.Handler interface的作用:

    • 要求每个该类型的变量实现 ServeHTTP 方法,来处理请求
    • 每个签名为 func(ResponseWriter, *Request) 的函数都可以通过 HandlerFunc() 来转换成 http.Handler 类型
  • 第三方包对于 net/http 的拓展实现特点:

    • 对于 ServeMux 的封装,比如 mux 中的 Router 需要确保每个 Route 都实现 ServeHTTP 函数,这样才能对接到 http.ListenAndServe
查看原文

赞 1 收藏 1 评论 0

BOBBAIcl 发布了文章 · 2020-10-08

golang 源码阅读 —— bufio

前言

文件读写一直是我在学习一门语言的时候比较难以记忆和弄懂的部分,每次当我使用比如 golang 去读取/写入一份文件的时候,总会在浏览器中不停的google: "how to read and write file via golang". 当隔一段时间再要实现上述功能的时候,我还是会去浏览器搜索相同的关键字,这样实际上很没有效率,因此借着这篇博客,我将解析bufio有关文件读写方面的源代码实现及其常用的方法。

Part 0: io库简要分析,以及bufio究竟做了什么

bufio做了什么?官方文档中这样描述:

Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O.

翻译过来的大致含义是:bufio是对IO操作的缓存的实现,它封装了 io.Reader 和 io.Writer 为新的对象,也就是 Reader 和 Writer;同时它也实现了相应的对于缓存的操作。学过《操作系统》这门课的同学应该对于 buffer 不陌生,buffer与操作系统的IO操作结合可以很好的提高文件操作的效率,因为这样避免了频繁的IO操作。同时缓冲区可以同时接受多次写入/读取,再写入文件系统,提高了我们程序的效率。
io库中,Reader和Writer实际上是两种interface, interface的定义如下:

// Reader 
// Reader 封装了基本的 Read 方法。Read 方法将 Reader 中的最多 len(p) 个byte读入p中
type Reader interface {
    Read(p [][byte](https://golang.org/pkg/builtin/#byte)) (n [int](https://golang.org/pkg/builtin/#int), err [error](https://golang.org/pkg/builtin/#error))
}

// Writer
// Write方法将 len(p) 个byte从p写入 writer中的底层数据流
type Writer interface {
    Write(p [][byte](https://golang.org/pkg/builtin/#byte)) (n [int](https://golang.org/pkg/builtin/#int), err [error](https://golang.org/pkg/builtin/#error))
}

Part 1: 如何通过bufio.Scanner读文件

为了避免写成一篇官方文档的中文翻译博客,我在这里从bufio的使用角度出发,来逐步深入bufio的实现。

1. 读文件

我们先从按行读取出发,实现如下:

func ReadByLine(filePath string) {        // filePath为文件的路径
 file, err := os.Open(filePath)       // 首先打开文件,file的类型为 *os.File
 if err != nil {
      log.Fatal(err)
   }
   defer func() {
      if err := file.Close(); err != nil {
         log.Fatal(err)
      }
   }()
   fmt.Println(file.Name())               // 打印文件名
 fileScanner := bufio.NewScanner(file)     // 申请一个fileScanner, 类型为 *bufio.Scanner
 for fileScanner.Scan() {
      fmt.Println(fileScanner.Text())          // 按行读取,打印每行的内容
 }
}
  • 测试:

    • 要读取的文件中的内容:

截屏2020-10-04 下午4.58.16.png

- 程序运行结果:

截屏2020-10-04 下午4.59.02.png

  • 源码分析

既然bufio是对于io库中Reader和Writer的封装,首先我们看一下bufio中的Reader定义,如下:

type Reader struct {
      buf          []byte
      rd           io.Reader // 由使用者传入的reader
      r, w         int       // 缓冲区读写的位置
      err          error
      lastByte     int // last byte read for UnreadByte; -1 means invalid
      lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
  }

而在按行读取文件例子中,我们并没有直接使用 Reader,而是使用了 Scanner, bufio中 Scanner 的定义如下:

type Scanner struct {
      r            io.Reader // The reader provided by the client.
      split        SplitFunc // The function to split the tokens.
      maxTokenSize int       // Maximum size of a token; modified by tests.
      token        []byte    // Last token returned by split.
      buf          []byte    // Buffer used as argument to split.
      start        int       // First non-processed byte in buf.
      end          int       // End of data in buf.
      err          error     // Sticky error.
      empties      int       // Count of successive empty tokens.
      scanCalled   bool      // Scan has been called; buffer is in use.
      done         bool      // Scan has finished.
  }

可以看到,Scanner 是对 io.Reader 更加完备的封装,而且值得注意的是,Scanner数据结构中还有一个函数类型的变量: SplitFunc, 这是Scanner 在调用 Scan 后能够实现按行读取的核心部分。
SplitFunc 的类型定义如下:

// SplitFunc 
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

SplitFunc 要求根据给定data求解是否还有token, 判断的规则由不同的SplitFunc类型函数给出;advance给出了从s.start位置读取token后前进的字节数。当按行读取的时候,Scanner中的default split函数是 ScanLines 函数,ScanLines 函数的定义如下:

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
    // dropCR 的作用是去掉byte末尾的\r
    //如果当前在EOF,并且data的长度为0,说明已经没有token
      if atEOF && len(data) == 0 {
          return 0, nil, nil
      }
    // 找到一个终结符,也就是换行符,说明当前data存在一个完整的token, 返回
      if i := bytes.IndexByte(data, '\n'); i >= 0 {
          // We have a full newline-terminated line.
          return i + 1, dropCR(data[0:i]), nil
      }
      // 处于eof,data长度不为0,返回未被终结符终结的token
      if atEOF {
          return len(data), dropCR(data), nil
      }
     // Request more data.
     return 0, nil, nil
}

基于ScanLines以及Scanner中的其他数据成员,我们终于可以分析代码中用到的Scan函数了,Scan函数的实现如下:

func (s *Scanner) Scan() bool {
   if s.done {
      return false
 }
   s.scanCalled = true
 // Loop until we have a token.
 for {
      // See if we can get a token with what we already have.
 // If we've run out of data but have an error, give the split function // a chance to recover any remaining, possibly empty token. if s.end > s.start || s.err != nil {
         advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)
         if err != nil {
            if err == ErrFinalToken {
               s.token = token
               s.done = true
 return true }
            s.setErr(err)
            return false
 }
         if !s.advance(advance) {
            return false
 }
         s.token = token
         if token != nil {
            if s.err == nil || advance > 0 {
               s.empties = 0
 } else {
               // Returning tokens not advancing input at EOF.
 s.empties++
               if s.empties > maxConsecutiveEmptyReads {
                  panic("bufio.Scan: too many empty tokens without progressing")
               }
            }
            return true
 }
      }
      // We cannot generate a token with what we are holding.
 // If we've already hit EOF or an I/O error, we are done. if s.err != nil {
         // Shut it down.
 s.start = 0
 s.end = 0
 return false
 }
      // Must read more data.
 // First, shift data to beginning of buffer if there's lots of empty space // or space is needed. if s.start > 0 && (s.end == len(s.buf) || s.start > len(s.buf)/2) {
         copy(s.buf, s.buf[s.start:s.end])
         s.end -= s.start
         s.start = 0
 }
      // Is the buffer full? If so, resize.
 if s.end == len(s.buf) {
         // Guarantee no overflow in the multiplication below.
 const maxInt = int(^uint(0) >> 1)
         if len(s.buf) >= s.maxTokenSize || len(s.buf) > maxInt/2 {
            s.setErr(ErrTooLong)
            return false
 }
         newSize := len(s.buf) * 2
 if newSize == 0 {
            newSize = startBufSize
 }
         if newSize > s.maxTokenSize {
            newSize = s.maxTokenSize
         }
         newBuf := make([]byte, newSize)
         copy(newBuf, s.buf[s.start:s.end])
         s.buf = newBuf
         s.end -= s.start
         s.start = 0
 }
      // Finally we can read some input. Make sure we don't get stuck with
 // a misbehaving Reader. Officially we don't need to do this, but let's // be extra careful: Scanner is for safe, simple jobs. for loop := 0; ; {
         n, err := s.r.Read(s.buf[s.end:len(s.buf)])
         s.end += n
         if err != nil {
            s.setErr(err)
            break
 }
         if n > 0 {
            s.empties = 0
 break
 }
         loop++
         if loop > maxConsecutiveEmptyReads {
            s.setErr(io.ErrNoProgress)
            break
 }
      }
   }
}

Scan()函数的流程可以总结为如下的几个步骤:

  1. 如果已经完成Scan, 即done为true,则直接返回false
  2. 否则尝试读取新的token/错误,即:

    1. 如果已经到达了最后的token(err为ErrFinalToken)则取出最后的token,并且设置done为true
    2. 如果发生了其他错误,则终止,返回false
    3. 如果读取了太多空token且没有发生错误,则抛出"bufio.scan: too many empty tokens without progressing"的异常(也就是发生了死循环)
  3. 在下一次读取token前要准备缓冲区的容量,确保不会发生溢出,或者缓冲不足的情况,具体处理如下:

    1. 首先将缓冲区的数据移到缓冲区的开头
    2. 如果缓冲区满,则将缓冲区的大小翻倍
  4. 将数据读入缓冲区

可以看到,Scanner 的数据结构允许我们自定义 SplitFunc, 从而根据不同的规则生成 token, 比如我们可以使用内置的 ScanRune 来作为 SplitFunc, 这样就实现了逐字符读取的需求,实现如下:

func ReadByRune(filePath string) {        // filePath为文件的路径
 file, err := os.Open(filePath)       // 首先打开文件,file的类型为 *os.File
 if err != nil {
      log.Fatal(err)
   }
   defer func() {
      if err := file.Close(); err != nil {
         log.Fatal(err)
      }
   }()
   fmt.Println(file.Name())               // 打印文件名
 fileScanner := bufio.NewScanner(file)     // 申请一个fileScanner, 类型为 *bufio.Scanner
 fileScanner.Split(bufio.ScanRunes)       // 这里替换SplitFunc为ScanRunes
 for fileScanner.Scan() {
      fmt.Println(fileScanner.Text())          // 按行读取,打印每行的内容
 }
}

Part2: 如何通过 bufio.Writer 写文件

先看一个 Write 的例子:

func WriteTo(dst *os.File, s string) {
   w := bufio.NewWriter(dst)
   fmt.Fprint(w, s)
   w.Flush() 
}

可以看到,我们首先创建了一个Writer实体,然后调用 fmt.Fprint 写入dst, 最后调用 Flush 就能成功写入文件。由于代码逻辑比较简单,这里我们直接看 bufio 中有关Writer的源码:

  • Writer的定义:
type Writer struct {
      err error
      buf []byte
      n   int
      wr  io.Writer
  }

可以看到,Writer的定义及其简介,只是在io.Writer的基础上增加了buf缓冲区。创建一个Writer的时候我们使用了NewWriter这一函数,而这个函数在实现的时候实际上是调用了 NewWriterSize, NewWriterSize 的实现如下:

func NewWriterSize(w io.Writer, size int) *Writer {

// 首先判断w是否已经为Writer,并且其缓冲区的size满足size需求
 b, ok := w.(*Writer)
   if ok && len(b.buf) >= size {
      return b
   }
   if size <= 0 {
      size = defaultBufSize
 }
 // 直接按照要求创建Writer对象
   return &Writer{
      buf: make([]byte, size),
 wr:  w,
 }
}

bufio中的Writer最重要的特性就是 buf 这个成员的使用。当且仅当 Writer 调用 Flush 方法时,buf中的内容才会被写进封装的 io.Writer 对象中。对 buf 的操作中,Write是较为重要的一个,它的实现如下:

func (b *Writer) Write(p []byte) (nn int, err error) {
   for len(p) > b.Available() && b.err == nil {
      var n int
 if b.Buffered() == 0 {     // 如果此时缓冲区为空,则直接写入io.Writer中
         // Large write, empty buffer.
 // Write directly from p to avoid copy. 
    n, b.err = b.wr.Write(p)
      } else {              // 否则使用缓冲区作为缓冲,调用copy
         n = copy(b.buf[b.n:], p)
         b.n += n
         b.Flush()
      }
      nn += n                   // 改变已经写入的大小,同时移动p
      p = p[n:]
   }
   if b.err != nil {
      return nn, b.err
   }
   n := copy(b.buf[b.n:], p)      // 将剩余部分写入
   b.n += n
   nn += n
   return nn, nil
}

Flush也是Writer的功能核心,它的实现如下:

func (b *Writer) Flush() error {
   if b.err != nil {
      return b.err
   }
   if b.n == 0 {
      return nil
 }
   n, err := b.wr.Write(b.buf[0:b.n])       // Write 的核心就是调用io.Writer中的Write函数
   if n < b.n && err == nil {
      err = io.ErrShortWrite
   }
   if err != nil {                          // 如果实际写入的byte数量小于buf中的byte数量,则调整buf中的数据位置之后抛出异常
      if n > 0 && n < b.n {
         copy(b.buf[0:b.n-n], b.buf[n:b.n])
      }
      b.n -= n
      b.err = err
      return err
   }
   b.n = 0
 return nil
}

基于上述的几个核心函数,bufio实现了对于文件的写操作。

查看原文

赞 1 收藏 1 评论 0

BOBBAIcl 关注了用户 · 2020-10-08

Jason @jason_5f0dbb9eaae42

以终为始,闭环迭代,持续提高。

关注 3361

BOBBAIcl 关注了专栏 · 2020-10-08

疯狂的技术宅

本专栏文章首发于公众号:前端先锋 。

关注 27202

BOBBAIcl 关注了用户 · 2020-10-08

Java旅途 @javatrip

公众号:Java旅途
github:https://github.com/binzh303/s...

关注 1672

BOBBAIcl 关注了专栏 · 2020-10-08

终身学习者

我要先坚持分享20年,大家来一起见证吧。

关注 48738

BOBBAIcl 关注了专栏 · 2020-10-08

民工哥技术之路

公众号:民工哥技术之路、《Linux系统运维指南 从入门到企业实战》作者。专注系统架构、高可用、高性能、高并发,数据库、大数据、数据分析、Python技术、集群中间件、后端等开源技术分享。

关注 26050

BOBBAIcl 关注了专栏 · 2020-10-08

SegmentFault 之声

在这里,我们将为你推送 SegmentFault 思否公司官方合作信息,和合作伙伴最新动态。SegmentFault 思否是中国领先的开发者社区和技术媒体,中国最大的 Hackathon 组织者。我们致力于成为科技企业和开发者沟通的桥梁,帮助科技企业和开发者对话。

关注 19047

BOBBAIcl 关注了专栏 · 2020-10-08

Sown

学习笔记

关注 239

认证与成就

  • 获得 2 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-09-21
个人主页被 517 人浏览