资料库的webrtc文件传输

news2024/11/24 6:38:30

一、一个看似简单的事情往往不简单

一个简单的事情往往会倾注你的心血,也许你看到很简单往往其实没那么简单;其实想想今年业余时间的大把代码,真正能成品的好像并不多。

马上年底了,写下这篇文章。每一行程序就像写小说一样,不管好不好;代码都倾注了我的心血。

真正把一个东西做好;做到好用,用的人多,具有很好的利他性,并不是一个容易的事情,需要不断的量变。

webrtc演示网址:

文件下载测试

二、为什么放弃百度网盘

把数据存储到百度网盘上大该有半年时间,后来发现了一些致命的问题

1、发现分享外链到了10几万条之后就不能分享了

2、分享的百度外链经常会被屏蔽掉

3、分享速度不能过快,1小时只能分享100多条外链,如果分享的过快整个网盘的分享功能都会

4、百度账号需要实名认证,很容易被人认出来,作为一个技术人员我认为应该藏在幕后,尽量不要让别人认出来,这回避免很多不必要的问题

三、为什么不用云存储

4个T的存储,每月就要400元,一年就是5600元,而且不算流量费用一年就是5600,这个费用是不高的,最关键的是使用了云存储很

1、可能被黑客共计,如果别人使用curl去恶意刷流量现有的安全策略很可能被人把费用都刷去

2、每年存储费用是递增的

3、需要实名认证,一旦绑定域名,绑定着手机号,很可能被人找到

四、我的WebRtc架构设计

拜托传统的中继服务器,使用p2p打洞,调研了一些方案,本来打算使用 coturn + janus 的架构设计,但是发现janus c 写的,没有文件传输的插件,如果自己再用c去做一个插件真的是很累,于是就打算用golang 实现一个文件传输的网关。

大体步骤

1、启动网关

2、存储节点 连接到网关

3、用户浏览器请求网关,请求和存储节点交换信令

4、信令交换完成,使用dataChannel进行通信

五、网关设计

网关主要是一个websocket server 采用golang编写

网关设计主要分成了几大模块

1、room

room是一个公共领域,主要是做client和node的鉴权操作,如果鉴权成功,那么会进入Manager进行调度

2、抽象client和node

client和node 有一个共性是他们都具有连接属性,所以应该设计一个公共接口,把他们的共性抽象出来

type Lifecycle interface {
	Stop()
	Start()
}

type Describable interface {
	Name() string
	Type() ConnType
}


type Component interface {
	Lifecycle
	Describable
	AddCallback(ctx EventCallback)
	GetConnNumber() uint64
	SetContext(interface{})
	GetContext() interface{}
	Product(ctx Context)
}

通过添加一个AddCallback回调函数,确保不同模块的业务处理完全玻璃开,node的逻辑就在node里处理,client的逻辑只在client里处理,不能把不同模块的代码交叉处理,至于上下问的传输,统一抽象一个Context,里面抽象存储着我们需要的上下文信息,供给不同的回调函数以及协成之间传输使用。

type Context interface {
	GetData() []byte
	Error() error
	MessageType() int
	GetContext() interface{}
	SetContext(interface{})
	Name() string
}

type EventCallback interface {
	OnReceive(Context, *sync.WaitGroup)
	OnError(Context, *sync.WaitGroup)
	OnClose(Context, *sync.WaitGroup)
	OnWriteComplete(Context, *sync.WaitGroup)
}

type NodeCallback interface {
	OnReceive(Context, *sync.WaitGroup)
	OnError(Context, *sync.WaitGroup)
	OnClose(Context, *sync.WaitGroup)
	OnWriteComplete(Context, *sync.WaitGroup)
}

4、manager

负责整体调度,比如client进入manager后,查找当前可以用的node存储节点,找到后进行信令交换

节点调度,遍历查找拥有client最少的node节点,然后进行通信

// 选择最优线路
func (m *Manager) selectNode() *node.NodeClient {
	if len(m.nodeTree) == 0 {
		return nil
	}

	// 找一个挂载链接最少的节点
	var usableNode *node.NodeClient
	var weight uint64
	for _, conn := range m.nodeTree {
		if uint64(conn.GetConnNumber()) <= weight {
			usableNode = conn.(*node.NodeClient)
		}
	}
	return usableNode
}

六、node节点收到信令后进行应答

1、创建RTCConnection

blockSize := 16384
	//前端页面会对sdp进行base64的encode
	b, err := base64.StdEncoding.DecodeString(sdp)
	if err != nil {
		log.Error("error:%s", err)
		return nil
	}

	str, err := url.QueryUnescape(string(b))
	if err != nil {
		log.Error("error:%s", err)
		return nil
	}

	sdpDes := webrtc.SessionDescription{}
	fmt.Println(str)
	err = json.Unmarshal([]byte(str), &sdpDes)
	if err != nil {
		log.Error("json.Unmarshal err:%s", err)
		return nil
	}

	//创建pc, 并且指定stun服务器
	pc, err := webrtc.NewPeerConnection(webrtc.Configuration{
		ICEServers: []webrtc.ICEServer{
			{
				URLs: []string{"stun:"},
			},
		},
	})

	stat, err := os.Stat("/home/zhanglei/Downloads/《实践论》(原文)毛泽东.pdf")
	if err != nil {
		log.Error("os.Stat %s error:%s", path, err)
		return nil
	}

	if offset > stat.Size() {
		log.Error("offset(%d) > stat.Size(%d)", offset, stat.Size())
		return nil
	}

	chunkSize := int(math.Ceil(float64(stat.Size() / int64(blockSize))))
	currentChunkSize := int(math.Ceil(float64(offset / int64(blockSize))))

	if err != nil {
		log.Error("%s", err)
		return nil
	}

	pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
		fmt.Println("OnConnectionStateChange")
		fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String())
		if s == webrtc.PeerConnectionStateFailed {
			fmt.Println("webrtc.PeerConnectionStateFailed")
		}
	})

	// Register data channel creation handling
	pc.OnDataChannel(func(d *webrtc.DataChannel) {
		fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())

		// Register channel opening handling
		d.OnOpen(func() {
			fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", d.Label(), d.ID())

			stat, err := os.Stat("/home/zhanglei/Downloads/《实践论》(原文)毛泽东.pdf")
			chunkSize = int(math.Ceil(float64(stat.Size() / int64(blockSize))))

			// 握手
			var chunk ChunkMessage
			chunk.Class = HANDSHAKE
			chunk.ChunkSize = uint64(chunkSize)
			handShakeBytes := serialize(&chunk)

			err = d.Send(handShakeBytes.Bytes())
			if err != nil {
				log.Error("%s", err)
				return
			}
		})

		// Register text message handling
		d.OnMessage(func(msg webrtc.DataChannelMessage) {
			fmt.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data))
			data, err := unSerialize(msg.Data)
			if err != nil {
				log.Error("os.Open is : %s", err)
				return
			}

			if data.Class == ACK {
				handle, err := os.Open("/home/zhanglei/Downloads/《实践论》(原文)毛泽东.pdf")
				if err != nil {
					log.Error("os.Open is : %s", err)
					return
				}

				defer handle.Close()

				bufferBytes := make([]byte, blockSize)
				read, err := handle.Read(bufferBytes)
				if err != nil {
					log.Error("handle.Read is : %s", err)
					return
				}

				if read < blockSize {
					bufferBytes = bufferBytes[:read]
				}

				var chunk ChunkMessage
				chunk.Class = SEND
				chunk.ChunkSize = uint64(chunkSize)
				chunk.CurrentChunk = uint64(currentChunkSize)
				chunk.Data = bufferBytes

				// 打包发送
				err = d.Send(serialize(&chunk).Bytes())
				if err != nil {
					log.Error("%s", err)
					return
				}
				return
			}

			if data.Class == RECEIVE {
				handle, err := os.Open("/home/zhanglei/Downloads/《实践论》(原文)毛泽东.pdf")
				if err != nil {
					log.Error("os.Open is : %s", err)
					return
				}

				if data.CurrentChunk == uint64(chunkSize) {
					log.Info("data transfer finish")
					return
				}

				nextChunk := data.CurrentChunk + 1

				bytes := make([]byte, blockSize)
				read, err := handle.ReadAt(bytes, int64(nextChunk)*int64(blockSize))

				if err != nil {
					if !errors.Is(err, io.EOF) {
						log.Error("handle.Read is : %s", err)
						return
					}

				}

				if read < blockSize {
					bytes = bytes[:read]
				}

				var sendData ChunkMessage
				sendData.Class = SEND
				sendData.CurrentChunk = nextChunk
				sendData.Data = bytes
				sendData.ChunkSize = uint64(chunkSize)
				sendDataBytes := serialize(&sendData)
				log.Info(" read %d", nextChunk)

				err = d.Send(sendDataBytes.Bytes())
				if err != nil {
					log.Error("%s", err)
					return
				}

				// 最后一块
				if nextChunk == uint64(chunkSize) {
					d.Close()
					pc.Close()
				}
				return
			}

		})
	})

	_, err = pc.CreateDataChannel("sendDataChannel", nil)
	if err != nil {
		log.Error("error:%s", err)
		return nil
	}

	//channel.OnOpen(func() {
	//
	//})

	//设置远端的sdp
	if err = pc.SetRemoteDescription(sdpDes); err != nil {
		log.Error("error:%s", err)
		return nil
	}

	//创建协商结果
	answer, err := pc.CreateAnswer(nil)
	if err != nil {
		log.Error("error:%s", err)
		return nil
	}

	pc.OnICECandidate(func(i *webrtc.ICECandidate) {
		fmt.Println("OnICECandidate")
		fmt.Println(i)

	})

	err = pc.SetLocalDescription(answer)
	if err != nil {
		log.Error("error:%s", err)
		return nil
	}

	//等待ice结束
	gatherCmp := webrtc.GatheringCompletePromise(pc)
	<-gatherCmp

	//将协商并且收集完candidate的answer,输出到控制台
	answerBytes, err := json.Marshal(*pc.LocalDescription())
	if err != nil {
		log.Error("error:%s", err)
		return nil
	}

	pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
		fmt.Println("OnICECandidate")
		fmt.Println(candidate)
	})

	t := &Transfer{
		sdp:              sdp,
		pc:               pc,
		offset:           uint64(offset),
		currentChunkSize: currentChunkSize,
		chunkSize:        chunkSize,
		answerSdp:        answerBytes,
		blockSize:        blockSize,
	}
	return t

2、打包传输

传输没有采用protobuf,自己写了个二进制传输

func serialize(data *ChunkMessage) *bytes.Buffer {
	data.Version = CodeVersion
	writeBuffer := bytes.NewBuffer(nil)
	writeBuffer.Write([]byte{data.Version})
	writeBuffer.Write([]byte{data.Class})

	// ChunkSize
	binary.Write(writeBuffer, binary.BigEndian, data.ChunkSize)

	// CurrentChunk
	binary.Write(writeBuffer, binary.BigEndian, data.CurrentChunk)

	// DataLen
	data.DataLen = uint64(len(data.Data))
	binary.Write(writeBuffer, binary.BigEndian, data.DataLen)

	if len(data.Data) > 0 {
		// 添加body
		writeBuffer.Write(data.Data)
	}

	return writeBuffer
}

//判断我们系统中的字节序类型
func systemEdian() binary.ByteOrder {
	var i int = 0x1
	bs := (*[int(unsafe.Sizeof(0))]byte)(unsafe.Pointer(&i))
	if bs[0] == 0 {
		return binary.LittleEndian
	} else {
		return binary.BigEndian
	}
}

func unSerialize(data []byte) (*ChunkMessage, error) {
	buf := bytes.NewBuffer(data)
	fmt.Println(buf)
	var chunk ChunkMessage
	binary.Read(buf, systemEdian(), &chunk.Version)
	binary.Read(buf, systemEdian(), &chunk.Class)
	binary.Read(buf, systemEdian(), &chunk.ChunkSize)
	binary.Read(buf, systemEdian(), &chunk.CurrentChunk)
	binary.Read(buf, systemEdian(), &chunk.DataLen)
	//chunkSize := uint64(unsafe.Pointer(&buf.Bytes()))
	//chunk.ChunkSize = chunkSize
	return &chunk, nil
}

7、js前端webrtc提供报价

创建webrtc连接

var pcConfig = {
            'iceServers': [{
                'urls': 'stun:',
            }]
        };
        localConnection = new RTCPeerConnection(pcConfig);

        receiveDataChannel = localConnection.createDataChannel("receiveDataChannel")

        receiveDataChannel.binaryType = "arraybuffer"

        receiveDataChannel.addEventListener('open', dataChannel.onopen);
        receiveDataChannel.addEventListener('close', dataChannel.onclose);
        receiveDataChannel.addEventListener('message', dataChannel.onmessage);
        receiveDataChannel.addEventListener('error', dataChannel.onError);

        try {
            this.offer = await localConnection.createOffer();
        } catch (e) {
            console.log('Failed to create session description: ', e);
            return
        }

        try {
            await localConnection.setLocalDescription(this.offer)
        } catch (e) {
            console.log('Failed to create session description: ', e);
            return
        }

        //eyJ1c2VyX3V1aWQiOiI1ZmZkNDE0N2JkMTMyNWNmMjYwNDAyMWYwODA5OWUyMyIsImxvZ2luX3RpbWUiOjE2Njg0NzgzOTEsIm5vd190aW1lIjoxNjY5OTgzNzkwLCJyYW5fc3RyIjoiZDA3MTczNzI3NjFjMzY0MGU2NmRlYWI5YmYyODZhNzYiLCJzaWduIjoiZjc3NzI0YjZmMTc3MzczNmVhZWFkMTM2NzllNTE0NTcifQ==

        transfer.ws.send((JSON.stringify(downloadRequest)));

前端对收到的数据进行序列化和反序列化

function serialize(data) {
    var bufLen = protoColMinSize;
    if (!data.Data) {
        bufLen += 0;
    } else {
        bufLen += data.Data.length;
    }
    data.Version = 1;
    var protocolBuf = new ArrayBuffer(bufLen);
    const bufView = new DataView(protocolBuf);
    bufView.setUint8(0, data.Version);

    bufView.setUint8(1, data.Class);

    if (!data.ChunkSize) {
        data.ChunkSize = 0
    }
    bufView.setBigUint64(2, BigInt(data.ChunkSize));


    if (!data.CurrentChunk) {
        data.CurrentChunk = 0
    }
    bufView.setBigUint64(10, BigInt(data.CurrentChunk));

    if (data.Data && data.Data.length > 0) {
        bufView.setBigUint64(18, BigInt(data.Data.length));
    } else {
        bufView.setBigUint64(18, BigInt(0));
    }

    console.log(protocolBuf)
    return protocolBuf;
}

function unSerialize(bytes) {

    var versionView = new DataView(bytes).getUint8(0);
    // 最小长度
    var classByteView = new DataView(bytes).getUint8(1);
    // chunk 长度
    var chunkSizeView = parseInt(new DataView(bytes).getBigUint64(2));
    var currentChunkView = parseInt(new DataView(bytes).getBigUint64(10));
    var bodyLenView = parseInt(new DataView(bytes).getBigUint64(18));
    var returnData = {
        Version: versionView,
        Class: classByteView,
        ChunkSize: (chunkSizeView),
        CurrentChunk: currentChunkView,
        PayloadLength: bodyLenView,
        Payload: [],
    };

    if (bodyLenView > 0) {
        returnData.Payload = new Uint8Array(bytes, protoColMinSize, bodyLenView)
    }

    return returnData;
}

八、webrtc文件传输的优缺

1)优点

1、点对点传输,不经过中继服务器

2、民用带宽比较便宜,最差的情况下是带宽打满,不会出现很高的流量费用

3、可以自建存储,存储节点可以使用群辉,可以买自己服务器

4、不需要固定的ip地址

2)缺点

1、宽带线民用的不知道能申请几根

2、存储维护硬件也是一个麻烦的事情,硬盘很可能出现故障,运维也是一个头痛的事情

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/58821.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Egg 1. 快速开始 Quick Start 1.3 一步步 Step by Step 1.3.1 初始化 1.3.2 创建控制器

Egg Egg 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录Egg1. 快速开始 Quick Start1.3 一步步 Step by Step1.3.1 初始化1.3.2 创建控制器1. 快速开始 Quick Start 1.3 一步步 Step by Step 之前我们直接使用 npm init egg&#xff0c;选择了…

# 智慧社区管理系统-基础管理-04业主管理

一后端 1entity package com.woniu.community.entity;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;Data AllArgsConstructor NoArgsConstructor public class Owner {private int id;private String userName;private String …

Linux I2C驱动框架, 调试神奇I2C-Tools介绍

1. IIC协议 2. Linux的I2C体系结构分为3个组成部分&#xff1a; I2C核心( i2c-core.c )&#xff1a;   I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法、I2C通信方法(”algorithm”)、与具体适配器无关的代码、探测设备、检测设备地址的上层代码等。 …

在线PDF查看器和PDF编辑器:GrapeCity Documents PDF (GcPdf)

跨平台 JavaScript PDF 查看器 使用我们的 JavaScript PDF 查看器在网络上阅读和编辑 PDF。跨浏览器和框架打开和打印。GrapeCity Documents PDF (GcPdf) 全功能的 JavaScript PDF 查看器和 PDF 编辑器 适用于所有现代浏览器和框架&#xff0c;包括 Edge、Chrome、Firefox、Op…

UML类图中 前缀符号 + - # ~ 的含义

UML类图中有各种符号&#xff0c;新手看着一脸懵逼&#xff0c;今天我就来讲一下各个符号到底是啥意思。 其实这些符号都是在描述类属性的可见性&#xff1a; UML中&#xff0c;可见性分为4级 1、public public 公用的用 前缀表示 &#xff0c;该属性对所有类可见 2、prote…

算法篇-----回溯1

文章目录什么是回溯呢&#xff1f;力扣690-----员工的重要性&#xff08;中等&#xff09;力扣733-----图像渲染&#xff08;简单&#xff09;力扣463-----岛屿的周长&#xff08;简单)力扣130------被围绕的区域&#xff08;中等&#xff09;力扣17--------电话号码的组合 &am…

kafka-consumer-offset位移

目录 1 offset的默认维护位置 1.1 消费offset案例 2 自动提交offset 3 手动提交offset 3.1 原理 3.2 代码示例 3.2.1 同步提交 3.2.2 异步提交(生产常用) 4 指定offset消费 5 指定时间消费 6 漏消费和重复消费分析 6.1 重复消费 6.2 漏消费 6.3 消费者事务 7 数据…

高通开发系列 - ALSA声卡驱动中tinymix返回时间过慢

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 问题背景问题分析验证第一个猜测验证第二个猜测问题原因解决方案问题背景 我们一个高通平台上出现一个问题: tingmix命令需要几秒钟…

一文带你深入理解Linux端口重用这一特性

【好文推荐】 需要多久才能看完linux内核源码&#xff1f; 概述Linux内核驱动之GPIO子系统API接口 一篇长文叙述Linux内核虚拟地址空间的基本概括 轻松学会Linux下查看内存频率,内核函数,cpu频率 大家好&#xff0c;我是Linux吴彦祖&#xff01; 开篇我先考大家一个小问题&…

Golang Map 基本原理

Go 语言中的 map 即哈希表。哈希表把元素分到多个桶里&#xff0c;每个桶里最多放8个元素。在访问元素时&#xff0c;首先用哈希算法根据 key 和哈希表种子获得哈希值(暂将其命名为 h)&#xff0c;然后利用 h 的低 bbb 位得到桶的序号。其中桶的个数为 2b2^b2b 个&#xff0c;是…

乐趣国学—品读“富润屋,德润身。”中的智慧

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 ✨当前专栏&#xff1a;国学周更-心性养成之路 …

java 基于 SpringMVC+Mybaties+ easyUI 快递公司管理系统 的 设计与实现

一.项目介绍 本系统 角色 权限 动态配置 默认配置了三种 第一种&#xff1a; 超级管理员 第二种&#xff1a; 运输公司 第三种&#xff1a; 订单跟踪人员 超级管理员拥有所有权限&#xff0c;包括车子、路线、订单、是否送达以及交易的统计报表 运输公司&#xff1a;车辆管理权…

使用 Python 和 Streamlit 创建一个很棒的 Web 应用程序

“我们如何制作一个机器学习脚本并将其转换为一个尽可能简单的应用程序,让它基本上感觉像是一个脚本练习?” — Adrien Treuille(Streamlit 的发明者) Web 应用程序是显示数据科学或机器学习项目结果的好方法。从头开始开发 Web 应用程序需要大量时间、精力和技术技能。另一…

世界杯海信再出圈,三星:“谈不上愉悦”

作者 | 曾响铃 文 | 响铃说 本届世界杯作为第一次在北半球冬季举行的世界杯&#xff0c;给全世界球迷带去了一次全新体验。且随着赛程的推进&#xff0c;更多的“惊喜”也一一浮现。 其一便是超多的爆冷&#xff0c;虽然没有具体统计&#xff0c;但此次应该是近几届爆冷最多…

[激光原理与应用-32]:典型激光器 -4- 半导体泵浦固体激光器

目录 第1章 概述 1.1 什么是半导体泵浦固体激光器 1.2 优势 1.3 典型的波长 第2章 半导体泵浦固体激光器的种类 2.1 端面泵浦固体激光器 2.2 侧面泵浦固体激光器 第1章 概述 1.1 什么是半导体泵浦固体激光器 半导体泵浦固体激光器&#xff08;Diode Pump Solid State …

Python函数

一、函数介绍 函数&#xff1a;是组织好的&#xff0c;可重复使用的&#xff0c;用来实现特定功能的代码段。 使用函数的好处是&#xff1a; 将功能封装在函数内&#xff0c;可供随时随地重复利用提高代码的复用性&#xff0c;减少重复代码&#xff0c;提高开发效率二、函数…

学习python第一天

关于Python的数据类型 Python数据类型包括&#xff1a; 数字类型&#xff0c;字符类型&#xff0c;布尔类型&#xff0c;空类型&#xff0c;列表类型&#xff0c;元组类型&#xff0c;字典类型 1、数字类型 包括&#xff1a;整型int 浮点型float(有小数位的都是是浮点型) 注…

代码随想录刷题|LeetCode 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划

目录 1143.最长公共子序列 思路 1、确定dp数组 2、确定递推公式 3、dp数组初始化 4、遍历顺序 5、推导dp数组 最长公共子序列 1035.不相交的线 思路 不相交的线 53. 最大子序和 思路 最大子序列 动态规划 贪心算法 1143.最长公共子序列 题目链接&#xff1a;力扣 思路 不知道…

你在终端启动的进程,最后都是什么下场?(下)

你在终端启动的进程&#xff0c;最后都是什么下场&#xff1f;&#xff08;下&#xff09; 在上期文章你在终端启动的进程&#xff0c;最后都是什么下场&#xff1f;&#xff08;上&#xff09;当中我们介绍了前台进程最终结束的几种情况&#xff0c;在本篇文章当中主要给大家…

好书分享丨区块链的骨骼——密码技术

开放隐私计算 开放隐私计算 开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。 180篇原创内容 …