首页 > 编程语言 >Free5GC源码研究(7) - NSSF研究

Free5GC源码研究(7) - NSSF研究

时间:2024-10-26 12:42:07浏览次数:1  
标签:string NssfConfig models NSSF factory 切片 源码 Free5GC

本文研究 Network Slice Selection Function(NSSF)主要实现的功能

NSSF的概念

NSSF,也就是网络切片选择功能,负责根据用户请求和网络的配置来选择最合适的网络切片实例(Network Slice Instance, NSI)来服务用户设备。

所谓网络切片,是5G核心网的重要概念,允许运营商在同一物理基础设施上创建多个虚拟网络,每个虚拟网络都可以有不同的服务等级和特性,以满足不同的业务需求。用TS23.501的原话来说,一个网络切片就是

"logical network that provides specific network capabilities and network characteristics"

"a set of Network Function instances and the required resources (e.g. compute, storage and networking resources) which form a deployed Network Slice".

这有点像在同一物理机器上创建多个虚拟机,每个虚拟机可以有不同的操作系统和分配给它的算力和存储空间,以满足不同业务的需求,比如

  • 一台虚拟机可以有较少的算力的存储、一般的网络带宽,但是装的Windows系统,可以服务普通用户的日常办公操作
  • 一台虚拟机也只有较少的算力的存储、较大的网络带宽,但是装的Ubuntu系统,可以服务程序员的日常软件开发
  • 一台虚拟机装Scientific Linux,配置较多的算力和存储和较小的网络带宽,用来搞大规模科学计算

同理,在同一个5G物理网络上也可以部署几个虚拟网络——也就是网络切片——配置不同的计算和网络资源(当然也还有不同的计费策略),来支持不同的需求,比如说一个切片专门用于大带宽(eMBB),一个切片用于海量接入(MIOT),一个用于高可靠低延迟(URLLC)。[email protected]定义了更多标准切片类型,各大运营商也可以按需定义自己的切片:

img

NSSF的功能划分为两个NFS:Nnssf_NSSAIAvailabilityNnssf_NSSelection,主要是对网路切片信息增删改查,更准确来说是对NSSAI(Network Slice Selection Assistance Information)增删改查。NSSAI是网络切片的标识符,当用户设备试图接入网络时,负责接入管理的AMF会向NSSF咨询合适的网络切片,此时NSSF返回给AMF的也是NAASI。NSSF的这两个服务在free5gc都实现了(毕竟也不复杂)。

img

要注意的是NSSF,顾名思义,是网络切片选择功能,也只是网路切片选择功能。网络切片的创建、管理、删除等不归NSSF管。知道目前为止,[email protected]也没有详细说明哪一个NF应当负责切片的管理,而是在另一组标准TS28.530中说明。

NSSF的实现

根前面研究过的AUSF、NRF、UDM、UDR都不同,NSSF的context看起来平平无奇,只有身为一个NF该有的基本信息。

// https://github.com/free5gc/nssf/blob/v1.2.3/internal/context/context.go#54
type NSSFContext struct {
	NfId         string
	Name         string
	UriScheme    models.UriScheme
	RegisterIPv4 string
	// HttpIpv6Address string
	BindingIPv4       string
	SBIPort           int
	NfService         map[models.ServiceName]models.NfService
	NrfUri            string
	NrfCertPem        string
	SupportedPlmnList []models.PlmnId
	OAuth2Required    bool
}

但我们知道NSSF是要管理网络切片信息的,那这些信息保存在哪里?前面研究过的NF要么把数据保存在自己的NFContext,要么保存在数据库中。但free5gc/nssf有点奇怪,主要把信息保存在Configuaration中。

// https://github.com/free5gc/nssf/blob/v1.2.3/pkg/factory/config.go
type Config struct {
	Info          *Info          `yaml:"info" valid:"required"`
	Configuration *Configuration `yaml:"configuration" valid:"required"`
	Subscriptions []Subscription `yaml:"subscriptions,omitempty"`
	Logger        *Logger        `yaml:"logger" valid:"required"`
	sync.RWMutex
}

type Configuration struct {
	NssfName                 string                  `yaml:"nssfName,omitempty"`
	Sbi                      *Sbi                    `yaml:"sbi"`
	ServiceNameList          []models.ServiceName    `yaml:"serviceNameList"`
	NrfUri                   string                  `yaml:"nrfUri"`
	NrfCertPem               string                  `yaml:"nrfCertPem,omitempty" valid:"optional"`
	SupportedPlmnList        []models.PlmnId         `yaml:"supportedPlmnList,omitempty"`
	SupportedNssaiInPlmnList []SupportedNssaiInPlmn  `yaml:"supportedNssaiInPlmnList"`
	NsiList                  []NsiConfig             `yaml:"nsiList,omitempty"`
	AmfSetList               []AmfSetConfig          `yaml:"amfSetList"`
	AmfList                  []AmfConfig             `yaml:"amfList"`
	TaList                   []TaConfig              `yaml:"taList"`
	MappingListFromPlmn      []MappingFromPlmnConfig `yaml:"mappingListFromPlmn"`
}

type Subscription struct {
	SubscriptionId   string                                  `yaml:"subscriptionId"`
	SubscriptionData *models.NssfEventSubscriptionCreateData `yaml:"subscriptionData"`
}

我们可以看到Configuration中有好几个List,这些就是free5gc/nssf保存数据的主要地方。在下文我们会看到各种函数对Config.Subscriptions做增删改操作,也会看到对Configuration中的各种List做增删改操作。我个人认为理应是保存在NSSFContext中的。保存在Config中逻辑上不太合适,而唯一的好处是Config存在于nssf/pkg/目录下,可以被导入到其他模块里被直接调用。然而目前我没看到出NSSF以外其他NF有直接调用其Config

这也侧面反映出了配置信息对于NSSF的重要性。无论怎么类型的切片,其具体组成成分和结构都和一个完整的网络类似,都有接入网(AN),核心网(CN),和传输网(TN):
img
每个切片都有自己的覆盖范围(Tracking Area),会向外界暴露自己的AMF的接口,让用户设备得以接入;也会向内部所有NF暴露自己的NRF接口,让新的NF能完成注册。这些在切片创建时就要配置好,当然也可以随网络演变而动态更新。这也导致了NSSF的默认配置文件是所有NF里最长的。除了基本的NF配置信息以外,还写明了该NSSF目前管理这哪些切片实例nsiList,每个实例的ID信息和它们的NRF信息和AMF信息,每个实例可以覆盖哪些范围supportedSnssaiList等。

Nnssf_NSSAIAvailability

该服务主要负责管理NAASI的可用性和订阅事件。这和前文NF_Management对于NF信息的管理有异曲同工之妙。就具体来说,Nnssf_NSSAIAvailability要做的事情就是应外部请求对NssaiAvailability数据“增删改”(后面的Nnssf_NSSelection负责剩下的“查”),以及帮其他NF订阅和网络切片相关的事件,当事件发生时通知感兴趣的NF。

NssaiAvailability

首先解释一下什么是NssaiAvailability。前文已经说过,“NSSAI是网络切片的标识符”。而一个NSSAI由由几个S-NSSAI(Single NSSA)组成,每个S-NSSAI又由两个属性组成

  • SST (Slice/Service Type): 切片/服务类型
  • SD (Slice Differentiator): 切片区分符,用于区分相同SST的不同切片实例
// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_snssai.go
type Snssai struct {
	Sst int32  `json:"sst" yaml:"sst" bson:"sst" mapstructure:"Sst"`
	Sd  string `json:"sd,omitempty" yaml:"sd" bson:"sd" mapstructure:"Sd"`
}

// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_nssai.go
type Nssai struct {
	SupportedFeatures   string   `json:"supportedFeatures,omitempty" yaml:"supportedFeatures" bson:"supportedFeatures" mapstructure:"SupportedFeatures"`
	DefaultSingleNssais []Snssai `json:"defaultSingleNssais" yaml:"defaultSingleNssais" bson:"defaultSingleNssais" mapstructure:"DefaultSingleNssais"`
	SingleNssais        []Snssai `json:"singleNssais,omitempty" yaml:"singleNssais" bson:"singleNssais" mapstructure:"SingleNssais"`
}

因为Nssai相当于网络切片的标识符,那么Nssai的可用性就相当于网络切片的可用性。Nnssf_NSSAIAvailability对Nssai的可用性进行管理就是动态更新网络切片的可用性信息,更准确地说,是网络中各个AMF所支持的网络切片信息。所以在下面地代码会看到,NssaiAvailabilityNfInstanceUpdate函数做的事情就是根据给定的nfId(AMF的id)以及nssaiAvailabilityInfo(通常来自AMF),在NSSF的NssfConfig.Configuration.AmfList找到相应的AMF,让后更新其SupportedNssaiAvailabilityData

https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_store.go#L161
func (p *Processor) NssaiAvailabilityNfInstanceUpdate(
	c *gin.Context,
	nssaiAvailabilityInfo models.NssaiAvailabilityInfo, 
    nfId string,
) {
	var (
		response       *models.AuthorizedNssaiAvailabilityInfo = &models.AuthorizedNssaiAvailabilityInfo{}
		problemDetails *models.ProblemDetails
	)
	hitAmf := false
	// Find AMF configuration of given NfId
	// If found, then update the SupportedNssaiAvailabilityData
	factory.NssfConfig.Lock()
	for i, amfConfig := range factory.NssfConfig.Configuration.AmfList {
		if amfConfig.NfId == nfId {
			factory.NssfConfig.Configuration.AmfList[i].SupportedNssaiAvailabilityData = nssaiAvailabilityInfo.SupportedNssaiAvailabilityData
			hitAmf = true
			break
		}
	}
	factory.NssfConfig.Unlock()

	// If no AMF record is found, create a new one
	if !hitAmf {
		var amfConfig factory.AmfConfig
		amfConfig.NfId = nfId
		amfConfig.SupportedNssaiAvailabilityData = nssaiAvailabilityInfo.SupportedNssaiAvailabilityData
		factory.NssfConfig.Lock()
		factory.NssfConfig.Configuration.AmfList = append(factory.NssfConfig.Configuration.AmfList, amfConfig)
		factory.NssfConfig.Unlock()
	}

	// Return authorized NSSAI availability information of updated TAI only
	for _, s := range nssaiAvailabilityInfo.SupportedNssaiAvailabilityData {
		authorizedNssaiAvailabilityData, err := util.AuthorizeOfAmfTaFromConfig(nfId, *s.Tai)
		response.AuthorizedNssaiAvailabilityData = append(response.AuthorizedNssaiAvailabilityData, authorizedNssaiAvailabilityData)	
	}
	c.JSON(http.StatusOK, response)
}
点击查看NssaiAvailabilityNfInstance的Patch和Delete方法
// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_store.go#L47
func (p *Processor) NssaiAvailabilityNfInstancePatch(
	c *gin.Context,
	nssaiAvailabilityUpdateInfo plugin.PatchDocument, nfId string,
) {
	var (
		response       *models.AuthorizedNssaiAvailabilityInfo = &models.AuthorizedNssaiAvailabilityInfo{}
		problemDetails *models.ProblemDetails
	)
	var amfIdx int
	var original []byte
	hitAmf := false
	factory.NssfConfig.RLock()
	for amfIdx, amfConfig := range factory.NssfConfig.Configuration.AmfList {
		if amfConfig.NfId == nfId {
			temp := factory.NssfConfig.Configuration.AmfList[amfIdx].SupportedNssaiAvailabilityData
			const dummyString string = "DUMMY"
			for i := range temp {
				for j := range temp[i].SupportedSnssaiList {
					if temp[i].SupportedSnssaiList[j].Sd == "" {
						temp[i].SupportedSnssaiList[j].Sd = dummyString
					}
				}
			}
			original = json.Marshal(temp)
			original = bytes.ReplaceAll(original, []byte(dummyString), []byte(""))
			hitAmf = true
			break
		}
	}
	factory.NssfConfig.RUnlock()

	for i, patchItem := range nssaiAvailabilityUpdateInfo {
		if reflect.ValueOf(patchItem.Value).Kind() == reflect.Map {
			_, exist := patchItem.Value.(map[string]interface{})["sst"]
			_, notExist := patchItem.Value.(map[string]interface{})["sd"]
			if exist && !notExist {
				nssaiAvailabilityUpdateInfo[i].Value.(map[string]interface{})["sd"] = ""
			}
		}
	}
	patchJSON, err := json.Marshal(nssaiAvailabilityUpdateInfo)
	patch, err := jsonpatch.DecodePatch(patchJSON)
	modified, err := patch.Apply(original)

	factory.NssfConfig.Lock()
	json.Unmarshal(modified, &factory.NssfConfig.Configuration.AmfList[amfIdx].SupportedNssaiAvailabilityData)
	factory.NssfConfig.Unlock()

	// Return all authorized NSSAI availability information
	response.AuthorizedNssaiAvailabilityData, err = util.AuthorizeOfAmfFromConfig(nfId)
	c.JSON(http.StatusOK, response)
}

// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_store.go#L26
func (p *Processor) NssaiAvailabilityNfInstanceDelete(c *gin.Context, nfId string) {
	var problemDetails *models.ProblemDetails
	for i, amfConfig := range factory.NssfConfig.Configuration.AmfList {
		if amfConfig.NfId == nfId {
			factory.NssfConfig.Configuration.AmfList = append(
				factory.NssfConfig.Configuration.AmfList[:i],
				factory.NssfConfig.Configuration.AmfList[i+1:]...)
			c.Status(http.StatusNoContent)
			return
		}
	}
}

NssaiAvailabilitySubscription

NSSF事件的主要订阅者通常是AMF,因为它负责为用户设备接入网络,所以需要及时了解网络切片的状态。当一些和切片有关的事件发生时,已经订阅该事件的AMF实例就会被通知到。具体来说,当订阅的TAI(Tracking Area Identity)范围内的切片可用性发生变化时,NSSF就会通知AMF,这使得AMF可以及时更新本地缓存的网络切片信息,提高网络切片选择的效率。但话说回来,目前和NSSF有关的事件只有一个SNSSAI_STATUS_CHANGE_REPORT而已。

/* https://github.com/free5gc/openapi/blob/main/models/model_nssf_event_type.go */
package models

type NssfEventType string
// List of NssfEventType
const (
	NssfEventType_SNSSAI_STATUS_CHANGE_REPORT NssfEventType = "SNSSAI_STATUS_CHANGE_REPORT"
)

下面的简化版代码显示了NSSF事件的订阅和取消订阅过程。NSSF收到事件订阅的请求,以及相应的createData,于是给它打个SubscriptionId以后添加到NssfConfig.Subscriptions里。类似的,取消订阅则是通过SubscriptionId找到这个订阅,然后把他从NssfConfig.Subscriptions里以除。

// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_subscription.go#L48
func (p *Processor) NssaiAvailabilitySubscriptionCreate(
	c *gin.Context,
	createData models.NssfEventSubscriptionCreateData,
) {
	var (
		response       *models.NssfEventSubscriptionCreatedData = &models.NssfEventSubscriptionCreatedData{}
		problemDetails *models.ProblemDetails
	)
	var subscription factory.Subscription
	tempID, err := getUnusedSubscriptionID()

	subscription.SubscriptionId = tempID
	subscription.SubscriptionData = new(models.NssfEventSubscriptionCreateData)
	*subscription.SubscriptionData = createData
	factory.NssfConfig.Subscriptions = append(factory.NssfConfig.Subscriptions, subscription)

	response.SubscriptionId = subscription.SubscriptionId
	if !subscription.SubscriptionData.Expiry.IsZero() {
		response.Expiry = new(time.Time)
		*response.Expiry = *subscription.SubscriptionData.Expiry
	}
	response.AuthorizedNssaiAvailabilityData = util.AuthorizeOfTaListFromConfig(subscription.SubscriptionData.TaiList)
	c.JSON(http.StatusOK, response)
}

https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_subscription.go#L88
func (p *Processor) NssaiAvailabilitySubscriptionUnsubscribe(c *gin.Context, subscriptionId string) {
	var problemDetails *models.ProblemDetails
	factory.NssfConfig.Lock()
	defer factory.NssfConfig.Unlock()
	for i, subscription := range factory.NssfConfig.Subscriptions {
		if subscription.SubscriptionId == subscriptionId {
			factory.NssfConfig.Subscriptions = append(factory.NssfConfig.Subscriptions[:i],
				factory.NssfConfig.Subscriptions[i+1:]...)
			c.Status(http.StatusNoContent)
			return
		}
	}
}
点击查看getUnusedSubscriptionID()函数

因为在free5gc里NSSF事件的SubscriptionID是整数,所以每个从1到\(2^32\)的整数都是稀有资源,每当有一个subscrption被取消了,它对应的ID就可以被回收再利用,因此getUnusedSubscriptionID()会遍历已有的subscription,然后找到尚未使用的最小整数作为新的subscriptionID。

// Get available subscription ID from configuration
// In this implementation, string converted from 32-bit integer is used as subscription ID
func getUnusedSubscriptionID() (string, error) {
	var idx uint32 = 1
	factory.NssfConfig.RLock()
	defer factory.NssfConfig.RUnlock()
	for _, subscription := range factory.NssfConfig.Subscriptions {
		tempID, err := strconv.Atoi(subscription.SubscriptionId)
		if err != nil {
			return "", err
		}
		if uint32(tempID) == idx {
			if idx == math.MaxUint32 {
				return "", fmt.Errorf("No available subscription ID")
			}
			idx++
		} else {
			break
		}
	}
	return strconv.Itoa(int(idx)), nil
}

这样做看起来有点多此一举,为什么不直接使用uuid之类随机自动生成的ID呢?毕竟类似的事件订阅机制里NRF就是这样做的

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/context/management_data.go#L47
func SetsubscriptionId() (string, error) {
	subscriptionIdSize := 16
	buffer := make([]byte, subscriptionIdSize)
	_, err := rand.Read(buffer)
	if err != nil {
		return "", err
	}
	return hex.EncodeToString(buffer), nil
}

我只能推测,free5gc的开发团队认为NSSF的事件订阅需求更旺盛,不止需要使用32位空间的整数,而且随机生成ID很可能会生成一个已被占用的ID,所以需要用更复杂的算法来生成ID。但如果真的是这样,会有如此庞大事件订阅量,那遍历已有的订阅for _, subscription := range factory.NssfConfig.Subscriptions将是灾难性的时间消耗。

最终,我更倾向于认为是free5gc团队内部在设计这个订阅机制的时候没想太多......

然而有一个问题让我百思不得其解。既然NssaiAvailabilitySubscription是订阅与NssaiAvailability相关的事件,那么事件发生时通知订阅了这些事件的NF的代码在哪里呢?可能是我眼拙没看到吧,也可能实现出发订阅通知的代码并不在NSSF模块里。

Nnssf_NSSelection

前文说到Nnssf_NSSAIAvailability负责对NSSAI进行“增删改”,而Nnssf_NSSelection负责“查”。具体而言,是外部NF(通常是AMF)向NSSF发一个查询请求,附带一些参数,然后NSSF根据查询返回合适的网络切片,既可用的NSSAI。

img

这与Nnrf_NFDiscovery很像。但Nnrf_NFDiscovery可以接受的查询参数有30页A4纸那么多,而Nnssf_NSSelection所接受的参数则只有半页纸,是可以把截图贴出来的程度:

img

处理这些请求的函数时下面的NSSelectionSliceInformationGet,接受从URL里解析出来的查询,返回相应的AuthorizedNetworkSliceInfo

// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nsselection_network_slice_information.go#L95
func (p *Processor) NSSelectionSliceInformationGet(c *gin.Context, query url.Values) {
	var (
		status         int
		response       *models.AuthorizedNetworkSliceInfo
		problemDetails *models.ProblemDetails
	)
	param, err := parseQueryParameter(query)
	err = checkNfServiceConsumer(*param.NfType)  // only AMF or NSSF can call this API
	if param.SliceInfoRequestForRegistration != nil {
		// Network slice information is requested during the Registration procedure
		status, response, problemDetails = nsselectionForRegistration(param)
	} else {
		// Network slice information is requested during the PDU session establishment procedure
		status, response, problemDetails = nsselectionForPduSession(param)
	}
	c.JSON(status, response)
}

// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_authorized_network_slice_info.go
type AuthorizedNetworkSliceInfo struct {
	AllowedNssaiList []AllowedNssai `json:"allowedNssaiList,omitempty" bson:"allowedNssaiList"`
	ConfiguredNssai []ConfiguredSnssai `json:"configuredNssai,omitempty" bson:"configuredNssai"`
	TargetAmfSet string `json:"targetAmfSet,omitempty" bson:"targetAmfSet"`
	CandidateAmfList []string `json:"candidateAmfList,omitempty" bson:"candidateAmfList"`
	RejectedNssaiInPlmn []Snssai `json:"rejectedNssaiInPlmn,omitempty" bson:"rejectedNssaiInPlmn"`
	RejectedNssaiInTa []Snssai `json:"rejectedNssaiInTa,omitempty" bson:"rejectedNssaiInTa"`
	NsiInformation *NsiInformation `json:"nsiInformation,omitempty" bson:"nsiInformation"`
	SupportedFeatures string `json:"supportedFeatures,omitempty" bson:"supportedFeatures"`
	NrfAmfSet string `json:"nrfAmfSet,omitempty" bson:"nrfAmfSet"`
}

可见根据请求查询的情景不同,在用户设备接入网络时建立PDU会话时对于请求的处理是不同的,也因此由不同的子函数继续深入处理。不过对于这些查询参数的处理过于琐碎,在此略过。


标签:string,NssfConfig,models,NSSF,factory,切片,源码,Free5GC
From: https://www.cnblogs.com/zrq96/p/18499009

相关文章

  • java+vue计算机毕设电商平台日志分析系统的设计与实现【开题+程序+论文+源码】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展和电子商务行业的蓬勃兴起,电商平台已成为现代商业活动的重要组成部分。这些平台每天产生大量的用户行为数据、交易记录以及......
  • 织梦怎么进数据库,织梦网站源码在哪里看数据库
    当织梦CMS(DedeCMS)无法连接到数据库时,可能是由多种原因引起的。以下是一些常见的原因及解决方法:1. 数据库服务未启动原因:MySQL服务没有运行。解决方法:Linux:使用命令 sudosystemctlstartmysql 或 sudoservicemysqlstart 启动MySQL服务。Windows:打开“服务”管......
  • java+vue计算机毕设多企业人力资源集成平台【开题+程序+论文+源码】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着全球经济一体化的加速发展,企业间的竞争日益激烈,人力资源作为企业的核心竞争力之一,其管理效率与质量直接关系到企业的生存与发展。当前,众多企业在......
  • springboot书画在线学习网站-计算机设计毕业源码11849
    springboot书画在线学习网站摘 要本篇论文旨在设计和开发基于SpringBoot的书画在线学习网站,提供用户便捷的学习方式和丰富的学习资源。在该系统中,用户可以通过网站浏览书画的相关内容,包括诗公告消息、书画资讯、课程信息等。同时,系统还将提供书画的学习计划和测试功能,帮......
  • springboot医疗物品采购系统-计算机设计毕业源码10210
    摘 要本文基于SpringBoot框架,设计并实现了一个医疗物品采购系统。该系统旨在解决医疗物品采购中的管理和信息化问题,提供便捷的服务和支持。通过系统的设计与实现,实现了医疗物品的供应商家管理、物品类型管理、物品仓库管理、采购计划管理、采购入库管理、出库申请管理、......
  • 基于SpringBoot+Vue的鲜牛奶订购管理系统设计与实现毕设(文档+源码)
            目录一、项目介绍二、开发环境三、功能介绍四、核心代码五、效果图六、源码获取:        大家好呀,我是一个混迹在java圈的码农。今天要和大家分享的是一款基于SpringBoot+Vue的鲜牛奶订购管理系统,项目源码请点击文章末尾联系我哦~目前有各类成......
  • 【开题报告】基于Springboot+vueHPV疫苗预约管理系统(程序+源码+论文) 计算机毕业设计
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在当今社会,随着健康意识的不断提升,预防疾病已成为人们日益关注的话题。其中,HPV(人乳头瘤病毒)疫苗作为预防宫颈癌等恶性疾病的重要手段,其接种需求在全球......
  • 短视频类app源码,线程创建并非使用线程池一种
    短视频类app源码,线程创建并非使用线程池一种,除此之外,以下线程池创建方式也不容错过。有三种使用线程的方法:实现Runnable接口;实现Callable接口;继承Thread类。实现Runnable和Callable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需......
  • 基于nodejs+vue基于的私人物品管理平台[开题+源码+程序+论文]计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容一、选题背景关于私人物品管理的研究,现有研究主要以企业物品管理或公共物品管理为主,专门针对私人物品管理的研究较少。在国内外,对于物品管理的研究多集中在大型组织......
  • 基于nodejs+vue基于的食品销售系统[开题+源码+程序+论文]计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容一、选题背景关于食品销售系统的研究,现有研究主要以传统的销售模式和管理方式为主。在国内外,虽然有不少企业已经在食品销售领域应用了信息化管理手段,但专门针对集会......