1.2024最新IM即时通讯四合一系统源码(PC+WEB+IOS+Android)客户端默往
2.通过etcd源码学习golang编程——build constraint
3.一文说透Raft协议
4.etcd 入门与实践
5.gRPCè´è½½åè¡¡ï¼èªå®ä¹è´è½½åè¡¡çç¥--etcdå®ç°ï¼
2024最新IM即时通讯四合一系统源码(PC+WEB+IOS+Android)客户端默往
本文旨在介绍一套集成PC、源码WEB、源码IOS、源码Android客户端的源码最新即时通讯系统源码,为用户提供一整套全面的源码即时通讯解决方案。下面将详细介绍此系统的源码Ng接口游戏源码搭建步骤和环境要求。 系统搭建主要依赖以下环境和工具:后端框架、源码服务器环境以及相关组件。源码 后端环境构建包括后台账号管理、源码服务器配置以及服务器管理工具如宝塔。源码 具体步骤如下: 1. 安装并配置宝塔在线命令,源码此工具将简化服务器管理。源码 2. 使用宝塔命令安装核心组件,源码包括: Minio: 为系统提供对象存储服务。源码 SSDB: 高性能的源码键值数据库,用于存储系统配置信息。 Kafka: 实现消息队列,支持实时数据流处理。 etcd: 分布式键值存储系统,用于分布式系统中存储配置数据。 3. 完成数据库导入,确保系统数据的完整性。 4. 创建并配置网站,整合即时通讯功能。 在前端开发层面,已提供详细的构建教程,包含界面设计、地方麻将源码交互实现以及性能优化技巧。 这套IM即时通讯四合一系统源码旨在为开发者提供高效、稳定、跨平台的即时通讯解决方案。通过遵循上述步骤,用户可以快速搭建起功能全面、性能优良的即时通讯应用。通过etcd源码学习golang编程——build constraint
在etcd源码中,文件处理部分有方法需区分操作系统,文件路径如下:
文件内容包含TryLockFile和LockFile函数定义,感觉得似C/C++的宏定义,用于跨平台编译。注释中使用 “//go:build”和“// +build”标识,具体用法需探究。
搜索得出,此为Go编程语言的编译约束,通过go help和go help buildconstraint查看帮助文档,官方文档提供了基于该文档的个人总结。
build constraint限定编译内容,类似C/C++宏定义。编译命令示例如下。
官方文档解答:Go1.及前版本使用"// +build",Gofmt命令自动添加"//go:build"约束。老版本使用空格和逗号分隔语法,Gofmt命令能正常转换。
了解GOOS和GOARCH,语音聊天室源码可通过go tool获取列举。输出对应GOOS/GOARCH。
总结完毕,持续学习!
一文说透Raft协议
分布式一致性共识算法指的是在分布式系统中,使得所有节点对同一份数据的认知能够达成共识的算法。这个命名强调了一致性,但是我们知道在满足强C的情况下,对应的A就会被破坏地支离破碎。所以分布式一致性共识算法一般基于各种精妙的算法机制,能够在尽可能少地牺牲 C 的基础之上,将 A 提升到尽可能高的水平。
在讨论 Raft 算法的可用性A方面,它确保了当分布式系统中多于半数的节点仍然存活时,系统能够稳定运行, 多数派原则是提高分布式系统可用性 A 的关键。此外,请求处理时间是基于大多数节点的最小响应时间,而非整个系统的最小响应时间。
在数据一致性C方面,Raft 算法的标准版本可以确保数据达到最终一致性。但是,在实际工程实践中,我们可以对 Raft 算法进行些许改良,由此实现数据的即时一致性,从而进一步确保强C特性的实现。
Raft协议基于quorum机制,promise 源码即大多数同意原则,任何的变更都需超过半数的成员确认。
状态机可以被视为节点用来实际存储数据的仓库。每个写入请求的最终步骤都是把结果保存到状态机里面,同时,读取请求也需要从这个状态机中提取数据以便响应。
预写日志(Write-Ahead Logging, WAL)是一种常用的数据持久化方法,其主要目标是在系统发生故障时保证数据的一致性和可靠性。当进行状态改变的操作时,系统首先将这些操作记录到日志中,然后再实际执行这些操作。只有在操作被成功记录到日志中后,才会修改内存中的状态。如果过程中发生了故障,例如系统崩溃或者电源中断,可以通过重新执行日志中的操作来恢复之前的状态。只有当一个日志(即写入请求)得到了集群大多数派的认同后,它才会被提交并将修改应用到状态机中。
wal日志采用二进制格式,解码后得到的是LogEntry数据结构。其首个字段type包含两个值,0代表Normal,1代表ConfChange(ConfChange用于同步Etcd的配置更改,例如新节点的加入)。其次是term字段,每个term标识一个主节点的spring源码学习任期,任期会在主节点更改时更新。第三个字段index是严格单调增加的序列,代表变更编号。最后一个字段是二进制data,存储了raft request对象的pb结构。在Etcd源代码中有一个工具叫做etcddump-logs,它可以将wal日志转储为文本格式,以便分析Raft协议。
Raft协议并不处理应用数据即data字段的内容,而是通过同步wal日志来保证一致性。每个节点需要将从主节点接收的data应用到其本地存储。在这个过程中,Raft只关心日志的同步状态。因此,如果在应用data到本地存储的过程中存在bug,可能会导致数据不一致,即使所有Raft日志已经同步。
"Term"或"任期"是一个核心概念,它用于标识领导者选举的轮次。
一个Term开始于一个候选人尝试成为领导者,并发起选举。这个候选人会增加他的Term计数,并将其与投票请求一同发送给其他所有节点。如果其他节点收到了带有更高Term的投票请求,那么他们就会更新自己的Term并投票给该候选人。
在 Raft 算法中,所有的节点(无论是领导者、候选人还是跟随者)在发送 RPC (Remote Procedure Call) 请求或响应时,都会在消息中带上自己当前的任期号(Term)。这是因为 Term 是 Raft 协议中用于标识消息新旧和避免过期操作的关键信息。
如果一个节点收到了一个包含较新任期(更大的任期号)的消息,那么它会立即更新自己的当前任期到这个新的任期,并将自己的状态切换为跟随者。如果收到的消息的任期号小于自身的任期号,则该消息会被视为过期信息并被忽略。
因此,在 Raft 中,无论是跟随者、候选人还是领导者,所有的 RPC 消息都会带上 Term 信息。
当一个新的日志条目被领导者创建并添加到其日志中时,这个条目会被赋予一个新的索引号,这个索引号等于当前日志列表的长度加一。因为日志索引号是递增的,所以每一条日志都会有一个唯一的索引号。
每一笔日志除了索引号之外,还有一个核心属性term: 用于标识这则日志是哪个任期的 leader写入的。
Raft协议中的领导者承担两种职责: 接收写请求 周期性发送心跳包
对于两阶段提交我们可以分别从单机和系统的两个角度去理解.
从单机层面,一笔写请求会分为添加到预写日志和应用到状态机两个步骤,这可以看做是一种两阶段提交.
在整个系统层面,两阶段提交的流程可拆解如下:
第2步是提议阶段(proposal),第4步是提交阶段(commit),两者构成了所谓的"两阶段提交"的流程.
如果跟随者(follower)发现当前领导者(leader)的任期小于自己记录的最新任期,跟随者会拒绝领导者的这次同步请求,并在响应中告诉领导者当前的最新任期。在感知到新任期的存在后,领导者也会适时地主动退位变为跟随者。
领导者(leader)的任期滞后主要发生在以下几种情况:
即使领导者(leader)的任期是最新的,如果跟随者(follower)在该最新同步日志之前的预写日志数据存在缺失,那么跟随者会拒绝领导者的同步请求。当领导者发现自己的任期与跟随者响应的任期相同,但同步请求被拒绝时,它会试图递归地逐个向跟随者同步预写日志数组中的早期日志,直到补全跟随者缺失的全部日志。一旦所有缺失日志被补全,流程将恢复正常。
当一个领导者试图向跟随者复制一个日志条目时,但在该日志条目还没被提交前,领导者突然宕机,然后新的领导者被选出。这种情况下,已经接收到旧领导者日志条目的节点的日志就会比新领导者的日志更“新”。当新领导者尝试将自己的日志复制给这些节点时,这些节点就需要删除"超前"的日志并同步新领导者的日志,以保持集群状态的一致性。
当跟随者(follower)接收到读请求时,它会将这些请求统一转发给领导者处理。只要领导者在处理写请求时,确保先更新状态机,然后再向客户端响应,就能保证状态机的数据实时一致性。然而,这种方式的缺点是领导者的负载可能过高,其他跟随者节点主要作用变成存储备份数据,相对较为被动。
这种强制读主存在另外一个问题,即在网络分区后仍错误地认为自己是领导者,提供的数据都是旧数据(因为多数派原则的存在,领导者写数据都会失败,但是读数据不受影响)。解决方案是在领导者提供读服务时,先广播所有节点以得到多数响应来确认自己的领导者身份。
这种设计可以确保,在任何时刻,所有正常运行的副本节点上的状态机都处于一个一致的状态。即使在发生故障转移的情况下,新选出的领导者也能够完成未完成的日志复制,并保证所有正常运行的副本节点上的状态机达到一致的状态。
在网络分区的情况下,处于小分区的节点可能会触发无效的选举,因为它们不能获得多数派的投票。这将导致多余的领导者选举以及不必要的term增加。
预检查或预投票机制可以有效地解决这个问题。在这种机制下,在真正进行选举之前,候选人会先向其他节点发送预投票请求。只有在收到多数节点的回应,即确认自己能够和集群中的大多数节点进行通信,候选人才会增加term并启动真正的选举流程。
这两种机制配合起来,可以有效地确保客户端请求的不丢失和不重复。
etcd 入门与实践
Etcd入门与实践概述
Etcd是一个由Go语言编写的分布式键值存储,专为需要在分布式系统或集群中访问的数据提供强一致性。它依赖于Raft一致性算法进行节点间通信,且在多个工业级项目中得到广泛应用,如Kubernetes、CoreDNS和ROOK等。Etcd与Redis场景的区别
面试中,面试官可能会询问Etcd和Redis的区别。Etcd更适合需要强一致性的场景,而Redis则更侧重于缓存和数据结构操作,且过期机制不同:Etcd的租约模式基于堆结构,而Redis是一对一绑定过期时间。实践操作
初学者可以从下载预编译二进制文件或编译源码开始,建立单节点服务。比如,使用goreman启动多个实例,理解PEER ADDRS和CLIENT ADDRS的含义。 尽管命令行工具etcdctl操作直观,但深入理解还需从代码层面入手。比如,初始化etcd客户端,执行put操作时,无论是普通key还是带有租约的key,都可通过同一方法实现,体现了装饰器设计模式。Get操作和MVCC机制
etcd v3版本引入了MVCC机制,允许查看key的历史版本,如通过etcdctl get hello --rev=?查看不同版本的值。Watch操作与事件通知
etcd通过event机制避免客户端轮询,客户端订阅感兴趣的key,key更新时,etcd通过channel进行通知。例如,./etcdctl watch hello会显示put和租约到期的事件。代码示例
本文详细介绍了Etcd的基础操作和工作原理,包括put、get、租约模式和watch功能,旨在帮助读者深入了解和实践Etcd的使用。gRPCè´è½½åè¡¡ï¼èªå®ä¹è´è½½åè¡¡çç¥--etcdå®ç°ï¼
èæ¯
å¨å·¥ä½å¦ä¹ ä¸ä½¿ç¨gRPCçå°æ¹æ¯è¾å¤ï¼é常æ们é½ä½¿ç¨çæ¯èªå¸¦çè´è½½åè¡¡ç®æ³ï¼ä½æ¯å¨æäºåºæ¯ä¸æ们éè¦å¯¹æå¡ççæ¬è¿è¡æ§å¶æ¯å¦[appV2åªè½å»é¾æ¥userV3],å¨è¿æ ·çæ åµä¸å°±åªè½éèªå®ä¹è´è½½åè¡¡çç¥
ç®æ å®ç°åºäºçæ¬ï¼versionï¼çgrpcè´è½½åè¡¡å¨ï¼äºè§£è¿ç¨åå¯èªå·±å®ç°æ´å¤çè´è½½åè¡¡åè½
注åä¸å¿
EtcdLeaseæ¯ä¸ç§æ£æµå®¢æ·ç«¯åæ´»ç¶åµçæºå¶ã群éæäºå ·æçåæ¶é´çç§çº¦ãå¦æetcd群éå¨ç»å®çTTLæ¶é´å æªæ¶å°keepAliveï¼åç§çº¦å°æã为äºå°ç§çº¦ç»å®å°é®å¼åå¨ä¸ï¼æ¯ä¸ªkeyæå¤å¯ä»¥éå ä¸ä¸ªç§çº¦
æå¡æ³¨å(注åæå¡)
å®æ¶ææ¬å°æå¡ï¼APPï¼å°å,çæ¬çä¿¡æ¯æ³¨åå°æå¡å¨
æå¡åç°(客æ·ç«¯åèµ·æå¡è§£æ请æ±ï¼APPï¼)
æ¥è¯¢æ³¨åä¸å¿ï¼APPï¼ä¸æé£äºæå¡
并åææçæå¡å»ºç«HTTP2é¿é¾æ¥
éè¿Etcdwatchçå¬æå¡ï¼APPï¼ï¼éè¿ååæ´æ°é¾æ¥
è´è½½åè¡¡(客æ·ç«¯å起请æ±ï¼APPï¼)
è´è½½åè¡¡éæ©åéçæå¡ï¼APPHTTP2é¿é¾æ¥ï¼
åèµ·è°ç¨
æå¡æ³¨å(注åæå¡)æºç register.go
funcNewRegister(opt...RegisterOptions)(*Register,error){ s:=&Register{ opts:newOptions(opt...),}varctx,cancel=context.WithTimeout(context.Background(),time.Duration(s.opts.RegisterTtl)*time.Second)defercancel()data,err:=json.Marshal(s.opts)iferr!=nil{ returnnil,err}etcdCli,err:=clientv3.New(s.opts.EtcdConf)iferr!=nil{ returnnil,err}s.etcdCli=etcdCli//ç³è¯·ç§çº¦resp,err:=etcdCli.Grant(ctx,s.opts.RegisterTtl)iferr!=nil{ returns,err}s.name=fmt.Sprintf("%s/%s",s.opts.Node.Path,s.opts.Node.Id)//注åèç¹_,err=etcdCli.Put(ctx,s.name,string(data),clientv3.WithLease(resp.ID))iferr!=nil{ returns,err}//ç»çº¦ç§çº¦s.keepAliveChan,err=etcdCli.KeepAlive(context.Background(),resp.ID)iferr!=nil{ returns,err}returns,nil}å¨etcdéé¢æ们å¯ä»¥çå°å¦ä¸ä¿¡æ¯APPv1çæ¬æå¡å¨èç¹çkey/hwholiday/srv/app/app-beb3cb-eb-eb-d-2cfdc7c
{ "node":{ "name":"app","path":"/hwholiday/srv/app","id":"app-beb3cb-eb-eb-d-2cfdc7c","version":"v1","address":"...:"}}APPv2çæ¬æå¡å¨èç¹çkey/hwholiday/srv/app/app-beb3cb-eb-eb-d-2cfdc7c
{ "node":{ "name":"app","path":"/hwholiday/srv/app","id":"app--eb-eb-c0-2cfdc7c","version":"v2","address":"...:"},}æå¡åç°(客æ·ç«¯åèµ·æå¡è§£æ请æ±ï¼APPï¼)æºç discovery.goå®ç°grpcå çresolver.Builderæ¥å£ï¼Builderå建ä¸ä¸ªè§£æå¨ï¼ç¨äºçè§å称解ææ´æ°ï¼
funcNewDiscovery(opt...ClientOptions)resolver.Builder{ s:=&Discovery{ opts:newOptions(opt...),}etcdCli,err:=clientv3.New(s.opts.EtcdConf)iferr!=nil{ panic(err)}s.etcdCli=etcdClireturns}//Buildå½è°ç¨`grpc.Dial()`æ¶æ§è¡func(d*Discovery)Build(targetresolver.Target,ccresolver.ClientConn,optsresolver.BuildOptions)(resolver.Resolver,error){ d.cc=ccres,err:=d.etcdCli.Get(context.Background(),d.opts.SrvName,clientv3.WithPrefix())iferr!=nil{ returnnil,err}for_,v:=rangeres.Kvs{ iferr=d.AddNode(v.Key,v.Value);err!=nil{ log.Println(err)continue}}gofunc(dd*Discovery){ dd.watcher()}(d)returnd,err}//æ ¹æ®å®æ¹ç建议æ们æä»æ³¨åä¸å¿æ¿å°çæå¡ä¿¡æ¯å¨åå°Attributesä¸//Attributescontainsarbitrarydataabouttheresolverintendedfor//consumptionbytheloadbalancingpolicy.//å±æ§å å«æå ³ä¾è´è½½å¹³è¡¡çç¥ä½¿ç¨ç解æå¨çä»»ææ°æ®ã//Attributes*attributes.Attributesfunc(d*Discovery)AddNode(key,val[]byte)error{ vardata=new(register.Options)err:=json.Unmarshal(val,data)iferr!=nil{ returnerr}addr:=resolver.Address{ Addr:data.Node.Address}addr=SetNodeInfo(addr,data)d.Node.Store(string(key),addr)returnd.cc.UpdateState(resolver.State{ Addresses:d.GetAddress()})}è´è½½åè¡¡(客æ·ç«¯å起请æ±ï¼APPï¼)æºç version_balancer.go
gRPCæä¾äºPickerBuilderåPickeræ¥å£è®©æ们å®ç°èªå·±çè´è½½åè¡¡çç¥
//PickerBuilderå建balancer.PickerãtypePickerBuilderinterface{ //Buildè¿åä¸ä¸ªéæ©å¨ï¼gRPCå°ä½¿ç¨å®æ¥éæ©ä¸ä¸ªSubConnãBuild(infoPickerBuildInfo)balancer.Picker}//gRPC使ç¨Pickeræ¥éæ©ä¸ä¸ªSubConnæ¥åéRPCã//æ¯æ¬¡å¹³è¡¡å¨çå é¨ç¶æåçååæ¶ï¼å®é½ä¼ä»å®çå¿«ç §ä¸çæä¸ä¸ªæ°çéæ©å¨ã//gRPC使ç¨çéæ©å¨å¯ä»¥éè¿ClientConn.UpdateState()æ´æ°ãtypePickerinterface{ //éæ©åéçåé¾æ¥åé请æ±Pick(infoPickInfo)(PickResult,error)}ä»ä¸é¢å¾ç¥æ们å¯ä»¥å¹²äºçå°æ¹å¨Buildæ¹æ³æè Pickæ¹æ³ï¼è°ç¨gRPCæ¹æ³æ¶å æ§è¡Buildåæ§è¡Pickï¼
Build(infoPickerBuildInfo)balancer.Pickerinfoéé¢ææå¡çé¾æ¥ï¼åé¾æ¥å¯¹åºçååéè¿AddNodeæ¹æ³åå ¥çæå¡ä¿¡æ¯è¿éæ们å¯ä»¥åºäºgrpc-clientå±é¢æ¥åè´è½½ï¼æ¯å¦ï¼å æéæºè´è½½ï¼
Pick(infoPickInfo)(PickResult,error)infoéé¢æè°ç¨çæ¹æ³ååcontext.Contextéè¿context.Contextæ们å¯ä»¥è·å¾è¿ä¸ªæ¥è·åå起请æ±çæ¶åå¡«å ¥çåæ°ï¼è¿æ ·æ们å¯ä»¥å¾çµæ´»çé对æ¯ä¸ªæ¹æ³è¿è¡ä¸åçè´è½½è¿éæ们å¯ä»¥åºäºgrpc-client-apiå±é¢æ¥åè´è½½
func(*rrPickerBuilder)Build(infobase.PickerBuildInfo)balancer.Picker{ iflen(info.ReadySCs)==0{ returnbase.NewErrPicker(balancer.ErrNoSubConnAvailable)}varscs=make(map[balancer.SubConn]*register.Options,len(info.ReadySCs))forconn,addr:=rangeinfo.ReadySCs{ nodeInfo:=GetNodeInfo(addr.Address)ifnodeInfo!=nil{ scs[conn]=nodeInfo}}iflen(scs)==0{ returnbase.NewErrPicker(balancer.ErrNoSubConnAvailable)}return&rrPicker{ node:scs,}}func(p*rrPicker)Pick(infobalancer.PickInfo)(balancer.PickResult,error){ p.mu.Lock()deferp.mu.Unlock()version:=info.Ctx.Value("version")varsubConns[]balancer.SubConnforconn,node:=rangep.node{ ifversion!=""{ ifnode.Node.Version==version.(string){ subConns=append(subConns,conn)}}}iflen(subConns)==0{ returnbalancer.PickResult{ },errors.New("nomatchfoundconn")}index:=rand.Intn(len(subConns))sc:=subConns[index]returnbalancer.PickResult{ SubConn:sc},nil}客æ·ç使ç¨æ们å®ä¹çversionè´è½½åè¡¡çç¥r:=discovery.NewDiscovery(discovery.SetName("hwholiday.srv.app"),discovery.SetEtcdConf(clientv3.Config{ Endpoints:[]string{ "...:"},DialTimeout:time.Second*5,}))resolver.Register(r)//è¿æ¥æå¡å¨conn,err:=grpc.Dial("hwholiday.srv.app",//没æ使ç¨è¿ä¸ªåæ°grpc.WithDefaultServiceConfig(fmt.Sprintf(`{ "LoadBalancingPolicy":"%s"}`,"version")),grpc.WithInsecure(),)iferr!=nil{ log.Fatalf("net.Connecterr:%v",err)}deferconn.Close()//è°ç¨æå¡apiClient:=api.NewApiClient(conn)ctx:=context.WithValue(context.Background(),"version","v1")_,err=apiClient.ApiTest(ctx,&api.Request{ Input:"v1v1v1v1v1"})iferr!=nil{ fmt.Println(err)}è¿è¡æææµè¯æºç
è¿è¡APPæå¡v1,è°ç¨grpc-client使ç¨v1
APPæå°
å¯å¨æå===>0.0.0.0:
input:"v1v1v1v1v1"
grpc-clientæå°
===RUNTestClient
v1v1v1v1v1v1v1v1v1v1
è¿è¡APPæå¡v1,è°ç¨grpc-client使ç¨v2
APPæå°
å¯å¨æå===>0.0.0.0:
grpc-clientæå°
===RUNTestClient
rpcerror:code=Unavailabledesc=nomatchfoundconn
æ»ç»è¯¦æ ä»ç»å°å
æºç å°å:/hwholiday/learning_tools/tree/master/etcd
éè¿å¦ä¹ æ们å¯ä»¥å®ç°åºäºversionçè´è½½çç¥ï¼è¿éåªæ¯æä¾ä¸ç§æè·¯æä¹å»å®ç°å¯è½æçè¿ä¸ªä¾åä¸å¤ªéåè¿ä¸ªï¼ä½æ¯æä¾äºä¸ç§æè·¯ï¼æ¬¢è¿ä¸èµ·è®¨è®ºã