maksim 发布的文章

我们要部署的程序很简单,我们使用 gin 对外提供一个接口返回 helloworld。

2023-03-04T09:50:41.png

然后利用交叉编译生成 go 的执行文件。

2023-03-04T09:51:19.png

我们在 ide 中点击开始按钮后,自动的帮我们生成了myserver可执行文件,我们将这个文件上传到 work 节点的 /data/k8s-volume/myapp 目录下。

scp build/myserver [email protected]:/data/k8s-volume/myapp

2023-03-04T09:51:33.png

接下来,我们来部署 GoAPI,我们还是点击部署工作负载。

2023-03-04T09:51:45.png

我们现将 workloads 的基础信息填写上。

2023-03-04T09:52:04.png

这一次我们选择 HostPort 直接将容器的端口与所在节点的端口进行映射,这种模式下和我们 docker -p xx:xx 的效果是一样的。

和 docker 一样,我们也可以将容器内的目录映射到宿主机上,在这里我们选择主机映射。

2023-03-04T09:52:23.png

通过配置,我们将容器 /app 目录下的内容映射到我们的宿主机上,目前我们只有一个 work 节点。

因为我们是吧运行程序上传到了服务器上,所以我们需要利用 k8s 来帮助我们的容器。

2023-03-04T09:52:33.png

这一步其实就相当于我们的 docker run。

Untitled

点击部署后,我们需要等待片刻。

如果部署失败,我们可以点进工程内进行查看失败原因。

2023-03-04T09:52:56.png

在事件中显示了失败原因,在拉取apline 的时候出现失败。原来是我们的镜像名写错了,那么修改镜像名称。

2023-03-04T09:53:10.png

修改后,将会新的 pod 来替换老 pod。

2023-03-04T09:53:23.png

好了,现在已经部署成功了,我们来进行访问。

2023-03-04T09:53:35.png
2023-03-04T09:53:48.png

我们再来尝试,192.168.0.210:8080 能否访问。

2023-03-04T09:54:12.png

我们可以看到 Master 节点是不会转发这次请求的,这就是 NodePort 和 HostPort 的区别。

本文的目的是帮助开发者快速了解 基于 Rancher 的 k8s 管理以及部署工作,我们将介绍 Rancher2.x 的基本使用,目前暂不涉及运维层面,请不要将这个文章中所涉及的内容直接搬运到生产环境,毕竟部署起来和稳定运行时两回事,

k8s 本身部署还是很复杂的,部署起来会碰到很多坑,如果使用 Rancher 那就特别简单了,即使你不去特意学习 k8s 也能部署。

先说一下环境,我们先准备三台虚拟主机,如果有钱的话,也可以买两台服务器,我这里使用的是 17 年生成的小米 Pro 笔记本,I7 处理器,16G 内存,已经满足了试验环境的搭建。

机器ip配置备注
CentOS7192.168.0.1992cpu 4G ramRancher
CentOS7192.168.0.2102cpu 4G ramMaster
CentOS7192.168.0.2112cpu 4G ramNode

这里建议一台机器专门跑 Rancher ,跑 k8s 的节点,最好配4G 以上的内存,要不然会很卡。这三台节点,需要内网可以进行访问。

我们接下来开始准备工作,在每个节点上分别执行下面的命令

#0. 安装必备软件
yum -y install wget net-tools nfs-utils lrzsz gcc gcc-c++ make cmake libxml2-devel openssl-devel curl curl-devel unzip sudo ntp libaio-devel wget vim ncurses-devel autoconf automake zlib-devel  python-devel epel-release openssh-server socat  ipvsadm conntrack ntpdate
#1. 关闭防火墙
systemctl stop firewalld && systemctl disable firewalld
#2. 关闭 SElinux
sed -i  's/SELINUX=enforcing/SELINUX=disabled/'  /etc/sysconfig/selinux
sed -i  's/SELINUX=enforcing/SELINUX=disabled/g'  /etc/selinux/config
#3. 关闭 swap
sed -i 's/.*swap.*/#&/' /etc/fstab
#4. 重启
reboot -f
#5. 安装 docker
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
#6. 启动并且设置自启动
systemctl enable docker
systemctl start docker

#7. 配置阿里云代理,这里我隐藏了自己的,这东西免费,自己可以去阿里云进行申请。
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://xxxxx.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
# 8.编辑时间同步,要不然 etcd 可能会报错,很麻烦
crontab -e
 
* */1 * * * /usr/sbin/ntpdate   cn.pool.ntp.org

在 rancher 虚拟机上部署单节点 rancher。


# 创建目录
mkdir -p /data/rancher
# 创建 docker 容器
docker run -d --restart=unless-stopped -p 8080:80 -p 8843:443 -v /data/rancher:/var/lib/rancher rancher/rancher:v2.4.5
在这里,我们使用的 rancher 单节点模式,如果是用于生产,请使用高可用的 rancher,要不然 rancher 也是有可能出现故障的。
我这里使用的 2.4.5 版本,这是因为我在工作和学习中使用的都是这个版本,就目前文章截止最新版本应该是已经到了 2.6。

启动时我们需要等待一会。我们可以用 docker logs -f 命令来监控日志输出。

访问https://192.168.0.199:8843/ 即可看到这个页面。

2023-03-04T08:07:31.png

我们设置自己的密码,就会跳转到主页,接下来,我们创建集群。

2023-03-04T08:07:57.png

我们在右下角可以更改系统语言。

2023-03-04T08:08:07.png

接下来,我们点击添加集群。

2023-03-04T08:08:50.png

Rancher 不光可以帮助我们建立新的集群,还可以在已有集群的基础上进行导入。

2023-03-04T08:08:23.png

在这里,我们选择自定义后填写集群名称,直接点击下一步,我们使用默认配置即可。

2023-03-04T08:09:29.png

2023-03-04T08:09:42.png

如果是增加 Master 节点,那么这三个我们都需要勾上。

这里需要注意,复制的时候要把 localhost 改成能够进行通信的 ip。

sudo docker run -d --privileged --restart=unless-stopped --net=host -v /etc/kubernetes:/etc/kubernetes -v /var/run:/var/run rancher/rancher-agent:v2.4.5 --server https://192.168.0.106:8843 --token 8bgdphz9mk9xbbdkrhvsqh6v5zh9gkc55276w5l7m8xlwkgr8xmz4w --ca-checksum 3db552d02da1618d971233431766d57485777d558395dbba6dc2c185ca3bc1b8 --etcd --controlplane

2023-03-04T08:10:04.png

我们分别添加完成后需要等待片刻,如果中途卡在 etcd 上,可以尝试下面的方法。

docker stop $(docker ps -aq)
docker system prune -f
docker volume rm $(docker volume ls -q)
docker image rm $(docker image ls -q)
rm -rf /etc/ceph \
       /etc/cni \
       /etc/kubernetes \
       /opt/cni \
       /opt/rke \
       /run/secrets/kubernetes.io \
       /run/calico \
       /run/flannel \
       /var/lib/calico \
       /var/lib/etcd \
       /var/lib/cni \
       /var/lib/kubelet \
       /var/lib/rancher/rke/log \
       /var/log/containers \
       /var/log/pods \
       /var/run/calico

这块我第一次安装的时候折腾了将近两个小时。

2023-03-04T08:10:17.png

当环境没有问题后,我们来部署一个 nginx,首先我们来创建一个项目

2023-03-04T08:10:39.png

2023-03-04T08:10:49.png

我们可以看到有几个默认的 Project,在这里我们就需要来了解一下什么是 namespace 了。

Namespace 是对一组资源和对象的抽象几何,用来将系统内部的对象划分为不同的项目组或者用户组。常用来隔离不同的用户,比如 k8s 自带的服务一般都运行在 kube-system namespace中。

2023-03-04T08:10:59.png

我们先创建一个项目。

2023-03-04T08:12:55.png

接着再增加一个 namespace,之后在左上角我们就可以进入到自己的项目了。

2023-03-04T08:11:41.png
2023-03-04T08:11:49.png

在这里我们可以创建四个内容:Workloads,Load Balancing, Service Discovery,Volumes。

workloads

工作负载是在 Kubernetes 上运行的应用程序。

在 Kubernetes 中,无论你的负载是由单个组件还是由多个一同工作的组件构成, 你都可以在一组 Pod 中运行它。 在 Kubernetes 中,Pod 代表的是集群上处于运行状态的一组 容器 的集合。

2023-03-04T07:55:54.png

Pod 是所有业务类型的基础,也是 k8s 管理的最小单位,可以理解为它是一个或多个容器的组合。

接下来,我们利用 workloads 来创建 nginx。

在这里我们先使用 NodePort,这代表在所有节点(虚拟机)上开放一个特定的端口,任何发送到该端口的流量都被转发到对应的服务上,端口范围胡思 30000~32767。

2023-03-04T07:55:41.png

2023-03-04T07:55:27.png

当显示 Active 的时候就证明已经部署好了,我们可以到 work 节点查看一下。

2023-03-04T07:55:06.png

访问 ip 查看是否能够访问。

2023-03-04T07:54:49.png

如果说访问不了可以尝试一下解决方案

# 安装 iptables
yum install iptables-services -y
# 关闭 iptables 防火墙
service iptables stop   && systemctl disable iptables

# 修改内核参数
cat <<EOF >  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
# 生效配置
sysctl --system

#设置网桥包 iptables

echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
echo 1 >/proc/sys/net/bridge/bridge-nf-call-ip6tables

echo """
vm.swappiness = 0
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
""" > /etc/sysctl.conf

#生效配置
sysctl -p

#开启ipvs,不开启ipvs将会使用iptables,但是效率低,所以官网推荐需要开通ipvs内核
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
#!/bin/bash
ipvs_modules="ip_vs ip_vs_lc ip_vs_wlc ip_vs_rr ip_vs_wrr ip_vs_lblc ip_vs_lblcr ip_vs_dh ip_vs_sh ip_vs_fo ip_vs_nq ip_vs_sed ip_vs_ftp nf_conntrack"
for kernel_module in \${ipvs_modules}; do
 /sbin/modinfo -F filename \${kernel_module} > /dev/null 2>&1
 if [ $? -eq 0 ]; then
 /sbin/modprobe \${kernel_module}
 fi
done
EOF

chmod 755 /etc/sysconfig/modules/ipvs.modules && bash
/etc/sysconfig/modules/ipvs.modules && lsmod | grep ip_vs

Goridge 是 SpiralsCout 开源的高性能的 PHP 到 Golang 编解码器库。该库可以帮助你在 PHP 调用 Go 的服务方法。

Goridge 入门

使用方法:

我们先来编写简单的 Go 程序,目的就是提供一个 rpc 接口给 PHP 进行访问。

GO111MODULE=on go get github.com/roadrunner-server/goridge/v3

package main

import (
    "fmt"
    "net"
    "net/rpc"

    goridgeRpc "github.com/roadrunner-server/goridge/v3/pkg/rpc"
)

type App struct{}

func (s *App) Hi(name string, r *string) error {
    *r = fmt.Sprintf("Hello, %s!", name)
    return nil
}

func main() {
    ln, err := net.Listen("tcp", ":6001")
    if err != nil {
        panic(err)
    }

    _ = rpc.Register(new(App))

    for {
        conn, err := ln.Accept()
        if err != nil {
            continue
        }
        _ = conn
        go rpc.ServeCodec(goridgeRpc.NewCodec(conn))
    }
}

Hi 函数用来接受一个 name 入参。

# 进入工程陌路
cd project
# 安装 spiral/goridge sdk
composer install spiral/goridge
# 编写 index.php
vim index.php

<?php

use Spiral\Goridge\RPC\RPC;
use Spiral\Goridge\SocketRelay;

include 'vendor/autoload.php';

$rpc1 = new RPC(new SocketRelay("127.0.0.1", 6001));
echo $rpc1->call("App.Hi", "Goridge RPC");

2023-03-08T12:31:55.png

PHP 访问 Go 原生 RPC 的方法

在 2019 年,我们的团队开始引入 Golang,原因是想通过 Golang 良好的并发性来提升我们的应用访问速度,但是正常情况下公司是不会给研发团队大片的时间去进行重构,这个时候我们就需要 PHP 和 Go 进行通讯。

不过那个时候我们并没有使用 Goridge,而是直接用 PHP 访问 Go RPC。现在我们来模拟一下当时的场景,还是用 Goridge 的那个例子,其实实现思路大体上是一致的,不过我们并没有写自己的编解码,而是利用了 jsonrpc。

package main

import (
    "fmt"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

type App struct{}

func (s *App) Hi(name string, r *string) error {
    *r = fmt.Sprintf("Hello, %s!", name)
    return nil
}

func main() {
    rpc := rpc.NewServer()
    app := new(App)
    rpc.Register(app)
    ln, err := net.Listen("tcp", ":6001")
    defer ln.Close()
    if err != nil {
        panic(err)
    }
    for {
        conn, err := ln.Accept()
        if err != nil {
            panic(err)
        }
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

在 go 中我,我们使用 jsonrpc 作为解码器,这样一来就是可以通过 socket 传递 json 的方式进行访问 go 的 rpc 接口了。

2023-03-08T13:16:34.png

如果对 go rpc 不是特别了解的话,可以通过 https://colobu.com/2016/09/18/go-net-rpc-guide/ 这篇文章了解一下,该文的作者是 rpcx 的作者鸟窝,是 Golang 界的一位大牛。

目前企业大多数都是使用微服务架构作为解决方案,在这里我们使用 yaf + yar 来作为基础实现一套企业级的微服务解决方案。

Yaf 是鸟哥开发的基于 C 语言扩展的高性能框架,而 yar 是一款 rpc 解决方案。

安装 yaf

最简单的安装方式:

# 利用 pecl 安装 yaf
➜  ~ sudo pecl install yaf
# 验证是否安装成功
➜  ~ php -m | grep yaf
yaf

yaf 的 hello world

在正式开始之前,我们先来解决的代码提示,这是因为 yaf 作为 C 语言编写的扩展,我们的 phpstrom 无法给出代码提醒,这就降低了我们的研发效率,所以我们要先解决代码提示的问题。

➜  ~ git clone https://github.com/elad-yosifon/php-yaf-doc.git

然后打开 phpstrom,选择外部库,然后添加刚才下载的 php-yaf-doc**

2023-03-04T21:05:48.png

2023-03-04T21:06:03.png

这个时候我们再编写代码的时候就可以用到代码提醒了,这个时候我们按照国际惯例,开始编写 hello world程序。

我们先来建立标准目录,鸟哥在文档中给了我们很好的案例。

- .htaccess // Rewrite rules
+ public
  | - index.php // Application entry
  | + css
  | + js
  | + img
+ conf
  | - application.ini // Configure 
- application/
  - Bootstrap.php   // Bootstrap
  + controllers
     - Index.php // Default controller
  + views    
     |+ index   
        - index.phtml // View template for default controller
  + library // libraries
  + models  // Models
  + plugins // Plugins

2023-03-04T21:06:23.png

首先我们来看 public 下的 index。public 是公共目录,其中 index.php就是我们的入口文件。

define('APPLICATION_PATH', dirname(__FILE__, 2));
try {
    $app = new Yaf_Application(APPLICATION_PATH . "/conf/application.ini");
    $app->bootstrap() //call bootstrap methods defined in Bootstrap.php
    ->run();
} catch (Yaf_Exception_StartupError|Yaf_Exception_TypeError $e) {

}

首先是定义了应用目录,然后初始化 Yaf_Application,这里的异常我们先不处理。

这里,我们需要先创建 application.ini 这 Bootstrap.php。

[product]
;CONSTANTS is supported
application.directory = APPLICATION_PATH "/application/"
<?php
class Bootstrap extends Yaf_Bootstrap_Abstract
{
}

之后就是 IndexController了。

<?php

class IndexController extends Yaf_Controller_Abstract
{
    public function indexAction()
    {
        $this->getView()->assign('context', 'hello world');
    }
}

与其他框架一样,Yaf 也是一款 MVC 框架,我们可以直接利用 assign 像页面复制。

2023-03-04T21:06:40.png

这里我们需要注意目录结构,index 代表的是 IndexController,index.phtml才是 indexAction。我们通过PHP 内置的服务器来运行我们的应用。

➜  yaf-micro php -S localhost:8080 -t public

2023-03-04T21:07:00.png

yaf 的配置

yaf 框架的配置主要分为运行时配置和应用配置。

运行时配置

运行时配置是配置在 php.ini配置文件中的。

配置名称默认值含义
yaf.library全局路径,Yaf_loader会从这个目录检索全局库
yaf.action_prefer0如果路径信息中只有一部分,当设置为0时,会把这部分作为controller,如果设置为1,会把这部分作为action
yaf.lowcase_path0在类自动加载过程中是否将所有的路径转换为小写
yaf.use_spl_autoload0当设置为0时,Yaf_Loader::autoload()将一直返回true,设置为1时,如果Yaf_Loader找不到类时,将返回false,由其他自动加载方法处理
yaf.forward_limit5Yaf_Controller_Abstract::forward()的最大次数,防止递归次数过多,设置最大保护机制
yaf.name_suffix1当设置为1时,Yaf_Loader将根据类后缀名判断是否是一个MVC类,若设置为0,将根据类前缀名判断
yaf.name_separator当该值不为空时,Yaf_Loader将根据这个值来鉴定类后缀和值,如:当值为"_"时,Yaf_Loader将把Index_Controller作为控制器类,IndexController作为普通类
yaf.cache_config0当设置为1时,同时使用ini配置文件作为Yaf_Application()的参数时,ini配置文件的编译结果将被缓存到php进程中,Yaf根据ini文件的mtime判断配置是否更新,如果更新,Yaf将重新加载ini配置,注意,yaf使用ini文件路径作为缓存key,所以在ini文件路径中使用绝对路径,否则,如果两个应用程序使用相同的ini配置相对路径,可能会出现一些冲突。
yaf.environproduct用于yaf获取ini配置文件的config部分,如果该值为“product”,那么yaf将使用ini配置文件(Yaf_Application第一个参数)中名为“product”的部分作为Yaf_Application的最终配置。
yaf.use_namespace0当设置为1时,Yaf所有的类都将以命名空间的方式使用

预定义常量

常量名称含义
YAF_VERSIONyaf框架版本
YAF_ENVIRONyaf框架环境
YAF_ERR_STARTUP_FAILED错误码
YAF_ERR_ROUTE_FAILED错误码
YAF_ERR_DISPATCH_FAILED错误码
YAF_ERR_AUTOLOAD_FAILED错误码
YAF_ERR_NOTFOUND_MODULE错误码
YAF_ERR_NOTFOUND_CONTROLLER错误码
YAF_ERR_NOTFOUND_ACTION错误码
YAF_ERR_NOTFOUND_VIEW错误码
YAF_ERR_CALL_FAILED错误码
YAF_ERR_TYPE_ERROR错误码
  • get_defined_constants()方法获取yaf预定义的常量

应用配置

应用配置是在工程目录conf 目录下的 application.ini 中进行配置。

配置名称默认值含义
application.directory应用程序的目录,包含"controllers", "views", "models", "plugins"等子目录
application.extphpPHP脚本的扩展名,Yaf_Loader自动加载类的时候需要用到它
application.view.extphtml视图模板扩展名
application.modulesindex注册的模块列表,以逗号分隔,用于路由处理,特别是当PATH_INFO超过三段的时候
application.libraryapplication.directory . "/library"本地类库的目录
application.library.directoryapplication.directory . "/library"本地类库的目录,application.library的别名
application.library.namespace""逗号分隔的本地类库命名空间前缀,Yaf2.1.6以后加入
application.bootstrapapplication.directory . "/Bootstrap" . application.extBootstrap类脚本文件的绝对路径
application.baseUri""路由处理中需要忽略的路径前缀。举个例子,请求"/prefix/controller/action"时。如果你将application.baseUri设置为"/prefix",那么只有"/controller/action"会被当做路由路径。通常不需要设置此值。
application.dispatcher.defaultRoute默认路由,如果未指定,静态路由会被当做是默认路由
application.dispatcher.throwException1开启此项,Yaf会在发生错误的地方抛出异常。
application.dispatcher.catchException0开启此项,如果有未捕获的异常,Yaf将会把它定向到Error controller, Error Action。
application.dispatcher.defaultModuleindex开启此项,如果有未捕获的异常,Yaf将会把它定向到Error controller, Error Action。
application.dispatcher.defaultControllerindex开启此项,如果有未捕获的异常,Yaf将会把它定向到Error controller, Error Action。
application.dispatcher.defaultActionindex开启此项,如果有未捕获的异常,Yaf将会把它定向到Error controller, Error Action。
application.system在application.ini中设置Yaf运行时配置

Web 核心 Server

在 《Golang 主流 Web 框架路由实现分析》中我们注意到对于一个 Web 框架来说,至少提供了三个抽象:

  • 代表服务器的吃哦续爱那个,我们这里称之为 Server
  • 代表上下文的抽象,我们这里称之为 Context
  • 路由树

从前面框架对比来看,对于一个 Web 框架来说,我们首先要有一个整体代表服务器的抽象,也就是 Server。

Server 从特性上来说,至少提供了三个功能:

  1. 生命周期管理:即启动、关闭。如果在后期,我们还要考虑增加生命周期回调特性
  2. 路由注册接口:提供路由注册功能
  3. 作为http包到 web 框架的桥梁

http.Handler接口

http 暴露了一个接口,Handler,它是我们引入自定义 Web 框架的相关连接点,自定义 Server 有两种实现方案:

方案 1. 只组合 http.Handler。

type Server interface {
    http.Handler
}

func TestServer(t * testing.T) {
    var s Server
    http.ListenAndServe(":8080", s)
    http.ListenAndServeTLS(":4000", "cretfile", "keyfile", s)
}

优点:

  1. 用户可在使用的饿时候只需要调用 Http.ListenAndServe 就可以
  2. 和 HTTPS 协议完全无缝衔接
  3. 极简设计

缺点

  1. 难以控制生命周期,并且在控制生命周期的时候增加回调支持
  2. 缺乏控制力:如果将来希望支持优雅退出的功能,将会难以支持

方案 2.组合 http.Handler 并增加 Start 方法

type Server interface {
    http.Handler
    Start(addr string) error
}

func TestServer(t * testing.T) {
    var s Server
    http.ListenAndServe(":8080", s)
    http.ListenAndServeTLS(":4000", "cretfile", "keyfile", s)

  s.Start(":8881")
}

优点:

  1. Server 既可以当成普通的 http.Handler 来使用,又可以作为一个独立的实体,拥有自己的管理生命周期能力
  2. 完全的控制

缺点:

  1. 如果用户不希望使用 ListenAndServeTLS,那么 Server 需要提供 HTTPS 的支持

这两个实现方案都直接耦合了 Go 自带的 http 包,如果我们希望切换为 fasthttp 或者类似的 http 包,则会非常困难。

HTTPServer 实现

// 确保 HTTPServer 实现了 Server 接口
var _ Server = &HTTPServer {}

type HTTPServer struct {

}

func (s * HttpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {

}

func (s * HttpServer) Start (addr string) error {
        return http.ListenAndServe(addr, s)
}

func Serve(l net.Listener, handler Handler) error {
        srv := &Server{Handler: handler}
        return srv.Serve(l)
}

该实现直接使用 http.ListenAndServe 来启动,后续可以根据需要替换为:

  • 内部创建 http.Server 来启动
  • 使用 http.Serve 来启动,环球聚更大得我灵活性,比如讲端口监听和服务器启动分离等呢个

ServeHTTP 则是我们整个 Web 框架的核心入口,我们将在整个方法内部完成:

  • Context 构建
  • 路由匹配
  • 执行业务逻辑

注册路由

暂时我们可以考虑站在用户的角度,考虑如何注册路由,在《Golang 主流 Web 框架路由实现分析》文章中,详细介绍了Beego、Gin 等框架的路由注册实现,我们可以参考一下。

大致上分为两类方法:

  • 针对任意的方法的:如 Gin 和 Iris 的 Handle 方法,Echo 的 Add 方法。
  • 针对不同 HTTP 方法的:如 Get、Post、Delete,这类方法基本很伤都是委托给前一类方法

所以实际上,核心方法只需要一个,例如 Handle。其他的方法都建立在这上面,在这里我们使用 AddRoute 作为核心方法,其函数只要接受三个参数。

  • method:请求方法例如 Get、Put、Post
  • path:请求路径
  • handleFunc: 业务逻辑

type HandleFunc func()

type Server interface {
    http.Handler
    Start(addr string) error

    AddRouter(method string, path string, handleFunc HandleFunc)
}

这里我们需要思考一下:

  • AddRoute 方法只接受一个 HandleFunc,因为我希望它只注册业务逻辑。即便真有多个场景,用户可以自己组合成一个。
  • 如果允许注册多个,那么在实现的时候需要考虑,其中一个失败了,是否还允许继续执行下去;反过来,如果其中一个 HandleFunc 中药终端执行,怎么中断?
  • 这里我们采用新的名字 AddRoute,我认为更加贴合方法本意,Handle 看上去是处理什么东西,而是实质上这里只是注册路由。

其他框架都是允许注册多个的,但是其实用起来体验不会很好

  • Gin 和 Iris 最后一个是不定参数,那么完全可以一个都不传,如 PUT(“path”),这个在编译器无法发现
  • Echo 则是存在我希望 h 传入 nil 的可能。实际上 Echo 是讲中间件真粗额逻辑和路由逻辑合并在一起了。

回到我们设计的 AddRoute 的设计上,这种设计思路很常用,针对不同的 HTTP 方法的注册 API,我们都可以委托给 Handle 方法,也就是我们的 AddRoute。

func (s *HttpServer) Post(path string, handler handleFunc) {
    s.AddRoute(http.MethodPost, path, handler);
}

Context 设计

ServeHTTP 方法作为 http 包与 web 框架的关联点,需要在 ServeHTTP 内部,执行:

  • 构建起Web 框架的杀好难过i行啊问
  • 查找路由树,并执行命中路由的代码