Slide 1

Slide 1 text

grpc-swiftΛ࢖ͬͯ iOSΞϓϦͰ΋շదͳ gRPC௨৴Λߦ͏ɹɹ 2018/08/31 iOSDC

Slide 2

Slide 2 text

About Me ҏ౻ɹګฏ Github : KyoheiG3 Twitter : @KyoheiG3

Slide 3

Slide 3 text

About Me ΩϡϨʔγϣϯ Ameba OWND AbemaTV SUPERCHOICE ࠓ·ͨ৽ن

Slide 4

Slide 4 text

gPRC

Slide 5

Slide 5 text

gRPCͬͯԿʁ • Google͕࡞ͬͨRPCΛ࣮ݱ͢ΔͨΊͷϑϨʔϜϫʔΫ • ࣾ಺޲͚ͷStubbyΛɺ2015೥ʹΦʔϓϯԽͨ͠΋ͷ • HTTP2

Slide 6

Slide 6 text

RPCͬͯԿʁ • ϦϞʔτϓϩγʔδϟίʔϧͷུ • ϓϩάϥϜ͔ΒผͷΞυϨεۭؒʹ͋Δαϒϧʔνϯ΍खଓ ͖Λ࣮ߦ͢Δ͜ͱΛՄೳʹ͢Δٕज़

Slide 7

Slide 7 text

HTTP2 • SPDYΛج൫ʹ࡞Γग़͞Εͨ • SPDY΋Google੡

Slide 8

Slide 8 text

Մೳͳ௨৴ํࣜ

Slide 9

Slide 9 text

Unaryʢ୯ൃͷ௨৴ʣ • 1ͭͷϦΫΤετʹର͠ɺϨεϙϯεΛ1ͭฦ͢ • HTTP1ܥͱಉ͡Α͏ͳεςʔτϨεͳ௨৴ํࣜ

Slide 10

Slide 10 text

Server StreamingʢαʔόετϦʔϛϯάʣ • 1ͭͷϦΫΤετʹର͠ɺϨεϙϯεΛෳ਺ฦ͢ • αʔόpush ϙʔϦϯάʹஔ͖׵ΘΔ

Slide 11

Slide 11 text

Client StreamingʢΫϥΠΞϯτετϦʔϛϯάʣ • ෳ਺ͷϦΫΤετʹର͠ɺϨεϙϯεΛ1ͭฦ͢ • ϑΝΠϧΞοϓϩʔυͳͲ

Slide 12

Slide 12 text

Bi-Directional Streamingʢ૒ํ޲ετϦʔϛϯάʣ • ෳ਺ͷϦΫΤετʹର͠ɺϨεϙϯεΛෳ਺ฦ͢ • νϟοτͳͲ

Slide 13

Slide 13 text

Αࣖ͘ʹ͢ΔProtocol Buffersͱͷҧ͍ͬͯʁ • Protocol Buffers͸௨৴͢Δσʔλͷܗࣜʢjson, xml...ʣ • gRPC͸௨৴ͷखஈ

Slide 14

Slide 14 text

͢͜͠Protocol Buffersͷ͓͞Β͍

Slide 15

Slide 15 text

protobuf • protobufͱུ͞ΕΔ͜ͱ͕ଟ͍ • όΠφϦϕʔε • .protoϑΝΠϧͰσʔλܗࣜΛఆٛ

Slide 16

Slide 16 text

echo.proto syntax = "proto3"; package echo; message EchoRequest { string text = 1; } message EchoResponse { string text = 1; }

Slide 17

Slide 17 text

protobuf • .protoϑΝΠϧ͔Βɺswift΍goϑΝΠϧΛ࡞੒Մೳ

Slide 18

Slide 18 text

protobuf $ protoc echo.proto --plugin=./protoc-gen-swift --swift_out=.

Slide 19

Slide 19 text

echo.pb.swift struct Echo_EchoRequest { var text: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() init() {} } struct Echo_EchoResponse { var text: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() init() {} }

Slide 20

Slide 20 text

protobuf • swiftͰ΋Apple͕ެࣜʹαϙʔτ1 • όΠφϦͷγϦΞϥΠζɾσγϦΞϥΠζͷ଎౓͸ϥΠϒϥ Ϧʹґଘ • ܰͯ͘ܕ҆શ 1 https://github.com/apple/swift-protobuf

Slide 21

Slide 21 text

protobuf σϝϦοτ • ௨৴σʔλͷՄಡੑ͕௿͗͢Δʢͱ͍͏͔ಡΊͳ͍ʣ • ಋೖʹҰखؒඞཁ

Slide 22

Slide 22 text

protobuf ϝϦοτ ׂѪ͠·͢ɻ ϝϦοτ͸ͨ͘͞Μ͋ΔͷͰɺσʔλܗࣜͷબ୒ࢶͷҰͭʹೖ ΕΔͷ͸͋Γͩͱࢥ͍·͢ɻ

Slide 23

Slide 23 text

gRPC × protobuf • gRPC͕ར༻͢ΔσϑΥϧτͷσʔλܗ͕ࣜprotobuf • gRPC͸͋͘·Ͱ௨৴ͷखஈͳͷͰɺσʔλܗࣜ͸ҰԠjsonͱ ͔΋Մೳ • ͚ͲɺprotobufͱgRPC͸ζϒζϒͳͷͰҰॹʹ࢖͏લఏ

Slide 24

Slide 24 text

SwiftGRPC

Slide 25

Slide 25 text

SwiftGRPC • objective-cͰ͸c++Ͱ࣮૷͞ΕͨgrpcΛར༻2 • swiftͰར༻͢Δʹ͸গʑ໘౗ 2 https://github.com/grpc/grpc

Slide 26

Slide 26 text

SwiftGRPC • grpcͷΠϯλʔϑΣʔεΛswiftͰϥοϓ

Slide 27

Slide 27 text

Officially Supported Platforms3 C/C++, C#, Dart *, Go, Java, Node.js, PHP *, Python, Ruby * ͸·ͩbeta Swift͸ΞϯΦϑΟγϟϧͰ͔͢?! 3 https://grpc.io/about/#osp

Slide 28

Slide 28 text

ಋೖ

Slide 29

Slide 29 text

ಋೖ • ͱʹ͔͘protobufલఏ • SwiftGRPCͷprotocϓϥάΠϯΛ࡞੒ • .protoϑΝΠϧ͔Β.swiftϑΝΠϧΛ࡞੒

Slide 30

Slide 30 text

protobufΛΠϯετʔϧ $ brew install protobuf

Slide 31

Slide 31 text

SwiftGRPCͷprotocϓϥάΠϯΛ࡞੒ • grpc-swiftΛcloneͯ͠build $ git clone https://github.com/grpc/grpc-swift.git $ cd grpc-swift $ make all > .protoc-gen-swift .protoc-gen-swiftgrpc

Slide 32

Slide 32 text

ඞཁʹԠͯ͡pathΛ௨͢ • ࣮ߦ࣌ʹϓϥάΠϯͷpathΛࢦఆͰ͖ΔͷͰඞਢͰ͸ͳ͍ $ mkdir ~/.protoc $ cp ./protoc-gen-swift ./protoc-gen-swiftgrpc ~/.protoc $ echo 'export PATH=$PATH:$HOME/.protoc' >> ~/.bash_profile $ source ~/.bash_profile

Slide 33

Slide 33 text

protoc-gen-swiftgrpc • SwiftGRPCͷprotocϓϥάΠϯ • protobufͱಉ༷ʹ.proto͔Β.swiftΛ࡞੒

Slide 34

Slide 34 text

echo.protoʢbeforeʣ syntax = "proto3"; package echo; message EchoRequest { string text = 1; } message EchoResponse { string text = 1; }

Slide 35

Slide 35 text

echo.protoʢserviceʣ • serviceͷதʹɺrpcϑΟʔϧυΛఆٛ͢Δ service Echo { rpc }

Slide 36

Slide 36 text

echo.protoʢserviceʣ • rpcϑΟʔϧυʹ೚ҙͷϝιουΛఆٛ͢Δ service Echo { rpc Get }

Slide 37

Slide 37 text

echo.protoʢserviceʣ • ϦΫΤετͷΦϒδΣΫτΛఆٛ͢Δ service Echo { rpc Get(EchoRequest) }

Slide 38

Slide 38 text

echo.protoʢserviceʣ • ϨεϙϯεͷΦϒδΣΫτΛఆٛ͢Δ service Echo { rpc Get(EchoRequest) returns(EchoResponse) {} }

Slide 39

Slide 39 text

echo.protoʢserviceʣ • stream͸ෳ਺ճͷॲཧΛՄೳʹ͢Δఆٛ service Echo { rpc Get(EchoRequest) returns (EchoResponse) {} rpc Expand(EchoRequest) returns (stream EchoResponse) {} rpc Collect(stream EchoRequest) returns (EchoResponse) {} rpc Update(stream EchoRequest) returns (stream EchoResponse) {} }

Slide 40

Slide 40 text

echo.protoʢafterʣ syntax = "proto3"; package echo; message EchoRequest { string text = 1; } message EchoResponse { string text = 1; } service Echo { rpc Get(EchoRequest) returns (EchoResponse) {} rpc Expand(EchoRequest) returns (stream EchoResponse) {} rpc Collect(stream EchoRequest) returns (EchoResponse) {} rpc Update(stream EchoRequest) returns (stream EchoResponse) {} }

Slide 41

Slide 41 text

protoc-gen-swiftgrpc $ protoc echo.proto --plugin=./protoc-gen-swiftgrpc --swiftgrpc_out=.

Slide 42

Slide 42 text

echo.grpc.swift protocol Echo_EchoProvider: ServiceProvider { func get(request: Echo_EchoRequest, session: Echo_EchoGetSession) throws -> Echo_EchoResponse func expand(request: Echo_EchoRequest, session: Echo_EchoExpandSession) throws -> ServerStatus? func collect(session: Echo_EchoCollectSession) throws -> Echo_EchoResponse? func update(session: Echo_EchoUpdateSession) throws -> ServerStatus? } protocol Echo_EchoService: ServiceClient { func get(_ request: Echo_EchoRequest) throws -> Echo_EchoResponse func get(_ request: Echo_EchoRequest, completion: @escaping (Echo_EchoResponse?, CallResult) -> Void) throws -> Echo_EchoGetCall func expand(_ request: Echo_EchoRequest, completion: ((CallResult) -> Void)?) throws -> Echo_EchoExpandCall func collect(completion: ((CallResult) -> Void)?) throws -> Echo_EchoCollectCall func update(completion: ((CallResult) -> Void)?) throws -> Echo_EchoUpdateCall }

Slide 43

Slide 43 text

EchoʢαϯϓϧʣͰίʔυղઆ

Slide 44

Slide 44 text

Echoͷಈ͔͠ํ • SwiftGRPC.xcodeprojΛ࡞੒͢Δ $ make project

Slide 45

Slide 45 text

Echoͷಈ͔͠ํ • ./third_party/swift-protobuf͕ඞཁ • ࠷ۙऔಘεΫϦϓτ͕࡟আ͞Εͨ4 $ mkdir third_party $ cd third_party $ git clone https://github.com/apple/swift-protobuf.git 4 https://github.com/grpc/grpc-swift/pull/296

Slide 46

Slide 46 text

Echoͷಈ͔͠ํ • ./Examples/EchoXcode/Echo.xcodeprojΛ։͘

Slide 47

Slide 47 text

Echoͷಈ͔͠ํ • ./SwiftGRPC.xcodeprojΛϓϩδΣΫτʹ௥Ճ͢Δ

Slide 48

Slide 48 text

Echoͷಈ͔͠ํ • Target DependenciesʹSwiftGRPC.frameworkΛ௥Ճ͢Δ

Slide 49

Slide 49 text

Echo for Client

Slide 50

Slide 50 text

Echo_EchoServiceClient • ΫϥΠΞϯτ͔Βαʔό΁ͷ઀ଓΛ؅ཧ • ಺෦ͰChannelΦϒδΣΫτΛอ࣋ • Echo_EchoServiceʹ४ڌ • get, expand, collect, updateϝιουΛ࣮ߦͯ͠ɺετ ϦʔϛϯάΛߦ͏ͨΊͷCallΦϒδΣΫτΛ࡞੒

Slide 51

Slide 51 text

Echo_EchoServiceClient let service = Echo_EchoServiceClient(address: "YOUR_ADDRESS", secure: false)

Slide 52

Slide 52 text

Echo_EchoExpandCallʢServerStreamingʣ • expand()ϝιουͰऔಘ • αʔόετϦʔϛϯά͕Մೳ • receive()ϝιουΛ࣮ߦՄೳ

Slide 53

Slide 53 text

Echo_EchoExpandCallʢServerStreamingʣ var requestMessage = Echo_EchoRequest() requestMessage.text = "message" let expandCall = try service.expand(requestMessage) { _ in } try expandCall.receive { response in // ϨεϙϯεΛड͚औΔ }

Slide 54

Slide 54 text

Echo_EchoCollectCallʢClientStreamingʣ • collect()ϝιουͰऔಘ • ΫϥΠΞϯτετϦʔϛϯά͕Մೳ • send(), closeAndReceive()ϝιουΛ࣮ߦՄೳ

Slide 55

Slide 55 text

Echo_EchoCollectCallʢClientStreamingʣ let collectCall = try service.collect { _ in } var requestMessage = Echo_EchoRequest() requestMessage.text = "message" try collectCall.send(requestMessage) { error in // ΤϥʔΛड͚औΔ } try collectCall.closeAndReceive { response in // close࣌ʹ1౓͚ͩσʔλΛड͚औΕΔ }

Slide 56

Slide 56 text

Echo_EchoUpdateCallʢBidirectionalStreamingʣ • update()ϝιουͰऔಘ • ૒ํ޲ετϦʔϛϯά͕Մೳ • send(), receive(), closeSend()ϝιουΛ࣮ߦՄೳ

Slide 57

Slide 57 text

Echo_EchoUpdateCallʢBidirectionalStreamingʣ let updateCall = try service.update { _ in } try updateCall.receive { response in // ϨεϙϯεΛड͚औΔ } var requestMessage = Echo_EchoRequest() requestMessage.text = "message" try updateCall.send(requestMessage) { error in // ΤϥʔΛड͚औΔ } try updateCall.closeSend {}

Slide 58

Slide 58 text

ΫϥΠΞϯτ࣮૷ͷجຊతͳྲྀΕ • Service͔ΒCallΦϒδΣΫτΛ࡞੒͢Δ • ࡞੒ͨ͠CallΦϒδΣΫτʹ༻ҙ͞Ε͍ͯΔstreamingͷϝ ιουΛݺͼग़͢ • CallΦϒδΣΫτ͸streaming͕ऴྃ͢Δ·Ͱอ͓࣋ͯ͘͠ ※ Unary͸ྫ֎

Slide 59

Slide 59 text

Echo_EchoGetCallʢUnaryʣ • get()ϝιουͰऔಘ • streaming༻ͷϝιου͸༻ҙ͞Ε͍ͯͳ͍

Slide 60

Slide 60 text

Echo_EchoGetCallʢUnaryʣ var requestMessage = Echo_EchoRequest() requestMessage.text = "message" let getCall = try service.get(requestMessage) { response, callResult in }

Slide 61

Slide 61 text

ΫϥΠΞϯτ࣮૷ͷಛ௃ • CallΦϒδΣΫτΛ࡞ͬͨ࣌఺͔ΒtimeoutΧ΢ϯτμ΢ϯ • σϑΥϧτ͸600ඵͰɺϦΫΤετݸผʹ͸ઃఆෆՄ • ClientStreamingͷsend()ʹҾ਺Ͱ౉ͤΔtimeout͸ɺಉظ ௨৴͢Δࡍͷsemaphoreͷ଴ػ࣌ؒ

Slide 62

Slide 62 text

ΫϥΠΞϯτ࣮૷ͷಛ௃ • receive()͸ɺ1౓Ͱ΋σʔλΛड͚औΔͱͦΕҎ࣮߱ߦ͞ Εͳ͍ͷͰɺ࠶౓receive()ΛݺͿඞཁ͕͋Δ • ͳʹ͔͠ΒͷΤϥʔ͕ग़ͨΒͦΕҎ߱ૹड৴Ͱ͖ͳ͍ͷͰɺ CallΦϒδΣΫτ͔Β࠶౓࡞੒͢Δඞཁ͕͋Δ • throws͕΍ͨΒଟ͍

Slide 63

Slide 63 text

ΫϥΠΞϯτͷstubʹ͍ͭͯ • ServerΛϩʔΧϧʹ࣮૷ͯ͠stubԽ

Slide 64

Slide 64 text

Echo for Server

Slide 65

Slide 65 text

ServiceServer • αʔό΁ͷΫϥΠΞϯτ͔Βͷ઀ଓΛ؅ཧ • ಺෦ͰServerΦϒδΣΫτΛอ࣋ • Echo_EchoProviderʹ४ڌͨ͠ΦϒδΣΫτΛड͚औΔ • startϝιουͰɺ઀ଓͷड৴Λ։࢝

Slide 66

Slide 66 text

ServiceServer class EchoProvider: Echo_EchoProvider { func get() {} func expand() {} func collect() {} func update() {} } let provider = EchoProvider() let server = ServiceServer(address: "YOUR_ADDRESS", serviceProviders: [provider]) server.start()

Slide 67

Slide 67 text

Echo_EchoProvider • get, expand, collect, updateϝιου಺ͰɺΫϥΠΞϯ τ΁ͷϨεϙϯεΛߦ͏ • Ҿ਺Ͱ౉͞ΕΔSessionΦϒδΣΫτͰৼΔ෣͍Λ࣮૷

Slide 68

Slide 68 text

Echo_EchoProviderʢget == Unaryʣ • Echo_EchoRequestΦϒδΣΫτΛऔಘ • Echo_EchoResponseΛฦͨ͠Βऴྃ

Slide 69

Slide 69 text

Echo_EchoProviderʢget == Unaryʣ func get(request: Echo_EchoRequest, session _: Echo_EchoGetSession) throws -> Echo_EchoResponse { var response = Echo_EchoResponse() response.text = request.text + " response" return response }

Slide 70

Slide 70 text

Echo_EchoProviderʢexpand == ServerStreamingʣ • Echo_EchoRequestΦϒδΣΫτΛऔಘ • ඞཁʹԠͯ͡sessionͷsend()Λ࣮ߦ • ServerStatusΛฦ͠ɺ͔ͭsend()͕׬ྃͨ͠Βऴྃ

Slide 71

Slide 71 text

Echo_EchoProviderʢexpand == ServerStreamingʣ func expand(request: Echo_EchoRequest, session: Echo_EchoExpandSession) throws -> ServerStatus? { var response = Echo_EchoResponse() response.text = request.text + " response" try! session.send(response) { // ΤϥʔΛड͚औΔ } return .ok }

Slide 72

Slide 72 text

Echo_EchoProviderʢcollect == ClientStreamingʣ • ϦΫΤετ͸sessionͷreceive()Ͱऔಘ • Echo_EchoResponseΛฦͨ͠Βऴྃ

Slide 73

Slide 73 text

Echo_EchoProviderʢcollect == ClientStreamingʣ func collect(session: Echo_EchoCollectSession) throws -> Echo_EchoResponse? { var response = Echo_EchoResponse() let request = try! session.receive() response.text = request.text + " response" return response }

Slide 74

Slide 74 text

Echo_EchoProviderʢupdate == BidiStreamingʣ • ϦΫΤετ͸sessionͷreceive()Ͱऔಘ • ඞཁʹԠͯ͡sessionͷsend()Λ࣮ߦ • ServerStatusΛฦ͠ɺ͔ͭsend()͕׬ྃͨ͠Βऴྃ

Slide 75

Slide 75 text

Echo_EchoProviderʢupdate == BidiStreamingʣ func update(session: Echo_EchoUpdateSession) throws -> ServerStatus? { let request = try! session.receive() var response = Echo_EchoResponse() response.text = request.text + " response" try! session.send(response) { // ΤϥʔΛड͚औΔ } return .ok }

Slide 76

Slide 76 text

αʔό࣮૷ͷجຊతͳྲྀΕ • Echo_EchoProviderʹ४ڌͨ͠ΦϒδΣΫτΛ࡞੒͢Δ • ProviderͷϝιουͰಉظతʹॲཧ͠ɺ஋Λฦͯ͠ऴྃ

Slide 77

Slide 77 text

SwiftGRPCClient

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

SwiftGRPCClient > https://github.com/cats-oss/grpc-swift-client • ΫϥΠΞϯτઐ༻ͷgRPCϥΠϒϥϦ • SwiftGRPCʹґଘ • timeoutΛϦΫΤετ୯ҐͰܾΊͨΓ • ϦΫΤετͷ్தͰॲཧΛΠϯλʔηϓτͨ͠Γ • receive()ΛࣗಈͰϦτϥΠͯ͘͠ΕͨΓ

Slide 80

Slide 80 text

protoc-gen-swiftgrpc-client $ protoc echo.proto --plugin=./protoc-gen-swiftgrpc-client --swiftgrpc-client_out=.

Slide 81

Slide 81 text

protoc-gen-swiftgrpc-client enum Echo_EchoMethod: String, CallMethod { case get = "Get" static let service = "echo.Echo" } protocol _Echo_EchoGetRequest { typealias InputType = Echo_EchoRequest typealias OutputType = Echo_EchoResponse } protocol Echo_EchoGetRequest: _Echo_EchoGetRequest, UnaryStreamingRequest {} extension Echo_EchoGetRequest { var method: CallMethod { return Echo_EchoMethod.get } }

Slide 82

Slide 82 text

SwiftGRPCʢClientStreamingʣ let collectCall = try service.collect { _ in } var requestMessage = Echo_EchoRequest() requestMessage.text = "message" try collectCall.send(requestMessage) { error in // ΤϥʔΛड͚औΔ } try collectCall.closeAndReceive { response in // close࣌ʹ1౓͚ͩσʔλΛड͚औΕΔ }

Slide 83

Slide 83 text

SwiftGRPCClientʢClientStreamingʣ let stream = Session.shared.stream(with: EchoClientRequest()) stream .send("message") { response in // ૹ৴݁ՌΛड͚औΔ } stream .closeAndReceive { response in // close࣌ʹ1౓͚ͩσʔλΛड͚औΕΔ }

Slide 84

Slide 84 text

SwiftGRPCʢServerStreamingʣ var requestMessage = Echo_EchoRequest() requestMessage.text = "message" let expandCall = try service.expand(requestMessage) { _ in } try expandCall.receive { response in // ϨεϙϯεΛड͚औΔ }

Slide 85

Slide 85 text

SwiftGRPCClientʢServerStreamingʣ let stream = Session.shared.stream(with: EchoServerRequest(text: "message")) .receive { response in // ϨεϙϯεΛड͚औΔ }

Slide 86

Slide 86 text

SwiftGPRCClientʢRxʣ extension Reactive where Base: Streaming, Base.Request: UnaryRequest { func data() -> Observable { return .create { observer in self.base.data { result in switch result { case .success(let data): observer.onNext(data) observer.onCompleted() case .failure(let error): observer.onError(error) } } return Disposables.create { self.base.cancel() } } } }

Slide 87

Slide 87 text

SwiftGPRCClientʢRxʣ Session.shared.stream(with: EchoUnaryRequest()).rx.data() .subscribe(onNext: { response in // ϨεϙϯεΛड͚औΔ }, onError: { error in // ΤϥʔΛड͚औΔ }) .disposed(by: disposeBag)

Slide 88

Slide 88 text

SwiftGPRCClient • ΫϥΠΞϯτʹSwiftGPRCClientΛར༻ͯ͠ɺstubͱͯ͠ SwiftGRPCͷαʔό࣮૷Λར༻͢Δ

Slide 89

Slide 89 text

ॴײͱࠓޙ

Slide 90

Slide 90 text

ॴײ • ࠷ॳ͸callͷcancel͢Βແ͔ͬͨ • ಋೖ͕ਏ͔ͬͨ • Τϥʔ࣌ͷࡉ͔͍ڍಈ͕௫ΈͮΒ͍

Slide 91

Slide 91 text

Swift Summit

Slide 92

Slide 92 text

ࠓޙ • ࠷ۙ͸υΠπਓͷΤϯδχΞ͕ฃಆͯ͠ϝϯςφϯε • Network.frameworkͷಋೖͷݕ౼ͳͲ͕͞Ε͍ͯΔ • ͱΓ͋͑ͣਖ਼ࣜʹgrpcҰ଒ʹೖͬͯཉ͍͠

Slide 93

Slide 93 text

grpc-swiftΛ࢖ͬͯiOSΞϓϦͰ΋շదͳgRPC௨৴Λߦ͏ɹɹ https://github.com/cats-oss/grpc-swift-client Github : KyoheiG3 Twitter : @KyoheiG3

Slide 94

Slide 94 text

Thanks!