Golang实践:讯飞云人脸比对中间件开发 | 豆包MarsCode AI 刷题

48 阅读7分钟

前文回顾

在上一篇文章中,我主要介绍了讯飞云人脸比对 demo 的实现过程,包括开发准备(申请账号、认证接口等)、比对流程(请求和响应阶段)。重点阐述了请求阶段的相关代码实现,如创建 FRClient 和 FRRequest 结构体及对应方法,还介绍了响应阶段的流程及实现,指出实现较简单,复杂点在于结构创建和鉴权认证。今天,我们来使用所学的知识开发一个简单的支持 HTTP 协议的人脸比对中间件。

开发准备

我们需要以下依赖,gin、cobra和MyGO。gin 是一个用 Go 语言编写的高性能 HTTP Web 框架。它提供了类似于流行的 PHP 框架 Laravel 的 API,但使用 Go 语言的性能和效率。gin 框架旨在快速构建高效的 Web 应用程序和服务,特别适用于需要高吞吐量和低延迟的场景。cobra 是一个强大的、用于构建命令行应用程序的Go语言框架。它提供了简单而灵活的接口来创建命令行工具,特别适用于开发像CLI(命令行界面)工具、管理工具或任何需要与用户通过命令行交互的程序。而 MyGO 是我个人开发的一个异步日志框架,如果有更好的选择可以换成其他的日志框架。

添加 gin 相关依赖
go get github.com/gin-gonic/gin
添加 cobra 相关依赖
go get github.com/spf13/cobra
添加 MyGO 相关依赖
go get github.com/ZSLTChenXiYin/MyGO

设计中间件

中间件拆分为三个部分,视图层、逻辑层和服务层。视图层主要是通过cobra实现通过命令行参数配置服务,逻辑层主要负责完成人脸识别相关服务的处理,服务层是为用户提供HTTP协议的API服务。

实现视图层

type xfyun_fr_server_config struct {
        AppID     string `json:"appid"`
        APIKey    string `json:"apikey"`
        APISecret string `json:"apisecret"`
}

var (
        XFYunFRServerConfig = &xfyun_fr_server_config{}
)

var root_cmd = &cobra.Command{
        Use:   "xfyun_fr_server",
        Short: "Server for xfyun face recognition",
        Long:  "Server for xfyun face recognition",
        Run: func(cmd *cobra.Command, args []string) {
                if !cmd.HasFlags() {
                        cmd.Help()
                }
        },
}

func Execute() (bool, error) {
        err := root_cmd.Execute()
        if err != nil {
                return false, err
        }

        if root_cmd.Flag("help").Changed {
                return true, nil
        }

        if config_path := root_cmd.Flag("config").Value.String(); config_path == "" {
                if root_cmd.Flag("appid").Value.String() == "" {
                        fmt.Print("appid:")
                        fmt.Scan(&XFYunFRServerConfig.AppID)
                }

                if root_cmd.Flag("apikey").Value.String() == "" {
                        fmt.Print("apikey:")
                        fmt.Scan(&XFYunFRServerConfig.APIKey)
                }

                if root_cmd.Flag("apisecret").Value.String() == "" {
                        fmt.Print("apisecret:")
                        fmt.Scan(&XFYunFRServerConfig.APISecret)
                }

                if XFYunFRServerConfig.AppID == "" || XFYunFRServerConfig.APIKey == "" || XFYunFRServerConfig.APISecret == "" {
                        return false, errors.New("xfyun_fr: Please input appid, apikey and apisecret")
                }
        } else {
                file, err := os.OpenFile(config_path, os.O_RDONLY, 0666)
                if err != nil {
                        return false, err
                }

                json_data, err := io.ReadAll(file)
                if err != nil {
                        return false, err
                }

                err = json.Unmarshal(json_data, XFYunFRServerConfig)
                if err != nil {
                        return false, err
                }
        }

        return false, nil
}

func init() {
        root_cmd.PersistentFlags().StringP("appid", "i", "", "Add AppID")
        root_cmd.PersistentFlags().StringP("apikey", "k", "", "Add APIKey")
        root_cmd.PersistentFlags().StringP("apisecret", "s", "", "Add APISecret")
        root_cmd.PersistentFlags().StringP("config", "c", "", "Use config file")
}

通过cobra,我们很轻松地实现了通过添加参数来获取服务地基础配置,用户可以自由选择配置服务的方式,即手动输入或使用配置文件。

实现逻辑层

type FRClient struct {
        app_id     string
        api_key    string
        api_secret string

        fr_request *FRRequest
}

type FRClientOption func(*FRClient)

func WithClientInfo(app_id string, api_key string, api_secret string) FRClientOption {
        return func(fr_client *FRClient) {
                fr_client.app_id = app_id
                fr_client.api_key = api_key
                fr_client.api_secret = api_secret
        }
}

func NewFRClient(options ...FRClientOption) *FRClient {
        fr_client := &FRClient{}
        for _, option := range options {
                option(fr_client)
        }
        fr_client.fr_request = NewFRRequest(WithFRRequestAppID(fr_client.app_id))
        return fr_client
}

const (
        FIRST_INPUT = 1
        LAST_INPUT  = 2
)

func (tho *FRClient) AddInput(input_option uint, encoding string, image []byte) error {
        err := tho.fr_request.AddInput(input_option, encoding, image)
        if err != nil {
                return err
        }
        return nil
}

func (tho *FRClient) nowRFC1123() string {
        return time.Now().UTC().Format(time.RFC1123)
}

func (tho *FRClient) getAuthenticationParameters(date string) (authorization string, err error) {
        host := "api.xf-yun.com"
        request_line := "POST /v1/private/s67c9c78c HTTP/1.1"

        // 生成signature的原始字段(signature_origin)
        signature_origin := fmt.Sprintf("host: %s\ndate: %s\n%s", host, date, request_line)

        // 使用hmac-sha256算法结合apiSecret对signature_origin签名,获得签名后的摘要signature_sha
        hmac_sha256 := hmac.New(sha256.New, []byte(tho.api_secret))
        _, err = hmac_sha256.Write([]byte(signature_origin))
        if err != nil {
                return "", err
        }
        signature_sha := hmac_sha256.Sum(nil)

        // 使用base64编码对signature_sha进行编码获得最终的signature
        signature := base64.StdEncoding.EncodeToString(signature_sha)

        // 生成authorization base64编码前(authorization_origin)的字符串
        authorization_origin := fmt.Sprintf("api_key=\"%s\",algorithm=\"hmac-sha256\",headers=\"host date request-line\",signature=\"%s\"", tho.api_key, signature)

        // 对authorization_origin进行base64编码获得最终的authorization参数
        authorization = base64.StdEncoding.EncodeToString([]byte(authorization_origin))
        return authorization, nil
}

func (tho *FRClient) getRequestURL(authorization string, date string) string {
        return fmt.Sprintf("https://api.xf-yun.com/v1/private/s67c9c78c?authorization=%s&host=api.xf-yun.com&date=%s", authorization, url.QueryEscape(date))
}

func (tho *FRClient) newHasSuffixsFinder(suffixs ...string) func(string) (bool, string) {
        return func(s string) (bool, string) {
                has_suffix := false
                for _, suffix := range suffixs {
                        if strings.HasSuffix(s, suffix) {
                                has_suffix = true
                                return has_suffix, suffix
                        }
                }
                return has_suffix, ""
        }
}

func (tho *FRClient) ReadLocalImage(image_path string) (encoding string, image []byte, err error) {
        has_suffixs_finder := tho.newHasSuffixsFinder(".jpg", ".jpeg", ".png", ".bmp")
        has_suffix, encoding := has_suffixs_finder(image_path)
        if !has_suffix {
                return "", nil, errors.New("face_recognition: The target file is not in jpg/png/bmp format")
        }

        image_file, err := os.OpenFile(image_path, os.O_RDONLY, 0666)
        if err != nil {
                return "", nil, err
        }
        defer image_file.Close()

        image, err = io.ReadAll(image_file)
        if err != nil {
                return "", nil, err
        }

        return encoding[1:], image, nil
}

func (tho *FRClient) Do() (*FRResponseText, error) {
        date := tho.nowRFC1123()

        authorization, err := tho.getAuthenticationParameters(date)
        if err != nil {
                return nil, err
        }

        request_url := tho.getRequestURL(authorization, date)

        json_data, err := json.Marshal(tho.fr_request)
        if err != nil {
                return nil, err
        }
        request, err := http.NewRequest("POST", request_url, bytes.NewBuffer(json_data))
        if err != nil {
                return nil, err
        }

        request.Header.Set("content-type", "application/json")
        request.Header.Set("host", "api.xf-yun.com")
        request.Header.Set("app_id", tho.app_id)

        http_client := &http.Client{}

        response, err := http_client.Do(request)
        if err != nil {
                return nil, err
        }
        defer response.Body.Close()

        json_data, err = io.ReadAll(response.Body)
        if err != nil {
                return nil, err
        }

        fr_response := &FRResponse{}
        err = json.Unmarshal(json_data, fr_response)
        if err != nil {
                return nil, err
        }

        return fr_response.GetResponseText(), nil
}

type FRRequest struct {
        Header struct {
                App_id string `json:"app_id"`
                Status int    `json:"status"`
        } `json:"header"`
        Parameter struct {
                S67c9c78c struct {
                        Service_kind        string `json:"service_kind"`
                        Face_compare_result struct {
                                Encoding string `json:"encoding"`
                                Compress string `json:"compress"`
                                Format   string `json:"format"`
                        } `json:"face_compare_result"`
                } `json:"s67c9c78c"`
        } `json:"parameter"`
        Payload struct {
                Input1 struct {
                        Encoding string `json:"encoding"`
                        Status   int    `json:"status"`
                        Image    string `json:"image"`
                } `json:"input1"`
                Input2 struct {
                        Encoding string `json:"encoding"`
                        Status   int    `json:"status"`
                        Image    string `json:"image"`
                } `json:"input2"`
        } `json:"payload"`
}

type FRRequestOption func(*FRRequest)

func WithFRRequestAppID(app_id string) FRRequestOption {
        return func(frr *FRRequest) {
                frr.Header.App_id = app_id
        }
}

func WithFRRequestInput(input_option uint, encoding string, image []byte) (FRRequestOption, error) {
        if input_option > 2 {
                return nil, errors.New("xfyun_fr: The input options can only be 1 or 2")
        }
        if image == nil {
                return nil, errors.New("xfyun_fr: The image is empty")
        }
        base64_image := base64.StdEncoding.EncodeToString(image)
        if len(base64_image) > 4*1024*1024 {
                return nil, errors.New("xfyun_fr: The base64 image is larger than 4M")
        }

        return func(frr *FRRequest) {
                if input_option == 1 {
                        frr.Payload.Input1.Encoding = encoding
                        frr.Payload.Input1.Image = base64_image
                } else if input_option == 2 {
                        frr.Payload.Input2.Encoding = encoding
                        frr.Payload.Input2.Image = base64_image
                }
        }, nil
}

func NewFRRequest(options ...FRRequestOption) *FRRequest {
        fr_request := &FRRequest{}
        fr_request.Header.Status = 3
        fr_request.Parameter.S67c9c78c.Service_kind = "face_compare"
        fr_request.Parameter.S67c9c78c.Face_compare_result.Encoding = "utf8"
        fr_request.Parameter.S67c9c78c.Face_compare_result.Compress = "raw"
        fr_request.Parameter.S67c9c78c.Face_compare_result.Format = "json"
        fr_request.Payload.Input1.Status = 3
        fr_request.Payload.Input2.Status = 3

        for _, option := range options {
                option(fr_request)
        }

        return fr_request
}

func (tho *FRRequest) AddInput(input_option uint, encoding string, image []byte) error {
        if input_option > 2 {
                return errors.New("xfyun_fr: The input options can only be 1 or 2")
        }
        if image == nil {
                return errors.New("xfyun_fr: The image is empty")
        }
        base64_image := base64.StdEncoding.EncodeToString(image)
        if len(base64_image) > 4*1024*1024 {
                return errors.New("xfyun_fr: The base64 image is larger than 4M")
        }

        if input_option == 1 {
                tho.Payload.Input1.Encoding = encoding
                tho.Payload.Input1.Image = base64_image
        } else if input_option == 2 {
                tho.Payload.Input2.Encoding = encoding
                tho.Payload.Input2.Image = base64_image
        }

        return nil
}

type FRResponseText struct {
        Ret   int     `json:"ret"`
        Score float64 `json:"score"`
}

func (tho *FRResponseText) IsSuccess() bool {
        return tho.Score > 0.67
}

type FRResponse struct {
        Header struct {
                Code    int    `json:"code"`
                Message string `json:"message"`
                Sid     string `json:"sid"`
        } `json:"header"`
        Payload struct {
                Face_compare_result struct {
                        Compress string `json:"compress"`
                        Encoding string `json:"encoding"`
                        Format   string `json:"format"`
                        Text     string `json:"text"`
                } `json:"face_compare_result"`
        } `json:"payload"`
}

type FRResponseOption func(*FRResponse)

func (tho *FRResponse) GetResponseText() *FRResponseText {
        base64_text := tho.Payload.Face_compare_result.Text
        json_text, err := base64.StdEncoding.DecodeString(base64_text)
        if err != nil {
                return nil
        }

        response_text := &FRResponseText{}
        err = json.Unmarshal(json_text, response_text)
        if err != nil {
                return nil
        }

        return response_text
}

逻辑层的实现逻辑,在上一篇文章中已经讲解过了,这里就不多赘述。主要的流程还是从向讯飞云人脸比对服务发起请求开始,经过接收并解析讯飞云人脸比对服务响应,最后获取比对信息。

实现服务层

var (
        FRClient *xfyun_fr.FRClient
)

func PostStreamFR(context *gin.Context) {
        image1_type := context.PostForm("image1_type")
        image1_response, err := context.FormFile("image1")
        if err != nil {
                context.JSON(http.StatusBadRequest, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }
        image_file, err := image1_response.Open()
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }
        image, err := io.ReadAll(image_file)
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }
        err = FRClient.AddInput(FIRST_INPUT, image1_type, image)
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }

        image2_type := context.PostForm("image2_type")
        image2_response, err := context.FormFile("image2")
        if err != nil {
                context.JSON(http.StatusBadRequest, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }
        image_file, err = image2_response.Open()
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }
        image, err = io.ReadAll(image_file)
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }
        err = FRClient.AddInput(LAST_INPUT, image2_type, image)
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }

        fr_response_text, err := FRClient.Do()
        if err != nil {
                context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error})
                logs.Error(err)
                return
        }

        fr_response := gin.H{
                "ret":   fr_response_text.Ret,
                "score": fr_response_text.Score,
        }
        context.JSON(http.StatusOK, fr_response)
        logs.Info(fmt.Sprintf("POST /stream_fr ip: %s result: %v", context.ClientIP(), fr_response))
}

这里,我开放了一个接收固定格式的表单返回json格式比对结果的服务。表单的字段如下:

url: https://localhost:8080/stream_fr
    method: POST
    content_type: multipart/form-data
    form: {
        "image1_type": "jpg",
        "image1": 文件数据,
        "image2_type": "jpg",
        "image2": 文件数据
    }

大家可以根据自己的需要来设计服务,不必拘泥于形式。

整体搭建

func main() {
        over, err := Execute()
        if err != nil {
                fmt.Println(err)
                return
        }
        if over {
                return
        }

        FRClient = NewFRClient(WithClientInfo(cmd.XFYunFRServerConfig.AppID, XFYunFRServerConfig.APIKey, XFYunFRServerConfig.APISecret))

        err = logs.OpenDefault("xfyun_fr_server.log")
        if err != nil {
                fmt.Println(err)
                return
        }

        err = logs.Run()
        if err != nil {
                fmt.Println(err)
                return
        }

        err = manager.CreateManager(2, 1)
        if err != nil {
                fmt.Println(err)
                return
        }

        manager.Events(func() bool {
                logs.Over()
                err := logs.CloseLogs()
                if err != nil {
                        fmt.Println(err)
                }

                return true
        }, syscall.SIGINT, syscall.SIGTERM)

        gin_server := gin.Default()

        gin_server.POST("/stream_fr", response.PostStreamFR)

        go func() {
                err = gin_server.Run(":8080")
                if err != nil {
                        fmt.Println(err)
                }
        }()

        manager.Run()
}

在构建服务时,程序会先启动日志系统,其次并行运行 HTTP 服务和信号量管理系统。当程序接收到退出程序的系统信号时,程序会先做收尾工作再关闭程序。