惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Palo Alto Networks Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
CERT Recently Published Vulnerability Notes
C
Cybersecurity and Infrastructure Security Agency CISA
S
Schneier on Security
S
Securelist
酷 壳 – CoolShell
酷 壳 – CoolShell
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cyberwarzone
Cyberwarzone
Apple Machine Learning Research
Apple Machine Learning Research
S
SegmentFault 最新的问题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
GbyAI
GbyAI
Security Latest
Security Latest
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
云风的 BLOG
云风的 BLOG
Recorded Future
Recorded Future
Webroot Blog
Webroot Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
C
Cisco Blogs
博客园 - 【当耐特】
Blog — PlanetScale
Blog — PlanetScale
Hugging Face - Blog
Hugging Face - Blog
B
Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
The Last Watchdog
The Last Watchdog
U
Unit 42
阮一峰的网络日志
阮一峰的网络日志
Project Zero
Project Zero
WordPress大学
WordPress大学
L
LINUX DO - 最新话题
F
Fortinet All Blogs
L
LINUX DO - 热门话题
PCI Perspectives
PCI Perspectives
Simon Willison's Weblog
Simon Willison's Weblog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
MongoDB | Blog
MongoDB | Blog
Latest news
Latest news
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
The Hacker News
The Hacker News
爱范儿
爱范儿
O
OpenAI News
J
Java Code Geeks
T
The Exploit Database - CXSecurity.com
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Razeen`s Blog

Let's Encrypt 推出 Gen Y 根证书架构:揭示 Web PKI 的五大未来趋势 通过 Wi-Fi 自动备份你的 iPhone 到 Nas 管理培训感悟:从技术视角看管理之路 UPS 一拖多:保护你的 NAS 和 Linux 服务器 从影音中心到 AI 助手:我的50款 Homelab 服务清单 从SSL证书有效期将缩短到47天聊开去 部署一个自己的 Running Page 谈谈特斯拉 Model Y 用车一年的感受和费用分析 弃用 Disqus 评论,使用自建 Waline 使用 Prometheus 和 Grafana 搭建你的证书监控面板 服务器迁移记:一次磁盘换板引发的Linux分区与挂载的学习 改善信息来源, 利用 RSS 高效获取资讯 (RSShub + Reeder5 + WeWe RSS) 多种 Docker 镜像拉取解决方案与实践 App分享 | AppCleaner - Mac上的卸载神器 Azure OpenAI API 申请和使用 如何开通 OneKey 虚拟信用卡,并充值消费 如何拥有一个可长期在国内使用的国外手机号码 分享开通 ChatGPT Plus 过程 如何开通 Depay 虚拟信用卡,并充值消费 如何开通欧易Web3钱包, 交易入账 如何5分钟内1块钱注册 ChatGPT 如何注册美区的 Apple ID (2023年/无需科学上网) NAS折腾记(11): NASTool3.0体验和降级 NAS折腾记(10): Docker版本的NASTool配置 NAS折腾记(9): NASTool与微信交互,微信发送消息远程下载电影【多图】 NAS折腾记(8):群晖安装 NASTool 实现影音半自动化【多图】 内网穿透(2):Tailscale 组网实现内网穿透,操作简单,无成本 内网穿透(1):总结了11中内网穿透的方式,总有一种适合你 NAS折腾记(7):从零开始设置(黑)群晖系统 NAS折腾记(6):黑群晖系统安装好后怎么洗白? NAS折腾记(5):群晖硬盘休眠设置与分析 NAS折腾记(4): 20分钟手把手带你完美安装 DS918+ 黑群晖7.1.1 NAS折腾记(3):NAS 装机 NAS折腾记(2):B360-ITX 双M.2 双2.5G网口 6 SATA主版 开箱 NAS折腾记(1):328元的4盘位Nas机箱开箱 Openwrt + Clash 全局科学上网 Newifi3 刷入 OpenWrt 固件 v21.02 利用 Markdown 画一些流程图、时序图、甘特图等 Homelab(8): 搭建自用 Gitlab 与 Docker 仓库 Nginx Tcp 转发保留客户端真实 IP (PROXY Protocol) Homelab (7):IPSec VPN(基于证书认证)客户端设置 Homelab (6): 基于自签发证书的 IPSec VPN 搭建 Homelab (5): DDNS 动态域名解析 Homelab (4): Linux 服务器基础环境准备 Homelab (3): 整体网络与基础硬件介绍 Go学习笔记(十)老项目迁移 go module 大型灾难记录 Github Actions 初体验之自动化部署 Hexo 博客 记一次 Nginx DNS 缓存导致转发问题 Homelab (2): 电信悦me网关修改桥接模式,路由器拨号 Homelab (1):5分钟上手黑群晖 NAS 终极 Bash 脚本指南 Typora 自动上传图片到七牛云 Ubuntu 20.04 LTS 有线网卡驱动安装 折腾 Ubuntu 20.04 LTS 开发环境 Go学习笔记(九) 计时器的生命周期[译] 利用 git hook 规范你的代码与 commit message 规范 git commit message 与自动化版本控制 超详细 vim 配置 (with MacVim) Golang 中的 RESTful API 最佳实践 折腾服务器(开篇) 我的第一台个人服务器 Newifi3 实现低成本家庭级科学上网 Go学习笔记(八) | 使用 os/exec 执行命令 如何用 Go 调用 Windows API Mac OS 自动根据 WI-FI 名字改变网络位置 关于 Docker 清理 MIME Types 速查表 Go学习笔记(七) | 理解OAuth 2.0并实现一个客户端 我又又又把博客迁移了 Go学习笔记(六) | 使用swaggo自动生成Restful API文档 Go学习笔记(五) | 使用代码片段(snippets)提高编码效率 书单 - 2018 IPFS 初体验,利用 IPFS 托管你的静态网站 记一次 PostgreSQL LIKE 索引优化,联合字段 LIKE 查询优化。 Disqus 添加有趣的 Reactions 的功能 TLS 1.3 详解 (RFC 8446解读) gRPC在Go中的使用(二)gRPC实现简单通讯 gRPC在Go中的使用(一)Protocol Buffers语法与相关使用 CentOS 安装 tshark 抓包工具 Go学习笔记(四) | win上使用VSCode搭建Go开发环境 日常 Postgres 数据库点滴记录 简单了解 PKCS 规范 美食篇 | DIY戚风蛋糕(烤箱做蛋糕) Go学习笔记(三) | 怎么写Go基准测试(性能测试) TLS1.3正式更新,为Nginx添加TLS1.3的支持 一次诡异的数据库删除 GitHub Pages自定义域名开启HTTPS 证书透明度是什么?它是怎么工作的? Go学习笔记(二) | 我对 recover 的一点误解 搭建证书透明度(certificate-transparency)日志服务之从入门到放弃 修复远程登陆 Centos 时,出现 UTF-8 Warning HTTPS篇之SSL握手过程详解 Go学习笔记(一) | postgres与golang点点滴滴 AWS 命令行界面(aws-cli)从安装到快速上手 数字证书分类及怎么区分各类数字证书 常用 linux 命令小结(一)文件目录操作 云服务器搭建 hexo 博客,git hooks自动更新 SSH 免密登陆, SSH Config 配置 Golang CGO Mac 交叉编译 Windows 使用 goose 让数据库迁移更加轻松 开始使用Ghost
gRPC在Go中的使用(三)gRPC实现TLS加密通信与流模式
2018-08-06 · via Razeen`s Blog

在前面的两篇博客中,我们已经知道了如何利用gRPC建立简单RPC通信。但这样简单的实现有时候满足不了我们的业务需求。在一些场景中我们需要防止数据被劫持,或是一些场景中我们希望客户端与服务器不是简单的一问一答,而是建立起一个流式的RPC通信,那么该怎么做到呢?

TLS加密通信

TLS加密无非就是认证客户端与服务器,如果对SSL/TLS加密通信有所了解的童鞋都知道我们首先需要两张证书。

所以作为准备工作,我们首先要申请两张测试证书。一张客户端证书,一张服务器证书。

生成测试证书

利用MySSL测试证书生成工具我们可以很简单的生成两张证书,如下所示:

如图,填入域名生成一张服务器证书,然后将私钥,证书链,根证书都下载下来,保存到文件。

同样,生成一张客户端证书并保存。

客户端与服务器TLS认证

在gRPC通信中,我们完成服务器认证与客户端认证主要使用的是grpc下的credentials库。下面通过实例来看看怎么使用。

代码实例

服务端实现

func main() {
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		panic(err)
	}

	// 加载证书和密钥 (同时能验证证书与私钥是否匹配)
	cert, err := tls.LoadX509KeyPair("certs/test_server.pem", "certs/test_server.key")
	if err != nil {
		panic(err)
	}

	// 将根证书加入证书池
	// 测试证书的根如果不加入可信池,那么测试证书将视为不可惜,无法通过验证。
	certPool := x509.NewCertPool()
	rootBuf, err := ioutil.ReadFile("certs/root.pem")
	if err != nil {
		panic(err)
	}
	
	if !certPool.AppendCertsFromPEM(rootBuf) {
		panic("fail to append test ca")
	}

	tlsConf := &tls.Config{
		ClientAuth:   tls.RequireAndVerifyClientCert,
		Certificates: []tls.Certificate{cert},
		ClientCAs:    certPool,
	}

	serverOpt := grpc.Creds(credentials.NewTLS(tlsConf))
	grpcServer := grpc.NewServer(serverOpt)

	pb.RegisterHelloWorldServiceServer(grpcServer, &SayHelloServer{})

	log.Println("Server Start...")
	grpcServer.Serve(lis)
}

客户端实现

func main() {
	cert, err := tls.LoadX509KeyPair("certs/test_client.pem", "certs/test_client.key")
	if err != nil {
		panic(err)
	}
    // 将根证书加入证书池
	certPool := x509.NewCertPool()
	bs, err := ioutil.ReadFile("certs/root.pem")
	if err != nil {
		panic(err)
	}

	if !certPool.AppendCertsFromPEM(bs) {
		panic("fail to append test ca")
	}
    
    // 新建凭证
    // ServerName 需要与服务器证书内的通用名称一致
	transportCreds := credentials.NewTLS(&tls.Config{
		ServerName:   "server.razeen.me",
		Certificates: []tls.Certificate{cert},
		RootCAs:      certPool,
	})

	dialOpt := grpc.WithTransportCredentials(transportCreds)

	conn, err := grpc.Dial("localhost:8080", dialOpt)
	if err != nil {
		log.Fatalf("Dial failed:%v", err)
	}
	defer conn.Close()

	client := pb.NewHelloWorldServiceClient(conn)
	resp1, err := client.SayHelloWorld(context.Background(), &pb.HelloWorldRequest{
		Greeting: "Hello Server 1 !!",
		Infos:    map[string]string{"hello": "world"},
	})
	if err != nil {
		log.Printf("%v", err)
	}

	log.Printf("Resp1:%+v", resp1)

	resp2, err := client.SayHelloWorld(context.Background(), &pb.HelloWorldRequest{
		Greeting: "Hello Server 2 !!",
	})
	if err != nil {
		log.Printf("%v", err)
	}

	log.Printf("Resp2:%+v", resp2)
}

从代码中,我们不难看出,主要是创建一个通信凭证(TransportCredentials)。利用credentials库的NewTLS方法从tls加载一个通信凭证用于通信。而在其中需要注意的是:

  • 如果你使用的是自签发的证书,注意将根加入证书池。如果你使用的是可信CA签发的证书大部分不用添加,因为系统的可信CA库已经有了。如果没有成功添加, 在通信时会出现以下错误:

    rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = “transport: authentication handshake failed: x509: certificate signed by unknown authority”

    rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = “transport: authentication handshake failed: remote error: tls: bad certificate”

  • 客户端凭证内 ServerName 需要与服务器证书内的通用名称一致,如果不一致会出现如下错误:

    rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = “transport: authentication handshake failed: x509: certificate is valid for server.razeen.me, not xxxxx”

之后,我们就可安心的通信了,在私钥不泄漏的情况下,基本不再担心数据劫持问题了。

这里我想多说一句:我们经常在提交代码时会直接 git add . ,这是个不好的习惯,有时后我们会将一些不必要的文件提交上去,特别是一些证书私钥密码之类的文件。

流式的RPC通信

流式PRC通信可以分为:

  • 服务器端流式 RPC;

    客户端发送请求到服务器,拿到一个流去读取返回的消息序列。 客户端读取返回的流,直到里面没有任何消息。如:

    rpc ListHello(HelloWorldRequest) returns (stream HelloWorldResponse) {}
    
  • 客户端流式 RPC;

    客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。如:

    rpc SayMoreHello(stream HelloWorldRequest) returns (HelloWorldResponse) {}
    
  • 双向流式 RPC;

    双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写。如:

    rpc SayHelloChat(stream HelloWorldRequest) returns (stream HelloWorldRequest) {}
    

从上面的定义不难看出,用stream可以定义一个流式消息。下面我们就通过实例来演示一下流式通信的使用方法。

首先,我们将上面三个rpc server加入.proto , 并且生成新的.pb.go代码。

在生成的代码hello_world.pb.go中,我们可以看到客户端接口如下:

type HelloWorldServiceClient interface {
	SayHelloWorld(ctx context.Context, in *HelloWorldRequest, opts ...grpc.CallOption) (*HelloWorldResponse, error)
	ListHello(ctx context.Context, in *HelloWorldRequest, opts ...grpc.CallOption) (HelloWorldService_ListHelloClient, error)
	SayMoreHello(ctx context.Context, opts ...grpc.CallOption) (HelloWorldService_SayMoreHelloClient, error)
	SayHelloChat(ctx context.Context, opts ...grpc.CallOption) (HelloWorldService_SayHelloChatClient, error)
}

服务端接口如下:

// HelloWorldServiceServer is the server API for HelloWorldService service.
type HelloWorldServiceServer interface {
	SayHelloWorld(context.Context, *HelloWorldRequest) (*HelloWorldResponse, error)
	ListHello(*HelloWorldRequest, HelloWorldService_ListHelloServer) error
	SayMoreHello(HelloWorldService_SayMoreHelloServer) error
	SayHelloChat(HelloWorldService_SayHelloChatServer) error
}

在客户段的接口中,生成了HelloWorldService_XXXXClient接口类型。 在服务端的接口中,生成了HelloWorldService_XXXXServer接口类型。 我们再查看这些接口的定义,发现这这几个接口都是实现了以下几个方法中的数个:

Send(*HelloWorldRequest) error
Recv() (*HelloWorldRequest, error)
CloseAndRecv() (*HelloWorldResponse, error)
grpc.ClientStream

看其名字,我们不难知道,流式RPC的使用,或者说流的收发也就离不开这几个方法了。下面我们通过几个实例来验证一下。

在服务端,我们实现这三个接口。

// 服务器端流式 RPC, 接收一次客户端请求,返回一个流
func (s *SayHelloServer) ListHello(in *pb.HelloWorldRequest, stream pb.HelloWorldService_ListHelloServer) error {
	log.Printf("Client Say: %v", in.Greeting)

    // 我们返回多条数据
	stream.Send(&pb.HelloWorldResponse{Reply: "ListHello Reply " + in.Greeting + " 1"})
	time.Sleep(1 * time.Second)
	stream.Send(&pb.HelloWorldResponse{Reply: "ListHello Reply " + in.Greeting + " 2"})
	time.Sleep(1 * time.Second)
	stream.Send(&pb.HelloWorldResponse{Reply: "ListHello Reply " + in.Greeting + " 3"})
	time.Sleep(1 * time.Second)
	return nil
}

// 客户端流式 RPC, 客户端流式请求,服务器可返回一次
func (s *SayHelloServer) SayMoreHello(stream pb.HelloWorldService_SayMoreHelloServer) error {
	// 接受客户端请求
	for {
		req, err := stream.Recv()
		if err == io.EOF {
			break
		}

		if err != nil {
			return err
		}

		log.Printf("SayMoreHello Client Say: %v", req.Greeting)
	}

	// 流读取完成后,返回
	return stream.SendAndClose(&pb.HelloWorldResponse{Reply: "SayMoreHello Recv Muti Greeting"})
}

// 双向流式 RPC
func (s *SayHelloServer) SayHelloChat(stream pb.HelloWorldService_SayHelloChatServer) error {
	// 开一个协程去处理客户端数据
	go func() {
		for {
			req, err := stream.Recv()
			if err == io.EOF {
				break
			}

			if err != nil {
				return
			}

			log.Printf("SayHelloChat Client Say: %v", req.Greeting)
		}
	}()

    // 向客户端写入多条数据
	stream.Send(&pb.HelloWorldRequest{Greeting: "SayHelloChat Server Say Hello 1"})
	time.Sleep(1 * time.Second)
	stream.Send(&pb.HelloWorldRequest{Greeting: "SayHelloChat Server Say Hello 2"})
	time.Sleep(1 * time.Second)
	stream.Send(&pb.HelloWorldRequest{Greeting: "SayHelloChat Server Say Hello 3"})
	time.Sleep(1 * time.Second)
	return nil
}

之后我们就可以在客户端分别请求这几个rpc服务。

    // 服务器端流式 RPC;
    // 我们向服务器SayHello 
	recvListHello, err := client.ListHello(context.Background(), &pb.HelloWorldRequest{Greeting: "Hello Server List Hello"})
	if err != nil {
		log.Fatalf("ListHello err: %v", err)
	}

    // 服务器以流式返回
    // 直到 err==io.EOF时,表示接收完毕。
	for {
		resp, err := recvListHello.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		log.Printf("ListHello Server Resp: %v", resp.Reply)
	}
// Client Out:
// 2018/08/06 01:27:55 ListHello Server Resp: ListHello Reply Hello Server List Hello 1
// 2018/08/06 01:27:56 ListHello Server Resp: ListHello Reply Hello Server List Hello 2
// 2018/08/06 01:27:57 ListHello Server Resp: ListHello Reply Hello Server List Hello 3
// Server Out:
// 2018/08/06 01:27:55 Client Say: Hello Server List Hello


	// 客户端流式 RPC;
	sayMoreClient, err := client.SayMoreHello(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	for i := 0; i < 3; i++ {
		sayMoreClient.Send(&pb.HelloWorldRequest{Greeting: fmt.Sprintf("SayMoreHello Hello Server %d", i)})
	}

	sayMoreResp, err := sayMoreClient.CloseAndRecv()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("SayMoreHello Server Resp: %v", sayMoreResp.Reply)

// Client Out:
// 2018/08/06 01:31:11 SayMoreHello Server Resp: SayMoreHello Recv Muti Greeting
// Server Out:
// 2018/08/06 01:31:11 SayMoreHello Client Say: SayMoreHello Hello Server 0
// 2018/08/06 01:31:11 SayMoreHello Client Say: SayMoreHello Hello Server 1
// 2018/08/06 01:31:11 SayMoreHello Client Say: SayMoreHello Hello Server 2


	// 双向流式 RPC;
	sayHelloChat, err := client.SayHelloChat(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		for i := 0; i < 3; i++ {
			sayHelloChat.Send(&pb.HelloWorldRequest{Greeting: fmt.Sprintf("SayHelloChat Hello Server %d", i)})
		}
	}()

	for {
		resp, err := sayHelloChat.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		log.Printf("SayHelloChat Server Say: %v", resp.Greeting)
	}
// Client Out:
// 2018/08/06 01:31:11 SayHelloChat Server Say: SayHelloChat Server Say Hello 1
// 2018/08/06 01:31:12 SayHelloChat Server Say: SayHelloChat Server Say Hello 2
// 2018/08/06 01:31:13 SayHelloChat Server Say: SayHelloChat Server Say Hello 3
// Server Out:
// 2018/08/06 01:31:11 SayHelloChat Client Say: SayHelloChat Hello Server 0
// 2018/08/06 01:31:11 SayHelloChat Client Say: SayHelloChat Hello Server 1
// 2018/08/06 01:31:11 SayHelloChat Client Say: SayHelloChat Hello Server 2

看了实例,是不是觉得很简单~。三种方式大同小异,只要掌握了怎么去收发流,怎么判断流的结束,基本就可以了。

好了,gRPC在Go中的使用三篇文章到这里也就结束了,如果博客中有错误或者你还有想知道的,记得留言哦。