gRPC应用实践
What is RPC?
Remote Procedure Call is a high-level model for client-server communication. Assume there are two computers, computer A(on local) and computer B(on some network). Computer B provides some API’s, let’s say it has some procedures which can be run and these procedures can be run on computer B itself.
What is gRPC?
gRPC is a high performance, open source universal RPC Framework. In simple words, it enables the server and client applications to communicate transparently and build connected systems. GRPC is developed and open sourced by Google.
How GRPC Works?
- A client application can call methods directly on a server-side application present on other machines.
- Service is defined, methods are specified which can be further remotely called with their parameters and return types.
- GRPC uses protocol buffers as the Interface Definition Language to enable communication between two different systems used for describing the service interface and the structure of payload messages. Switch from JSON to protocol buffers. Because of binary data format, it gets much lighter.
- GRPC is built on HTTP 2. Break free from the call-and-response architecture. Allows client-side and server-side Streaming, and bidirectional streaming
Windows client (C++) Setup
- Follow the link to install “Chocolatey”: https://chocolatey.org/install
- Follow the link to build gRPC C++ from source using CMake: https://github.com/grpc/grpc/blob/master/BUILDING.md (Replace cmake .. -G “Visual Studio 14 2015” with cmake .. -G “Visual Studio 15 2017 Win64” to create x64 Visual Studio 2017 solution)
- Run Visual Studio 2017 as administrator, open the “grpc.sln” and build the ALL_BUILD project, then the INSTALL project. (1. Some of the generated files are not installed. You can find them in “your path to grpc\grpc\.build\Release”)
- Using below commands to generate the C++ client and server code (Copy grpc_cpp_plugin.exe and protoc.exe to the directory where the proto is located)
Protoc -I=$SRC_DIR –grpc_out=$DST_DIR $SRC_DIR/helloworld.proto
(e.g. protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe helloworld.proto)
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/helloworld.proto
(e.g. protoc -I . --cpp_out=. helloworld.proto)
5. Create C++ project
- Add “_WIN32_WINNT=0x0A00” to Preprocessor
- Add the include file folder, lib folder and input libs
Windows client (C#) Setup
Visual Studio provides a convenient way to generate C# codes automatically from .proto files.
https://github.com/grpc/grpc/blob/master/src/csharp/BUILD-INTEGRATION.md
- Create a C# class library (.NET Framework) project.(This project is only used to build the .proto file automatically)
- Install below packages through NuGet Package Manager for the project
3. Add proto file to the project (e.g. helloworld.proto)
4. Edit .csproj file to add a new <ItemGroup>
5. Create a C#/WPF project as the gRPC client and add reference to the project which defines the .proto files.
6. Build the solution
Linux server (C++) Setup
gRPC C++ - Build from source
(1) Pre-requisites
$ sudo apt-get install build-essential autoconf libtool pkg-config
# If you plan to build from source and run tests, install the following as well:
$ sudo apt-get install libgflags-dev libgtest-dev
$ sudo apt-get install clang-5.0 libc++-dev
(2) Clone the repository (including submodules)
$ git clone -b $(curl -L https://grpc.io/release) https://github.com/grpc/grpc
$ cd grpc
$ git submodule update --init
(3) Building with CMake
$ sudo mkdir -p cmake/build # Check if the cmake version > 3.6, otherwise install the latest version follow below steps:
$ cd cmake/build $ sudo apt-get install golang $ sudo cmake ../.. -DBUILD_SHARED_LIBS=ON $ sudo make # Copy all needed libs, dlls and executables to the specified folder $ sudo ln -sf ~/min-projects/grpc/cmake/build/* /usr/bin/ $ sudo cp -f /home/mzhu/min-projects/grpc/cmake/build/*.so /usr/local/lib $ sudo cp -f /home/mzhu/min-projects/grpc/cmake/build/*.a /usr/local/lib $ sudo cp -f /home/ mzhu /min-projects/grpc/libs/opt/pkgconfig/*.pc /usr/lib/pkgconfig/ # Add the line “/usr/local/lib” into the file “/etc/ld.so.conf” $ sudo gedit /etc/ld.so.conf # Refresh shared library cache $ sudo ldconfig
(4) Install protoc along with the C++ runtime
$ cd ~/min-projects/grpc/third_party/protobuf/ $ ./autogen.sh $ ./configure $ sudo make $ sudo make install
(5) Run an example to verify the environment
使用案例:
C#客户端界面代码:
<Window x:Class="TransferImage.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TransferImage"
xmlns:utils="clr-namespace:TransferImage.Utils"
mc:Ignorable="d"
Title="MainWindow" Height="760" Width="805">
<Grid>
<Grid Margin="10,10,0,0" VerticalAlignment="Top" Height="500" HorizontalAlignment="Left" Width="770">
<GroupBox HorizontalAlignment="Left" VerticalAlignment="Top" Height="80" Width="770">
<GroupBox.Header>
<TextBlock Text="Connect to server: "/>
</GroupBox.Header>
<DockPanel>
<Label Content="IP Address:" Margin="50,20,0,0"></Label>
<TextBox Name="addressTxt" Text="10.192.132.217" Width="200" Height="28" Margin="0,15,0,0"></TextBox>
<Label Content="Port:" Margin="0,20,0,0"></Label>
<TextBox Name="portTxt" Text="50051" Margin="0,15,0,0" Width="120" Height="28"></TextBox>
<Button Name="btnConnect" Content="Connect" Click="BtnConnect_Click" Margin="20,15,0,0" Height="28" Width="200"></Button>
</DockPanel>
</GroupBox>
<GroupBox Margin="0,100,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="400" Width="770">
<GroupBox.Header>
<TextBlock Text="gRPC Testing: "/>
</GroupBox.Header>
<DockPanel>
<StackPanel Width="220" DockPanel.Dock="Left" Margin="10">
<Button Name="btnUpload" Content="Upload Images" Click="BtnUpload_Click" Margin="0,15,0,0" Height="28"></Button>
<Button Name="btngRPC" IsEnabled="False" Content="SayHello" Click="BtngRPC_Click" Margin="0,15,0,0" Height="28"></Button>
<Button Name="btnClientStreamRPC" IsEnabled="False" Content="SendRequestStream" Click="BtnClientStreamRPC_Click" Margin="0,15,0,0" Height="28"></Button>
<Button Name="btnServerStreamRPC" IsEnabled="False" Content="GetReturnStream" Click="BtnServerStreamRPC_Click" Margin="0,15,0,0" Height="28"></Button>
<Button Name="btnBidirectionalStreamRPC" IsEnabled="False" Content="BidirectionalStream" Click="BtnBidirectionalStreamRPC_Click" Margin="0,15,0,0" Height="28"></Button>
<Button Name="btnSRImgCase" IsEnabled="False" Content="Image Super-Resolution" Click="BtnSRImgCase_Click" Margin="0,15,0,0" Height="28"></Button>
</StackPanel>
<StackPanel Margin="10,10,10,10">
<Label Name="titleLbl" Height="28"></Label>
<utils:ZoomBorder x:Name="border" ClipToBounds="True" Background="Gray">
<Image Name="imgOutput" Stretch="UniformToFill" Width="450"></Image>
</utils:ZoomBorder>
<Label Name="imgSizeLbl" Height="28"></Label>
</StackPanel>
</DockPanel>
</GroupBox>
</Grid>
<Grid Margin="10,520,0,0" VerticalAlignment="Top" Height="200" HorizontalAlignment="Left" Width="776">
<GroupBox HorizontalAlignment="Left" VerticalAlignment="Top" Height="190" Width="770">
<GroupBox.Header>
<TextBlock Text="Logging Information: "/>
</GroupBox.Header>
<ScrollViewer x:Name="InfoBlockViewer" VerticalScrollBarVisibility="Auto" Margin="10,10,10,10">
<StackPanel Orientation="Horizontal" Background="White" Margin="0,0,0,0">
<TextBlock x:Name="RcInfo" Width="750">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Clear" Click="MenuItem_Clear_Click">
<MenuItem.Icon>
<Image Source="/Images/clear.ico" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy" Click="MenuItem_Copy_Click">
<MenuItem.Icon>
<Image Source="/Images/copy.ico" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</StackPanel>
</ScrollViewer>
</GroupBox>
</Grid>
</Grid>
</Window>
后台逻辑代码:
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Helloworld;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging; namespace TransferImage
{ /// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private List<string> pictureList = new List<string>();
private const int LogTrimSize = 6 * 1024;
private const int LogSize = 8 * 1024;
private int _exactLogTrimSize;
private Greeter.GreeterClient client;
private Channel channel;
private const int GRPC_MAX_RECEIVE_MESSAGE_LENGTH = (4 * 1024 * 1024) * 5; private void LogRcInfo(string str)
{
if (RcInfo.Text.Length > LogSize)
{
RcInfo.Text = RcInfo.Text.Substring(_exactLogTrimSize);
_exactLogTrimSize = RcInfo.Text.Length;
} RcInfo.Text += str;
InfoBlockViewer.ScrollToBottom(); if (_exactLogTrimSize < LogTrimSize)
{
_exactLogTrimSize += str.Length;
} } public void LogInfo(string str)
{
LogRcInfo(DateTime.Now.ToString(@"yyyy-MM-dd HH:mm:ss\:fff") + ": " + str + "\n");
} public MainWindow()
{
InitializeComponent();
_exactLogTrimSize = 0;
} private void BtnConnect_Click(object sender, RoutedEventArgs e)
{
try
{
string address = addressTxt.Text;
string port = portTxt.Text;
string target = address + ":" + port;
var channelOptions = new List<ChannelOption>();
channelOptions.Add(new ChannelOption(ChannelOptions.MaxReceiveMessageLength, GRPC_MAX_RECEIVE_MESSAGE_LENGTH));
channel = new Channel(target, ChannelCredentials.Insecure, channelOptions);
client = new Greeter.GreeterClient(channel);
client.TestConnection(new HelloRequest { Name = "test connection"});
if (channel.State == ChannelState.Ready)
{
btnConnect.Content = "Connected";
btnConnect.Foreground = Brushes.Green;
LogInfo("Build connection with the server " + target + " successfully ...");
btnConnect.IsEnabled = true;
btnClientStreamRPC.IsEnabled = true;
btnBidirectionalStreamRPC.IsEnabled = true;
btnServerStreamRPC.IsEnabled = true;
btngRPC.IsEnabled = true;
btnSRImgCase.IsEnabled = true;
}
}
catch (RpcException ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); }
} public static System.Drawing.Image BytesToImage(byte[] buffer)
{
MemoryStream ms = new MemoryStream(buffer);
System.Drawing.Image image = System.Drawing.Image.FromStream(ms);
return image;
} public static ByteString ImageToByteString(string filepath)
{
FileStream fs = File.OpenRead(filepath);
int filelength = 0;
filelength = (int)fs.Length;
Byte[] image = new Byte[filelength];
fs.Read(image, 0, filelength);
return ByteString.CopyFrom(image);
} public BitmapImage BytesToBitmapImage(byte[] array)
{
using (var ms = new MemoryStream(array))
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = ms;
image.EndInit();
return image;
}
} private ImageSource GetImageSource(string filePath)
{
return new BitmapImage(new Uri(filePath, UriKind.RelativeOrAbsolute));
} private void BtngRPC_Click(object sender, RoutedEventArgs e)
{
try
{
LogInfo("Click " + btngRPC.Content + " Button"); String user = "C# client simple method"; for (int i = 0; i < pictureList.Count; i++)
{
ByteString str = ImageToByteString(pictureList[i]); var reply = client.SayHello(new Request { Name = user + i.ToString(), Photo = str });
LogInfo("Greeting: " + reply.Message.ToString()); var return_img = reply.Image;
byte[] bytes = return_img.ToByteArray();
BitmapImage img = BytesToBitmapImage(bytes);
imgOutput.Source = img;
}
LogInfo("Click " + btngRPC.Content + " Button Done ...");
}
catch (RpcException ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); }
} private async void BtnClientStreamRPC_Click(object sender, RoutedEventArgs e)
{
LogInfo("Click " + btnClientStreamRPC.Content + " Button"); await SendRequestsAsync(); LogInfo("Click " + btnClientStreamRPC.Content + " Button Done ...");
} private async void BtnServerStreamRPC_Click(object sender, RoutedEventArgs e)
{
LogInfo("Click " + btnServerStreamRPC.Content + " Button"); if (pictureList.Count > 0)
{
await ListResponsesAsync("Server-streaming", ImageToByteString(pictureList[0]));
}
else
{
MessageBox.Show("Please select the image firstly", "Warning");
}
LogInfo("Click " + btnServerStreamRPC.Content + " Button Done ...");
} //Calling async method on button click: https://stackoverflow.com/questions/28601678/calling-async-method-on-button-click
private async void BtnBidirectionalStreamRPC_Click(object sender, RoutedEventArgs e)
{
LogInfo("Click " + btnBidirectionalStreamRPC.Content + " Button"); await BidirectionalStreamAsync(); LogInfo("Click " + btnBidirectionalStreamRPC.Content + " Button Done ...");
} public async Task SendRequestsAsync()
{
try
{
using (var call = client.ClientStreamingMethod())
{
var rand = new Random();
foreach (var item in pictureList)
{
await call.RequestStream.WriteAsync(new StreamRequest
{
Message = item,
RequestData = ImageToByteString(item)
});
await Task.Delay(rand.Next(200));
}
await call.RequestStream.CompleteAsync(); var response = await call.ResponseAsync;
LogInfo("Recv client streaming response: " + response.Message);
}
}
catch (RpcException ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); }
} public async Task ListResponsesAsync(string user, ByteString str)
{
try
{
Request request = new Request
{
Name = user,
Photo = str
}; using (var call = client.GetReturnStream(request))
{
var responseStream = call.ResponseStream;
StringBuilder responseLog = new StringBuilder("Recv server stream response: ");
while (await responseStream.MoveNext())
{
var reply = responseStream.Current;
responseLog.Append(reply.Description);
}
LogInfo(responseLog.ToString());
}
}
catch (RpcException ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); }
} public async Task BidirectionalStreamAsync()
{
try
{
StringBuilder responseLog = new StringBuilder("Recv bidirectional stream response: ");
using (var call = client.BidirectionalStreamingMethod())
{
var responseTask = Task.Run(async () =>
{
var responseStream = call.ResponseStream;
while (await responseStream.MoveNext())
{
var reply = responseStream.Current;
responseLog.Append(reply.Description + ", " + responseStream.Current.ResponseData.Length.ToString() + " bytes. ");
//Whenever you update your UI elements from a thread other than the main thread, use below method:
this.Dispatcher.Invoke(() =>
{
LogInfo(responseLog.ToString());
});
}
}); var rand = new Random();
foreach (var item in pictureList)
{
await call.RequestStream.WriteAsync(new StreamRequest
{
Message = item,
RequestData = ImageToByteString(item)
});
await Task.Delay(rand.Next(200));
}
await call.RequestStream.CompleteAsync();
await responseTask;
}
}
catch (RpcException ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); }
} private void BtnUpload_Click(object sender, RoutedEventArgs e)
{
try
{
var openFileDialog = new Microsoft.Win32.OpenFileDialog()
{
Filter = "All Image Files|*.bmp;*.ico;*.gif;*.jpeg;*.jpg;*.png;*.tif;*.tiff|" +
"Windows Bitmap(*.bmp)|*.bmp|Windows Icon(*.ico)|*.ico|" +
"Graphics Interchange Format (*.gif)|(*.gif)|" +
"JPEG File Interchange Format (*.jpg)|*.jpg;*.jpeg|" +
"Portable Network Graphics (*.png)|*.png|" +
"Tag Image File Format (*.tif)|*.tif;*.tiff"
};
openFileDialog.Multiselect = true;
var result = openFileDialog.ShowDialog();
pictureList.Clear();
if (result == true)
{
foreach (string filename in openFileDialog.FileNames)
pictureList.Add(filename);
LogInfo("Selected " + pictureList.Count + " images!");
if (pictureList.Count > 0)
{
titleLbl.Content = "Raw Image:";
var img = GetImageSource(pictureList[0]);
imgOutput.Source = img;
imgSizeLbl.Content = ShowImageSize((img as BitmapSource).PixelWidth, (img as BitmapSource).PixelHeight);
}
}
else
{
LogInfo("Selected 0 images!");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
} private void MenuItem_Clear_Click(object sender, RoutedEventArgs e)
{
RcInfo.Text = "";
} private void MenuItem_Copy_Click(object sender, RoutedEventArgs e)
{
Clipboard.SetText(RcInfo.Text);
} private string ShowImageSize(int width, int height)
{
return "Image Size:" + width.ToString() + "x" + height.ToString();
} private void BtnSRImgCase_Click(object sender, RoutedEventArgs e)
{
LogInfo("Click " + btnSRImgCase.Content + " Button");
try
{
for (int i = 0; i < pictureList.Count; i++)
{
ByteString str = ImageToByteString(pictureList[i]);
var reply = client.GetSRImg(new RequestSRImg { ScaleFactor = 4, LRImg = str });
var return_img = reply.Img;
byte[] bytes = return_img.ToByteArray();
BitmapImage bitmap = BytesToBitmapImage(bytes);
titleLbl.Content = "X4 Super-resolved Image:";
imgOutput.Source = bitmap;
imgSizeLbl.Content = ShowImageSize(bitmap.PixelWidth, bitmap.PixelHeight);
LogInfo("Completed the image super-resolution request processing");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
} LogInfo("Click " + btnSRImgCase.Content + " Button Done ...");
}
} }
参考:
https://www.xenonstack.com/insights/what-is-grpc/
gRPC应用实践的更多相关文章
- Asp.Net Core Grpc 入门实践
Grpc简介 gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架. 在 gRPC 中,客户端应用程序可以直接调用不同计算机上的服务器应用程序上的方法,就像它是本地对象一样,从而更轻松地创 ...
- 一款不错的 Go Server/API boilerplate,使用 K8S+DDD+CQRS+ES+gRPC 最佳实践构建
Golang API Starter Kit 该项目的主要目的是使用最佳实践.DDD.CQRS.ES.gRPC 提供样板项目设置. 为开发和生产环境提供 kubernetes 配置.允许与反映生产的 ...
- protobuffer、gRPC、restful gRPC的相互转化
转自:https://studygolang.com/articles/12510 文档 grpc中文文档 grpc-gateway,restful和grpc转换库 protobuf 官网 proto ...
- gRPC应用C++
1. gRPC简述 RPC,远程方法调用,就是像调用本地方法一样调用远程方法. gRPC是Google实现的一种RPC框架,基于HTTP/2标准设计,带来诸如双向流.流控.头部压缩.单 TCP 连接 ...
- python3和grpc的微服务探索实践
对于微服务的实践,一般都是基于Java和Golang的,博主最近研究了下基于Python的微服务实践,现在通过一个简单的服务来分析Python技术栈的微服务实践 技术栈:Python3 + grpc ...
- Laravel + go-micro + grpc 实践基于 Zipkin 的分布式链路追踪系统 摘自https://mp.weixin.qq.com/s/JkLMNabnYbod-b4syMB3Hw?
分布式调用链跟踪系统,属于监控系统的一类.系统架构逐步演进时,后期形态往往是一个平台由很多不同的服务.组件构成,用户请求过来后,可能会经过其中多个服务,如图 不过,出问题时往往很难排查,如整个请求变慢 ...
- 漫谈grpc 3:从实践到原理,带你参透 gRPC
原文链接:万字长文 | 从实践到原理,带你参透 gRPC 大家好,我是煎鱼. gRPC 在 Go 语言中大放异彩,越来越多的小伙伴在使用,最近也在公司安利了一波,希望这一篇文章能带你一览 gRPC ...
- GRPC在NET上的实践(记录篇)
GRPC是什么? GRPC是一个开源RPC框架,于2015年3月开源,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于Protobuf 3.0(Protocol Buffer ...
- 从实践到原理,带你参透 gRPC
gRPC 在 Go 语言中大放异彩,越来越多的小伙伴在使用,最近也在公司安利了一波,希望这一篇文章能带你一览 gRPC 的巧妙之处,本文篇幅比较长,请做好阅读准备.本文目录如下: 简述 gRPC 是一 ...
随机推荐
- python中的FQA (python 学习篇 1)
Q:1. " if __name__=='__main__' " 这句是什么意思,可以不加吗? A: 如果单独运行该文件,则该模块的内容会被执行: 若运行的文件引用该 ...
- ubuntu上安装及使用docker
关于Docker: Docker 是一个开源项目,为开发者和系统管理员提供了一个开放的平台,在任何地方通过打包和运行应用程序作为一个轻量级的容器.Docker 在软件容器内自动部署应用程序.Docke ...
- C Looooops
看了半天的同余 扩展欧几里得 练练手 C Looooops Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 27079 A ...
- Tomcat中的Host和Engine级别的servlet容器
这边文章主要介绍的是Host容器 和 Engine容器.如果你想在同一个Tomcat上部署运行多个Context容器的话,你就需要使用Host容器,从理论上来讲,如果你的Tomcat只想要部署一个Co ...
- 版本控制工具SVN学习
教学视频链接:https://edu.aliyun.com/course/83?spm=5176.10731334.0.0.778e6580zC0Ri0 版本控制工具SVN学习 1,SVN的简介 在实 ...
- 分库分布的几件小事(四)分库分表的id主键生成
1.问题 其实这是分库分表之后你必然要面对的一个问题,就是id咋生成?因为要是分成多个表之后,每个表都是从1开始累加,那肯定不对啊,需要一个全局唯一的id来支持.所以这都是你实际生产环境中必须考虑的问 ...
- Redis总结2
一.Redis效率高的原因 众所周知,Redis常用来做缓存,从而提高项目QPS(每秒查询率).QPS = 并发量 / 平均响应时间 然而其效率高的原因包含但不仅限于如下几点: 1.Redis基于内存 ...
- python之字典二 内置方法总结
Python字典包含了以下内置方法: clear()函数用于删除字典内所有元素 dict1 = {, 'Class': 'First'} print('the start len %d' % len( ...
- C++虚函数和纯虚函数的用法和区别
C++虚函数与纯虚函数用法与区别(转) 1. 虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能 ...
- 第十章、typing模块
目录 第十章.typing模块 一.说明 二.typing模块的作用 三.使用typing模块 四.typing常用类型 第十章.typing模块 一.说明 二.typing模块的作用 类型检查,防止 ...