Free5GC源码研究(8) - PCF研究(上)
本文研究 Policy Control Function(PCF)主要实现的功能
前面研究过的AUSF、NRF、UDM、UDR、NSSF等,都是相对比较简单的NF。现在开始要着手研究一下更复杂的NF了。他们复杂即既是因为代码量大,也因为他们的逻辑与其他实体互相交织。比如PCF,就与AMF和SMF高度相关,也与UE和UPF紧密相连。因此,想要理解PCF的原理,就会不得不涉及其他NF、甚至用户设备的工作流程,而研究其他复杂NF时也会如此。因此,对后面各个复杂NF的研究会更加激进地略过多数细节,之关注整体架构和关键细节。
PCF的概念
Policy Control Function, 就是使用各种policy去control通信网络和用户设备行为的网络功能。那么PCF的Policy是怎样的?它们又如何control?如果我们打算找一个定义PCF的标准文档,我们会发现找不到这么个东西,倒是发现与PCF相关的内容散落在一堆文档里。比如
- 描述5G系统架构的TS23.501(5.14)和5G系统过程的TS23.502(4.16)
- 定义5G网络中策略控制和计费框架的TS23.503
- 控制设备接入和移动管理(AMPolicy)29.512、控制设备与网络会话(SMPolicy)的TS29.512,控制背景数据传输(BDTPolicy)的TS29.554、控制用户设备(UEPOolicy)的TS 29.525、描述Policy对其他AF(Application Function)授权细节(PolicyAuthorization)的TS 29.514
- 还有“策略与计费控制信号流及QoS参数映射”TS29.513。尽管这份文档的标题看上去有点奇怪,似乎是讲某个很具体的点,但是free5gc/pcf的代码结构就是照着这份文档的目录写的。
TS23.501中的这个表格给出了一个比较详细的PCF内部服务索引:
所谓Policy是什么,这些文档里都没有清晰明确的定义,不过给出了Policy and Charging Control decision的定义:
PCC decision: A PCF decision for policy and charging control provided to the SMF (consisting of PCC rules and PDU Session related attributes), a PCF decision for access and mobility related policy control provided to the AMF, a PCF decision for UE policy information provided to the UE or a PCF decision for service related policy (e.g. background data transfer policy) provided to the AF.
可以看出,PCF要控制AMF、SMF、UE、和其他AF的服务。其实PCF给我一种一箩筐,啥都可以往里面装的感觉。也正因如此,很难严格定义PCF中的Policy究竟是什么,因为控制不同的实体用到的具体信息和手段都不一样。现在我们只好笼统地理解为Policy是一组控制信息,比如规则和指引。PCF决定好Policy后会告知其他实体,这些实体按照Policy去执行应该做什么不该做什么,这便是PCF去control的方式。
想要更深入理解Policy到底是什么,还是得研究一下与Policy相关的数据类型。下面列举了控制接入与移动性的AmPolicyData
,控制背景数据迁移服务的BdtPolicy
,以及控制会话管理的SmPolicyData
。这些Policy类型中,除了类型名类似以外,基本上找不到什么相似点。这也坐实了PCF是一箩筐的比喻。要想理解PCF的机制,还是得单独研究PCF对各个AMF、SMF、UE、和其他AF的服务是怎么工作的。
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_am_policy_data.go */ // Contains the AM policy data for a given subscriber. type AmPolicyData struct { // Subscription Categoties,也就是用户的订阅套餐,比如是预付prepaid,还是后付postpaid SubscCats []string `json:"subscCats,omitempty" bson:"subscCats"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_am_policy_data.go */ // Represents an Individual BDT policy resource. type BdtPolicy struct { BdtPolData *BdtPolicyData `json:"bdtPolData,omitempty" yaml:"bdtPolData" bson:"bdtPolData" mapstructure:"BdtPolData"` BdtReqData *BdtReqData `json:"bdtReqData,omitempty" yaml:"bdtReqData" bson:"bdtReqData" mapstructure:"BdtReqData"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_bdt_policy_data.go */ // Describes the authorization data of an Individual BDT policy resource. type BdtPolicyData struct { // string identifying a BDT Reference ID as defined in subclause 5.3.3 of 3GPP TS 29.154. BdtRefId string `json:"bdtRefId" yaml:"bdtRefId" bson:"bdtRefId" mapstructure:"BdtRefId"` // Contains transfer policies. TransfPolicies []TransferPolicy `json:"transfPolicies" yaml:"transfPolicies" bson:"transfPolicies" mapstructure:"TransfPolicies"` // Contains an identity of the selected transfer policy. SelTransPolicyId int32 `json:"selTransPolicyId,omitempty" yaml:"selTransPolicyId" bson:"selTransPolicyId" mapstructure:"SelTransPolicyId"` SuppFeat string `json:"suppFeat,omitempty" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_sm_policy_data.go */ // Contains the SM policy data for a given subscriber. type SmPolicyData struct { SmPolicySnssaiData map[string]SmPolicySnssaiData `json:"smPolicySnssaiData" bson:"smPolicySnssaiData"` UmDataLimits map[string]UsageMonDataLimit `json:"umDataLimits,omitempty" bson:"umDataLimits"` UmData map[string]UsageMonData `json:"umData,omitempty" bson:"umData"` }
点击查看更多Policy相关数据类型
与BDT Policy相关的数据类型
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_transfer_policy.go */ // Describes a transfer policy. type TransferPolicy struct { MaxBitRateDl string `json:"maxBitRateDl,omitempty" yaml:"maxBitRateDl" bson:"maxBitRateDl" mapstructure:"MaxBitRateDl"` MaxBitRateUl string `json:"maxBitRateUl,omitempty" yaml:"maxBitRateUl" bson:"maxBitRateUl" mapstructure:"MaxBitRateUl"` // Indicates a rating group for the recommended time window. RatingGroup int32 `json:"ratingGroup" yaml:"ratingGroup" bson:"ratingGroup" mapstructure:"RatingGroup"` RecTimeInt *TimeWindow `json:"recTimeInt" yaml:"recTimeInt" bson:"recTimeInt" mapstructure:"RecTimeInt"` // Contains an identity of a transfer policy. TransPolicyId int32 `json:"transPolicyId" yaml:"transPolicyId" bson:"transPolicyId" mapstructure:"TransPolicyId"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_bdt_req_data.go */ // Contains service requirements for creation a new Individual BDT policy resource. type BdtReqData struct { // Contains an identity of an application service provider. AspId string `json:"aspId" yaml:"aspId" bson:"aspId" mapstructure:"AspId"` DesTimeInt *TimeWindow `json:"desTimeInt" yaml:"desTimeInt" bson:"desTimeInt" mapstructure:"DesTimeInt"` NwAreaInfo *NetworkAreaInfo `json:"nwAreaInfo,omitempty" yaml:"nwAreaInfo" bson:"nwAreaInfo" mapstructure:"NwAreaInfo"` // Indicates a number of UEs. NumOfUes int32 `json:"numOfUes" yaml:"numOfUes" bson:"numOfUes" mapstructure:"NumOfUes"` VolPerUe *UsageThreshold `json:"volPerUe" yaml:"volPerUe" bson:"volPerUe" mapstructure:"VolPerUe"` SuppFeat string `json:"suppFeat,omitempty" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` }
与SMF Policy相关的数据类型
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_sm_policy_snssai_data.go */ // Contains the SM policy data for a given subscriber and S-NSSAI. type SmPolicySnssaiData struct { Snssai *Snssai `json:"snssai" bson:"snssai"` SmPolicyDnnData map[string]SmPolicyDnnData `json:"smPolicyDnnData,omitempty" bson:"smPolicyDnnData"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_sm_policy_dnn_data.go */ // Contains the SM policy data for a given DNN (and S-NSSAI). type SmPolicyDnnData struct { Dnn string `json:"dnn" bson:"dnn"` AllowedServices []string `json:"allowedServices,omitempty" bson:"allowedServices"` SubscCats []string `json:"subscCats,omitempty" bson:"subscCats"` GbrUl string `json:"gbrUl,omitempty" bson:"gbrUl"` GbrDl string `json:"gbrDl,omitempty" bson:"gbrDl"` AdcSupport bool `json:"adcSupport,omitempty" bson:"adcSupport"` SubscSpendingLimits bool `json:"subscSpendingLimits,omitempty" bson:"subscSpendingLimits"` Ipv4Index int32 `json:"ipv4Index,omitempty" bson:"ipv4Index"` Ipv6Index int32 `json:"ipv6Index,omitempty" bson:"ipv6Index"` Offline bool `json:"offline,omitempty" bson:"offline"` Online bool `json:"online,omitempty" bson:"online"` ChfInfo *ChargingInformation `json:"chfInfo,omitempty" bson:"chfInfo"` RefUmDataLimitIds map[string]LimitIdToMonitoringKey `json:"refUmDataLimitIds,omitempty" bson:"refUmDataLimitIds"` MpsPriority bool `json:"mpsPriority,omitempty" bson:"mpsPriority"` ImsSignallingPrio bool `json:"imsSignallingPrio,omitempty" bson:"imsSignallingPrio"` MpsPriorityLevel int32 `json:"mpsPriorityLevel,omitempty" bson:"mpsPriorityLevel"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_usage_mon_data_limit.go */ // Contains usage monitoring control data for a subscriber. type UsageMonDataLimit struct { LimitId string `json:"limitId" bson:"limitId"` Scopes map[string]UsageMonDataScope `json:"scopes,omitempty" bson:"scopes"` UmLevel UsageMonLevel `json:"umLevel,omitempty" bson:"umLevel"` StartDate *time.Time `json:"startDate,omitempty" bson:"startDate"` EndDate *time.Time `json:"endDate,omitempty" bson:"endDate"` UsageLimit *UsageThreshold `json:"usageLimit,omitempty" bson:"usageLimit"` ResetPeriod *time.Time `json:"resetPeriod,omitempty" bson:"resetPeriod"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_usage_mon_data.go */ // Contains remain allowed usage data for a subscriber. type UsageMonData struct { LimitId string `json:"limitId" bson:"limitId"` Scopes map[string]UsageMonDataScope `json:"scopes,omitempty" bson:"scopes"` UmLevel UsageMonLevel `json:"umLevel,omitempty" bson:"umLevel"` AllowedUsage *UsageThreshold `json:"allowedUsage,omitempty" bson:"allowedUsage"` ResetTime *TimePeriod `json:"resetTime,omitempty" bson:"resetTime"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_sm_policy_decision.go */ type SmPolicyDecision struct { // A map of Sessionrules with the content being the SessionRule as described in subclause 5.6.2.7. SessRules map[string]*SessionRule `json:"sessRules,omitempty" yaml:"sessRules" bson:"sessRules" mapstructure:"SessRules"` // A map of PCC rules with the content being the PCCRule as described in subclause 5.6.2.6. PccRules map[string]*PccRule `json:"pccRules,omitempty" yaml:"pccRules" bson:"pccRules" mapstructure:"PccRules"` // If it is included and set to true, it indicates the P-CSCF Restoration is requested. PcscfRestIndication bool `json:"pcscfRestIndication,omitempty" yaml:"pcscfRestIndication" bson:"pcscfRestIndication" mapstructure:"PcscfRestIndication"` // Map of QoS data policy decisions. QosDecs map[string]*QosData `json:"qosDecs,omitempty" yaml:"qosDecs" bson:"qosDecs" mapstructure:"QosDecs"` // Map of Charging data policy decisions. ChgDecs map[string]*ChargingData `json:"chgDecs,omitempty" yaml:"chgDecs" bson:"chgDecs" mapstructure:"ChgDecs"` ChargingInfo *ChargingInformation `json:"chargingInfo,omitempty" yaml:"chargingInfo" bson:"chargingInfo" mapstructure:"ChargingInfo"` // Map of Traffic Control data policy decisions. TraffContDecs map[string]*TrafficControlData `json:"traffContDecs,omitempty" yaml:"traffContDecs" bson:"traffContDecs" mapstructure:"TraffContDecs"` // Map of Usage Monitoring data policy decisions. UmDecs map[string]*UsageMonitoringData `json:"umDecs,omitempty" yaml:"umDecs" bson:"umDecs" mapstructure:"UmDecs"` // Map of QoS characteristics for non standard 5QIs. This map uses the 5QI values as keys. QosChars map[string]*QosCharacteristics `json:"qosChars,omitempty" yaml:"qosChars" bson:"qosChars" mapstructure:"QosChars"` ReflectiveQoSTimer int32 `json:"reflectiveQoSTimer,omitempty" yaml:"reflectiveQoSTimer" bson:"reflectiveQoSTimer" mapstructure:"ReflectiveQoSTimer"` // A map of condition data with the content being as described in subclause 5.6.2.9. Conds map[string]*ConditionData `json:"conds,omitempty" yaml:"conds" bson:"conds" mapstructure:"Conds"` RevalidationTime *time.Time `json:"revalidationTime,omitempty" yaml:"revalidationTime" bson:"revalidationTime" mapstructure:"RevalidationTime"` // Indicates the offline charging is applicable to the PDU session or PCC rule. Offline bool `json:"offline,omitempty" yaml:"offline" bson:"offline" mapstructure:"Offline"` // Indicates the online charging is applicable to the PDU session or PCC rule. Online bool `json:"online,omitempty" yaml:"online" bson:"online" mapstructure:"Online"` // Defines the policy control request triggers subscribed by the PCF. PolicyCtrlReqTriggers []PolicyControlRequestTrigger `json:"policyCtrlReqTriggers,omitempty" yaml:"policyCtrlReqTriggers" bson:"policyCtrlReqTriggers" mapstructure:"PolicyCtrlReqTriggers"` // Defines the last list of rule control data requested by the PCF. LastReqRuleData []RequestedRuleData `json:"lastReqRuleData,omitempty" yaml:"lastReqRuleData" bson:"lastReqRuleData" mapstructure:"LastReqRuleData"` LastReqUsageData *RequestedUsageData `json:"lastReqUsageData,omitempty" yaml:"lastReqUsageData" bson:"lastReqUsageData" mapstructure:"LastReqUsageData"` // Map of PRA information. PraInfos map[string]*PresenceInfoRm `json:"praInfos,omitempty" yaml:"praInfos" bson:"praInfos" mapstructure:"PraInfos"` Ipv4Index int32 `json:"ipv4Index,omitempty" yaml:"ipv4Index" bson:"ipv4Index" mapstructure:"Ipv4Index"` Ipv6Index int32 `json:"ipv6Index,omitempty" yaml:"ipv6Index" bson:"ipv6Index" mapstructure:"Ipv6Index"` QosFlowUsage QosFlowUsage `json:"qosFlowUsage,omitempty" yaml:"qosFlowUsage" bson:"qosFlowUsage" mapstructure:"QosFlowUsage"` SuppFeat string `json:"suppFeat,omitempty" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` }
与UE Policy相关的数据类型
/* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_ue_policy_set.go */ // Contains the UE policy data for a given subscriber. type UePolicySet struct { SubscCats []string `json:"subscCats,omitempty" bson:"subscCats"` UePolicySections map[string]UePolicySection `json:"uePolicySections,omitempty" bson:"uePolicySections"` Upsis []string `json:"upsis,omitempty" bson:"upsis"` } /* https://github.com/free5gc/openapi/blob/v1.0.8/models/model_ue_policy_section.go */ // Contains the UE policy section. type UePolicySection struct { UePolicySectionInfo string `json:"uePolicySectionInfo" bson:"uePolicySectionInfo"` Upsi string `json:"upsi" bson:"upsi"` }
PCF的实现
如果我们查看一下pcf/internal/sbi/processor/
中所有代码文件的行数
/home/ricky/free5gc-3.4.3/NFs/pcf/internal/sbi/processor$ wc -l * | sort 23 processor.go 67 oam.go 106 notifier.go 277 bdtpolicy.go 428 ampolicy.go 1202 smpolicy.go 1770 policyauthorization.go 3873 total
就会发现smpolicy.go
和policyauthorization.go
两个文件的代码量远高于其它,而这两个文件都与网络会话,具体来说是PDU Session以及QoS Flow强相关。因此,这两者的内容值得单独研究。除此以外,bdtpolicy.go
是对BDT服务的实现,ampolicy.go
是对AMF的支持。notifier.go
是消息通知机制,里面实现的都是PCF让其它NF在事件发生时时通知它的用的回调函数。oam.go
这份代码有点奇怪,从名字上看是支持后台管理的(Operation, Administration, Management),但里面只有一个HandleOAMGetAmPolicyRequest(c *gin.Context, supi string)
函数,作用是获取一个用户的AM Policy。也许以后会扩展更多功能?这里面似乎缺了一个管UE的uepolicy.go
。查阅internal/sbi/api_uepolicy.go
,会看到所有的处理函数都没有实现,也就是PCF中与用户设备有关的功能尚未实现。
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/api_uepolicy.go#L43C1-L61C2 func (s *Server) PoliciesPolAssoIdDelete(c *gin.Context) { c.JSON(http.StatusNotImplemented, nil) } func (s *Server) PoliciesPolAssoIdGet(c *gin.Context) { c.JSON(http.StatusNotImplemented, nil) } func (s *Server) PoliciesPolAssoIdUpdatePost(c *gin.Context) { c.JSON(http.StatusNotImplemented, nil) } func (s *Server) PoliciesPost(c *gin.Context) { c.JSON(http.StatusNotImplemented, nil) }
PCF的Context
PCF在数据管理方面既会用到内存Context,又会用到本地数据库的MongoDB,还会用到统一数据仓库UDR,不过更多还是用的Context。由于PCF管理者各种实体的Policy,所以其Context的设计也比较复杂。下面是PCF的主要Context:
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/context/context.go#L23 type PCFContext struct { // 省略正常的NF属性...... DefaultBdtRefId string // UePool map[string]*UeContext UePool sync.Map // Bdt Policy related BdtPolicyPool sync.Map BdtPolicyIDGenerator *idgenerator.IDGenerator // App Session related AppSessionPool sync.Map // AMF Status Change Subscription related AMFStatusSubsData sync.Map // map[string]AMFStatusSubscriptionData; subscriptionID as key // lock DefaultUdrURILock sync.RWMutex // Charging RatingGroupIdGenerator *idgenerator.IDGenerator }
我们知道PCF能控制AMF、SMF、UE(尚未实现),还有BDT服务。在上面的Context中很明显可以看出,BdtPolicyPool
存储着各种和BDT服务相关的Ploicy,而AppSessionPool
则保存各种AF Application Context:
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/context/context.go#L64 type AppSessionData struct { AppSessionId string AppSessionContext *models.AppSessionContext // (compN/compN-subCompN/appId-%s) map to PccRule RelatedPccRuleIds map[string]string PccRuleIdMapToCompId map[string]string // EventSubscription Events map[models.AfEvent]models.AfNotifMethod EventUri string // related Session SmPolicyData *UeSmPolicyData }
当一些提供应用的网络功能(AF,不在5G标准中定义的,第三方开发的NF)需要像核心网申请资源时,会向PCF申请并创建一些Policy来管理AF绑定的资源。这个AppSession
就是PCF用来管理和追踪应用程序对网络资源请求的机制,确保应用程序获得所需的网络服务质量,同时也便于网络进行资源的合理分配和管理。AF申请的资源里包括了网络与设备的会话,所以AppSession
会绑定一个smPolicyData
。
与用户设备有关的Policy会存储在UePool
的UeContext
中,当然也包括与用户设备关联的AMPolicyData
、SmPolicyData
、以及AppSession
:
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/context/ue.go // key is supi type UeContext struct { // Ue Context Supi string Gpsi string Pei string GroupIds []string PolAssociationIDGenerator uint32 AMPolicyData map[string]*UeAMPolicyData // use PolAssoId(ue.Supi-numPolId) as key // Udr Ref UdrUri string // SMPolicy SmPolicyData map[string]*UeSmPolicyData // use smPolicyId(ue.Supi-pduSessionId) as key // App Session Related // AppSessionIDGenerator uint64 AppSessionIDGenerator *idgenerator.IDGenerator // PolicyAuth AfRoutReq *models.AfRoutingRequirement AspId string // Policy Decision AppSessionIdStore *AppSessionIdStore PolicyDataSubscriptionStore *models.PolicyDataSubscription PolicyDataChangeStore *models.PolicyDataChangeNotification // ChargingRatingGroup RatingGroupData map[string][]int32 // use smPolicyId(ue.Supi-pduSessionId) as key } type UeAMPolicyData struct { PolAssoId string AccessType models.AccessType NotificationUri string ServingPlmn *models.NetworkId AltNotifIpv4Addrs []string AltNotifIpv6Addrs []string // TODO: AMF Status Change AmfStatusUri string Guami *models.Guami ServiveName string // TraceReq *TraceData // Policy Association Triggers []models.RequestTrigger ServAreaRes *models.ServiceAreaRestriction Rfsp int32 UserLoc *models.UserLocation TimeZone string SuppFeat string // about AF request Pras map[string]models.PresenceInfo // related to UDR Subscription Data AmPolicyData *models.AmPolicyData // Svbscription Data // Corresponding UE PcfUe *UeContext } type UeSmPolicyData struct { PackFiltIdGenerator int32 PccRuleIdGenerator int32 ChargingIdGenerator int32 // FlowMapsToPackFiltIds map[string][]string // use Flow Description(in TS 29214) as key map to pcc rule ids PackFiltMapToPccRuleId map[string]string // use PackFiltId as Key // Related to GBR RemainGbrUL *float64 RemainGbrDL *float64 // related to UDR Subscription Data SmPolicyData *models.SmPolicyData // Svbscription Data // related to Policy PolicyContext *models.SmPolicyContextData PolicyDecision *models.SmPolicyDecision // related to AppSession AppSessions map[string]bool // related appSessionId // Corresponding UE PcfUe *UeContext InfluenceDataToPccRule map[string]string SubscriptionID string }
BDT Policy
Background data transfer: feature that enables a 3rd party service provider to keep their costs lower by favouring time windows for data transfer to specific UEs in a geographical area during non-busy hours that are less costly and able to handle larger bitrates
BDT, 全程Background Data Transfer,是一种智能化的数据传输服务,能让数据传输在网络中相对不那么繁忙的时段进行,以最优化网络资源利用,同时也最小化网费,同时也能协调网络中各个设备的数据传输,避免出现网络拥堵。这对物联网场景就特别有用,比如车联网里的各种智能汽车之间,重要的安全和导航相关数据能够及时传递,而级别较低的高清路况图片等数据就可以延后传输。PCF对BDT服务的支持,就包括向第三方NF提供咨询服务,告诉他们什么情况下用什么Policy合适,以及必要时发出警告提示。不过目前free5gc/pcf只实现了最基本的功能,亦即对policy的管理,没有发出BDT告警的功能。
HandleCreateBDTPolicyContextRequest
这个函数的名字有点误导性,乍看之下好像是在PCF创建一个BDT Policy,但其实它的功能是告诉其他NF应当使用怎样的Policy,重点是怎样的Tansfer policy,也就是最大的上传、下载速率,推荐的进行数据传输的时间窗口,以及这次传输会怎么计费(e.g., RatingGroup 1: 普通上网流量、2: 视频流量、3: 游戏流量)。
// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_transfer_policy.go type TransferPolicy struct { MaxBitRateDl string `json:"maxBitRateDl,omitempty" yaml:"maxBitRateDl" bson:"maxBitRateDl" mapstructure:"MaxBitRateDl"` MaxBitRateUl string `json:"maxBitRateUl,omitempty" yaml:"maxBitRateUl" bson:"maxBitRateUl" mapstructure:"MaxBitRateUl"` // Indicates a rating group for the recommended time window. RatingGroup int32 `json:"ratingGroup" yaml:"ratingGroup" bson:"ratingGroup" mapstructure:"RatingGroup"` RecTimeInt *TimeWindow `json:"recTimeInt" yaml:"recTimeInt" bson:"recTimeInt" mapstructure:"RecTimeInt"` // Contains an identity of a transfer policy. TransPolicyId int32 `json:"transPolicyId" yaml:"transPolicyId" bson:"transPolicyId" mapstructure:"TransPolicyId"` }
当一些第三方应用想要使用BDT服务时,它们会把这次传输的各种参数,如涉及的设备数量NumOfUes
,每个设备用到的数据量VolPerUe
,这次传输偏好的时间窗口desTimeInt
、这次传输的覆盖的网络范围NwAreaInfo
等,打包成一个BdtReqData
:
// // Contains service requirements for creation a new Individual BDT policy resource. type BdtReqData struct { // Contains an identity of an application service provider. AspId string `json:"aspId" yaml:"aspId" bson:"aspId" mapstructure:"AspId"` DesTimeInt *TimeWindow `json:"desTimeInt" yaml:"desTimeInt" bson:"desTimeInt" mapstructure:"DesTimeInt"` NwAreaInfo *NetworkAreaInfo `json:"nwAreaInfo,omitempty" yaml:"nwAreaInfo" bson:"nwAreaInfo" mapstructure:"NwAreaInfo"` // Indicates a number of UEs. NumOfUes int32 `json:"numOfUes" yaml:"numOfUes" bson:"numOfUes" mapstructure:"NumOfUes"` VolPerUe *UsageThreshold `json:"volPerUe" yaml:"volPerUe" bson:"volPerUe" mapstructure:"VolPerUe"` SuppFeat string `json:"suppFeat,omitempty" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` }
然后调用这个函数HandleCreateBDTPolicyContextRequest
。PCF会先去UDR查一下这个第三方应用AspId
以前是不是来问过,有的话把之前的policy拿过来改一改时间窗口就行;没有的话就要为这个应用新建一个默认policy返回给来咨询的第三方应用。这个新创建的policy也要保存会Context和UDR中以备后用。
func (p *Processor) HandleCreateBDTPolicyContextRequest(c *gin.Context, requestMsg models.BdtReqData) { client := util.GetNudrClient(p.getDefaultUdrUri(p.Context())) bdtDatas, httpResponse, err := client.DefaultApi.PolicyDataBdtDataGet(ctx) response := &models.BdtPolicy{} response.BdtReqData = deepcopy.Copy(requestMsg).(*models.BdtReqData) var bdtData *models.BdtData var bdtPolicyData models.BdtPolicyData for _, data := range bdtDatas { // If the Application Service Provider has existed, use its background data policy if requestMsg.AspId == data.AspId { bdtData = &data break } } // Only support one bdt policy, TODO: more policy for decision if bdtData != nil { // found // modify policy according to new request bdtData.TransPolicy.RecTimeInt = requestMsg.DesTimeInt } else { // use default bdt policy, TODO: decide bdt transfer data policy bdtData = &models.BdtData{ AspId: requestMsg.AspId, BdtRefId: uuid.New().String(), TransPolicy: getDefaultTransferPolicy(1, *requestMsg.DesTimeInt), } } if requestMsg.NwAreaInfo != nil { bdtData.NwAreaInfo = *requestMsg.NwAreaInfo } bdtPolicyData.SelTransPolicyId = bdtData.TransPolicy.TransPolicyId // no support feature in subclause 5.8 of TS29554 bdtPolicyData.BdtRefId = bdtData.BdtRefId bdtPolicyData.TransfPolicies = append(bdtPolicyData.TransfPolicies, bdtData.TransPolicy) response.BdtPolData = &bdtPolicyData bdtPolicyID, err := pcfSelf.AllocBdtPolicyID() pcfSelf.BdtPolicyPool.Store(bdtPolicyID, response) // Update UDR BDT Data(PUT) param := Nudr_DataRepository.PolicyDataBdtDataBdtReferenceIdPutParamOpts{ BdtData: optional.NewInterface(*bdtData), } client.DefaultApi.PolicyDataBdtDataBdtReferenceIdPut(ctx, bdtPolicyData.BdtRefId, ¶m) c.JSON(http.StatusCreated, response) }
在以后知道bdtPolicyID
的话,就可以凭此到PCF的Context中直接拿到相应的BdtPolicy
。
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/bdtpolicy.go#L20 func (p *Processor) HandleGetBDTPolicyContextRequest(c *gin.Context, bdtPolicyID string) { value, ok := p.Context().BdtPolicyPool.Load(bdtPolicyID); ok { bdtPolicy := value.(*models.BdtPolicy) c.JSON(http.StatusOK, bdtPolicy) }
上述的HandleCreateBDTPolicyContextRequest
只会新建一个默认policy,如果觉得默认设置不够很好的话),就可以对其update。
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/bdtpolicy.go#L44 func (p *Processor) HandleUpdateBDTPolicyContextProcedure(c *gin.Context, bdtPolicyID string, bdtPolicyDataPatch models.BdtPolicyDataPatch, ) { var bdtPolicy *models.BdtPolicy value, ok := p.Context().BdtPolicyPool.Load(bdtPolicyID) bdtPolicy = value.(*models.BdtPolicy) for _, policy := range bdtPolicy.BdtPolData.TransfPolicies { if policy.TransPolicyId == bdtPolicyDataPatch.SelTransPolicyId { polData := bdtPolicy.BdtPolData polReq := bdtPolicy.BdtReqData // 修改selected TansPolicy polData.SelTransPolicyId = bdtPolicyDataPatch.SelTransPolicyId bdtData := models.BdtData{ AspId: polReq.AspId, TransPolicy: policy, BdtRefId: polData.BdtRefId, } if polReq.NwAreaInfo != nil { bdtData.NwAreaInfo = *polReq.NwAreaInfo } param := Nudr_DataRepository.PolicyDataBdtDataBdtReferenceIdPutParamOpts{ BdtData: optional.NewInterface(bdtData), } client := util.GetNudrClient(p.getDefaultUdrUri(p.Context())) ctx, pd, err := p.Context().GetTokenCtx(models.ServiceName_NUDR_DR, models.NfType_UDR) rsp, err := client.DefaultApi.PolicyDataBdtDataBdtReferenceIdPut(ctx, bdtData.BdtRefId, ¶m) c.JSON(http.StatusOK, bdtPolicy) return } } }
然而这更新仅限调整其selected Tansfer Policy,无法修改其他,比如一个TransferPolicy
的MaxBitRateDl
。我快速查看了一下其他NF的代码,也没发现哪里能够直接修改TransferPolicy.MaxBitRateDl
。也许这需要运营商在后台操作来修改吧。
TransferPolicy, BdtData, BdtPolicyData, BdtPolicy
在一开始研究BDT的这些数据类型时我越看越糊涂。有必要辨析一下它们的关系。
// Describes a transfer policy. type TransferPolicy struct { MaxBitRateDl string `json:"maxBitRateDl,omitempty" yaml:"maxBitRateDl" bson:"maxBitRateDl" mapstructure:"MaxBitRateDl"` MaxBitRateUl string `json:"maxBitRateUl,omitempty" yaml:"maxBitRateUl" bson:"maxBitRateUl" mapstructure:"MaxBitRateUl"` // Indicates a rating group for the recommended time window. RatingGroup int32 `json:"ratingGroup" yaml:"ratingGroup" bson:"ratingGroup" mapstructure:"RatingGroup"` RecTimeInt *TimeWindow `json:"recTimeInt" yaml:"recTimeInt" bson:"recTimeInt" mapstructure:"RecTimeInt"` // Contains an identity of a transfer policy. TransPolicyId int32 `json:"transPolicyId" yaml:"transPolicyId" bson:"transPolicyId" mapstructure:"TransPolicyId"` } // Contains the background data transfer data. type BdtData struct { AspId string `json:"aspId,omitempty" bson:"aspId"` TransPolicy TransferPolicy `json:"transPolicy" bson:"transPolicy"` BdtRefId string `json:"bdtRefId,omitempty" bson:"bdtRefId"` NwAreaInfo NetworkAreaInfo `json:"nwAreaInfo,omitempty" bson:"nwAreaInfo"` } // Describes the authorization data of an Individual BDT policy resource. type BdtPolicyData struct { // string identifying a BDT Reference ID as defined in subclause 5.3.3 of 3GPP TS 29.154. BdtRefId string `json:"bdtRefId" yaml:"bdtRefId" bson:"bdtRefId" mapstructure:"BdtRefId"` // Contains transfer policies. TransfPolicies []TransferPolicy `json:"transfPolicies" yaml:"transfPolicies" bson:"transfPolicies" mapstructure:"TransfPolicies"` // Contains an identity of the selected transfer policy. SelTransPolicyId int32 `json:"selTransPolicyId,omitempty" yaml:"selTransPolicyId" bson:"selTransPolicyId" mapstructure:"SelTransPolicyId"` SuppFeat string `json:"suppFeat,omitempty" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` } // Represents an Individual BDT policy resource. type BdtPolicy struct { BdtPolData *BdtPolicyData `json:"bdtPolData,omitempty" yaml:"bdtPolData" bson:"bdtPolData" mapstructure:"BdtPolData"` BdtReqData *BdtReqData `json:"bdtReqData,omitempty" yaml:"bdtReqData" bson:"bdtReqData" mapstructure:"BdtReqData"` }
首先是TransferPolicy
,他是BDT最基本的单位。然后是BdtPolicy
,它是Create和Update函数的返回给调用者的类型。关键是BdtData
和BdtPolicyData
,前者对应一个BdtRefId
和一个TansferPolicy
;后者对应一个BdtRefId
和多个TansferPolicy
。他们俩有什么关系和区别?
一个明显的区别是BdtData
保存在UDR中,而BdtPolicyData
保存在PCFContext。进而,一个解释是BdtPolicyData
作为本地运行时数据,可以包含多个可选的TransferPolicy
,第三方应用通过SelTransPolicyId
指示当前选中使用哪个策略。本地更新完毕后,将选中的策略同步到UDR的BdtData
中。这是一个典型的通过不同层次的数据结构来管理和维护本地与远程中心化信息的设计。
然而,这个解释说不通的地方在于BdtPolicyData
需要包含多个可选的TransferPolicy
,但整份bdtpolicy.go代码中只有HandleCreateBDTPolicyContextRequest
函数在新建BdtPolicyData
时顺带把一个默认TransferPolicy
添加到了一个空的切片中append(bdtPolicyData.TransfPolicies, bdtData.TransPolicy)
,其他地方再也没有对这个切片进行改动。也就是说bdtPolicyData.TransfPolicies
永远只有一个可选项!我并没有在PCF模块的其他地方找到能为这个切片新增policy的代码,更没有在free5gc代码库其他地方找到(理论上也不可能,因为其他模块不能调用PCF的Context)。虽然这又说不过去的地方,但我目前没找到更合理的解释,只能认为BDT的功能还不完善,以后的版本会改进吧。
AM Policy
PCF的第二组重要功能时当用户设备接入网络时,也就是我们每次开机或者重新打开数据开关连接移动网络时,为AMF提供policy,告知AMF应该怎样管理设备的接入和移动性。确切来说,PostPoliciesProcedure
函数在PCF的context中为(新的)AMF传来的policy与用户设备UE构建一个联系PolicyAssociation
。当核心网内负责管理某个设备的AMF发生变化时(旧的AMF替代新的AMF),这个函数也会被调用。
简单来说,PCF首先创建或获取用户设备的UeContext,然后从UDR获取该设备的AmPolicyData(其实就只是用户的订阅类型),据此设置policy特性和参数(实际上没啥可做),为用户设备与AMF传过来的数据创建一个关联类型UeAMPolicyData
,订阅一下这个AMF的状态,最后返回创建好的PolicyAssociation
。
PolicyAssociation, PolicyAssociationRequest, UeAMPolicyData
// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_policy_association.go type PolicyAssociation struct { Request *PolicyAssociationRequest `json:"request,omitempty" yaml:"request" bson:"request" mapstructure:"Request"` // Request Triggers that the PCF subscribes. Only values \"LOC_CH\" and \"PRA_CH\" are permitted. Triggers []RequestTrigger `json:"triggers,omitempty" yaml:"triggers" bson:"triggers" mapstructure:"Triggers"` ServAreaRes *ServiceAreaRestriction `json:"servAreaRes,omitempty" yaml:"servAreaRes" bson:"servAreaRes" mapstructure:"ServAreaRes"` Rfsp int32 `json:"rfsp,omitempty" yaml:"rfsp" bson:"rfsp" mapstructure:"Rfsp"` Pras map[string]PresenceInfo `json:"pras,omitempty" yaml:"pras" bson:"pras" mapstructure:"Pras"` SuppFeat string `json:"suppFeat" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` } // https://github.com/free5gc/openapi/blob/v1.0.8/models/model_request_trigger.go type RequestTrigger string const ( RequestTrigger_LOC_CH RequestTrigger = "LOC_CH" // 位置变化 RequestTrigger_PRA_CH RequestTrigger = "PRA_CH" // 存在状态变化 RequestTrigger_SERV_AREA_CH RequestTrigger = "SERV_AREA_CH" // 服务范围变化 RequestTrigger_RFSP_CH RequestTrigger = "RFSP_CH" // RFSP 变化 ) // https://github.com/free5gc/openapi/blob/v1.0.8/models/model_policy_association_request.go type PolicyAssociationRequest struct { NotificationUri string `json:"notificationUri" yaml:"notificationUri" bson:"notificationUri" mapstructure:"NotificationUri"` // Alternate or backup IPv4 Address(es) where to send Notifications. AltNotifIpv4Addrs []string `json:"altNotifIpv4Addrs,omitempty" yaml:"altNotifIpv4Addrs" bson:"altNotifIpv4Addrs" mapstructure:"AltNotifIpv4Addrs"` // Alternate or backup IPv6 Address(es) where to send Notifications. AltNotifIpv6Addrs []string `json:"altNotifIpv6Addrs,omitempty" yaml:"altNotifIpv6Addrs" bson:"altNotifIpv6Addrs" mapstructure:"AltNotifIpv6Addrs"` Supi string `json:"supi" yaml:"supi" bson:"supi" mapstructure:"Supi"` Gpsi string `json:"gpsi,omitempty" yaml:"gpsi" bson:"gpsi" mapstructure:"Gpsi"` AccessType AccessType `json:"accessType,omitempty" yaml:"accessType" bson:"accessType" mapstructure:"AccessType"` Pei string `json:"pei,omitempty" yaml:"pei" bson:"pei" mapstructure:"Pei"` UserLoc *UserLocation `json:"userLoc,omitempty" yaml:"userLoc" bson:"userLoc" mapstructure:"UserLoc"` TimeZone string `json:"timeZone,omitempty" yaml:"timeZone" bson:"timeZone" mapstructure:"TimeZone"` ServingPlmn *NetworkId `json:"servingPlmn,omitempty" yaml:"servingPlmn" bson:"servingPlmn" mapstructure:"ServingPlmn"` RatType RatType `json:"ratType,omitempty" yaml:"ratType" bson:"ratType" mapstructure:"RatType"` GroupIds []string `json:"groupIds,omitempty" yaml:"groupIds" bson:"groupIds" mapstructure:"GroupIds"` ServAreaRes *ServiceAreaRestriction `json:"servAreaRes,omitempty" yaml:"servAreaRes" bson:"servAreaRes" mapstructure:"ServAreaRes"` Rfsp int32 `json:"rfsp,omitempty" yaml:"rfsp" bson:"rfsp" mapstructure:"Rfsp"` Guami *Guami `json:"guami,omitempty" yaml:"guami" bson:"guami" mapstructure:"Guami"` // If the NF service consumer is an AMF, it should provide the name of a service produced by the AMF that makes use of information received within the Npcf_AMPolicyControl_UpdateNotify service operation. ServiveName string `json:"serviveName,omitempty" yaml:"serviveName" bson:"serviveName" mapstructure:"ServiveName"` TraceReq *TraceData `json:"traceReq,omitempty" yaml:"traceReq" bson:"traceReq" mapstructure:"TraceReq"` SuppFeat string `json:"suppFeat" yaml:"suppFeat" bson:"suppFeat" mapstructure:"SuppFeat"` } // https://github.com/free5gc/pcf/blob/v1.2.5/internal/context/ue.go#L44C1-L69C2 type UeAMPolicyData struct { PolAssoId string AccessType models.AccessType NotificationUri string ServingPlmn *models.NetworkId AltNotifIpv4Addrs []string AltNotifIpv6Addrs []string // TODO: AMF Status Change AmfStatusUri string Guami *models.Guami ServiveName string // TraceReq *TraceData // Policy Association Triggers []models.RequestTrigger ServAreaRes *models.ServiceAreaRestriction Rfsp int32 UserLoc *models.UserLocation TimeZone string SuppFeat string // about AF request Pras map[string]models.PresenceInfo // related to UDR Subscription Data AmPolicyData *models.AmPolicyData // Svbscription Data // Corresponding UE PcfUe *UeContext }
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/ampolicy.go#L190 func (p *Processor) PostPoliciesProcedure(polAssoId string, policyAssociationRequest models.PolicyAssociationRequest, ) (*models.PolicyAssociation, string, *models.ProblemDetails) { var response models.PolicyAssociation pcfSelf := p.Context() var ue *pcf_context.UeContext if val, ok := pcfSelf.UePool.Load(policyAssociationRequest.Supi); ok { ue = val.(*pcf_context.UeContext) } else { ue, err := pcfSelf.NewPCFUe(policyAssociationRequest.Supi) } response.Request = deepcopy.Copy(&policyAssociationRequest).(*models.PolicyAssociationRequest) assolId := fmt.Sprintf("%s-%d", ue.Supi, ue.PolAssociationIDGenerator) amPolicy := ue.AMPolicyData[assolId] ctx, pd, err := p.Context().GetTokenCtx(models.ServiceName_NUDR_DR, models.NfType_UDR) if amPolicy == nil || amPolicy.AmPolicyData == nil { client := util.GetNudrClient(udrUri) amData, response, err := client.DefaultApi.PolicyDataUesUeIdAmDataGet(ctx, ue.Supi) if amPolicy == nil { amPolicy = ue.NewUeAMPolicyData(assolId, policyAssociationRequest) } amPolicy.AmPolicyData = &amData } // TODO: according to PCF Policy to determine ServAreaRes, Rfsp, SuppFeat // amPolicy.ServAreaRes = // amPolicy.Rfsp = ue.PolAssociationIDGenerator++ // increment the counter // if consumer is AMF then subscribe this AMF Status if policyAssociationRequest.Guami != nil { // if policyAssociationRequest.Guami has been subscribed, then no need to subscribe again needSubscribe := true pcfSelf.AMFStatusSubsData.Range(func(key, value interface{}) bool { data := value.(pcf_context.AMFStatusSubscriptionData) for _, guami := range data.GuamiList { if reflect.DeepEqual(guami, *policyAssociationRequest.Guami) { needSubscribe = false break } } // if no need to subscribe => stop iteration return needSubscribe }) if needSubscribe { // 但目前没什么卵用,因为即便AMF的状态发生变化,PCF什么也不会做——相关处理函数尚未实现 // https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/notifier.go#L15 amfUri := p.Consumer().SendNFInstancesAMF(pcfSelf.NrfUri, *policyAssociationRequest.Guami, models.ServiceName_NAMF_COMM) if amfUri != "" { p.Consumer().AmfStatusChangeSubscribe(amfUri, []models.Guami{*policyAssociationRequest.Guami}) amPolicy.Guami = policyAssociationRequest.Guami } } } return &response, locationHeader, nil }
说是PCF告诉AMF该怎么做,但细看下来PostPoliciesProcedure
函数只是把AMF传过来的PolicyAssociationRequest
与自己context中的UE关联起来:ue.NewUeAMPolicyData(assolId, policyAssociationRequest)
,最后把PolicyAssociationRequest
打包成PolicyAssociation
就返回了。唯一有实际意义的事情时从UDR把用户设备得到Subscription Categories添加入policy中。以后的版本应该会做更多policy决策。
ue.NewUeAMPolicyData
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/context/ue.go#L106 func (ue *UeContext) NewUeAMPolicyData(assolId string, req models.PolicyAssociationRequest) *UeAMPolicyData { ue.Gpsi = req.Gpsi ue.Pei = req.Pei ue.GroupIds = req.GroupIds ue.AMPolicyData[assolId] = &UeAMPolicyData{ PolAssoId: assolId, ServAreaRes: req.ServAreaRes, AltNotifIpv4Addrs: req.AltNotifIpv4Addrs, AltNotifIpv6Addrs: req.AltNotifIpv6Addrs, AccessType: req.AccessType, NotificationUri: req.NotificationUri, ServingPlmn: req.ServingPlmn, TimeZone: req.TimeZone, Rfsp: req.Rfsp, Guami: req.Guami, UserLoc: req.UserLoc, ServiveName: req.ServiveName, PcfUe: ue, } ue.AMPolicyData[assolId].Pras = make(map[string]models.PresenceInfo) return ue.AMPolicyData[assolId] }
相比其Post的逻辑,Get和Delete的逻辑异常简单。
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/ampolicy.go#L18 func (p *Processor) HandleDeletePoliciesPolAssoId(c *gin.Context, polAssoId string) { ue := p.Context().PCFUeFindByPolicyId(polAssoId) delete(ue.AMPolicyData, polAssoId) c.JSON(http.StatusNoContent, nil) } // // https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/ampolicy.go#L35 func (p *Processor) HandleGetPoliciesPolAssoId(c *gin.Context, polAssoId string) { ue := p.Context().PCFUeFindByPolicyId(polAssoId) amPolicyData := ue.AMPolicyData[polAssoId] // 注意,这个是`UeAMPolicyData`类型!!! rsp := models.PolicyAssociation{ SuppFeat: amPolicyData.SuppFeat, } // check other fields, set default values to rsp ...... c.JSON(http.StatusOK, rsp) }
更新的逻辑是综合了查找和新增的逻辑,在此略过。
ampolicy.go
剩下的函数是在AM Policy发生(不是由AMF发起的)变更时通知AMF。下面时简化版的代码,逻辑相对简单。和之前NRF的通知机制类似,我没发现这两个通知函数在哪里被调用了。应该在后续版本会完善这个变更机制吧。
// https://github.com/free5gc/pcf/blob/v1.2.5/internal/sbi/processor/ampolicy.go#L311 // Send AM Policy Update to AMF if policy has changed func (p *Processor) SendAMPolicyUpdateNotification(ue *pcf_context.UeContext, PolId string, request models.PolicyUpdate, ) { amPolicyData := ue.AMPolicyData[PolId] ctx, _, err := p.Context().GetTokenCtx(models.ServiceName_NPCF_AM_POLICY_CONTROL, models.NfType_PCF) client := util.GetNpcfAMPolicyCallbackClient() uri := amPolicyData.NotificationUri for uri != "" { rsp, err := client.DefaultCallbackApi.PolicyUpdateNotification(ctx, uri, request) } } // Send AM Policy Update to AMF if policy has been terminated func (p *Processor) SendAMPolicyTerminationRequestNotification(ue *pcf_context.UeContext, PolId string, request models.TerminationNotification, ) { amPolicyData := ue.AMPolicyData[PolId] client := util.GetNpcfAMPolicyCallbackClient() uri := amPolicyData.NotificationUri ctx, _, err := p.Context().GetTokenCtx(models.ServiceName_NPCF_AM_POLICY_CONTROL, models.NfType_PCF) for uri != "" { rsp, err := client.DefaultCallbackApi.PolicyAssocitionTerminationRequestNotification(ctx, uri, request) } }
由于smpolicy.go
和policyauthorization.go
两个文件的代码量远高于其它,而这两个文件都与SMF强相关。因此,这两者内容值得单独研究。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!