【GoTeams】-4:为项目引入etcd

在这里插入图片描述

1. 书接上回

本节是为项目引入etcd这个环节,然后我们来看看具体该怎么实现。

首先来谈谈为什么要引入服务发现?

动态服务注册与发现:微服务系统通常由多个服务组成,这些服务可能分布在不同的机器上,并且可能会动态地启动或停止。etcd 提供了一个集中化的存储,服务实例可以在启动时向 etcd 注册自己的信息(如 IP 地址、端口、健康状态等),并在停止时注销。客户端可以通过 etcd 动态地发现可用的服务实例,从而实现高可用性和弹性扩展。

高可用性和容错性:在分布式系统中,服务实例可能会因为各种原因(如机器故障、网络问题等)变得不可用。etcd 通过其高可用性设计(如 Raft 协议)确保服务注册信息的一致性和可靠性。即使部分节点故障,etcd 集群仍然可以正常工作,从而保证服务发现的高可用性。

配置管理:除了服务发现,etcd 还可以用于配置管理。分布式系统中的配置信息(如数据库地址、API 密钥等)可以存储在 etcd 中,并且可以动态更新。客户端可以通过监听 etcd 中的配置变化,实时获取最新的配置信息,从而实现配置的动态更新而无需重启服务。

之前我们是直接写入gRPC的地址,那么现在需要引入etcd,就可以实现服务发现,我们只需要监听etcd就可以了,如果服务地址变了,那么就能够立即发现。会更加方便一些。

2. 引入etcd

在api下和user下、common下都需要安装etcd的依赖。

go get go.etcd.io/etcd/client/v3

接下来写服务发现的代码,目前我们先实现单机版的etcd,来看看具体怎么实现。

首先需要启动etcd,这里我已经提前下载好并且运行起来了,简单尝试一下。

在这里插入图片描述
所以用法就是,把服务注册进etcd,然后监听etcd,如果有变化,立马进行变动即可。

discovery

package discovery

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	clientv3 "go.etcd.io/etcd/client/v3"
	"go.uber.org/zap"
)

// Register for grpc server
type Register struct {
	EtcdAddrs   []string
	DialTimeout int

	closeCh     chan struct{}
	leasesID    clientv3.LeaseID
	keepAliveCh <-chan *clientv3.LeaseKeepAliveResponse

	srvInfo Server
	srvTTL  int64
	cli     *clientv3.Client
	logger  *zap.Logger
}

// NewRegister create a register base on etcd
func NewRegister(etcdAddrs []string, logger *zap.Logger) *Register {
	return &Register{
		EtcdAddrs:   etcdAddrs,
		DialTimeout: 3,
		logger:      logger,
	}
}

// Register a service
func (r *Register) Register(srvInfo Server, ttl int64) (chan<- struct{}, error) {
	var err error

	if strings.Split(srvInfo.Addr, ":")[0] == "" {
		return nil, errors.New("invalid ip")
	}

	if r.cli, err = clientv3.New(clientv3.Config{
		Endpoints:   r.EtcdAddrs,
		DialTimeout: time.Duration(r.DialTimeout) * time.Second,
	}); err != nil {
		return nil, err
	}

	r.srvInfo = srvInfo
	r.srvTTL = ttl

	if err = r.register(); err != nil {
		return nil, err
	}

	r.closeCh = make(chan struct{})

	go r.keepAlive()

	return r.closeCh, nil
}

// Stop stop register
func (r *Register) Stop() {
	r.closeCh <- struct{}{}
}

// register 注册节点
func (r *Register) register() error {
	leaseCtx, cancel := context.WithTimeout(context.Background(), time.Duration(r.DialTimeout)*time.Second)
	defer cancel()

	leaseResp, err := r.cli.Grant(leaseCtx, r.srvTTL)
	if err != nil {
		return err
	}
	r.leasesID = leaseResp.ID
	if r.keepAliveCh, err = r.cli.KeepAlive(context.Background(), leaseResp.ID); err != nil {
		return err
	}

	data, err := json.Marshal(r.srvInfo)
	if err != nil {
		return err
	}
	_, err = r.cli.Put(context.Background(), BuildRegPath(r.srvInfo), string(data), clientv3.WithLease(r.leasesID))
	return err
}

// unregister 删除节点
func (r *Register) unregister() error {
	_, err := r.cli.Delete(context.Background(), BuildRegPath(r.srvInfo))
	return err
}

// keepAlive
func (r *Register) keepAlive() {
	ticker := time.NewTicker(time.Duration(r.srvTTL) * time.Second)
	for {
		select {
		case <-r.closeCh:
			if err := r.unregister(); err != nil {
				r.logger.Error("unregister failed", zap.Error(err))
			}
			if _, err := r.cli.Revoke(context.Background(), r.leasesID); err != nil {
				r.logger.Error("revoke failed", zap.Error(err))
			}
			return
		case res := <-r.keepAliveCh:
			if res == nil {
				if err := r.register(); err != nil {
					r.logger.Error("register failed", zap.Error(err))
				}
			}
		case <-ticker.C:
			if r.keepAliveCh == nil {
				if err := r.register(); err != nil {
					r.logger.Error("register failed", zap.Error(err))
				}
			}
		}
	}
}

// UpdateHandler return http handler
func (r *Register) UpdateHandler() http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		wi := req.URL.Query().Get("weight")
		weight, err := strconv.Atoi(wi)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			w.Write([]byte(err.Error()))
			return
		}

		var update = func() error {
			r.srvInfo.Weight = int64(weight)
			data, err := json.Marshal(r.srvInfo)
			if err != nil {
				return err
			}
			_, err = r.cli.Put(context.Background(), BuildRegPath(r.srvInfo), string(data), clientv3.WithLease(r.leasesID))
			return err
		}

		if err := update(); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write([]byte(err.Error()))
			return
		}
		w.Write([]byte("update server weight success"))
	})
}

func (r *Register) GetServerInfo() (Server, error) {
	resp, err := r.cli.Get(context.Background(), BuildRegPath(r.srvInfo))
	if err != nil {
		return r.srvInfo, err
	}
	info := Server{}
	if resp.Count >= 1 {
		if err := json.Unmarshal(resp.Kvs[0].Value, &info); err != nil {
			return info, err
		}
	}
	return info, nil
}

接下来看看discovery中的各个部分含义和作用。

在这里插入图片描述
Register 是一个结构体,用于管理服务的注册和更新。

EtcdAddrs:etcd 服务的地址列表、DialTimeout:连接 etcd 的超时时间。closeCh:一个关闭通道,用于停止注册流程。leasesID:etcd 的租约 ID,用于保持服务的存活状态。keepAliveCh:租约续期的响应通道。srvInfo:服务的详细信息。srvTTL:服务的存活时间(TTL)。cli:etcd 客户端实例。logger:日志记录器。

在这里插入图片描述
NewRegister 是一个构造函数,用于创建一个新的 Register 实例。接收 etcd 地址列表和日志记录器作为参数。默认设置连接超时时间为 3 秒。

在这里插入图片描述
Register 方法用于在 etcd 中注册服务,接收服务信息 srvInfo 和存活时间 ttl 作为参数。首先检查服务地址是否有效,然后创建 etcd 客户端。调用 register 方法将服务信息注册到 etcd。启动一个后台协程 keepAlive,用于保持服务的存活状态。返回一个关闭通道,用于停止注册流程。
在这里插入图片描述

Stop 方法用于停止注册流程。向关闭通道发送一个信号,通知后台协程停止运行。

struct{}{}

这里有两个{},来看看是为什么。

struct{} 是类型声明:定义了一个空结构体类型,不包含任何字段,占用 0 字节内存。

第二个 {} 是实例化:创建一个空结构体的实例,相当于 new(struct{})

这种写法常用于通道信号传递,因为只需要一个信号,不需要传递具体的数据,并且孔结构体不占用内存,效率高,常用于停止信号、同步信号等场景。

在这里插入图片描述

register 方法用于将服务信息注册到 etcd。首先创建一个租约,然后将服务信息序列化为 JSON 格式。使用 Put 方法将服务信息存储到 etcd,并将其与租约关联。

在这里插入图片描述
unregister 方法用于从 etcd 中删除服务信息,使用 Delete 方法删除服务对应的键值对。

在这里插入图片描述
keepAlive 方法用于保持服务的存活状态。使用一个定时器和租约续期通道,定期检查租约状态。如果收到关闭信号,调用 unregister 方法删除服务信息,并撤销租约。如果租约续期失败或超时,重新调用 register 方法注册服务。

在这里插入图片描述
UpdateHandler 方法返回一个 HTTP 处理器,用于更新服务的权重。通过 HTTP 请求的查询参数获取新的权重值,然后更新服务信息并重新注册到 etcd。

在这里插入图片描述
GetServerInfo 方法用于从 etcd 中获取服务信息。使用 Get 方法查询服务对应的键值对,并反序列化为服务信息结构体。

在etcd中,租约(Lease) 是一种机制,用于确保服务实例的注册信息在一定时间内有效。如果服务实例在租约到期前没有续期,那么注册信息会被自动删除。这种机制可以防止服务实例在故障或网络问题后仍然被客户端调用。

租约(Lease) 是一种机制,它允许你为某些键值对设置有效期。租约的作用是确保在一定时间内,某个特定的键值对不会被意外删除或者修改,同时也可以在租约到期后自动删除。

自动过期:当一个键值对绑定了一个租约时,该键值对会在租约到期后自动删除。这个特性对于管理短期或临时数据(例如服务发现中的节点信息)非常有用。

防止僵尸数据:如果某个服务崩溃或失效,未能在租约到期前刷新租约,那么租约绑定的键值对将会自动过期,防止僵尸数据长期占用资源。

服务发现:通常在分布式系统中,服务注册时会绑定租约。如果服务失效或宕机,绑定该服务的键值对会在租约到期后自动删除,其他服务能够及时感知。

举个例子来说明下,假设有一个分布式系统中的服务需要定期将自己的健康状况注册到 etcd 中,作为服务发现的一部分。服务会在启动时向 etcd 注册自己的信息,并设置一个租约,例如设置租约为10秒。

  • 如果服务正常运行,它会在10秒内刷新租约。
  • 如果服务崩溃或停止,它就无法刷新租约。
  • 10秒后,etcd 会发现该服务的租约已过期,并删除与该服务相关的键值对,其他节点就不再看到该服务的信息。

这种机制确保了如果服务不再可用,它的注册信息会被及时清除,从而避免系统中有过期的服务信息。


resolver

package discovery

import (
	"context"
	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
	"go.uber.org/zap"
	"google.golang.org/grpc/resolver"
	"time"
)

const (
	schema = "etcd"
)

// Resolver for grpc client
type Resolver struct {
	schema      string
	EtcdAddrs   []string
	DialTimeout int

	closeCh      chan struct{}
	watchCh      clientv3.WatchChan
	cli          *clientv3.Client
	keyPrifix    string
	srvAddrsList []resolver.Address

	cc     resolver.ClientConn
	logger *zap.Logger
}

// NewResolver create a new resolver.Builder base on etcd
func NewResolver(etcdAddrs []string, logger *zap.Logger) *Resolver {
	return &Resolver{
		schema:      schema,
		EtcdAddrs:   etcdAddrs,
		DialTimeout: 3,
		logger:      logger,
	}
}

// Scheme returns the scheme supported by this resolver.
func (r *Resolver) Scheme() string {
	return r.schema
}

// Build creates a new resolver.Resolver for the given target
func (r *Resolver) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	r.cc = cc

	r.keyPrifix = BuildPrefix(Server{Name: target.URL.Host, Version: target.URL.Path})
	if _, err := r.start(); err != nil {
		return nil, err
	}
	return r, nil
}

// ResolveNow resolver.Resolver interface
func (r *Resolver) ResolveNow(o resolver.ResolveNowOptions) {}

// Close resolver.Resolver interface
func (r *Resolver) Close() {
	r.closeCh <- struct{}{}
}

// start
func (r *Resolver) start() (chan<- struct{}, error) {
	var err error
	r.cli, err = clientv3.New(clientv3.Config{
		Endpoints:   r.EtcdAddrs,
		DialTimeout: time.Duration(r.DialTimeout) * time.Second,
	})
	if err != nil {
		return nil, err
	}
	resolver.Register(r)

	r.closeCh = make(chan struct{})

	if err = r.sync(); err != nil {
		return nil, err
	}

	go r.watch()

	return r.closeCh, nil
}

// watch update events
func (r *Resolver) watch() {
	ticker := time.NewTicker(time.Minute)
	r.watchCh = r.cli.Watch(context.Background(), r.keyPrifix, clientv3.WithPrefix())

	for {
		select {
		case <-r.closeCh:
			return
		case res, ok := <-r.watchCh:
			if ok {
				r.update(res.Events)
			}
		case <-ticker.C:
			if err := r.sync(); err != nil {
				r.logger.Error("sync failed", zap.Error(err))
			}
		}
	}
}

// update
func (r *Resolver) update(events []*clientv3.Event) {
	for _, ev := range events {
		var info Server
		var err error

		switch ev.Type {
		case mvccpb.PUT:
			info, err = ParseValue(ev.Kv.Value)
			if err != nil {
				continue
			}
			addr := resolver.Address{Addr: info.Addr, Metadata: info.Weight}
			if !Exist(r.srvAddrsList, addr) {
				r.srvAddrsList = append(r.srvAddrsList, addr)
				r.cc.UpdateState(resolver.State{Addresses: r.srvAddrsList})
			}
		case mvccpb.DELETE:
			info, err = SplitPath(string(ev.Kv.Key))
			if err != nil {
				continue
			}
			addr := resolver.Address{Addr: info.Addr}
			if s, ok := Remove(r.srvAddrsList, addr); ok {
				r.srvAddrsList = s
				r.cc.UpdateState(resolver.State{Addresses: r.srvAddrsList})
			}
		}
	}
}

// sync 同步获取所有地址信息
func (r *Resolver) sync() error {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	res, err := r.cli.Get(ctx, r.keyPrifix, clientv3.WithPrefix())
	if err != nil {
		return err
	}
	r.srvAddrsList = []resolver.Address{}

	for _, v := range res.Kvs {
		info, err := ParseValue(v.Value)
		if err != nil {
			continue
		}
		addr := resolver.Address{Addr: info.Addr, Metadata: info.Weight}
		r.srvAddrsList = append(r.srvAddrsList, addr)
	}
	r.cc.UpdateState(resolver.State{Addresses: r.srvAddrsList})
	return nil
}

总的来说,实现一个基于 etcd 的 gRPC 客户端解析器(resolver),用于动态发现和更新服务地址。它允许 gRPC 客户端根据 etcd 中的注册信息动态调整连接的目标地址。

在这里插入图片描述
Resolver 是一个结构体,用于管理 gRPC 客户端解析器。

包含以下字段:
schema:解析器支持的协议前缀。
EtcdAddrs:etcd 服务的地址列表。
DialTimeout:连接 etcd 的超时时间。
closeCh:一个关闭通道,用于停止解析器。
watchCh:etcd 的监听通道,用于接收 etcd 的变更事件。
cli:etcd 客户端实例。
keyPrifix:etcd 中存储服务信息的键前缀。
srvAddrsList:当前已知的服务地址列表。
cc:gRPC 客户端连接。
logger:日志记录器。

在这里插入图片描述
NewResolver 是一个构造函数,用于创建一个新的 Resolver 实例。接收 etcd 地址列表和日志记录器作为参数。默认设置连接超时时间为 3 秒。

在这里插入图片描述
Scheme 方法返回解析器支持的协议前缀。在 gRPC 解析器接口中,Scheme 方法用于标识解析器支持的协议(如 etcd://)。

在这里插入图片描述
Build 方法用于创建一个新的 gRPC 解析器实例。接收目标地址、客户端连接和构建选项作为参数。根据目标地址构建 etcd 的键前缀,并启动解析器。返回解析器实例或错误。

在这里插入图片描述
ResolveNow 方法是 gRPC 解析器接口的一部分,用于触发解析器的即时解析。在这个实现中,ResolveNow 方法为空,因为解析器通过监听 etcd 的变更事件动态更新服务地址。

在这里插入图片描述
start 方法用于启动解析器,首先创建 etcd 客户端实例,并注册解析器,启动一个后台协程用于监听 etcd 的变更事件,返回关闭通道或错误。

在这里插入图片描述
watch 方法用于监听 etcd 的变更事件。使用 Watch 方法监听 etcd 中的键前缀变化。
如果收到关闭信号,停止监听。如果收到变更事件,调用 update 方法更新服务地址。
定期调用 sync 方法同步服务地址。

在这里插入图片描述
update 方法用于处理 etcd 的变更事件。遍历事件列表,根据事件类型(PUT 或 DELETE)更新服务地址列表。如果是 PUT 事件,解析服务信息并添加到地址列表。如果是 DELETE 事件,从地址列表中移除服务地址。更新 gRPC 客户端连接的状态。

在这里插入图片描述
sync 方法用于同步获取 etcd 中的所有服务地址信息。使用 Get 方法查询 etcd 中的键前缀。解析查询结果,构建服务地址列表。更新 gRPC 客户端连接的状态。

也就是这实现了一个基于 etcd 的 gRPC 客户端解析器,用于动态发现和更新服务地址。它通过监听 etcd 的变更事件,实时更新 gRPC 客户端的连接目标地址。

server

package discovery

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"google.golang.org/grpc/resolver"
)

type Server struct {
	Name    string `json:"name"`
	Addr    string `json:"addr"`    //服务地址
	Version string `json:"version"` //服务版本
	Weight  int64  `json:"weight"`  //服务权重
}

func BuildPrefix(info Server) string {
	if info.Version == "" {
		return fmt.Sprintf("/%s/", info.Name)
	}
	return fmt.Sprintf("/%s/%s/", info.Name, info.Version)
}

func BuildRegPath(info Server) string {
	return fmt.Sprintf("%s%s", BuildPrefix(info), info.Addr)
}

func ParseValue(value []byte) (Server, error) {
	info := Server{}
	if err := json.Unmarshal(value, &info); err != nil {
		return info, err
	}
	return info, nil
}

func SplitPath(path string) (Server, error) {
	info := Server{}
	strs := strings.Split(path, "/")
	if len(strs) == 0 {
		return info, errors.New("invalid path")
	}
	info.Addr = strs[len(strs)-1]
	return info, nil
}

// Exist helper function
func Exist(l []resolver.Address, addr resolver.Address) bool {
	for i := range l {
		if l[i].Addr == addr.Addr {
			return true
		}
	}
	return false
}

// Remove helper function
func Remove(s []resolver.Address, addr resolver.Address) ([]resolver.Address, bool) {
	for i := range s {
		if s[i].Addr == addr.Addr {
			s[i] = s[len(s)-1]
			return s[:len(s)-1], true
		}
	}
	return nil, false
}

func BuildResolverUrl(app string) string {
	return schema + ":///" + app
}

简单说说,首先代码定义了一个 Server 结构体,用于表示服务的基本信息。它包含服务的名称、地址、版本和权重。这些字段在服务发现和负载均衡中非常关键,例如,权重可以用于控制服务的流量分配

BuildPrefix 函数用于根据服务信息构建一个路径前缀。如果服务版本为空,路径前缀将只包含服务名称;否则,它将包含服务名称和版本。这个前缀用于在 etcd 或类似的存储系统中组织服务信息。

BuildRegPath 函数进一步扩展了 BuildPrefix 的功能,它通过在前缀后面添加服务地址,生成一个完整的注册路径。这个路径可以用于在 etcd 中存储服务实例的具体信息。

ParseValue 函数的作用是从 etcd 中获取的字节数据中解析出服务信息。它使用 json.Unmarshal 将字节数据反序列化为 Server 结构体。如果解析过程中出现错误,它会返回错误信息。

SplitPath 函数则用于从路径中提取服务地址。它通过分割路径字符串来获取地址部分。如果路径格式不正确,它会返回一个错误。

Exist 函数用于检查一个地址是否已经存在于地址列表中,这对于避免重复添加服务地址非常有用。Remove 函数则用于从地址列表中移除一个特定的地址,这在服务下线或更新时非常有用。

3. 将服务注册到etcd中

在user下的router.go中把服务注册到etcd中去,代码如下:

func RegisterEtcdServer() {
	etcdRegister := discovery.NewResolver(config.C.EtcdConfig.Addrs, logs.LG)
	resolver.Register(etcdRegister)
	info := discovery.Server{
		Name:    config.C.GC.Name,
		Addr:    config.C.GC.Addr,
		Version: config.C.GC.Version,
		Weight:  config.C.GC.Weight,
	}
	r := discovery.NewRegister(config.C.EtcdConfig.Addrs, logs.LG)
	_, err := r.Register(info, 2)
	if err != nil {
		log.Fatalln(err)
	}
}

创建 gRPC 客户端解析器 (etcd resolver):这部分的作用是 客户端 用来解析和查找服务的位置(如 IP 和端口)。在分布式系统中,客户端通常不知道服务的具体地址,因此它需要一个 解析器,它会向 etcd 注册中心询问目标服务的地址信息。这样可以确保客户端在不同的服务器间寻找服务时能够动态地获取服务位置。

这里的 discovery.NewResolver 创建了一个新的 etcd 解析器,它使用 etcd 存储服务的位置。该解析器注册到 gRPC 中 (resolver.Register),使得客户端能够通过它查找服务。这一步是 客户端 侧的操作,客户端通过解析器可以在 etct 中查询并获得服务的信息。

注册服务端:这部分是 服务端 将自身的信息注册到 etcd 中,供客户端发现。它会向 etcd 注册服务的信息(如名称、地址、版本等),使得客户端能够基于这些信息去访问和调用服务。

discovery.NewRegister 创建了一个新的 注册器,用于将服务信息注册到 etcd。
r.Register(info, 2) 将服务信息 info 注册到 etcd 中,并设置一个租约,表示该服务信息在 2 秒内有效。租约过期后,服务信息会从 etcd 中自动删除。

4. 梳理下etcd调用逻辑

可以这么理解,也就是gRPC内部的解析注册表是m,m = make(map[string]Builder),string是key,表示协议方案scheme,比如etcd、dns等,Builder是value,是一个接口类型,用于构建解析器,我们的Resolver结构体就实现了这个接口

不同的服务发现机制etcd、consul等会注册自己的解析器,gRPC根据地址中的scheme找到对应的解析器,解析器负责将服务器名转回为实际地址。

在这里插入图片描述

标题“51单片机通过MPU6050-DMP获取姿态角例程”解析 “51单片机通过MPU6050-DMP获取姿态角例程”是一个基于51系列单片机(一种常见的8位微控制器)的程序示例,用于读取MPU6050传感器的数据,并通过其内置的数字运动处理器(DMP)计算设备的姿态角(如倾斜角度、旋转角度等)。MPU6050是一款集成三轴加速度计和三轴陀螺仪的六自由度传感器,广泛应用于运动控制和姿态检测领域。该例程利用MPU6050的DMP功能,由DMP处理复杂的运动学算法,例如姿态融合,将加速度计和陀螺仪的数据进行整合,从而提供稳定且实时的姿态估计,减轻主控MCU的计算负担。最终,姿态角数据通过LCD1602显示屏以字符形式可视化展示,为用户提供直观的反馈。 从标签“51单片机 6050”可知,该项目主要涉及51单片机和MPU6050传感器这两个关键硬件组件。51单片机基于8051内核,因编程简单、成本低而被广泛应用;MPU6050作为惯性测量单元(IMU),可测量设备的线性和角速度。文件名“51-DMP-NET”可能表示这是一个与51单片机及DMP相关的网络资源或代码库,其中可能包含C语言等适合51单片机的编程语言的源代码、配置文件、用户手册、示例程序,以及可能的调试工具或IDE项目文件。 实现该项目需以下步骤:首先是硬件连接,将51单片机与MPU6050通过I2C接口正确连接,同时将LCD1602连接到51单片机的串行数据线和控制线上;接着是初始化设置,配置51单片机的I/O端口,初始化I2C通信协议,设置MPU6050的工作模式和数据输出速率;然后是DMP配置,启用MPU6050的DMP功能,加载预编译的DMP固件,并设置DMP输出数据的中断;之后是数据读取,通过中断服务程序从DMP接收姿态角数据,数据通常以四元数或欧拉角形式呈现;再接着是数据显示,将姿态角数据转换为可读的度数格
MathorCup高校数学建模挑战赛是一项旨在提升学生数学应用、创新和团队协作能力的年度竞赛。参赛团队需在规定时间内解决实际问题,运用数学建模方法进行分析并提出解决方案。2021年第十一届比赛的D题就是一个典型例子。 MATLAB是解决这类问题的常用工具。它是一款强大的数值计算和编程软件,广泛应用于数学建模、数据分析和科学计算。MATLAB拥有丰富的函数库,涵盖线性代数、统计分析、优化算法、信号处理等多种数学操作,方便参赛者构建模型和实现算法。 在提供的文件列表中,有几个关键文件: d题论文(1).docx:这可能是参赛队伍对D题的解答报告,详细记录了他们对问题的理解、建模过程、求解方法和结果分析。 D_1.m、ratio.m、importfile.m、Untitled.m、changf.m、pailiezuhe.m、huitu.m:这些是MATLAB源代码文件,每个文件可能对应一个特定的计算步骤或功能。例如: D_1.m 可能是主要的建模代码; ratio.m 可能用于计算某种比例或比率; importfile.m 可能用于导入数据; Untitled.m 可能是未命名的脚本,包含临时或测试代码; changf.m 可能涉及函数变换; pailiezuhe.m 可能与矩阵的排列组合相关; huitu.m 可能用于绘制回路图或流程图。 matlab111.mat:这是一个MATLAB数据文件,存储了变量或矩阵等数据,可能用于后续计算或分析。 D-date.mat:这个文件可能包含与D题相关的特定日期数据,或是模拟过程中用到的时间序列数据。 从这些文件可以推测,参赛队伍可能利用MATLAB完成了数据预处理、模型构建、数值模拟和结果可视化等一系列工作。然而,具体的建模细节和解决方案需要查看解压后的文件内容才能深入了解。 在数学建模过程中,团队需深入理解问题本质,选择合适的数学模
以下是关于三种绘制云图或等高线图算法的介绍: 一、点距离反比插值算法 该算法的核心思想是基于已知数据点的值,计算未知点的值。它认为未知点的值与周围已知点的值相关,且这种关系与距离呈反比。即距离未知点越近的已知点,对未知点值的影响越大。具体来说,先确定未知点周围若干个已知数据点,计算这些已知点到未知点的距离,然后根据距离的倒数对已知点的值进行加权求和,最终得到未知点的值。这种方法简单直观,适用于数据点分布相对均匀的情况,能较好地反映数据在空间上的变化趋势。 二、双线性插值算法 这种算法主要用于处理二维数据的插值问题。它首先将数据点所在的区域划分为一个个小的矩形单元。当需要计算某个未知点的值时,先找到该点所在的矩形单元,然后利用矩形单元四个顶点的已知值进行插值计算。具体过程是先在矩形单元的一对对边上分别进行线性插值,得到两个中间值,再对这两个中间值进行线性插值,最终得到未知点的值。双线性插值能够较为平滑地过渡数据值,特别适合处理图像缩放、地理数据等二维场景中的插值问题,能有效避免插值结果出现明显的突变。 三、面距离反比 + 双线性插值算法 这是一种结合了面距离反比和双线性插值两种方法的算法。它既考虑了数据点所在平面区域对未知点值的影响,又利用了双线性插值的平滑特性。在计算未知点的值时,先根据面距离反比的思想,确定与未知点所在平面区域相关的已知数据点集合,这些点对该平面区域的值有较大影响。然后在这些已知点构成的区域内,采用双线性插值的方法进行进一步的插值计算。这种方法综合了两种算法的优点,既能够较好地反映数据在空间上的整体分布情况,又能保证插值结果的平滑性,适用于对插值精度和数据平滑性要求较高的复杂场景。
内容概要:本文详细介绍并展示了基于Java技术实现的微信小程序外卖点餐系统的设计与实现。该系统旨在通过现代信息技术手段,提升外卖点餐管理的效率和用户体验。系统涵盖管理员、外卖员、餐厅和用户四个角色,提供了包括菜品管理、订单管理、外卖员管理、用户管理等功能模块。后台采用SSM框架(Spring + Spring MVC + MyBatis)进行开发,前端使用微信小程序,数据库采用MySQL,确保系统的稳定性和安全性。系统设计遵循有效性、高可靠性、高安全性、先进性和采用标准技术的原则,以满足不同用户的需求。此外,文章还进行了详细的可行性分析和技术选型,确保系统开发的合理性与可行性。 适用人群:计算机科学与技术专业的学生、从事Java开发的技术人员、对微信小程序开发感兴趣的开发者。 使用场景及目标:①为中小型餐饮企业提供低成本、高效的外卖管理解决方案;②提升外卖点餐的用户体验,实现便捷的点餐、支付和评价功能;③帮助传统餐饮企业通过数字化工具重构消费场景,实现线上线下一体化运营。 其他说明:该系统通过详细的系统分析、设计和实现,确保了系统的稳定性和易用性。系统不仅具备丰富的功能,还注重用户体验和数据安全。通过本项目的开发,作者不仅掌握了微信小程序和Java开发技术,还提升了独立解决问题的能力。系统未来仍需进一步优化和完善,特别是在功能模块的细化和用户体验
Retinex理论是计算机视觉和图像处理领域中一种重要的图像增强技术,由生理学家Walter S. McCann和James G. Gilchrist在20世纪70年代提出,旨在模拟人类视觉系统对光照变化的鲁棒性。该理论将图像视为亮度和色度的函数,分别对应局部强度和颜色信息。其核心思想是将图像分解为反射分量(物体自身颜色)和光照分量(环境光影响),通过分离并独立调整这两个分量来增强图像对比度和细节。 在Matlab中实现Retinex算法通常包括以下步骤:首先对输入图像进行预处理,如灰度化或色彩空间转换(例如从RGB到Lab或YCbCr),具体取决于图像特性;然后应用Retinex理论,通常涉及对图像进行高斯滤波以平滑图像,并计算局部对比度。可以采用多尺度Retinex(MSR)或单尺度Retinex(SSR)方法,其中MSR使用不同尺度的高斯滤波器估计光照分量,以获得更平滑的结果;接着对分离后的反射分量进行对比度拉伸或其他对比度增强处理,以提升图像视觉效果;最后将调整后的反射分量与原始光照分量重新组合,生成增强后的图像。如果存在“retinex.txt”文件,其中可能包含实现这些步骤的Matlab代码。通过阅读和理解代码,可以学习如何在实际项目中应用Retinex算法,代码通常会涉及定义图像处理函数、调用Matlab内置图像处理工具箱函数以及设置参数以适应不同图像。 在研究和应用Retinex算法时,需要注意以下关键点:一是参数选择,算法性能依赖于高斯滤波器尺度、对比度拉伸范围等参数,需根据具体应用调整;二是运算复杂性,由于涉及多尺度处理,算法计算复杂度较高,在实时或资源受限环境中需优化或寻找高效实现方式;三是噪声处理,Retinex算法可能放大噪声较大的图像中的噪声,因此实际应用中可能需要结合去噪方法,如中值滤波或非局部均值滤波。通过深入理解和应用Retinex算法,不
内容概要:本文详细介绍了Graylog这款开源日志管理平台的核心功能及其优势,涵盖日志收集、实时搜索分析、可视化展示和警报通知等方面。文章首先阐述了日志管理的重要性,接着对比了Graylog与ELK Stack、Splunk等工具的不同之处,突出了其开源免费、实时处理、易于扩展、界面友好和社区活跃等特点。随后,逐步讲解了在Linux系统上安装Graylog的过程,包括Java环境、Elasticsearch、MongoDB以及Graylog服务器和Web界面的安装与配置。接着,描述了如何配置Graylog实现高效日志管理,如设置输入源、创建日志收集规则(Stream)和配置告警规则。最后,通过一个电商项目的实战案例展示了Graylog在实际运维中的应用,并总结了常见问题及解决方法。 适合人群:从事IT运维工作的技术人员,尤其是负责日志管理和系统监控的工程师。 使用场景及目标:①帮助运维人员快速收集、筛选和分析海量的日志数据,迅速定位系统故障的根源;②通过直观的可视化展示和及时的告警通知,保障系统的稳定运行;③适用于微服务架构下的多服务日志管理,以及需要对日志进行深度分析和告警设置的场景。 阅读建议:Graylog是一款功能强大的日志管理工具,其安装配置和使用涉及多个组件和技术细节。建议读者在学习过程中,结合实际操作进行练习,尤其是在安装部署阶段,注意各组件之间的版本兼容性和资源配置。同时,充分利用Graylog提供的可视化和告警功能,提升日常运维效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值