RPC项目学习2

项目被拆分为一个客户端调用RPC获得结果,两个服务器接收调用。

项目client与server流程图

客户端发送数据流程

应用层

限流 - > 服务发现 + 负载均衡 - > 注册熔断器 - > 池化管理网络连接 - > 获取连接 - > 序列化参数 -> 传递请求数据 - > 接收数据记录熔断信息。

传输层

多路复用同一个TCP -> SendAsync:生产请求序列Id,本地缓存一份Id与future的映射,帮助未来接收到数据后根据对应Id找到对应future。发送数据。

TCP建立时维护一个接收协程:readLoop(),阻塞直到接收到信息,接收到信息之后根据Id获得future,并关闭channel通知完成。

数据格式:

bash 复制代码
magic(2Byte) + HeaderLen(4Byte) + BodyLen(4Byte) + Head + Body

(边界问题)粘包/半包处理:socket的数据一直读,如果有新数据就拼接到本地保存的buffer中,拿到新到达的数据包的HeaderLen + BodyLen,将magic + HeaderLen + BodyLen = 2 + 4 + 4 = 10与获得的HeaderLen + BodyLen长度求和,如果数据超过,就说明一个完整的包已经到达,则切分并传回这个包。

协议处理

Decode编解码问题:拿到data包检查magic,长度正常后,读取头部,反序列出使用的压缩工具,进行解压缩并封装Message返回。

服务端接收数据流程

传输层:

server :Listen监听tcp连接请求,阻塞直到有TCP连接建立。处理连接。并读取信息。

(边界问题)粘包/半包处理:socket的数据一直读,如果有新数据就拼接到本地保存的buffer中,拿到新到达的数据包的HeaderLen + BodyLen,将magic + HeaderLen + BodyLen = 2 + 4 + 4 = 10与获得的HeaderLen + BodyLen长度求和,如果数据超过,就说明一个完整的包已经到达,则切分并传回这个包。

应用层:

处理请求调用invoke,反射获得服务类型,服务名称,进行调用后得到结果返回。对结果序列化后拼接头部返回。

RPC程序设计细节

限流

限流器被New出来时自动开启一条协程每秒装填token。限流器限制了一秒内超过rate的额外请求全被丢弃。

Go 复制代码
package limiter

import (
	"sync"
	"time"
)

type TokenBucket struct {
	tokens   int
	rate     int
	mu       sync.Mutex
	stopChan chan struct{}
}

func NewTokenBucket(rate int) *TokenBucket {
	tb := &TokenBucket{tokens: rate, rate: rate, stopChan: make(chan struct{})}
	go func() {
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				tb.Reset()
			case <-tb.stopChan:
				return
			}
		}
	}()
	return tb
}

func (tb *TokenBucket) Allow() bool {
	tb.mu.Lock()
	defer tb.mu.Unlock()
	if tb.tokens > 0 {
		tb.tokens--
		return true
	}
	return false
}

func (tb *TokenBucket) Reset() {
	tb.mu.Lock()
	defer tb.mu.Unlock()
	tb.tokens = tb.rate
}

func (tb *TokenBucket) Stop() {
	close(tb.stopChan)
}
服务发现

当寻找服务时首先向ETCD查询一次主机以及对应的服务地址缓存本地

重点服务发现,开启一个协程获得ETCD的通知针对不同类型进行处理

|-----------------|-----------------------|--------------|
| 事件类型 | 触发时机 | 处理 |
| EventTypePut | 新服务注册到了etcd,新server启动 | 添加新服务信息到本地缓存 |
| EventTypeDelete | 服务被移除或宕机 | 从本地缓存删除 |

Go 复制代码
func (r *Registry) watch(service string) {
	key := fmt.Sprintf("%s%s/", r.prefix, service)

	for {//建立watch通道,监听服务的所有实例变化
		watchChan := r.client.Watch(r.ctx, key, clientv3.WithPrefix())

		for watchResp := range watchChan {
			for _, event := range watchResp.Events {
				switch event.Type {

				case clientv3.EventTypePut:
					addr := string(event.Kv.Value)
					r.mu.Lock()
					r.services[service][addr] = Instance{Addr: addr}
					r.mu.Unlock()

				case clientv3.EventTypeDelete:
					deletedKey := string(event.Kv.Key)
					addr := strings.TrimPrefix(deletedKey, r.prefix+service+"/")
					r.mu.Lock()
					delete(r.services[service], addr)
					r.mu.Unlock()
				}
			}
		}

		// watch 断了,稍后重连
		time.Sleep(time.Second)
	}
}
负载均衡

负载均衡支持三种算法轮询,顺序是server1 -> server2 -> server3 -> server4 -> server1 ->.... ->

权重轮询,根据权重增加概率增加。以及随机。

以最简单常用的的轮询为例。

实现对外Select接口,让外界可以直接调用。

Go 复制代码
package loadbalance

import (
	"kamaRPC/internal/registry"
	"sync/atomic"
)

type RoundRobin struct {
	idx uint64
}

func NewRR() *RoundRobin {
	r := &RoundRobin{}
	return r
}

func (r *RoundRobin) Select(list []registry.Instance) registry.Instance {
	i := atomic.AddUint64(&r.idx, 1)
	return list[i%uint64(len(list))]
}
熔断机制

什么是熔断?

类似于与下游服务调用的"保险丝",如果某个服务出现故障比如响应超时、错误率过高。自动切断服务调用,防止故障扩散影响整个系统。

作用是:防止雪崩、快速失败、自动恢复(关闭之后进入半开状态,少量请求探测下游服务,如果成功关闭熔断;否则继续保持)。

  • 关闭(Closed):正常调用下游服务,同时监控失败率。
  • 打开(Open):当失败率达到阈值(如5秒内失败率超过50%),熔断器跳闸,直接拒绝后续请求。
  • 半开(Half-Open):经过一段冷却时间后,允许少量请求通过,用于探测服务是否恢复。
Go 复制代码
package breaker

import (
	"log"
	"sync"
	"time"
)

type State int

const (
	Closed State = iota
	Open
	HalfOpen
)

type CircuitBreaker struct {
	mu sync.Mutex

	state State

	// 统计数据
	failureCount int
	successCount int

	// 配置参数
	windowSize       int           // 统计窗口大小(次数)
	failureThreshold float64       // 失败率阈值
	openTimeout      time.Duration // 熔断持续时间

	// 状态控制
	lastStateChange time.Time
	halfOpenProbe   bool // 半开状态下是否已有探测请求
}

func NewCircuitBreaker(windowSize int, failureThreshold float64, openTimeout time.Duration) *CircuitBreaker {
	return &CircuitBreaker{
		state:            Closed,
		windowSize:       windowSize,
		failureThreshold: failureThreshold,
		openTimeout:      openTimeout,
		lastStateChange:  time.Now(),
	}
}
func (cb *CircuitBreaker) Allow() bool {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	switch cb.state {

	case Closed:
		return true

	case Open:
		// 熔断时间到了,进入半开
		if time.Since(cb.lastStateChange) > cb.openTimeout {
			cb.state = HalfOpen
			cb.halfOpenProbe = false
			return true
		}
		return false

	case HalfOpen:
		// 只允许一个探测请求
		if cb.halfOpenProbe {
			return false
		}
		cb.halfOpenProbe = true
		return true
	}

	return true
}
func (cb *CircuitBreaker) RecordSuccess() {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	switch cb.state {

	case Closed:
		cb.successCount++

	case HalfOpen:
		// 探测成功 → 恢复
		cb.toClosed()

	case Open:
		//按道理是不会进入这块的
		log.Println("理论不发生触发")
	}
}
func (cb *CircuitBreaker) RecordFailure() {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	switch cb.state {

	case Closed:
		cb.failureCount++

		total := cb.failureCount + cb.successCount
		if total < cb.windowSize {
			return
		}

		rate := float64(cb.failureCount) / float64(total)
		if rate >= cb.failureThreshold {
			cb.toOpen()
			return
		}

		cb.resetCounts()

	case HalfOpen:
		// 探测失败 → 重新熔断
		cb.toOpen()

	case Open:
		// 已经熔断,不处理
	}
}
func (cb *CircuitBreaker) toOpen() {
	cb.state = Open
	cb.lastStateChange = time.Now()
	cb.resetCounts()
	cb.halfOpenProbe = false
}

func (cb *CircuitBreaker) toClosed() {
	cb.state = Closed
	cb.lastStateChange = time.Now()
	cb.resetCounts()
	cb.halfOpenProbe = false
}
func (cb *CircuitBreaker) resetCounts() {
	cb.failureCount = 0
	cb.successCount = 0
}

func (cb *CircuitBreaker) State() State {
	cb.mu.Lock()
	defer cb.mu.Unlock()
	return cb.state
}
池化管理TCP连接
Go 复制代码
func (c *Client) getPool(addr string) *transport.ConnectionPool {
	if pool, ok := c.pools.Load(addr); ok {
		return pool.(*transport.ConnectionPool)
	}

	newPool := transport.NewConnectionPool(addr, 0, 1) //连接池
	actual, _ := c.pools.LoadOrStore(addr, newPool)
	return actual.(*transport.ConnectionPool)
}
TCP多路复用

本地实现发送序列seq与对应future映射。数据传出后挂起future等待对应回应返回。

Go 复制代码
func (c *TCPClient) SendAsync(msg *protocol.Message) (*Future, error) {
	if atomic.LoadInt32(&c.closed) == 1 {
		return nil, errors.New("connection closed")
	}

	seq := c.nextSeq()
	msg.Header.RequestID = seq

	future := NewFuture()
	c.pending.Store(seq, future)

	c.writeMu.Lock()
	err := c.conn.Write(msg)
	c.writeMu.Unlock()

	if err != nil {
		c.pending.Delete(seq)
		c.fail(err) // 关键:write 失败也要彻底杀死连接(解决之前连接bug)
		return nil, err
	}

	return future, nil
}

开启协程readloop接收响应。

Go 复制代码
func (c *TCPClient) readLoop() {
	for {
		msg, err := c.conn.Read()
		if err != nil {
			c.fail(err)
			return
		}

		seq := msg.Header.RequestID

		val, ok := c.pending.LoadAndDelete(seq)
		if !ok {
			continue
		}

		future := val.(*Future)

		if msg.Header.Error != "" {
			future.Done(nil, errors.New(msg.Header.Error))
		} else {
			future.Done(msg.Body, nil)
		}
	}
}
粘包/半包边界处理

拿到头部和身体部分长度后拼出一个packet的总长,然后把一个packet总体返回。

Go 复制代码
func (pb *PacketBuffer) Read() []byte {
	pb.lock.Lock()
	defer pb.lock.Unlock()

	// 最小包头长度校验
	if len(pb.buf) < 10 {
		return nil
	}

	headerLen := int(protocol.DecodeHeaderLen(pb.buf[2:6]))
	bodyLen := int(protocol.DecodeBodyLen(pb.buf[6:10]))
	totalLen := 10 + headerLen + bodyLen

	if len(pb.buf) < totalLen {
		return nil
	}

	packet := make([]byte, totalLen)
	copy(packet, pb.buf[:totalLen])

	// 移动窗口
	pb.buf = pb.buf[totalLen:]
	return packet
}
相关推荐
恋喵大鲤鱼2 小时前
认识 RPC 的不同模式
rpc
码喽7号2 小时前
vue学习五:前端路由VueRouter
前端·vue.js·学习
_李小白2 小时前
【OSG学习笔记】Day 49: 实战鼠标拾取与高亮显示
笔记·学习·计算机外设
何如呢2 小时前
FIFO的IP核学习
学习·fpga开发
帐篷Li2 小时前
创建Controller HTTP测试脚本
网络·网络协议·http
捞的不谈~2 小时前
LUCID相机(HTR003S-001)更改IP地址
网络·网络协议·tcp/ip
折锦烟3 小时前
AI Agent 开发 0-1 学习路线(学习目标)
学习
艾莉丝努力练剑3 小时前
【Linux线程】Linux系统多线程(六):<线程同步与互斥>线程同步(上)
java·linux·运维·服务器·c++·学习·线程
brave_zhao3 小时前
什么是增值税
学习