Free5GC源码研究(3) - 单个NF的软件架构(下)

前文以一个example-nf为例研究了free5gc中单个NF的软件架构,然而example-nf毕竟不是真正的NF,有一些真正的NF的细节没能在其中展现,比如最重要的:各个NF之间如何交互。本文以AUSF@v1.2.3,一个真正的NF为例,继续深入研究单个NF软件架构的更多细节。

首先我们对比一下ausfexample-nf在目录结构上的不同:

$ tree nf-example                        $ tree NFs/ausf/
├── cmd                                  ├── cmd                                
│   └── main.go                          │   └── main.go                                
├── config                               ├── internal                                
│   └── nfcfg.yaml                       │   ├── context                                
├── internal                             │   │   ├── ausf_context_init.go                                
│   ├── context                          │   │   └── context.go                                
│   │   └── context.go                   │   ├── logger                                
│   ├── logger                           │   │   └── logger.go                                
│   │   └── logger.go                    │   ├── sbi                                
│   └── sbi                              │   │   ├── consumer                                
│       ├── processor                    │   │   │   ├── consumer.go                                
│       │   ├── processor.go             │   │   │   ├── nrf_service.go                                
│       │   ├── processor_mock.go        │   │   │   └── udm_service.go                                
│       │   ├── spy_family.go            │   │   ├── processor                                
│       │   └── spy_family_test.go       │   │   │   ├── processor.go                                
│       ├── api_default.go               │   │   │   └── ue_authentication.go                                
│       ├── api_spyfamily.go             │   │   ├── api_sorprotection.go                                
│       ├── api_spyfamily_test.go        │   │   ├── api_ueauthentication.go                                
│       ├── router.go                    │   │   ├── api_upuprotection.go                                
│       ├── server.go                    │   │   ├── routes.go                                
│       └── server_mock.go               │   │   └── server.go                                
├── pkg                                  │   └── util                                
│   ├── app                              │       ├── router_auth_check.go                                
│   │   └── app.go                       │       └── router_auth_check_test.go                                
│   ├── factory                          ├── pkg                                
│   │   ├── config.go                    │   ├── app                                
│   │   └── factory.go                   │   │   └── app.go                                
│   └── service                          │   ├── factory                                
│       └── init.go                      │   │   ├── config.go                                
├── Makefile                             │   │   └── factory.go                                
├── go.mod                               │   └── service                                
├── go.sum                               │       └── init.go                                
└── README.md                            ├── LICENSE                                
                                         ├── go.mod                                
                                         └── go.sum 

可以看到两者的差别并不大,本质的差别在于ausf/sbi/目录下多了一个consumer/目录以及相应的代码文件。考察AusfApp以及ServerAusf这两个承担主要任务的类型,会发现他们的函数定义里也多了一些与consumer相关的代码:

/* https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/server.go */
type ServerAusf interface {
	app.App

	Consumer() *consumer.Consumer  // import "github.com/free5gc/ausf/internal/sbi/consumer"
	Processor() *processor.Processor
}

// other functions ......

func (s *Server) Run(traceCtx context.Context, wg *sync.WaitGroup) error {
	var err error
	_, s.Context().NfId, err = s.Consumer().RegisterNFInstance(context.Background())
	// other code
}

上面的代码中,Server结构体里多了一个Consumer属性,而Run函数里也调用了consumerRegisterNFInstance函数。

/* https://github.com/free5gc/ausf/blob/v1.2.3/pkg/service/init.go */
type AusfApp struct {
	ausfCtx *ausf_context.AUSFContext
	cfg     *factory.Config

	ctx    context.Context
	cancel context.CancelFunc
	wg     sync.WaitGroup

	sbiServer *sbi.Server
	consumer  *consumer.Consumer  // import "github.com/free5gc/ausf/internal/sbi/consumer"
	processor *processor.Processor
}

// other functions ......

func (a *AusfApp) terminateProcedure() {
	problemDetails, err := a.Consumer().SendDeregisterNFInstance()
    // other code ......
    a.CallServerStop()
}

类似的,AusfApp结构体里也了一个Consumer属性,而terminateProcedure函数里也调用了consumerSendDeregisterNFInstance函数。

从字面上看,AUSF的consumer有着帮忙“注册NF”和“注销NF”的功能。那么consumer到底是干什么用的?

internal/sbi/consumer

深入查看,consumer包定义了Consumer结构体,该结构体包含两个子结构体nnrfServicenudmService

/* https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/consumer/consumer.go */
package consumer

import (
	"github.com/free5gc/ausf/pkg/app"
	"github.com/free5gc/openapi/Nnrf_NFDiscovery"
	"github.com/free5gc/openapi/Nnrf_NFManagement"
	"github.com/free5gc/openapi/Nudm_UEAuthentication"
)

type ConsumerAusf interface {
	app.App
}

type Consumer struct {
	ConsumerAusf

	*nnrfService
	*nudmService
}

func NewConsumer(ausf ConsumerAusf) (*Consumer, error) {
	c := &Consumer{
		ConsumerAusf: ausf,
	}

	c.nnrfService = &nnrfService{
		consumer:        c,
		nfMngmntClients: make(map[string]*Nnrf_NFManagement.APIClient),
		nfDiscClients:   make(map[string]*Nnrf_NFDiscovery.APIClient),
	}

	c.nudmService = &nudmService{
		consumer:    c,
		ueauClients: make(map[string]*Nudm_UEAuthentication.APIClient),
	}

	return c, nil
}

NewConsumer函数用于创建Consumer实例,并初始化其子结构体。其主要做的事情就是创建并初始化Consumer的两个重要属性:ndmfServicennrfmService,他们分别被定义在下面两个代码文件里:

/* https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/consumer/udm_service.go */
import (
	"sync"
	// other imports ...
	Nudm_UEAU "github.com/free5gc/openapi/Nudm_UEAuthentication"
	"github.com/free5gc/openapi/models"
)
type nudmService struct {
	consumer *Consumer
	ueauMu sync.RWMutex
	ueauClients map[string]*Nudm_UEAU.APIClient
}
/* https://github.com/free5gc/ausf/blob/v1.2.3/internal/sbi/consumer/nrf_service.go */
import (
	"sync"
	// other imports ...
	"github.com/free5gc/openapi/Nnrf_NFDiscovery"
	"github.com/free5gc/openapi/Nnrf_NFManagement"
	"github.com/free5gc/openapi/models"
)
type nnrfService struct {
	consumer *Consumer
	nfMngmntMu sync.RWMutex
	nfDiscMu   sync.RWMutex

	nfMngmntClients map[string]*Nnrf_NFManagement.APIClient
	nfDiscClients   map[string]*Nnrf_NFDiscovery.APIClient
}

func (s *nnrfService) SendDeregisterNFInstance() (problemDetails *models.ProblemDetails, err error) {
    // ...
    client := s.getNFManagementClient(ausfContext.NrfUri)
	res, err = client.NFInstanceIDDocumentApi.DeregisterNFInstance(ctx, ausfContext.NfId)
    // ...
}

func (s *nnrfService) RegisterNFInstance(ctx context.Context) (
	resouceNrfUri string, retrieveNfInstanceID string, err error,
) {
    // ...
    client := s.getNFManagementClient(ausfContext.NrfUri)
    nf, res, err = client.NFInstanceIDDocumentApi.RegisterNFInstance(ctx, ausfContext.NfId, nfProfile)
    // ...
}

// other fucntions ...

读到这里,我们初步可以得出结论,ausf的Consumer提供了分别用于与nrfudm这两外两个NF进行通信的功能,其中我们在AusfApp以及ServerAusf里看到的RegisterNFInstanceSendDeregisterNFInstance函数就是与nrf通信的函数——也就是当ausf自己创建成功时,告诉网络中的nrf自己的存在,可以为其他NF提供服务了;而当ausf被终止时,也告诉nrf自己不呢个再提供服务了。

加入了consumer后,前文的NF结构图可以进一步迭代成如下形式:

nf-arch

现在的问题是,ausf的consumer具体如何与nrf和udm进行通信?在consumer的代码文件中可以看到对free5gc/openapi的依赖,这个openapi又是什么?

OpenAPI

OpenAPI是一种描述HTTP API的规范。5G标准选择使用OpenAPI来描述每一个NF可以提供怎样的HTTP服务,以及如何使用这些服务。下面是一个经过简化的OpenAPI的例子:

# https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/uspto.yaml
openapi: "3.0.0"
info:
  title: Dataset API
  version: 2.0.0
paths:
  /{dataset}/fields:
    get:
      tags:
        - metadata
      summary: >-
        Provides the general information about the API and the list of fields
        that can be used to query the dataset.
      operationId: list-searchable-fields
      parameters:
        - name: dataset
          in: path
          description: 'Name of the dataset.'
          required: true
          schema:
            type: string
      responses:
        '200':
          description: >-
            The dataset API for the given version is found and it is accessible
            to consume.
          content:
            application/json:
              schema:
                type: string
        '404':
          description: >-
            The combination of dataset name and version is not found in the
            system or it is not published yet to be consumed by public.
          content:
            application/json:
              schema:
                type: string

以上yaml文档描述了这么一个HTTP API:API路径为/{dataset}/fields,支持GET方法,并且需要传入一个名为dataset的字符串类型参数。可能的返回值有200和404,分别对应API成功和失败,而返回的内容都是字符串,且能被解析为json对象。假设我们在本地有这么一个API服务器,那么我们就可以通过GET /{dataset}/fields来获取这个API的元数据,包括可以查询的字段列表,例如$curl -X GET https://localhost:8080/api/userDataSet/userName

OpenAPI的水很深,以上的例子仅仅揭示了冰山一角。更多的内容还需研读OpenAPI规范。言归正传,5G标准为每一个NF的NFS(Network Function Service)提供了相应的OpenAPI文档,通过研究i这些文档,我们可以清楚地知道每一个NFS提供什么服务,应该怎么使用这些服务。也正因为有了这些清晰定义的文档,我们甚至可以(应该)使用代码生成工具,例如OpenAPI Generator,来生成对应的服务端和客户端的部分代码,从而大大减少开发工作量和手工编程中的bug,并提高代码的规范性。

free5gc/openapi这个代码仓库中的代码就是使用OpenAPI Generator通过解析相应NFS的OpenAPI文档生成的。需要注意的是,free5gc团队使用的OpenAPI Generator是定制版的,所以我们使用开源版工具生成的代码与官方的代码是不相同的。

目前 free5GC 使用的 openapi generator 為團隊維護的客製版本,且暫時尚沒有公開的計畫。如果你有貢獻 free5GC 的計畫,可以告訴我們需要使用的規格書和版本,由我們來產生對應的程式碼。

那么官方的openapi代码又是怎样的?

$ls openapi
CHANGELOG.md            Nnrf_AccessToken          Nudm_ParameterProvision        json_query_builder.go
LICENSE.txt             Nnrf_NFDiscovery          Nudm_SubscriberDataManagement  media_type.go
Namf_Communication      Nnrf_NFManagement         Nudm_UEAuthentication          models
Namf_EventExposure      Nnssf_NSSAIAvailability   Nudm_UEContextManagement       models_nef
Namf_Location           Nnssf_NSSelection         Nudr_DataRepository            multipart_related.go
Namf_MT                 Npcf_AMPolicy             PfdManagement                  oauth
Nausf_SoRProtection     Npcf_BDTPolicyControl     auth.go                        problem_details.go
Nausf_UEAuthentication  Npcf_PolicyAuthorization  client.go                      serialize.go
Nausf_UPUProtection     Npcf_SMPolicyControl      convert.go                     supported_feature.go
Nbsf_Management         Npcf_UEPolicy             convert_test.go                supported_feature_test.go
Nchf_ConvergedCharging  Nsmf_EventExposure        error.go                       util.go
Nnef_PFDmanagement      Nsmf_PDUSession           go.mod                         util_test.go
Nnef_TrafficInfluence   Nudm_EventExposure        go.sum

可以看到,官方的openapi代码中包含了每一个NFS的客户端和服务端代码,以及一些辅助代码。这些代码都是通过OpenAPI Generator生成的。我们再哪一个NFS深入看看。就拿会与ausf以及所有其他NF通信的nrf来说,

$ tree openapi/Nnrf_NFManagement/
.
├── api
│   └── openapi.yaml
├── api_nf_instance_id_document.go
├── api_nf_instances_store.go
├── api_notification.go
├── api_subscription_id_document.go
├── api_subscriptions_collection.go
├── CHANGELOG.md
├── client.go
└── configuration.go

可以看到,除了api/openapi.yaml之外,还有client.goconfiguration.go,以及四个api_*.go文件。这四个api_*.go文件实现了对具体API服务的调用,而client.go则整合了这些API服务,并提供了对外的接口。因此,我们可以看到ausf通过创建一个Nnrf_NFManagement.APIClient对象来调用Nnrf_NFManagement提供的所有API服务。

client.go的代码非常简短,且每一个NFS的client.go文件都是这样大同小异,因此我们在这里不妨仔细阅读一个。

// https://github.com/free5gc/openapi/blob/v1.0.8/Nnrf_NFManagement/client.go
/*
 * NRF NFManagement Service
 *
 * API version: 1.0.0
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package Nnrf_NFManagement

// APIClient manages communication with the NRF NFManagement Service API v1.0.0
// In most cases there should be only one, shared, APIClient.
type APIClient struct {
	cfg    *Configuration
	common service // Reuse a single struct instead of allocating one for each service on the heap.

	// API Services
	NFInstanceIDDocumentApi    *NFInstanceIDDocumentApiService
	NFInstancesStoreApi        *NFInstancesStoreApiService
	SubscriptionIDDocumentApi  *SubscriptionIDDocumentApiService
	SubscriptionsCollectionApi *SubscriptionsCollectionApiService
	NotificationApi            *NotificationApiService
}

type service struct {
	client *APIClient
}

// NewAPIClient creates a new API client. Requires a userAgent string describing your application.
// optionally a custom http.Client to allow for advanced features such as caching.
func NewAPIClient(cfg *Configuration) *APIClient {
	c := &APIClient{}
	c.cfg = cfg
	c.common.client = c

	// API Services
	c.NFInstanceIDDocumentApi = (*NFInstanceIDDocumentApiService)(&c.common)
	c.NFInstancesStoreApi = (*NFInstancesStoreApiService)(&c.common)
	c.SubscriptionIDDocumentApi = (*SubscriptionIDDocumentApiService)(&c.common)
	c.SubscriptionsCollectionApi = (*SubscriptionsCollectionApiService)(&c.common)
	c.NotificationApi = (*NotificationApiService)(&c.common)
	return c
}

api_*.go文件则实现了对具体API服务的调用,例如,api_nf_instance_id_document.go文件实现了对curl -X PUT https://example.com/nnrf/v1/nf-instances/{nfInstanceID}的调用,也就是往nrf注册一个新的NF,简化版的代码如下:

// https://github.com/free5gc/openapi/blob/v1.0.8/Nnrf_NFManagement/api_nf_instance_id_document.go#L260
func (a *NFInstanceIDDocumentApiService) RegisterNFInstance(
        ctx context.Context, nfInstanceID string, nfProfile models.NfProfile) 
    (models.NfProfile, *http.Response, error) {
        // create path and map variables
        // set Accept header
        // body params
        r, err := openapi.PrepareRequest(ctx, a.client.cfg, localVarPath, localVarHTTPMethod, 
                                        localVarPostBody, localVarHeaderParams, localVarQueryParams, 
                                        localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
        localVarHTTPResponse, err := openapi.CallAPI(a.client.cfg, r)

        switch localVarHTTPResponse.StatusCode {
	        case 200: // ......
        }
    }

这个函数做的事情就是把各种信息整合并且构造一个http.Request对象,然后调用openapi.CallAPI函数发送请求并处理返回的响应。

另外值得一提的是,free5gc/openapi里除了NFS的客户端和服务端代码,还有很大一部分是数据模型/数据结构,主要集中在modelsmodels_nef目录下,例如models/model_nf_service.go文件定义了NFService类型。这个类型主要是会被Nnrf_NFManagement用到,但其他的NFS也可能会使用到。

// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_nf_service.go
/*
 * NRF NFManagement Service
 *
 * API version: 1.0.1
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package models

import (
	"time"
)

type NfService struct {
	ServiceInstanceId                string                            `json:"serviceInstanceId" yaml:"serviceInstanceId" bson:"serviceInstanceId" mapstructure:"ServiceInstanceId"`
	ServiceName                      ServiceName                       `json:"serviceName" yaml:"serviceName" bson:"serviceName" mapstructure:"ServiceName"`
	Versions                         *[]NfServiceVersion               `json:"versions" yaml:"versions" bson:"versions" mapstructure:"Versions"`
	Scheme                           UriScheme                         `json:"scheme" yaml:"scheme" bson:"scheme" mapstructure:"Scheme"`
	NfServiceStatus                  NfServiceStatus                   `json:"nfServiceStatus" yaml:"nfServiceStatus" bson:"nfServiceStatus" mapstructure:"NfServiceStatus"`
	Fqdn                             string                            `json:"fqdn,omitempty" yaml:"fqdn" bson:"fqdn" mapstructure:"Fqdn"`
	InterPlmnFqdn                    string                            `json:"interPlmnFqdn,omitempty" yaml:"interPlmnFqdn" bson:"interPlmnFqdn" mapstructure:"InterPlmnFqdn"`
	IpEndPoints                      *[]IpEndPoint                     `json:"ipEndPoints,omitempty" yaml:"ipEndPoints" bson:"ipEndPoints" mapstructure:"IpEndPoints"`
	ApiPrefix                        string                            `json:"apiPrefix,omitempty" yaml:"apiPrefix" bson:"apiPrefix" mapstructure:"ApiPrefix"`
	DefaultNotificationSubscriptions []DefaultNotificationSubscription `json:"defaultNotificationSubscriptions,omitempty" yaml:"defaultNotificationSubscriptions" bson:"defaultNotificationSubscriptions" mapstructure:"DefaultNotificationSubscriptions"`
	AllowedPlmns                     *[]PlmnId                         `json:"allowedPlmns,omitempty" yaml:"allowedPlmns" bson:"allowedPlmns" mapstructure:"AllowedPlmns"`
	AllowedNfTypes                   []NfType                          `json:"allowedNfTypes,omitempty" yaml:"allowedNfTypes" bson:"allowedNfTypes" mapstructure:"AllowedNfTypes"`
	AllowedNfDomains                 []string                          `json:"allowedNfDomains,omitempty" yaml:"allowedNfDomains" bson:"allowedNfDomains" mapstructure:"AllowedNfDomains"`
	AllowedNssais                    *[]Snssai                         `json:"allowedNssais,omitempty" yaml:"allowedNssais" bson:"allowedNssais" mapstructure:"AllowedNssais"`
	Priority                         int32                             `json:"priority,omitempty" yaml:"priority" bson:"priority" mapstructure:"Priority"`
	Capacity                         int32                             `json:"capacity,omitempty" yaml:"capacity" bson:"capacity" mapstructure:"Capacity"`
	Load                             int32                             `json:"load,omitempty" yaml:"load" bson:"load" mapstructure:"Load"`
	RecoveryTime                     *time.Time                        `json:"recoveryTime,omitempty" yaml:"recoveryTime" bson:"recoveryTime" mapstructure:"RecoveryTime"`
	ChfServiceInfo                   *ChfServiceInfo                   `json:"chfServiceInfo,omitempty" yaml:"chfServiceInfo" bson:"chfServiceInfo" mapstructure:"ChfServiceInfo"`
	SupportedFeatures                string                            `json:"supportedFeatures,omitempty" yaml:"supportedFeatures" bson:"supportedFeatures" mapstructure:"SupportedFeatures"`
	Oauth2Required                   string                            `json:"oauth2Required,omitempty" yaml:"oauth2Required" bson:"oauth2Required" mapstructure:"oauth2Required"`
}

这些数据类型全部都在TS29.571由定义。


至此,我们就完成了对单个NF的软件架构的探究。可以看到,Free5GC的软件架构非常清晰(至少在v3.4.3),每一个NF都有自己的模块,并且通过API服务进行通信。我们将开始对每个NF源码的研究,而第一个NF就是AUSF

posted @ 2024-09-27 23:48  zrq96  阅读(386)  评论(0)    收藏  举报