前文回顾
在上一篇文章中,我主要介绍了讯飞云人脸比对 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 服务和信号量管理系统。当程序接收到退出程序的系统信号时,程序会先做收尾工作再关闭程序。