Compare commits
37
Commits
daf90f07d9
...
b638a1b9fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b638a1b9fd | ||
|
|
db35f38c05 | ||
|
|
50f63633ea | ||
|
|
d90553ecdf | ||
|
|
600a950806 | ||
|
|
9de3f5144d | ||
|
|
d83fe114d0 | ||
|
|
20a818520c | ||
|
|
8bb11fe205 | ||
|
|
8f183d597e | ||
|
|
27d5b59106 | ||
|
|
fe913d7870 | ||
|
|
558f17eeed | ||
|
|
4697f7e069 | ||
|
|
b193602d1e | ||
|
|
d1e09a3179 | ||
|
|
cfefef5478 | ||
|
|
78d29b033e | ||
|
|
d35620bae2 | ||
|
|
62a50dd760 | ||
|
|
044f0e4d42 | ||
|
|
5408a70118 | ||
|
|
243262bc72 | ||
|
|
f9e86885fc | ||
|
|
f9a56dda6d | ||
|
|
ba643fe013 | ||
|
|
ef174799a5 | ||
|
|
b2304aa53e | ||
|
|
ee63765be0 | ||
|
|
902af18555 | ||
|
|
50edea0225 | ||
|
|
89580a829c | ||
|
|
1431e7394a | ||
|
|
9794a57d77 | ||
|
|
c8ed4742d0 | ||
|
|
f3c003fdf3 | ||
|
|
ab594e4391 |
@@ -13,3 +13,6 @@ update.sh
|
||||
|
||||
# Workspace-specific scripts
|
||||
/resume_all.sh
|
||||
|
||||
# Local downloaded binary tools
|
||||
/bin/
|
||||
|
||||
@@ -25,24 +25,29 @@
|
||||
grpccanary/
|
||||
├── README.md # 본 프로젝트 종합 소개 및 실행 가이드 (교육용)
|
||||
├── go.mod / go.sum # Go 모듈 의존성 정의 (Go 1.25.4+)
|
||||
├── protoapi.proto # gRPC 인터페이스 정의서 (IDL)
|
||||
├── protoapi/ # protoc로 컴파일 생성된 Go Stub 코드
|
||||
├── examples/
|
||||
├── lib/
|
||||
│ ├── main.go # 학습 예제 통합 실행 진입점 (수동 전환)
|
||||
│ ├── jsonexample/ # [1단계] encoding/json 표준 직렬화 예제
|
||||
│ ├── httpentity/ # [2단계] Gin-gonic 기반 HTTP API 서버 (WIP)
|
||||
│ └── grpcentity/ # [3단계] gRPC 서비스 구현체 (서버/클라이언트 데모)
|
||||
│ ├── jsonexample/ # 1단계: JSON 데이터 다루기 실습 예제
|
||||
│ ├── httpentity/ # 2단계: HTTP & Gin 웹 서버 실습 예제 (WIP)
|
||||
│ └── grpc/
|
||||
│ └── basic/ # 3단계: gRPC 통신 구현 실습 예제 (서버/클라이언트 데모)
|
||||
│ ├── protoapi.proto # gRPC 인터페이스 정의서 (IDL)
|
||||
│ └── protoapi/ # protoc로 컴파일 생성된 Go Stub 코드
|
||||
└── docs/
|
||||
└── MANUSCRIPT.md # JSON, HTTP, gRPC에 대한 통합 상세 개념서
|
||||
├── MANUSCRIPT.md # 학습 로드맵 및 각 단계별 심화 가이드로의 안내서
|
||||
├── JSON.md # 1단계: JSON 데이터 다루기 상세 가이드
|
||||
├── HTTP.md # 2단계: HTTP & Gin 웹 서버 상세 가이드
|
||||
└── GRPC.md # 3단계: gRPC 통신 구현 상세 가이드
|
||||
```
|
||||
|
||||
### 1단계: JSON 데이터 다루기 (`examples/jsonexample`)
|
||||
### 1단계: JSON 데이터 다루기 (`lib/jsonexample`)
|
||||
* Go 표준 라이브러리인 `encoding/json`을 활용하여 구조체(Struct)와 JSON 데이터 간의 마샬링(Serialization) 및 언마샬링(Deserialization) 기법을 학습합니다.
|
||||
|
||||
### 2단계: Gin 기반 HTTP 웹 서버 (`examples/httpentity`)
|
||||
### 2단계: HTTP & Gin 웹 서버 (`lib/httpentity`)
|
||||
* 대중적인 Go 웹 프레임워크 `gin-gonic`을 활용해 RESTful API 사양을 구축하는 방법을 이해합니다. (현재 주석 해제 후 실습하도록 설계된 Work-in-Progress 단계)
|
||||
|
||||
### 3단계: gRPC 통신 구현 (`examples/grpcentity`)
|
||||
### 3단계: gRPC 통신 구현 (`lib/grpc/basic`)
|
||||
* `.proto` 정의를 바탕으로 통신 스키마 계약을 강제하고, Go 언어로 gRPC 서버를 띄워 클라이언트가 날짜/시간, 무작위 비밀번호 및 정수 데이터를 실시간 원격 호출로 송수신하는 분산 통신 기초를 학습합니다.
|
||||
* `.proto` 정의를 바탕으로 통신 스키마 계약을 강제하고, Go 언어로 gRPC 서버를 띄워 클라이언트가 날짜/시간, 무작위 비밀번호 및 정수 데이터를 실시간 원격 호출로 송수신하는 분산 통신 기초를 학습합니다.
|
||||
|
||||
---
|
||||
@@ -69,13 +74,13 @@ grpccanary/
|
||||
### 2. 실습 예제 실행 방법 (진입점 전환)
|
||||
이 프로젝트는 교육적 목적을 위해 **하나의 `main.go` 파일 안에서 주석 처리를 통해 학습 단계를 수동 전환**하여 실행하도록 설계되어 있습니다.
|
||||
|
||||
1. **[examples/main.go](examples/main.go)** 파일을 엽니다.
|
||||
1. **[lib/main.go](lib/main.go)** 파일을 엽니다.
|
||||
2. 아래와 같이 실행하고자 하는 예제의 주석을 해제하고 다른 예제는 주석 처리합니다.
|
||||
* *JSON 예제 실행 시*: `jsonexample.JsonParsingExample()` 활성화
|
||||
* *gRPC 예제 실행 시*: `grpcSample()` 활성화 (미사용 import 에러를 피하기 위해 `jsonexample` import는 주석 처리 필요)
|
||||
3. 루트 디렉토리에서 다음 명령어로 실행합니다:
|
||||
```bash
|
||||
go run ./examples
|
||||
go run ./lib
|
||||
```
|
||||
|
||||
### 3. gRPC 예제 동작 흐름 및 기대 출력
|
||||
@@ -83,15 +88,23 @@ gRPC 예제 실행 시, 서버가 백업 고루틴으로 `:8080` 포트에 대
|
||||
```text
|
||||
Serving requests...
|
||||
Client:
|
||||
Server Date and Time: 2026-07-12 20:00:00.123456789 +0900 KST
|
||||
Server Date and Time: 2026-07-12 20:00:00.123456789 +0900 KST m=+1.000000001
|
||||
Random Password: &c(D7f/G#s%d
|
||||
Random Integer 1: 42
|
||||
Random Integer 2: 87
|
||||
Received sensing data - Device: sensor-room-01, Temp: 24.50°C, Humid: 52.30%
|
||||
Sensing Update Success: true
|
||||
Sensing Update Message: Sensing data updated successfully for device sensor-room-01
|
||||
```
|
||||
|
||||
### 4. 트러블슈팅
|
||||
* **`module grpccanary: package ... is not in GOROOT`**:
|
||||
- 모듈 로드 에러가 발생한 경우, 리포지토리 루트 경로(작업 디렉토리)에서 명령어를 올바르게 실행했는지 재확인하고, `go clean -modcache` 후 `go mod tidy`를 수행해 보십시오.
|
||||
|
||||
---
|
||||
|
||||
## 📚 추가 학습 리소스
|
||||
|
||||
* **[docs/MANUSCRIPT.md](docs/MANUSCRIPT.md)**: JSON, HTTP, gRPC 통신의 원리와 배경지식(REST의 한계와 gRPC 도입 이유 등)을 담은 본 프로젝트 공식 개념서
|
||||
* **[examples/grpcentity/README.md](examples/grpcentity/README.md)**: Protobuf 빌드 컴파일 가이드 및 상세 소스코드 구현체 분석 자료
|
||||
* **[docs/MANUSCRIPT.md](docs/MANUSCRIPT.md)**: JSON, HTTP, gRPC 통신의 원리와 배경지식(REST의 한계와 gRPC 도입 이유 등)을 담은 학습 로드맵 및 가이드 안내서
|
||||
* **[docs/JSON.md](docs/JSON.md)**: 1단계 JSON 데이터 다루기 상세 학습 가이드
|
||||
* **[docs/HTTP.md](docs/HTTP.md)**: 2단계 HTTP & Gin 웹 프레임워크 상세 학습 가이드
|
||||
* **[docs/GRPC.md](docs/GRPC.md)**: 3단계 gRPC & Protobuf 상세 학습 가이드 (컴파일 절차 및 소스코드 구현체 분석 포함)
|
||||
* **[lib/grpc/basic/README.md](lib/grpc/basic/README.md)**: gRPC 실습 디렉토리 안내 및 `docs/GRPC.md` 심화 가이드로의 링크
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# AskingDateTime() gRPC 워크플로우 분석 보고서
|
||||
|
||||
> [!NOTE]
|
||||
> **심화/선택 학습 안내**
|
||||
> 이 문서는 3단계 기본 실습을 모두 성공적으로 마친 후, gRPC의 내부 동작 원리와 상세 네트워크 흐름을 깊이 탐구하고 싶은 분들을 위한 선택 학습 자료입니다.
|
||||
|
||||
본 문서는 클라이언트의 `AskingDateTime()` 함수 호출을 기점으로, gRPC 네트워크 채널을 거쳐 서버에서 실행 및 응답이 완료되는 엔드투엔드(End-to-End) 워크플로우를 기술적으로 분석하여 기록합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 동작 흐름 시퀀스 다이어그램 (Sequence Diagram)
|
||||
|
||||
다음 다이어그램은 호출이 시작되어 응답이 반환되기까지 각 구성요소 간의 패킷 전송 및 메서드 호출 흐름을 보여줍니다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant ClientApp as [클라이언트 앱] (client.go)
|
||||
participant ClientStub as [Go Client Stub] (protoapi_grpc.pb.go)
|
||||
participant ClientEngine as [gRPC Client Engine] (google.golang.org/grpc)
|
||||
participant Network as [네트워크 레이어] (TCP / HTTP/2)
|
||||
participant ServerEngine as [gRPC Server Engine] (google.golang.org/grpc)
|
||||
participant ServerStub as [Go Server Stub] (protoapi_grpc.pb.go)
|
||||
participant ServerApp as [서버 구현체] (server.go)
|
||||
|
||||
ClientApp->>ClientApp: AskingDateTime() 호출 실행
|
||||
Note over ClientApp: RequestDateTime 구조체 정의<br/>("Please send me the date and time")
|
||||
ClientApp->>ClientStub: IoTServiceClient.GetDate(ctx, request) 호출
|
||||
ClientStub->>ClientEngine: grpc.ClientConn.Invoke() 위임 (호출 식별자 전달)
|
||||
|
||||
Note over ClientEngine: Protobuf 직렬화 실행 (바이너리화)<br/>HTTP/2 스트림 개설 및 헤더 구성
|
||||
ClientEngine->>Network: HTTP/2 POST 전송<br/>Path: /protoapi.IoTService/GetDate
|
||||
|
||||
Network->>ServerEngine: TCP 패킷 수신 및 스트림 복원
|
||||
Note over ServerEngine: HTTP/2 프레임 파싱<br/>호출 타깃 확인 및 서비스 라우팅<br/>바이너리 패킷 역직렬화
|
||||
|
||||
ServerEngine->>ServerStub: _IoTService_GetDate_Handler() 트리거
|
||||
ServerStub->>ServerApp: IoTServiceServer.GetDate(ctx, request) 디스패치
|
||||
|
||||
Note over ServerApp: GetDate() 함수 연산 수행<br/>time.Now() 호출 및 DateTime 구조체 바인딩
|
||||
ServerApp-->>ServerStub: DateTime 응답 구조체 포인터 반환
|
||||
ServerStub-->>ServerEngine: 응답 데이터 전달
|
||||
|
||||
Note over ServerEngine: 응답 구조체 바이너리 직렬화<br/>HTTP/2 Response Headers 및 DATA 구성
|
||||
ServerEngine->>Network: HTTP/2 Response 전송
|
||||
|
||||
Network->>ClientEngine: TCP 패킷 수신 및 페이로드 분석
|
||||
Note over ClientEngine: 바이너리 패킷 역직렬화 (Go 구조체 복원)
|
||||
ClientEngine-->>ClientStub: Invoke() 완료 처리 및 결과 전달
|
||||
ClientStub-->>ClientApp: DateTime 반환 및 수신 출력
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 2. 각 단계별 상세 라이프사이클 분석
|
||||
|
||||
<details>
|
||||
<summary><b>🔍 2.1 ~ 2.6 단계별 상세 라이프사이클 분석 내용 펼쳐보기</b></summary>
|
||||
|
||||
### 2.1 클라이언트의 호출 개시 (Client-side Initiation)
|
||||
1. **함수 기동**: `client.go`에서 `AskingDateTime(ctx, client)` 함수가 동작을 시작합니다.
|
||||
2. **요청 메시지 바인딩**: 클라이언트는 호출에 전송할 매개변수인 `protoapi.RequestDateTime` 인스턴스를 메모리에 할당하고 요청 문자열(`Value: "Please send me the date and time"`)을 채워 넣습니다.
|
||||
3. **인터페이스 대리자 호출**: 클라이언트 인스턴스(`m`)의 `GetDate(ctx, request)` 인터페이스 메서드를 트리거합니다.
|
||||
|
||||
### 2.2 클라이언트 스터브 및 gRPC 엔진 처리 (Client-side Serialization & Transport)
|
||||
4. **스터브 메서드 실행**: `protoapi_grpc.pb.go` 내부에 컴파일된 `GetDate` 바인딩 구현체로 진입합니다.
|
||||
5. **호출 식별자 바인딩**: 스터브는 통신 채널에 대해 `grpc.ClientConn.Invoke(...)` 함수를 대리 기동하며, 대상 호출 식별자 경로인 `"/protoapi.IoTService/GetDate"` 와 요청 객체 주소를 주입합니다.
|
||||
6. **직렬화 및 HTTP/2 포장**:
|
||||
* gRPC 엔진은 주입된 `RequestDateTime` Go 구조체를 프로토콜 버퍼 직렬화 메커니즘을 경유하여 용량이 최소화된 바이너리 바이트 배열로 변환합니다.
|
||||
* 확보된 TCP 세션 커넥션에서 독립적인 **HTTP/2 스트림(Stream)**을 새로 개설하고, `POST /protoapi.IoTService/GetDate HTTP/2` 형식의 헤더 프레임과 직렬화된 데이터 바디 프레임을 순차 전송합니다.
|
||||
|
||||
### 2.3 네트워크 전송 (Network Layer)
|
||||
7. **이진 데이터 라우팅**: 데이터는 TCP 소켓 및 인터넷 통신망을 경유하여 `server.go`가 리스닝 포트(예: `:8080`)를 점유하고 기다리고 있는 대상 서버 주소로 이동합니다.
|
||||
|
||||
### 2.4 서버의 수신 및 역직렬화 (Server-side Dispatching & Deserialization)
|
||||
8. **리스너 캐치**: 서버의 TCP 리스너가 수신 포트로 유입된 소켓 접속 및 HTTP/2 스트림 유입을 인식합니다.
|
||||
9. **패킷 분석 및 분기**: gRPC 서버 라이브러리는 수신된 HTTP/2 경로(`/protoapi.IoTService/GetDate`)를 판별하여, 현재 기동 중인 서비스 중 `IoTService`의 `GetDate` 메서드로 패킷을 라우팅합니다.
|
||||
10. **역직렬화**: 이진 데이터 바이너리 페이로드를 꺼내어, 수신 측 메모리 구조체인 `protoapi.RequestDateTime` 객체로 역직렬화(Unmarshalling)를 진행합니다.
|
||||
11. **핸들러 전달**: 디바이스 정보가 담긴 구조체 포인터를 파라미터로 적재하여 `_IoTService_GetDate_Handler` 내부 라우터를 깨웁니다.
|
||||
|
||||
### 2.5 서버 비즈니스 로직 수행 (Server-side Execution)
|
||||
12. **핸들러 대리 실행**: `server.go` 내부의 `IoTServer` 구조체가 바인딩된 `GetDate(ctx, r)` 메서드가 호출됩니다.
|
||||
13. **연산 가동**:
|
||||
* `time.Now()`를 호출하여 서버의 현재 시간 값을 획득합니다.
|
||||
* 획득된 시간 값을 문자열로 변환하여 반환용 `protoapi.DateTime` 구조체에 대입합니다.
|
||||
14. **반환 값 전달**: 완성된 응답 구조체 메모리 주소를 `return response, nil`로 통신망 레이어에 토스합니다.
|
||||
|
||||
### 2.6 응답 회신 및 완료 (Response Delivery & Return)
|
||||
15. **서버 응답 전송**: gRPC 서버 엔진은 전달받은 `DateTime` Go 구조체를 바이너리로 압축(직렬화)한 후, 기존에 개설된 HTTP/2 응답 스트림의 DATA 프레임에 적재하여 송신 포트로 내보냅니다.
|
||||
16. **클라이언트 수신 및 전달**:
|
||||
* 클라이언트 gRPC 엔진이 소켓으로 수신된 바이너리 데이터를 해독하여 Go 언어의 `protoapi.DateTime` 구조체로 역직렬화합니다.
|
||||
* 대기 상태에 있던 `client.go` 내의 `AskingDateTime` 함수가 최종 반환값을 가로채어 복귀합니다.
|
||||
* 복귀된 값을 받아 화면(`Server Date and Time: [시간 문자열]`)에 출력하며 시퀀스가 종료됩니다.
|
||||
|
||||
</details>
|
||||
|
||||
+648
@@ -0,0 +1,648 @@
|
||||
# 3단계: gRPC 통신 구현 상세 가이드
|
||||
|
||||
⬅ [학습 로드맵으로 돌아가기](MANUSCRIPT.md)
|
||||
|
||||
이 문서는 `grpccanary` 프로젝트의 **3단계: gRPC 통신 구현**에 대한 이론적 배경, 스키마 명세, 컴파일 기법 및 구체적인 소스코드 분석을 설명합니다.
|
||||
|
||||
gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격 프로시저 호출(RPC) 시스템입니다. 사물인터넷(IoT) 장비나 에이전트 간의 데이터 통신 시 가볍고 구조화된 데이터 통신을 유지하는 데에 가장 적합한 프레임워크입니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. gRPC 개요 및 기술 배경
|
||||
|
||||
쉽게 말해 gRPC는 **다른 컴퓨터에 있는 함수를, 마치 내 코드 안에 있는 함수처럼 그냥 호출할 수 있게 해주는 기술**입니다. 평소 `sum(1, 2)`처럼 내 프로그램 안의 함수를 부르듯, `client.GetDate()`라고 쓰면 실제로는 저 멀리 다른 컴퓨터(서버)의 함수가 실행되고 그 결과를 돌려받는 것입니다. 이런 '원격에 있는 함수를 부르는 방식'을 원격 프로시저 호출(RPC)이라고 부릅니다.
|
||||
|
||||
### 1.1 gRPC의 핵심 차별점
|
||||
* **강력한 스키마 계약**: `.proto` 파일 하나로 서비스 통신 규약을 명확히 선언하고, 컴파일 단계에서 이를 바탕으로 여러 언어의 클라이언트/서버 코드를 자동 생성합니다. 따라서 런타임 단계에서의 통신 필드 누락이나 타입 불일치 버그를 완벽하게 방지합니다.
|
||||
* **이진 프로토콜 (바이너리 포맷)**: 텍스트가 아닌 컴팩트한 이진 형식을 사용하므로 데이터 크기가 매우 작고 네트워크 대역폭 리소스 효율이 뛰어납니다.
|
||||
* **HTTP/2 기반**: 하나의 네트워크 커넥션을 재사용해 다중화(Multiplexing) 전송이 가능하고, 실시간 스트리밍(양방향 스트리밍 포함) 서비스에 탁월한 환경을 제공합니다.
|
||||
|
||||
### 1.2 Protobuf (프로토콜 버퍼)의 장단점
|
||||
|
||||
* **장점**
|
||||
* **높은 전송 효율성**: 데이터 교환 시 텍스트가 아닌 바이너리 인코딩 형식을 사용하므로 JSON에 비해 직렬화/역직렬화 속도가 매우 빠르고 크기도 가볍습니다.
|
||||
* **일관성 있는 코드 생성 (Stub)**: 동일한 정의서로부터 다국어 API 클라이언트를 빌드하여 언어별 클라이언트 코드를 중복 작성해야 하는 오버헤드를 제거합니다.
|
||||
* **하위 호환성**: 고유 필드 번호 매핑 방식을 사용하므로 스키마가 개정되어도 이전 시스템과의 통신 호환을 보장합니다.
|
||||
|
||||
* **단점**
|
||||
* **가독성 부재**: 패킷이 암호화는 아니지만 바이너리로 전달되므로 사람이 브라우저 개발자 도구 등으로 바로 읽어 디버깅하기 곤란합니다.
|
||||
* **빌드 종속성**: 명세 변경 시마다 Stub 코드를 컴파일하여 빌드에 바인딩하는 과정이 요구됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 실습 프로젝트 소개: IoT 센싱 데이터 수집 서비스
|
||||
|
||||
본 튜토리얼에서는 gRPC 분산 통신 기법을 실증적으로 학습하기 위해 현업에서 가장 범용적으로 쓰이는 가상의 **IoT 센싱 데이터 수집 및 기기 관리 서비스** 프로젝트를 직접 설계하고 구현해 나갑니다.
|
||||
|
||||
### 2.1 프로젝트 시나리오
|
||||
사물인터넷(IoT) 센서 노드나 분산 멀티 에이전트 환경에서는 엣지 기기들이 중앙 서버에 접속해 통신 가능 여부를 검증하고 상태 정보(날짜/시간)를 수집하거나, 임시 보안 인증을 위한 비밀번호 발급을 요청하고, 실시간으로 센싱한 환경 정보(온도, 습도 등)를 지속적으로 업데이트해야 하는 현실적인 시나리오가 요구됩니다.
|
||||
우리가 개발할 `IoTService`는 이에 대응하는 다음 3가지 핵심 원격 프로시저(RPC)를 수행합니다:
|
||||
1. **서버 시간 및 날짜 조회 (`GetDate`)**: 기기가 접속 상태를 확인하며 동기화를 위해 서버의 현재 날짜와 시간 포맷 문자열을 반환받습니다.
|
||||
2. **센싱 데이터 업데이트 (`UpdateSensingData`)**: 센서 노드가 주기적으로 수집한 물리 데이터(온도, 습도) 및 기기 식별자(Device ID)를 전달하면, 서버는 데이터 정합성을 검증한 후 성공 여부를 반환합니다.
|
||||
3. **일회성 보안 패스워드 발급 (`GetRandomPass`)**: 기기가 임시 통신 세션 수립을 위해 난수 생성 시드와 보안 문자열 길이를 전달하면, 무작위 ASCII 임시 패스워드를 연산하여 응답받습니다.
|
||||
(참고: 여기서는 핵심 3종 Unary RPC를 우선 다루며, 대용량 파일 전송과 실시간 알림 기능은 §6 및 §7에서 스트리밍 RPC 4종으로 이어서 확장합니다.)
|
||||
|
||||
### 2.2 학습 목표 및 진행 방법
|
||||
이 유기적인 IoT 데이터 통신 모듈을 구축하는 실습을 통해 우리는 다음과 같은 gRPC의 핵심 개발 과정을 아주 쉽게 단계별로 마스터하게 됩니다:
|
||||
* **1단계 - 통신 약속 문서 작성하기 (스키마 설계)**: 기기와 서버가 서로 어떤 형태로 데이터를 주고받을지 `.proto`라는 명세서 파일에 미리 정의해 둡니다. 이 약속을 바탕으로 데이터의 이름과 형태(예: 기기 ID는 문자열, 온도는 실수 등)를 컴파일 전에 확실하게 강제합니다.
|
||||
* **2단계 - 말을 알아듣는 코드 자동으로 만들기 (Stub 컴파일)**: 작성한 약속 문서를 컴파일러(`protoc`)에 넣어주면, 네트워크 통신과 데이터 변환 처리를 담당하는 Go 언어 코드를 컴퓨터가 자동으로 만들어 줍니다. 이를 통해 개발자가 직접 복잡한 JSON 변환이나 소켓 통신 코드를 일일이 짤 필요가 없어집니다.
|
||||
* **3단계 - 실제로 서버와 기기를 연결하여 대화하기 (네트워크 구현)**: 자동으로 만들어진 코드를 바탕으로 실제 gRPC 서버를 실행하고, 클라이언트 기기가 서버에 접속하여 데이터(날짜 요청, 센싱 값 전송 등)를 실시간으로 주고받는 엔드투엔드(End-to-End) 통신 흐름을 완성합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 인터페이스 명세서 (`protoapi.proto`)
|
||||
|
||||
### 3.1 직관적인 비유로 이해하는 명세서 동작 원리
|
||||
|
||||
인터페이스 명세서의 다소 생소한 기술적 개념들은 일상적인 요소에 빗대어 다음과 같이 쉽게 해석할 수 있습니다:
|
||||
|
||||
1. **인터페이스 명세서의 실체 (통합 약속 장부)**:
|
||||
* 센서 단말기는 저수준 임베디드 언어(예: C언어)를 사용하고, 중앙 서버는 고수준 언어(예: Go 언어)를 사용할 때, 두 기기는 첫인사부터 말이 통하지 않습니다. 이때 통신 규격이 적힌 약속 장부(`.proto` 파일)를 가운데에 공유함으로써, 기기와 서버가 동일한 기준으로 대화할 수 있는 소통 채널을 구축하게 됩니다.
|
||||
2. **명세서를 선언해야 하는 이유 (오타 예방과 자동 코드 생성)**:
|
||||
* **데이터 이름 불일치 차단**: 송신 측은 기기 ID를 `device_id`로 보내고, 수신 측은 `deviceId`로 처리하여 시스템이 오작동하는 사소한 오타 버그를 컴파일(빌드) 단계에서 완전히 차단합니다.
|
||||
* **자동 코드 생성**: 명세서 장부 하나만 기재해 두면 Go, Python 등 원하는 언어의 네트워크 연동 소스코드를 번역기(`protoc`)가 스스로 작성해 줍니다. 개발자가 일일이 수백 줄의 네트워크 통신 코드를 손으로 직접 짤 필요가 없어집니다.
|
||||
3. **메시지(Message)와 서비스(Service)의 구분 (규격 포장 상자와 기능 메뉴판)**:
|
||||
* **`service` (기능 메뉴판)**: "본 서버 관제소에서는 시간 조회(`GetDate`)와 센서 보고(`UpdateSensingData`) 두 가지 기능 메뉴를 접수합니다" 하고 처리 가능한 통신 API 목록을 공시하는 것과 같습니다.
|
||||
* **`message` (규격 포장 상자)**: 전송할 데이터를 알맞게 담는 전용 포장 박스입니다. 상자 안에는 기기 ID, 온도, 습도라는 세 종류의 알맹이가 들어가도록 크기와 형식을 고정해 둡니다.
|
||||
4. **`= 1`, `= 2` 식별자의 의미 (데이터의 고유 번호표)**:
|
||||
* 프로토콜 버퍼에서 필드 이름 뒤에 붙는 `= 1`, `= 2`는 변수에 특정 값을 집어넣는 대입 연산자가 아닙니다.
|
||||
* 통신 시 패킷에 매번 `DeviceId`나 `Temperature` 같은 구구절절한 문자열 이름표를 전부 태워 보내면 네트워크 대역폭이 낭비됩니다. 대신 데이터를 보낼 때 **"1번 칸에는 기기 ID가 들어있고, 2번 칸에는 온도가 들어있다"** 하고 짧은 번호표만 달아서 이진화(바이너리)하여 보냅니다. 수신 측 컴퓨터는 이 번호표만 보고 순서대로 데이터를 꺼내 해석하기 때문에 통신 오버헤드가 극적으로 줄어듭니다. 이미 가동 중인 이전 단말 기기들이 헷갈리지 않게, **한 번 지정한 번호표는 절대로 바꾸지 않고 유지**하는 것이 설계의 절대 법칙입니다.
|
||||
|
||||
### 3.2 인터페이스 명세서의 개념 및 도입 목적
|
||||
인터페이스 명세서(Interface Description Document)란 시스템을 구성하는 상이한 노드나 기기들이 데이터를 어떠한 규격과 규약으로 상호 교환할지 사전에 조율하고 합의하여 기술해 둔 설계 문서입니다. gRPC 환경에서는 이를 프로토콜 버퍼(Protocol Buffers)의 `.proto` 파일 형식을 활용해 데이터 명세와 서비스를 통합 기술하는 IDL(Interface Description Language)로 구체화합니다.
|
||||
|
||||
인터페이스 명세서를 강제하여 설계하는 목적은 다음과 같습니다:
|
||||
1. **강력한 스키마 계약 강제(Schema Contract)**: 송수신할 데이터의 이름, 형태(타입) 및 지원 함수를 코드 레벨에서 선언적으로 봉인하여, 런타임 단계가 아닌 컴파일 단계에서 데이터 구조 불일치 오류를 완전히 원천 차단합니다.
|
||||
2. **이종 스택/다국어 간의 원활한 호환성**: 하나의 명세서만을 공유하면, 번역 도구 체인을 통하여 Go, C++, Python, Java 등 서로 다른 소스 언어로 동작하는 클라이언트와 서버 기기들이 상호 간에 기술적 문맥 충돌 없이 원활하게 패킷 데이터를 해석하고 소통할 수 있게 유도합니다.
|
||||
3. **효율적인 패킹을 통한 대역폭 절약**: JSON 등 텍스트 기반 통신 규약에 비해 바이너리 압축 포맷을 생성하는 메커니즘을 명세 단계에서 조율함으로써 패킷 용량을 극대화하여 낮추고, 분산 에이전트와 센싱 노드 간 통신 오버헤드를 경감합니다.
|
||||
|
||||
### 3.3 인터페이스 명세서 설계 가이드 (작성 요령)
|
||||
* **사양 정의**: 파일 최상단에 `syntax = "proto3";` 스펙 버전을 선언하고, 대상 플랫폼에 맞는 패키지 출력 경로(`go_package` 옵션 등)를 제공합니다.
|
||||
* **원격 프로시저 선언**: `service` 블록을 구성하여 외부 단말이 호출을 제기할 수 있는 진입 함수군(RPC 메서드)을 정의하며, 입력과 반환값으로 매핑될 데이터 규격을 선언합니다.
|
||||
* **메시지 직렬화 필드 정의**: `message` 블록을 선언하여 구조체 데이터를 정의합니다. 이때 필드값 뒤에 정의하는 식별자 번호(예: `= 1`, `= 2`)는 값을 대입하는 대입 연산자가 아니라, 컴퓨터가 데이터 직렬화 및 역직렬화 시 순서를 식별하는 **필드 번호 태그(Field Number Tag)**입니다. 한 번 릴리즈된 인터페이스의 태그 번호는 과거 하위 호환성을 보장하기 위하여 임의로 수정하거나 재사용해서는 안 되며, 확장 시 새로운 번호를 꼬리에 덧붙이는 전방향 호환성 설계를 준수해야 합니다.
|
||||
|
||||
### 3.4 명세서 소스코드 및 구체적 분석
|
||||
|
||||
실습 디렉토리 내에 선언된 [protoapi.proto](../lib/grpc/basic/protoapi.proto) 명세서 코드는 앞서 기획한 `IoTService` 통신 구조를 수립하기 위해 다음과 같이 사양을 기재해 둡니다.
|
||||
|
||||
※ 이 코드는 기본 Unary RPC 3종만 발췌한 것이며, 전체 스펙(스트리밍 4종 포함)은 §6.1에서 이어집니다.
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
// 엣지 센싱 단말과 중앙 관리 서버가 연동할 원격 호출(RPC) 창구 정의
|
||||
service IoTService {
|
||||
rpc GetDate (RequestDateTime) returns (DateTime);
|
||||
rpc UpdateSensingData (SensingData) returns (SensingResponse);
|
||||
rpc GetRandomPass (RequestPass) returns (RandomPass);
|
||||
}
|
||||
|
||||
// 센싱 디바이스가 측정하여 전송할 환경 데이터 구조
|
||||
message SensingData {
|
||||
string DeviceId = 1; // 기기 식별 고유 일련번호
|
||||
double Temperature = 2; // 수집된 대기 온도 센싱값
|
||||
double Humidity = 3; // 수집된 상대 습도 센싱값
|
||||
}
|
||||
|
||||
// 데이터 수신 완료 및 가공 결과를 통보하는 응답 영수증 구조
|
||||
message SensingResponse {
|
||||
bool Success = 1; // 트랜잭션 정상 반영 성공 여부
|
||||
string Message = 2; // 상태 상세 정보 안내 메시지
|
||||
}
|
||||
|
||||
message DateTime {
|
||||
string Value = 1;
|
||||
}
|
||||
|
||||
message RequestDateTime {
|
||||
string Value = 2;
|
||||
}
|
||||
|
||||
message RequestPass {
|
||||
int64 Seed = 1;
|
||||
int64 Length = 8;
|
||||
}
|
||||
|
||||
message RandomPass {
|
||||
string Password = 1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 명세 구조의 구성 분석
|
||||
* **`IoTService`**: 센서 단말이 중앙 관제소(서버)에 접근하여 실행 가능한 세 가지 원격 서비스의 인터페이스 스펙을 나타냅니다. 기기는 서버 시계를 연계 조회(`GetDate`)하거나, 측정값을 안전하게 전달(`UpdateSensingData`)하고, 암호 세션 비밀번호 발급(`GetRandomPass`)을 동기식으로 호출할 수 있습니다.
|
||||
* **`SensingData`**: 실제 환경에 노출된 IoT 단말 기기의 정보를 포장하여 송신하기 위한 데이터 모델입니다. 기기 식별을 위한 식별자(`DeviceId`) 및 물리 센서 측정 변수(`Temperature`, `Humidity`)를 순서 번호 태그 1, 2, 3으로 매핑하여 순서가 흐트러지지 않도록 보장합니다.
|
||||
* **`SensingResponse`**: 서버가 수신 데이터를 트랜잭션 처리한 결과를 다시 단말로 되돌려주기 위한 수신 회신 메커니즘입니다. 통신 처리 및 갱신의 안전한 성패 지표(`Success`) 및 원격 디버깅을 위한 가시적인 스트링 로그(`Message`)를 캡슐화해 줍니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Go Stub 컴파일 및 도구 체인
|
||||
|
||||
작성된 `.proto` 명세 파일을 Go 언어 소스코드로 변환하기 위해 프로토콜 버퍼 컴파일러(`protoc`)와 Go 전용 플러그인을 로컬 개발 환경에 구성하는 가이드라인입니다.
|
||||
|
||||
### 4.1 핵심 도구 체인 구성과 기능 개요
|
||||
|
||||
`.proto` 파일이 '약속 장부'라면, 이 3가지 도구는 그 장부를 실제 프로그램이 알아듣는 Go 언어 코드로 옮겨주는 '번역팀'입니다. `protoc`은 장부를 읽고 이해하는 통역사, `protoc-gen-go`는 장부 속 데이터 모양(message)을 Go 구조체로 옮겨 적는 필경사, `protoc-gen-go-grpc`는 장부 속 기능 목록(service)을 실제로 호출 가능한 Go 함수 뼈대로 옮겨 적는 필경사입니다.
|
||||
|
||||
gRPC 빌드 및 코드 생성 환경을 구축하기 위해 사용되는 세 가지 핵심 바이너리의 세부 역할은 다음과 같습니다:
|
||||
|
||||
1. **`protobuf-compiler` (또는 `protoc` 본체)**:
|
||||
* **역할**: 프로토콜 버퍼 코어 컴파일러 엔진
|
||||
* **목적**: `.proto` 파일의 스키마 명세를 구문 분석하는 역할을 수행합니다. 특정 프로그래밍 언어에 의존하지 않는 공통 파서 역할을 하며, 생성된 구문 분석 정보를 아래의 언어별 플러그인 모듈에 위임하여 타깃 코드를 출력하도록 제어합니다.
|
||||
2. **`protoc-gen-go` (Go 데이터 직렬화 플러그인)**:
|
||||
* **역할**: Go 구조체 및 직렬화 소스코드 생성 플러그인
|
||||
* **목적**: 명세서 내에 선언된 `message` 정의를 Go 언어의 구조체(`struct`) 코드로 자동 생성합니다. 이를 통해 바이너리 직렬화/역직렬화 인터페이스 및 필드 매핑 코드가 구현된 `*.pb.go` 파일을 얻을 수 있습니다.
|
||||
3. **`protoc-gen-go-grpc` (Go gRPC 서비스 플러그인)**:
|
||||
* **역할**: Go gRPC 통신 인터페이스 소스코드 생성 플러그인
|
||||
* **목적**: 명세서 내에 선언된 `service` 정의를 해석하여 Go 언어 환경의 gRPC 서버 핸들러 뼈대와 클라이언트 송수신용 채널 인터페이스를 제공하는 `*_grpc.pb.go` 파일을 출력합니다.
|
||||
|
||||
### 4.2 컴파일 도구 체인 설치
|
||||
* **macOS (Homebrew 사용)**:
|
||||
터미널에 아래 명령어를 입력해 컴파일러와 Go 언어 통신용 변환 플러그인을 설치합니다.
|
||||
```bash
|
||||
brew install protobuf
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
```
|
||||
* **Linux (Ubuntu 기준)**:
|
||||
패키지 관리자를 통해 컴파일러를 다운로드하고 마찬가지로 Go 플러그인을 환경에 바인딩합니다.
|
||||
|
||||
```bash
|
||||
sudo apt install -y protobuf-compiler
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
```
|
||||
|
||||
### 4.3 프로토콜 버퍼 컴파일 수행
|
||||
작성된 스키마 명세를 빌드하여 Go 소스코드를 생성하기 위해 다음 명령어를 구동합니다:
|
||||
```bash
|
||||
# 해당 실습 디렉터리로 이동 후 컴파일 실행
|
||||
cd lib/grpc/basic
|
||||
protoc --go_out=. --go-grpc_out=. protoapi.proto
|
||||
```
|
||||
|
||||
#### 컴파일 옵션의 출력 경로 지정 동작 방식
|
||||
명령어 실행 시 주입하는 각 옵션은 빌드 파일이 생성될 **기준 디렉터리(Base Directory)**를 개별 정의합니다:
|
||||
* **`--go_out=.`**: 데이터 명세 구현체인 `protoapi.pb.go`가 출력될 기준 경로를 현재 컴파일 실행 디렉터리(`.`)로 선언합니다.
|
||||
* **`--go-grpc_out=.`**: gRPC 통신 구현체인 `protoapi_grpc.pb.go`가 출력될 기준 경로를 현재 실행 디렉터리(`.`)로 선언합니다.
|
||||
|
||||
**Go 패키지 지정 옵션과의 결합 규칙**:
|
||||
해당 출력 경로 옵션들은 단독으로 파일의 최종 위치를 고정하지 않습니다. 컴파일러는 지정된 기준 경로(예: `.`)에 `.proto` 스펙 내부의 `option go_package = "./protoapi;protoapi"` 설정값을 조합하여 최종 디렉터리 경로를 생성합니다.
|
||||
이에 따라 **컴파일 대상 디렉터리(`.`)**와 **상세 패키지 주소(`./protoapi`)**가 결합되어 `lib/grpc/basic/protoapi/` 경로가 자동 생성되며, 그 하위에 다음 소스코드들이 정상 배치됩니다:
|
||||
* `protoapi.pb.go`: 명세에 정의된 메시지(Message) 규격을 Go 구조체로 변환한 파일입니다.
|
||||
* `protoapi_grpc.pb.go`: 클라이언트와 서버 통신을 위한 원격 호출(Service) 규격을 구현한 파일입니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 실습 소스코드 상세 구현 분석
|
||||
|
||||
컴파일러를 통해 통신을 위한 스터브(Stub) 코드가 확보되었으므로, 이를 기반으로 서버와 클라이언트의 비즈니스 로직을 연결하는 상세 코드를 검토합니다.
|
||||
|
||||
### 5.1 gRPC 서버 구현 분석 ([server.go](../lib/grpc/basic/server.go))
|
||||
|
||||
* **서버 서비스 인터페이스 매핑 구조체 (`IoTServer`)**:
|
||||
```go
|
||||
type IoTServer struct {
|
||||
protoapi.UnimplementedIoTServiceServer
|
||||
}
|
||||
```
|
||||
* **간단 설명**: 이 구조체는 '나는 IoTService가 약속한 기능들을 구현하는 서버입니다'라고 선언하는 역할을 합니다. **Go 언어의 gRPC 규칙상 이 구절(`Unimplemented...`)을 빼놓으면 서버가 아예 컴파일(빌드)되지 않고 에러가 발생하므로, '있으면 좋은 것'이 아니라 반드시 그대로 넣어주어야 하는 필수 구성 요소입니다.** (이렇게 넣어두면 부수적으로, 나중에 약속 장부에 새 기능이 추가되어도 기존 서버 코드가 빌드 오류 없이 구동되는 효과도 함께 얻습니다.)
|
||||
* **Q. 만약 이 줄(`protoapi.UnimplementedIoTServiceServer`)을 지우면 어떻게 되나요?**
|
||||
gRPC가 자동 생성한 인터페이스와의 호환성이 깨져 Go 컴파일러가 아래와 같은 에러를 내며 빌드를 거부합니다:
|
||||
```text
|
||||
cannot use IoTServer{} (value of type IoTServer) as protoapi.IoTServiceServer in argument to protoapi.RegisterIoTServiceServer:
|
||||
IoTServer does not implement protoapi.IoTServiceServer (missing method mustEmbedUnimplementedIoTServiceServer)
|
||||
```
|
||||
따라서 나중에 메서드를 더 추가할 일이 없더라도, gRPC 서버의 정상적인 구동을 위해 반드시 포함시켜야 합니다.
|
||||
* **상세 설명**: `UnimplementedIoTServiceServer`를 임베딩하여 정의합니다. 이는 향후 새로운 메서드가 프로토콜 스펙에 추가되더라도 기존 서버가 빌드 오류 없이 구동 호환성(미구현 메서드 호출 시 unimplemented 에러 반환)을 안전하게 유지하도록 제약하기 위한 설정입니다.
|
||||
* **센싱 데이터 업데이트 수신 처리 (`UpdateSensingData`)**:
|
||||
```go
|
||||
func (IoTServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) {
|
||||
fmt.Printf("Received sensing data - Device: %s, Temp: %.2f°C, Humid: %.2f%%\n", r.GetDeviceId(), r.GetTemperature(), r.GetHumidity())
|
||||
return &protoapi.SensingResponse{Success: true, Message: "Sensing data updated successfully!"}, nil
|
||||
}
|
||||
```
|
||||
* **간단 설명**: 기기(클라이언트)가 온습도 데이터를 전송해 왔을 때, 서버 콘솔 화면에 이를 예쁘게 출력한 뒤 "성공적으로 업데이트되었습니다"라는 확인 영수증(`SensingResponse`)을 만들어 돌려주는 실제 서비스 동작 부위입니다.
|
||||
* **상세 설명**: 클라이언트 디바이스로부터 센싱 패킷을 수신하면, 기기 식별 및 온습도 측정 매개변수를 포맷팅하여 표준 출력에 표시한 후 정상 처리 완료 플래그를 담은 응답 구조체(`SensingResponse`)를 반환합니다.
|
||||
|
||||
> [!NOTE]
|
||||
> **(심화) 기술 분석: 왜 Set 메서드는 없고 Get 메서드만 제공되는가? (Go와 Protobuf의 설계 철학)**
|
||||
>
|
||||
> 자동으로 생성된 Go 소스코드를 들여다보면, 필드 데이터를 추출하는 `r.GetDeviceId()` 메서드는 존재하나 값을 쓰는 `r.SetDeviceId()` 메서드는 설계 단계에서 배제되어 있습니다. 이 비대칭 구조에는 아래와 같은 언어 및 아키텍처적 지향점이 투영되어 있습니다:
|
||||
>
|
||||
> 1. **Go 언어의 구조적 단순함 (Simplicity)**: Go 언어는 불필요한 은닉용 Getter/Setter 작성을 권장하지 않습니다. 외부 공유가 필요한 구조체 필드는 대문자(`DeviceId`)로 시작하도록 설계되어 외부에서 직접 대입(`r.DeviceId = ...`)하여 제어하는 것이 보편적인 방식이기 때문입니다.
|
||||
> 2. **포인터 예외(Nil-Safety) 방지를 위한 안전장치**: 수신한 구조체 변수가 초기화되지 않은 `nil` 포인터일 때 필드에 직접 접근하면 프로그램이 즉시 종료(Segmentation Fault)됩니다. 하지만 자동 생성된 `GetDeviceId()` 메서드는 내부적으로 수신 객체의 `nil` 검사를 사전에 집행하여 에러를 막고, 객체가 비어있을 경우 해당 필드 타입의 안전한 기본값(Zero-value, 예: `""` 또는 `0`)을 반환하도록 설계된 **안전 읽기 인터페이스**입니다.
|
||||
> 3. **데이터 불변성 (Immutability)**: 분산 환경에서 통신 객체(DTO)는 한 번 직렬화되어 전송되기 시작한 시점부터 중간 변경이 억제되는 불변 객체로 취급하는 것이 데이터 정합성에 유리합니다. 인스턴스 중간 변조를 허용하는 세터(Setter)의 무분별한 사용을 차단하여 통신의 신뢰성을 고정하고자 하는 아키텍처적 의도도 포함되어 있습니다.
|
||||
|
||||
* **gRPC 리스너 구동 및 서빙 (`ServerRun`)**:
|
||||
```go
|
||||
listen, _ := net.Listen("tcp", port)
|
||||
server.Serve(listen)
|
||||
```
|
||||
* **간단 설명**: `:8080` 포트로 통하는 소켓(전화선)을 개통하고, 기기들의 전화(접속 및 호출)가 오기를 기다리며 대기 상태로 들어가는 서버 구동 시작점입니다.
|
||||
* **상세 설명**: 지정된 포트(기본 포트 `:8080`)의 TCP 소켓 포트를 활성화하고, 클라이언트의 접속 및 RPC 서비스 호출에 대해 지속적으로 대기하는 리스너 구동의 진입점입니다.
|
||||
|
||||
### 5.2 gRPC 클라이언트 구현 분석 ([client.go](../lib/grpc/basic/client.go))
|
||||
|
||||
* **원격 서비스 클라이언트 기동 (`NewIoTServiceClient`)**:
|
||||
```go
|
||||
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := protoapi.NewIoTServiceClient(conn)
|
||||
```
|
||||
* **간단 설명**: 서버 주소로 전화를 거는 통신선(TCP 채널)을 안전 보안(TLS) 없이 개설한 뒤, 이 선을 통해 gRPC 약속 장부(`IoTService`)대로 서버에 원격 호출을 요청할 수 있는 전용 전화기(클라이언트 인스턴스)를 획득하는 과정입니다.
|
||||
* **상세 설명**: 서버와의 TCP 채널(`conn`)을 평문 전송(insecure) 기반으로 바인딩한 뒤, 해당 채널을 통해 원격 서비스를 호출할 수 있는 전송용 클라이언트 인스턴스를 확보합니다.
|
||||
* **패킷 구성 및 RPC 호출 실행 (`AskUpdateSensingData`)**:
|
||||
```go
|
||||
request := &protoapi.SensingData{DeviceId: deviceId, Temperature: temp, Humidity: humid}
|
||||
return m.UpdateSensingData(ctx, request)
|
||||
```
|
||||
* **간단 설명**: 온습도 데이터 상자(`SensingData`)를 접어서 기기 번호와 센서값을 가지런히 담은 뒤, 전용 전화기(클라이언트 인터페이스)를 통해 서버의 `UpdateSensingData` 기능을 직접 원격 실행(호출)하는 부분입니다.
|
||||
* **상세 설명**: 센서 측정값을 메시지 스펙 규격에 맞추어 `SensingData` 구조체 인스턴스로 바인딩한 후, 기설정된 클라이언트 인터페이스를 경유하여 서버의 `UpdateSensingData` 엔드포인트를 호출합니다.
|
||||
|
||||
### 5.3 통신 세션 동작 시퀀스 및 흐름
|
||||
실습 예제를 기동하면 클라이언트와 서버 간에 아래와 같은 동작 흐름이 순차적으로 실행됩니다:
|
||||
1. **서버 날짜/시간 조회**: 클라이언트가 `GetDate`를 호출하여 서버의 현재 가동 시계 정보 문자열을 회신받아 표준 출력에 노출합니다.
|
||||
2. **보안 토큰용 임시 암호 발급**: 클라이언트가 암호화 연산 시드와 길이를 전달하여 `GetRandomPass`를 호출하고, 서버가 생성한 일회성 보안 패스워드를 응답받습니다.
|
||||
3. **환경 수집 센싱 데이터 동기화**: 클라이언트가 기기 식별값("sensor-room-01")과 가상의 온습도 변수를 실어 `UpdateSensingData`를 기동하면, 서버는 수신 값을 화면에 검증 및 로깅한 후 완료 보고 영수증을 반환합니다.
|
||||
|
||||
> [!TIP]
|
||||
> **(선택 학습) 상세 네트워크 시퀀스 및 엔진 내부 동작 분석**:
|
||||
> 기본 실습 과정을 모두 마치고 gRPC 내부에서 네트워크 패킷이 구체적으로 어떻게 직렬화되고 HTTP/2 프레임으로 오가는지 깊이 알고 싶다면, [ASKING_DATETIME_WORKFLOW.md](ASKING_DATETIME_WORKFLOW.md) 문서에서 상세한 동작 시퀀스 다이어그램과 라이프사이클 분석 자료를 선택적으로 확인할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 대용량 데이터 전송을 위한 스트리밍(Streaming) 구현
|
||||
|
||||
쉽게 말해, 지금까지 배운 방식(Unary)이 '궁금한 게 있을 때마다 전화를 걸어 질문 하나씩 하고 바로 끊는 것'이라면, 스트리밍은 **'전화를 한 번 걸어 끊지 않은 상태로, 준비된 데이터 조각(Chunk)들을 긴 띠처럼 끊임없이 흘려보내는 것'**입니다. 큰 파일을 한 번에 통째로 보내면 메모리가 가득 차서 전송하기 어려우므로, 전화선은 그대로 유지한 채 데이터를 작게 나누어 연속으로 전달하는 방법을 배웁니다.
|
||||
|
||||
일반적인 단발성 요청/응답(Unary) 통신은 전송할 전체 데이터를 단일 메모리에 전부 올려 적재한 상태에서 동작하므로, 펌웨어나 대형 이미지 같은 대용량 데이터를 다룰 때 메모리 고갈(OOM)이나 네트워크 대역폭 병목을 초래하기 쉽습니다. gRPC는 HTTP/2 프로토콜의 스트림(Stream) 채널을 기본 가용하므로, 데이터를 일정 크기(Chunk) 단위로 쪼개 연속적으로 전송할 수 있는 강력한 **스트리밍(Streaming)** 기법을 지원합니다. 본 예제에서는 클라이언트가 파일을 조각내어 보내는 **클라이언트 스트리밍(Client Streaming)**, 업로드된 파일 메타 정보를 모아 한 번에 내려주는 **단일 조회(Unary RPC)**, 그리고 서버가 데이터를 쪼개어 클라이언트에게 보내는 **서버 스트리밍(Server Streaming)**까지 모두 종합 설계하여 탑재했습니다.
|
||||
|
||||
### 6.1 스키마 설계 (`protoapi.proto`)
|
||||
업로드와 리스트 조회, 그리고 다운로드를 위한 gRPC 메시지 규격을 명세합니다:
|
||||
```proto
|
||||
service IoTService {
|
||||
// ... 기존 RPC ...
|
||||
rpc UploadFile (stream FileChunk) returns (UploadStatus);
|
||||
rpc ListFiles (EmptyRequest) returns (FileList);
|
||||
rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
|
||||
}
|
||||
|
||||
message FileChunk {
|
||||
string FileName = 1;
|
||||
bytes Content = 2; // 쪼개진 바이너리 데이터 조각
|
||||
}
|
||||
|
||||
message UploadStatus {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
int64 BytesUploaded = 3;
|
||||
}
|
||||
|
||||
message EmptyRequest {}
|
||||
|
||||
message FileMetadata {
|
||||
string FileName = 1;
|
||||
int64 FileSize = 2;
|
||||
int64 UploadedAt = 3;
|
||||
}
|
||||
|
||||
message FileList {
|
||||
repeated FileMetadata Files = 1;
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string FileName = 1;
|
||||
}
|
||||
```
|
||||
* **설계 포인트**: `stream` 키워드가 들어간 위치에 주목합니다.
|
||||
* **`stream` 키워드의 역할**: gRPC에서 `stream` 키워드는 단발성 요청/응답(Unary RPC) 방식과 달리, **하나의 HTTP/2 커넥션을 유지한 채 데이터를 연속적인 흐름(Stream)으로 쪼개서 전송하겠다**고 선언하는 지시어입니다. 이 키워드가 지정되면 컴파일러(`protoc`)는 데이터를 연속으로 송수신할 수 있는 스트림 파이프라인 형태의 Go 인터페이스와 스터브 코드를 생성합니다.
|
||||
* **클라이언트 스트리밍 (`UploadFile`)**: 호출 매개변수 정의 앞부분에 `stream`(`stream FileChunk`)이 붙습니다. 클라이언트가 데이터를 여러 번에 걸쳐 청크로 송신하고, 서버는 최종 수신 완료 시점에 단 한 번 응답(`returns (UploadStatus)`)을 반환합니다.
|
||||
* **서버 스트리밍 (`DownloadFile` 및 `SubscribeAlerts`)**: 반환형(`returns`) 정의의 괄호 내부에 `stream`(`returns (stream FileChunk)`)이 붙습니다. 클라이언트의 단일 호출 요청에 대해, 서버가 데이터를 여러 조각으로 쪼개어 연속적으로 클라이언트에게 푸시 전송합니다.
|
||||
* **🚨 `stream` 키워드 없이 대용량 데이터를 전송할 때의 한계와 위험성**:
|
||||
만약 대용량 파일나 수기가바이트(GB)에 달하는 대용량 데이터를 `stream` 키워드 없이 일반 Unary RPC(단발성 요청/응답)로 전송하려고 시도하면 다음과 같은 심각한 기술적 문제가 발생합니다:
|
||||
1. **메모리 고갈 (OOM - Out Of Memory)**: 전송할 데이터 전체가 직렬화되기 전 단일 바이트 슬라이스(`[]byte`) 형태로 클라이언트와 서버 메모리에 한 번에 적재되어야 합니다. 이는 메모리 리소스가 극도로 제한된 IoT 임베디드 단말기나 동시 요청이 몰리는 서버 환경에서 즉각적인 OOM 에러 및 프로세스 강제 종료를 유발합니다.
|
||||
2. **gRPC 기본 메시지 수신 한도 초과**: gRPC 엔진은 악의적인 디도스(DDoS) 공격 방지와 메모리 보호를 위해 **단일 RPC 호출당 최대 메시지 수신 크기를 기본 4MB**로 제한하고 있습니다. 따라서 4MB를 넘는 파일 데이터를 Unary로 전송할 경우 즉각 `ResourceExhausted` 에러가 발생하며 통신이 차단됩니다. (설정으로 한도를 늘릴 수 있으나 메모리 병목 우려로 권장되지 않습니다.)
|
||||
3. **네트워크 유실 시 재전송 오버헤드**: 단 한 번의 네트워크 패킷 전송 오류(Glitch)가 발생해도 전체 대용량 데이터를 처음부터 완전히 다시 보내야 하므로 네트워크 비용 낭비가 심화됩니다. 스트리밍을 사용하면 청크 단위로 분할하여 안정적으로 주고받을 수 있습니다.
|
||||
|
||||
### 6.2 서버 저장 및 송수신 구현 ([server.go](../lib/grpc/basic/server.go))
|
||||
클라이언트가 스트리밍으로 업로드한 파일의 메타데이터를 서버에서 관리하기 위해 인메모리 파일 저장소인 `fileStore`를 정의합니다. `fileStore`는 파일 이름을 키(Key)로 하고 파일 메타데이터 및 바이트 데이터를 포함하는 `UploadedFile` 구조체 포인터를 값(Value)으로 갖는 맵(`map[string]*UploadedFile`) 구조체입니다.
|
||||
|
||||
이 저장소를 활용하여 다음 3가지 핸들러를 구현합니다:
|
||||
1. **파일 업로드 (`UploadFile` - Client Streaming)**: 클라이언트가 스트리밍을 통해 분할 전송하는 파일 데이터 조각(`FileChunk`)들을 수신해 병합한 후 `fileStore`에 등록합니다.
|
||||
2. **파일 목록 조회 (`ListFiles` - Unary RPC)**: `fileStore`에 보관된 모든 파일들의 메타데이터(파일명, 크기, 타임스탬프) 목록을 빌드하여 일괄 반환합니다.
|
||||
3. **파일 다운로드 (`DownloadFile` - Server Streaming)**: 요청된 파일 데이터를 `fileStore`에서 탐색한 후, 1KB 단위의 청크 조각으로 나누어 클라이언트에게 순차적으로 스트리밍 전송합니다.
|
||||
|
||||
|
||||
```go
|
||||
// 1. 인메모리 파일 보관소
|
||||
type UploadedFile struct {
|
||||
FileName string
|
||||
Content []byte
|
||||
UploadedAt int64
|
||||
}
|
||||
|
||||
var (
|
||||
fileStore = make(map[string]*UploadedFile)
|
||||
storeMu sync.RWMutex
|
||||
)
|
||||
|
||||
// 2. 파일 업로드 Client Streaming RPC
|
||||
func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error {
|
||||
var totalBytes int64
|
||||
var fileName string
|
||||
var buffer []byte
|
||||
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
fmt.Printf("File upload completed. Received %d bytes for file '%s'\n", totalBytes, fileName)
|
||||
|
||||
if fileName != "" {
|
||||
storeMu.Lock()
|
||||
fileStore[fileName] = &UploadedFile{
|
||||
FileName: fileName,
|
||||
Content: buffer,
|
||||
UploadedAt: time.Now().Unix(),
|
||||
}
|
||||
storeMu.Unlock()
|
||||
}
|
||||
|
||||
return stream.SendAndClose(&protoapi.UploadStatus{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("File '%s' uploaded successfully.", fileName),
|
||||
BytesUploaded: totalBytes,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("File upload error:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if fileName == "" {
|
||||
fileName = chunk.GetFileName()
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
totalBytes += int64(len(chunk.GetContent()))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 파일 리스트 조회 Unary RPC
|
||||
func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) {
|
||||
storeMu.RLock()
|
||||
defer storeMu.RUnlock()
|
||||
|
||||
var files []*protoapi.FileMetadata
|
||||
for _, f := range fileStore {
|
||||
files = append(files, &protoapi.FileMetadata{
|
||||
FileName: f.FileName,
|
||||
FileSize: int64(len(f.Content)),
|
||||
UploadedAt: f.UploadedAt,
|
||||
})
|
||||
}
|
||||
return &protoapi.FileList{Files: files}, nil
|
||||
}
|
||||
|
||||
// 4. 파일 다운로드 Server Streaming RPC
|
||||
func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTService_DownloadFileServer) error {
|
||||
storeMu.RLock()
|
||||
f, exists := fileStore[r.GetFileName()]
|
||||
storeMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return status.Errorf(codes.NotFound, "file %s not found", r.GetFileName())
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 단위 분할 송출
|
||||
totalBytes := len(f.Content)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: f.FileName,
|
||||
Content: f.Content[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
* **간단 설명**:
|
||||
* **파일 업로드**: 클라이언트가 쪼개서 던지는 파일 조각 상자(`stream.Recv()`)들을 루프를 돌며 계속 수집하여 하나의 임시 보관 버퍼(`buffer`)에 합칩니다. 마지막 조각 전송 완료(`io.EOF`) 신호가 오면, 모인 바이트들을 파일 이름과 함께 서버의 보관함(`fileStore`)에 안전하게 저장하고 영수증을 클라이언트에게 발행합니다.
|
||||
* **목록 조회**: 서버의 파일 보관함(`fileStore`)을 열고 그 안에 든 모든 파일의 메타데이터(이름, 크기, 업로드 시각)를 리스트로 포장해 한번에 리턴해 줍니다.
|
||||
* **파일 다운로드**: 보관함에서 요청받은 파일을 찾은 뒤, 파일 내용 전체를 1KB 크기의 패킷 조각들로 잘라 통로를 타고 차례대로 연속 전송(`stream.Send()`)해 줍니다.
|
||||
* **상세 설명**:
|
||||
* **파일 업로드**: 수신 파이프라인 스트림의 `Recv()`를 호출하여 개별 `FileChunk` 객체들을 수령합니다. 수신 스트림이 종료(`io.EOF`)되면 버퍼링된 바이트 데이터와 타임스탬프를 묶어 스레드 동기화 락(`storeMu.Lock()`)을 획득하고 인메모리 맵에 적재한 뒤, `SendAndClose`로 마감 처리합니다.
|
||||
* **목록 조회**: 동시 접근 보호(Race condition 방지)를 위해 읽기 전용 락(`RLock`)을 획득한 후 인메모리 맵을 순회하며 메타데이터 구조체 목록을 집계해 반환합니다.
|
||||
* **파일 다운로드**: 대상 파일 쿼리 실패 시 gRPC 표준 에러(`codes.NotFound`)를 반환합니다. 검증 통과 시 루프 내에서 가상 윈도우 슬라이싱을 집행해 청크 구조체를 구성하고, `stream.Send()`로 직렬화 패킷을 클라이언트 버퍼 큐에 기입합니다.
|
||||
|
||||
### 6.3 클라이언트 송수신 기동 ([client.go](../lib/grpc/basic/client.go))
|
||||
클라이언트는 업로드에 성공한 뒤, 서버에 파일 목록 조회를 요구하고, 다운로드 스트림을 개설해 조각 데이터를 재조립하여 무결성을 검사합니다.
|
||||
|
||||
```go
|
||||
// 1. 파일 업로드 송신 (클라이언트 스트리밍)
|
||||
func AskUploadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string, dummyData []byte) (*protoapi.UploadStatus, error) {
|
||||
stream, err := m.UploadFile(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 단위 분할 송신
|
||||
totalBytes := len(dummyData)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: fileName,
|
||||
Content: dummyData[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return stream.CloseAndRecv()
|
||||
}
|
||||
|
||||
// 2. 파일 목록 조회 호출
|
||||
func AskListFiles(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.FileList, error) {
|
||||
return m.ListFiles(ctx, &protoapi.EmptyRequest{})
|
||||
}
|
||||
|
||||
// 3. 파일 다운로드 수신 및 조립
|
||||
func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string) ([]byte, error) {
|
||||
stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer []byte
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break // 서버가 송신 완료하고 채널을 닫음
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
```
|
||||
* **간단 설명**:
|
||||
* **파일 업로드**: 보낼 파일을 1KB 조각 크기로 나누어 준비한 뒤, gRPC 전용 파이프 스트림 통로에 차례대로 흘려보냅니다(`stream.Send()`). 모든 조각을 던진 후 채널을 끊고 영수증(`UploadStatus`)을 받습니다.
|
||||
* **목록 조회**: 서버에게 "보관 중인 파일 이름 목록을 달라"고 요구하여 화면에 출력합니다.
|
||||
* **파일 다운로드**: 다운로드 통로를 열어 서버가 던져주는 조각들을 계속 수령(`stream.Recv()`)하여 버퍼에 차곡차곡 합칩니다. 서버가 보내기를 끝마치면(`io.EOF`) 조립을 중단하고 최종 완성된 온전한 바이트 파일을 최종 사용처에 반환합니다.
|
||||
* **상세 설명**:
|
||||
* **파일 업로드**: `UploadFile` 채널을 기동하여 클라이언트 사이드 스트림 핸들을 획득합니다. 슬라이스 윈도우 방식으로 데이터를 순차 분할 송출하고, `CloseAndRecv` 메서드를 최종 호출해 스트림 종결 프레임을 송신한 뒤 서버의 단발성 최종 회신 상태를 획득합니다.
|
||||
* **`CloseAndRecv()`의 역할**: 클라이언트에서 송신 스트림을 닫는(Half-close) 동시에, 서버가 전송 완료 후 최종 반환하는 응답 영수증(`UploadStatus`)을 수신할 때까지 블로킹 대기(Blocking wait)하여 최종 응답과 에러 객체를 받아오는 복합 기능을 수행합니다.
|
||||
* **목록 조회**: 빈 메시지(`EmptyRequest`)를 동봉해 Unary RPC 채널을 트리거하고 메타데이터 배열 결과를 동기적으로 (Synchronously) 획득합니다.
|
||||
* **파일 다운로드**: 서버 스트리밍 엔드포인트 기동 후, `stream.Recv()` 블로킹 수신 루프에 진입합니다. 채널 해제 지점(`io.EOF`)에 도달할 때까지 메모리 버퍼 슬라이스에 청크 바이트 배열을 병합 누적하여 재조립(Reassembly)을 마친 후 반환합니다.
|
||||
|
||||
## 7. 실시간 알림을 위한 Pub/Sub (발행/구독) 브로드캐스팅 구현
|
||||
|
||||
쉽게 말해 Pub/Sub은 '신문 구독'과 같습니다. 구독자(클라이언트)가 한 번 신청해 두면, 발행자(서버)는 새로운 소식(경보)이 생길 때마다 모든 구독자에게 알아서 배달해 줍니다. 클라이언트가 매번 '무슨 일 없어요?'라고 다시 물어볼 필요가 없다는 것이 핵심입니다.
|
||||
|
||||
스마트 가전이나 센서 등 실시간 경보 통지가 필요한 AIoT 도메인에서는, 서버가 상시 대기하는 여러 디바이스(클라이언트)들에게 비동기로 이벤트를 밀어 넣어주는 **발행/구독(Publish/Subscribe)** 연동 구조가 필수적입니다. gRPC의 **서버 스트리밍(Server Streaming)** 채널을 응용하면, 다수의 클라이언트가 스트림 통로를 상시 유지한 채 대기하고, 서버가 특정 이벤트 발생 시 채널 리스트를 순회하며 실시간 이벤트를 **브로드캐스팅(Broadcasting)**하는 Pub/Sub 인프라를 단순하고 가볍게 완성할 수 있습니다.
|
||||
|
||||
### 7.1 스키마 설계 (protoapi.proto)
|
||||
구독 신청을 위한 파라미터(`AlertSubscription`)와 서버가 밀어 넣어줄 이벤트 규격(`AlertMessage`)을 IDL에 선언합니다:
|
||||
```proto
|
||||
message AlertSubscription {
|
||||
string ClientId = 1;
|
||||
string Topic = 2; // 구독할 주제 (예: "temperature_warnings")
|
||||
}
|
||||
|
||||
message AlertMessage {
|
||||
string AlertId = 1;
|
||||
string DeviceId = 2;
|
||||
string Message = 3; // 실시간 발생 경보 문자열
|
||||
int64 Timestamp = 4;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 서버 사이드 구독자 관리 및 발행 구현 ([server.go](../lib/grpc/basic/server.go))
|
||||
서버는 구독을 신청한 클라이언트들에게 메시지를 안전하게 분배하기 위해 스레드 세이프 맵과 고루틴 채널(`chan`) 구조를 구성합니다:
|
||||
|
||||
```go
|
||||
type AlertSubscriber struct {
|
||||
ClientId string
|
||||
Channel chan *protoapi.AlertMessage
|
||||
}
|
||||
|
||||
var (
|
||||
subscribers = make(map[string]*AlertSubscriber)
|
||||
subMu sync.Mutex
|
||||
)
|
||||
|
||||
// 실시간 모든 구독 채널에 알림 이벤트 분배 (Publish/Broadcast)
|
||||
func publishAlert(alert *protoapi.AlertMessage) {
|
||||
subMu.Lock()
|
||||
defer subMu.Unlock()
|
||||
for _, sub := range subscribers {
|
||||
select {
|
||||
case sub.Channel <- alert:
|
||||
default:
|
||||
// 채널 버퍼가 가득 찬 경우 병목 차단을 방지하기 위해 드롭 처리
|
||||
fmt.Printf("Alert channel blocked for client %s, dropping event\n", sub.ClientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 구독 채널 대기 핸들러
|
||||
func (IoTServer) SubscribeAlerts(r *protoapi.AlertSubscription, stream protoapi.IoTService_SubscribeAlertsServer) error {
|
||||
clientId := r.GetClientId()
|
||||
ch := make(chan *protoapi.AlertMessage, 10) // 버퍼 10의 수신 채널 생성
|
||||
sub := &AlertSubscriber{
|
||||
ClientId: clientId,
|
||||
Channel: ch,
|
||||
}
|
||||
|
||||
subMu.Lock()
|
||||
subscribers[clientId] = sub
|
||||
subMu.Unlock()
|
||||
|
||||
fmt.Printf("Client %s subscribed to alerts on topic '%s'\n", clientId, r.GetTopic())
|
||||
|
||||
// 스트림 연결 유지 및 채널 대기 감시 루프
|
||||
for {
|
||||
select {
|
||||
case alert := <-ch:
|
||||
err := stream.Send(alert)
|
||||
if err != nil {
|
||||
subMu.Lock()
|
||||
delete(subscribers, clientId)
|
||||
subMu.Unlock()
|
||||
return err
|
||||
}
|
||||
case <-stream.Context().Done():
|
||||
subMu.Lock()
|
||||
delete(subscribers, clientId)
|
||||
subMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* **간단 설명**:
|
||||
* **구독 신청**: 클라이언트가 전화를 걸면 서버는 그 선을 닫지 않고 메모장(`subscribers`)에 해당 전화번호와 연결된 통로(Go 채널)를 적어둡니다. 그리고 그 선을 계속 붙잡고 대기(`stream.Send`) 상태를 유지합니다.
|
||||
* **경보 발행**: 센서값 수신 핸들러(`UpdateSensingData`)에서 임계치(40도)를 초과하는 위험 열기가 감지되면, 메모장에 적힌 모든 연결된 통로에 경보 엽서(`AlertMessage`)를 휙 던져(Broadcast) 줍니다.
|
||||
* **상세 설명**:
|
||||
* **구독 신청**: `SubscribeAlerts` 엔드포인트는 호출과 동시에 전용 Go 비동기 버퍼 채널을 생성하고 전역 가입 맵에 등록합니다. `stream.Context().Done()` 채널 수신이나 스트림 유실 이벤트가 포착되기 전까지 루프 대기 상태를 안전하게 고정합니다.
|
||||
* **경보 발행**: 동시성 경쟁 방지 락(`subMu.Lock()`) 임계 구역 내에서 연결된 모든 채널에 데이터를 `select-default` 논블로킹 패턴으로 분배 기입하여, 특정 클라이언트의 수신 병목이 서버 전체 성능에 미치는 파급 효과를 예방합니다.
|
||||
|
||||
### 7.3 클라이언트 비동기 청취 구현 ([client.go](../lib/grpc/basic/client.go))
|
||||
클라이언트는 메인 흐름을 방해하지 않고 알림을 백그라운드에서 실시간으로 대기 청취할 수 있도록 별도의 독자적인 비동기 고루틴 구조로 가동합니다.
|
||||
|
||||
```go
|
||||
func AskSubscribeAlerts(ctx context.Context, m protoapi.IoTServiceClient, clientId string, topic string) {
|
||||
stream, err := m.SubscribeAlerts(ctx, &protoapi.AlertSubscription{
|
||||
ClientId: clientId,
|
||||
Topic: topic,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to subscribe alerts:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
alert, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Printf("\n🔔 [ALERT RECEIVED] ID: %s | Device: %s | Msg: %s | Time: %s\n\n",
|
||||
alert.GetAlertId(), alert.GetDeviceId(), alert.GetMessage(),
|
||||
time.Unix(alert.GetTimestamp(), 0).Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
```
|
||||
* **간단 설명**: 클라이언트는 메인 로직이 다른 볼일(파일 업로드/다운로드 등)을 보러 간 동안, 옆방에서 전화를 붙잡고 계속 귀를 기울이는 전담 직원(비동기 고루틴)을 기동시킵니다. 서버에서 "벨(알림)"이 울릴 때마다 그 내용을 즉시 가로채 화면에 실시간 경보 창을 출력해 줍니다.
|
||||
* **상세 설명**: 메인 쓰레드의 블로킹을 방지하기 위해 Go의 경량 쓰레드 고루틴(`go AskSubscribeAlerts`)으로 리스너 루프를 위임 기동합니다. gRPC 스트림 클라이언트의 `stream.Recv()` 메서드는 서버로부터 메시지가 전달될 때까지 스레드 리소스를 낭비하지 않는 대기 상태로 머물며, 데이터 수령 시 콘솔 스트림에 이를 비동기 매핑 출력합니다.
|
||||
|
||||
## 8. 트러블슈팅 (Troubleshooting)
|
||||
|
||||
실습 구동 과정에서 마주할 수 있는 전형적인 에러 현상과 대처 방안입니다.
|
||||
|
||||
### 8.1 bind: address already in use (네트워크 소켓 포트 충돌)
|
||||
* **발생 원인**: gRPC 서버 기동 시 설정한 통신 포트 `:8080`이 이미 다른 네트워크 프로세스나 이전 실습 서버의 비정상 종료 등으로 인해 점유되어 바인딩에 실패한 상태입니다.
|
||||
* **조치 방법**:
|
||||
- `lib/main.go`의 `port` 변수 값을 다른 유휴 포트(예: `:9090`)로 변경한 뒤 다시 실행하십시오. `ServerRun(addr)` 함수가 이 값을 메인 진입점으로부터 인자로 전달받아 TCP 리스너를 생성하므로, `main.go` 한 곳만 수정하면 서버와 클라이언트의 통신 포트가 동시에 성공적으로 변경됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 참고 자료
|
||||
|
||||
* [gRPC와 REST의 차이점 (AWS)](https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/): 두 방식의 특징과 언제 어떤 기술을 선택해야 하는지 친절하게 정리된 공식 블로그 자료입니다.
|
||||
@@ -0,0 +1,66 @@
|
||||
# 2단계: HTTP & Gin 웹 서버 상세 가이드
|
||||
|
||||
⬅ [학습 로드맵으로 돌아가기](MANUSCRIPT.md)
|
||||
|
||||
이 문서는 `grpccanary` 프로젝트의 **2단계: HTTP & Gin 웹 서버**에 대한 이론적 배경과 코드 구조를 설명합니다.
|
||||
|
||||
HTTP(Hypertext Transfer Protocol)는 클라이언트와 웹 서버가 웹에서 리소스를 교환하기 위해 정의한 규약입니다. Go 언어에서는 전통적인 `net/http` 표준 라이브러리 외에도, 성능이 우수하고 사용하기 쉬운 서드파티 웹 프레임워크인 **Gin-gonic**을 주로 사용하여 RESTful API를 신속하게 설계합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. REST API와 HTTP 통신
|
||||
|
||||
REST(Representational State Transfer)는 웹 상의 리소스를 HTTP 메서드(`GET`, `POST`, `PUT`, `DELETE` 등)와 URI 경로를 활용해 상태를 제어하는 아키텍처 스타일입니다.
|
||||
|
||||
* **JSON 데이터 연계**: 대다수의 HTTP REST API는 클라이언트와 데이터를 송수신할 때 데이터 교환 바디(Body)에 텍스트 기반의 **JSON 포맷**을 담아 통신합니다.
|
||||
* **유연성과 한계**: JSON을 사용한 HTTP 통신은 어떤 기기에서나 쉽게 파싱이 가능해 클라이언트-서버 통신에 널리 쓰이지만, 엄격한 스키마 정의가 없어 타입 런타임 오류가 발생하기 쉽고 바이너리 통신에 비해 무겁다는 특징이 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Gin 웹 프레임워크
|
||||
|
||||
Go 진영에서 대표적으로 사랑받는 웹 프레임워크 중 하나로, 빠른 속도와 미들웨어 체인 구성, 편리한 라우팅 그룹 관리 기능을 지원합니다.
|
||||
|
||||
### `gin.Default()` vs `gin.New()`
|
||||
* **`gin.New()`**: 미들웨어가 탑재되지 않은 완전히 빈 라우터 엔진을 인스턴스화합니다. 성능에 민감하고 로깅이나 복구 미들웨어를 직접 커스텀할 때 활용합니다.
|
||||
* **`gin.Default()`**: 기본적인 로깅 미들웨어(`gin.Logger()`)와 오류 복구 미들웨어(`gin.Recovery()`)가 사전 탑재된 상태로 기동되어 보통의 개발 시에 편리하게 사용됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 실습 코드 분석 (`lib/httpentity/server.go`)
|
||||
|
||||
저장소의 [server.go](../lib/httpentity/server.go) 파일에는 Gin 라우터를 구성하고 API 서버와 정적 웹 서빙을 혼합하여 라우팅을 우회 처리하는 설계 패턴이 주석 상태로 존재합니다.
|
||||
|
||||
### 3.1 라우터 엔진 분리 및 통합 핸들링
|
||||
```go
|
||||
apiEngine := gin.New()
|
||||
apiGroup := apiEngine.Group("/api")
|
||||
{
|
||||
apiGroup.GET("/randomNumber", GET_RandomNumber)
|
||||
apiGroup.GET("/randomPassword", GET_RandomPassword)
|
||||
apiGroup.GET("/randomDate", GET_RandomDate)
|
||||
}
|
||||
|
||||
staticEngine := gin.New()
|
||||
staticEngine.Static("/", "./web")
|
||||
```
|
||||
* **API 그룹 분리**: `/api`로 들어오는 모든 요청을 `apiEngine`이 받아서 각각 `/randomNumber`, `/randomPassword` 등의 실제 비즈니스 로직 핸들러로 전달하게 설계되어 있습니다.
|
||||
* **정적 서빙**: 그 외의 요청에 대해서는 `./web` 경로의 정적 파일(HTML, CSS, JS)을 제공하기 위한 `staticEngine`을 별개 구성합니다.
|
||||
* **단일 엔트리 우회**: 메인 엔진(`r`)은 전방위 라우팅 `/*any`를 구성하여, 경로 패턴에 `/api`가 들어있으면 API 엔진에 역할을 위임하고, 그렇지 않으면 정적 파일 서버로 포워딩합니다.
|
||||
|
||||
### 3.2 JSON 디코딩 및 API 핸들러
|
||||
```go
|
||||
func GET_RandomNumber(c *gin.Context) {
|
||||
dec := json.NewDecoder(c.Request.Body)
|
||||
obj := map[string]interface{}{}
|
||||
dec.Decode(&obj)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"value": 10,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
```
|
||||
* `c.Request.Body`를 `json.NewDecoder`로 받아서 맵에 디코딩하여 요청 매개변수(시드 등)를 취득합니다.
|
||||
* 비즈니스 연산 후 `c.JSON()` 함수를 통해 원하는 응답 구조를 손쉽게 클라이언트에 JSON 문자열 형태로 내보내 줍니다.
|
||||
@@ -0,0 +1,409 @@
|
||||
# HTTP/3(QUIC) 기반 gRPC 구현기: 마주친 문제와 해결 전략
|
||||
|
||||
이 문서는 `lib/grpc/http3` 예제를 구현하는 과정에서 실제로 부딪혔던 기술적 난제들과 그 해결 방법을 기록한 실전 참고 자료입니다. HTTP/2 기반의 기존 `lib/grpc/basic` 예제와 달리, HTTP/3의 전송 계층인 **QUIC**은 Go 표준 네트워킹 인터페이스나 `google.golang.org/grpc`(grpc-go)가 기대하는 모양과 근본적으로 다르기 때문에, 단순히 프로토콜만 바꾼다고 되는 일이 아니었습니다. 추후 HTTP/3 기반 gRPC를 직접 구현하려는 분들이 동일한 시행착오를 반복하지 않도록, 문제 상황 → 해결 전략 → 실제 코드 순서로 정리했습니다.
|
||||
|
||||
각 절은 **간단 설명**(핵심을 한두 문장으로 요약)과 **상세 설명**(기술적 근거와 세부 동작)의 이중 레이어로 구성되어 있습니다.
|
||||
|
||||
> [!WARNING]
|
||||
> **이 문서가 다루는 어댑터 구조는 "진짜 HTTP/3 프레이밍"이 아니며, QUIC의 스트림 단위 HOL 블로킹 방지 효과를 완전히 누리지 못합니다.** 자세한 내용은 §3.3을 반드시 읽어보시기 바랍니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 근본 제약: grpc-go는 QUIC을 모른다
|
||||
|
||||
* **간단 설명**: `google.golang.org/grpc`는 HTTP/2 프로토콜을 스스로 구현해서 사용하며, HTTP/3(QUIC)를 인식하지 못합니다. 그래서 "QUIC 위에서 도는 gRPC"를 만들려면 grpc-go의 코드를 고치는 대신, grpc-go가 이미 알고 있는 표준 Go 인터페이스(`net.Listener`, `net.Conn`)의 모습으로 QUIC을 "위장"시켜야 했습니다.
|
||||
* **상세 설명**: grpc-go의 `grpc.Server.Serve(lis net.Listener)`와 `grpc.WithContextDialer(...)`는 전송 계층이 TCP인지, Unix 소켓인지, 심지어 인메모리 파이프(`bufconn`)인지 전혀 신경 쓰지 않습니다. `net.Listener`/`net.Conn` 인터페이스만 만족하면 grpc-go는 그 위에서 자신의 HTTP/2 프레이밍을 그대로 수행합니다. 이 성질을 이용해, `github.com/quic-go/quic-go`가 제공하는 QUIC 커넥션·스트림을 `net.Conn`으로 감싸는 어댑터만 작성하면 grpc-go 코드를 한 줄도 건드리지 않고 QUIC 전송 위에 gRPC를 얹을 수 있습니다. 이 접근은 "RFC 9114를 완전히 준수하는 진짜 HTTP/3 프레이밍"은 아니지만(그건 `quic-go/http3`의 `http.Handler` 기반 서버가 담당하는 영역이며 grpc-go의 API를 쓸 수 없게 됩니다), 전송 계층 자체는 실제 QUIC/UDP이며 gRPC의 개발 경험(protoc 스텁, 서비스 등록 등)을 그대로 유지할 수 있다는 점에서 이 예제가 채택한 실용적인 절충안입니다. **다만 이 절충안에는 중요한 성능적 제약이 뒤따르며, 이는 §3.3에서 자세히 다룹니다.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Go `net.Listener`/`net.Conn` 규격에 QUIC 맞춰 넣기
|
||||
|
||||
### 2.1 `quicNetConn`: QUIC 스트림을 `net.Conn`으로 감싸기
|
||||
|
||||
* **간단 설명**: `quic-go`의 스트림(`quic.Stream`)은 읽고 쓰는 기능은 이미 갖추고 있지만, "내 주소가 뭐야?"에 해당하는 `LocalAddr()`/`RemoteAddr()`는 없습니다. 이 두 메서드만 얇게 대신 구현해 주는 별도 오브젝트를 만들면, `net.Conn`의 모든 요구 조건이 채워집니다.
|
||||
* **상세 설명**: `net.Conn` 인터페이스는 `Read`/`Write`/`Close`/`LocalAddr`/`RemoteAddr`/`SetDeadline`/`SetReadDeadline`/`SetWriteDeadline`를 요구합니다. `quic.Stream`은 `Read`/`Write`/`Close`/`SetDeadline`류를 이미 제공하지만, 주소 정보는 스트림이 아니라 그 스트림이 속한 `quic.Conn`(QUIC 커넥션)이 들고 있습니다. 따라서 다음과 같이 Go의 구조체 임베딩(embedding)을 활용해 `*quic.Stream`의 메서드를 그대로 승격시키고, 부족한 두 메서드만 `conn` 필드로 위임하는 방식으로 해결했습니다:
|
||||
|
||||
```go
|
||||
// lib/grpc/http3/server.go
|
||||
type quicNetConn struct {
|
||||
*quic.Stream // Read/Write/Close/SetDeadline류를 그대로 물려받음
|
||||
conn *quic.Conn // LocalAddr/RemoteAddr 위임용
|
||||
}
|
||||
|
||||
func (c *quicNetConn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *quicNetConn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
```
|
||||
이 타입 하나로 서버(`quicListener.Accept()`가 반환)와 클라이언트(`quicDialer`가 반환) 양쪽에서 재사용됩니다(같은 `package http3` 안이므로 `client.go`에서도 별도 선언 없이 그대로 참조).
|
||||
|
||||
### 2.2 클라이언트 측: `grpc.WithContextDialer`로 다이얼러 갈아끼우기
|
||||
|
||||
* **간단 설명**: grpc-go 클라이언트가 "연결을 만들어라"라고 요청할 때 기본으로는 TCP로 연결하지만, `WithContextDialer` 옵션에 원하는 함수를 꽂아주면 그 연결 방식을 통째로 바꿀 수 있습니다. 이 자리에 QUIC 연결·스트림 개설 로직을 넣었습니다.
|
||||
* **상세 설명**:
|
||||
```go
|
||||
// lib/grpc/http3/client.go
|
||||
func quicDialer(tlsConf *tls.Config) func(context.Context, string) (net.Conn, error) {
|
||||
return func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
qconn, err := quic.DialAddr(ctx, addr, tlsConf, &quic.Config{
|
||||
KeepAlivePeriod: 10 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("quic dial failed: %w", err)
|
||||
}
|
||||
|
||||
stream, err := qconn.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
_ = qconn.CloseWithError(0, "failed to open stream")
|
||||
return nil, fmt.Errorf("failed to open stream: %w", err)
|
||||
}
|
||||
|
||||
return &quicNetConn{Stream: stream, conn: qconn}, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
`quic.DialAddr`로 QUIC 커넥션(및 TLS 핸드셰이크)을 먼저 완성한 뒤, `OpenStreamSync`로 그 커넥션 위에 논리적 스트림을 **딱 하나만** 열고, 이를 `quicNetConn`으로 감싸 grpc-go에게 "이게 네가 원하는 `net.Conn`이야"라고 건네주는 구조입니다. 스트림 개설이 실패하면 이미 맺어진 QUIC 커넥션을 `CloseWithError`로 정리해, 자원이 새지 않도록 했습니다. **이 "스트림을 하나만 연다"는 설계가 §3.3에서 다루는 제약의 직접적인 원인이 됩니다.**
|
||||
|
||||
---
|
||||
|
||||
## 3. 비동기 리스너 아키텍처와 헤드오브라인(HOL) 블로킹 예방
|
||||
|
||||
이 예제에서 **가장 많은 시행착오를 겪은 부분**입니다.
|
||||
|
||||
### 3.1 처음에 시도했다가 폐기한 설계 — 왜 위험했는가
|
||||
|
||||
* **간단 설명**: 처음에는 "새 QUIC 연결이 들어오면 그 자리에서 바로 스트림까지 열어서 반환하자"는 단순한 방식으로 `Accept()`를 짰습니다. 하지만 이 방식은 클라이언트 한 명이 스트림을 늦게 열면, **다른 모든 클라이언트의 접속이 전부 멈춰버리는** 심각한 문제가 있었습니다.
|
||||
* **상세 설명**: grpc-go의 `Server.Serve(lis)`는 내부적으로 `for { rawConn, err := lis.Accept(); ... go s.handleRawConn(rawConn) }` 형태의 **단일 고루틴 직렬 루프**로 동작합니다(`google.golang.org/grpc`의 `server.go` 소스에서 직접 확인한 구조입니다). 즉 `lis.Accept()` 한 번의 호출이 끝나야만 다음 `Accept()`로 넘어갑니다. 만약 커스텀 `Accept()` 구현이 내부에서 `quic.Listener.Accept(ctx)` → `quic.Conn.AcceptStream(ctx)`를 **동기적으로 순차 호출**한다면:
|
||||
1. **헤드오브라인(HOL) 블로킹**: 클라이언트 A가 QUIC 커넥션은 맺었지만 스트림 개설(`OpenStreamSync`)이 지연되면, 서버의 `Accept()`는 A의 `AcceptStream()` 호출에서 멈춥니다. 그 사이 클라이언트 B가 새로 접속을 시도해도, 서버의 단일 accept 루프가 A에 붙잡혀 있으므로 B는 무한정 대기하게 됩니다.
|
||||
2. **동일 커넥션의 두 번째 이후 스트림을 영영 받을 수 없음**: `Accept()` 한 번이 스트림 하나만 반환하고 나면, 다음 반복에서는 다시 `quic.Listener.Accept()`(새 커넥션 수락)로 넘어갑니다. 즉 이미 맺어진 커넥션 위에서 클라이언트가 스트림을 추가로 열어도, 서버는 그 커넥션으로 다시 돌아와 확인하지 않으므로 영원히 수락되지 않습니다.
|
||||
|
||||
이 두 가지는 실제로 재현 가능한 설계 결함이며, 특히 2번은 단일 QUIC 연결 안에서 여러 RPC(예: Unary 호출을 여러 번 하거나, 스트리밍 RPC를 여러 개 열 때)를 처리해야 하는 gRPC의 일반적인 사용 패턴과 정면으로 충돌합니다.
|
||||
|
||||
### 3.2 최종 채택한 구조: 커넥션 수락과 스트림 수락의 완전한 비동기 분리
|
||||
|
||||
* **간단 설명**: "새 연결을 받는 일"과 "그 연결 안에서 스트림을 받는 일"을 서로 다른 고루틴으로 완전히 분리하고, 그 결과물을 채널(channel) 하나로 모아서 `Accept()`가 그 채널에서만 기다리게 만들었습니다. 이렇게 하면 한 클라이언트의 지연이 다른 클라이언트나 같은 클라이언트의 다른 스트림에 전혀 영향을 주지 않습니다.
|
||||
* **상세 설명**: 아래 세 함수가 각자 독립적인 역할을 맡아 비동기로 동작합니다.
|
||||
|
||||
```go
|
||||
// lib/grpc/http3/server.go
|
||||
type quicListener struct {
|
||||
lis *quic.Listener
|
||||
connChan chan net.Conn
|
||||
errChan chan error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewQuicListener(lis *quic.Listener) *quicListener {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ql := &quicListener{
|
||||
lis: lis,
|
||||
connChan: make(chan net.Conn, 100),
|
||||
errChan: make(chan error, 10),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
go ql.listenLoop()
|
||||
return ql
|
||||
}
|
||||
|
||||
// 커넥션 수락 전담 — 새 QUIC 연결이 오면 즉시 별도 고루틴에 위임하고 곧바로 다음 연결을 기다림
|
||||
func (ql *quicListener) listenLoop() {
|
||||
for {
|
||||
qconn, err := ql.lis.Accept(ql.ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case ql.errChan <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
go ql.acceptStreams(qconn)
|
||||
}
|
||||
}
|
||||
|
||||
// 스트림 수락 전담 — 커넥션 1개마다 독립된 고루틴으로 실행되며, 그 커넥션이 살아있는 한 계속 스트림을 받아들임
|
||||
func (ql *quicListener) acceptStreams(qconn *quic.Conn) {
|
||||
for {
|
||||
stream, err := qconn.AcceptStream(ql.ctx)
|
||||
if err != nil {
|
||||
// 이 커넥션이 끊기면 이 고루틴만 조용히 종료 (다른 커넥션은 영향 없음)
|
||||
return
|
||||
}
|
||||
ql.connChan <- &quicNetConn{Stream: stream, conn: qconn}
|
||||
}
|
||||
}
|
||||
|
||||
// grpc-go가 실제로 호출하는 지점 — 채널에서 결과가 나올 때까지만 기다림 (뒷단의 QUIC 세부사항과 완전히 분리됨)
|
||||
func (ql *quicListener) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn := <-ql.connChan:
|
||||
return conn, nil
|
||||
case err := <-ql.errChan:
|
||||
return nil, err
|
||||
case <-ql.ctx.Done():
|
||||
return nil, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
func (ql *quicListener) Close() error {
|
||||
ql.cancel()
|
||||
return ql.lis.Close()
|
||||
}
|
||||
|
||||
func (ql *quicListener) Addr() net.Addr {
|
||||
return ql.lis.Addr()
|
||||
}
|
||||
```
|
||||
|
||||
핵심은 `listenLoop`(커넥션 수락)과 `acceptStreams`(스트림 수락, **커넥션 1개당 고루틴 1개**)가 서로를 절대 기다리지 않는다는 점입니다. `acceptStreams`는 자신이 담당하는 커넥션이 살아있는 동안 계속 루프를 돌며 새 스트림이 열릴 때마다 이를 `connChan`에 밀어 넣고, `Accept()`는 이 채널만 바라보므로 몇 명의 클라이언트가 붙어있든, 각 클라이언트가 스트림을 몇 개나 여는지와 무관하게 즉시 반응할 수 있습니다. `net.Listener` 인터페이스가 요구하는 `Close()`/`Addr()`도 함께 구현해 커스텀 리스너가 완전한 `net.Listener`로 동작하도록 마감했습니다.
|
||||
|
||||
이 어댑터를 `grpc.NewServer().Serve(qlis)`에 그대로 넘기기만 하면, grpc-go는 이후 표준 HTTP/2 프레이밍을 그 위에서 동일하게 수행합니다(§1 참고) — 즉 이 절 이후로는 grpc-go 코드를 전혀 건드릴 필요가 없습니다.
|
||||
|
||||
* **검증 방법**: 단순히 빌드/실행이 되는 것만으로는 이 문제가 실제로 해결됐는지 알 수 없습니다. 반드시 **하나의 QUIC 연결(하나의 `ClientRun` 세션) 안에서 2회 이상의 RPC 호출**(Unary 반복 호출 또는 스트리밍 RPC)이 모두 성공하는지 확인해야 합니다. `lib/grpc/http3/http3_test.go`의 `TestHttp3ServerClient`가 정확히 이 시나리오(Unary Ping 2회 + 양방향 스트리밍 3회, 모두 같은 연결 위에서)를 검증합니다.
|
||||
|
||||
### 3.3 ⚠️ 이 구조가 해결하는 HOL 블로킹과 해결하지 못하는 HOL 블로킹은 다르다 (중요한 제약사항)
|
||||
|
||||
* **간단 설명**: §3.2에서 해결한 것은 "서버가 여러 클라이언트의 접속을 동시에 잘 받아주는가"라는 **리스너(Accept) 레벨의 HOL 블로킹**입니다. 이것과 별개로, **하나의 gRPC 연결 안에서 오가는 여러 RPC들이 QUIC 스트림 하나에 몰려 있다는 문제**는 이 구조로 해결되지 않습니다. 패킷 유실이 발생하면 그 스트림에 실려 있던 모든 RPC가 함께 지연됩니다 — 이는 기존 HTTP/2-over-TCP와 동일한 한계입니다.
|
||||
* **상세 설명**: §2.2에서 확인했듯, `quicDialer`는 `qconn.OpenStreamSync(ctx)`를 **정확히 한 번만** 호출해 QUIC 스트림을 하나 열고, 이를 grpc-go에게 "네가 쓸 유일한 `net.Conn`"으로 건네줍니다. grpc-go는 통상 하나의 대상(target)에 대해 하나의 전송 연결만 수립하고, 그 위에서 자신의 HTTP/2 멀티플렉싱으로 **모든 동시 RPC(Unary, 서버/클라이언트/양방향 스트리밍 불문)를 처리**합니다. 즉:
|
||||
|
||||
```text
|
||||
[클라이언트가 동시에 호출하는 RPC 3개]
|
||||
│
|
||||
▼
|
||||
grpc-go의 HTTP/2 멀티플렉싱 (여러 논리적 스트림 프레임 생성)
|
||||
│
|
||||
▼
|
||||
단 하나의 quicNetConn (= 단 하나의 QUIC 스트림)
|
||||
│
|
||||
▼
|
||||
QUIC 전송 계층 (신뢰적·순서 보장 단일 스트림)
|
||||
```
|
||||
|
||||
QUIC의 핵심 이점 중 하나는 "스트림이 여러 개일 때, 한 스트림의 패킷 유실이 다른 스트림에 영향을 주지 않는다"는 것입니다. 그러나 위 구조에서는 애초에 QUIC 스트림이 **1개뿐**이므로, 그 위에서 벌어지는 QUIC의 재전송·순서 보장 대기는 결국 grpc-go가 그 위에 얹어 놓은 모든 논리적 gRPC 호출에 똑같이 영향을 미칩니다. 다시 말해, **"UDP/QUIC이라는 전송 계층을 사용한다"는 사실과 "QUIC의 스트림 단위 HOL 블로킹 방지 이점을 실제로 누린다"는 것은 별개의 문제이며, 이 어댑터 방식은 전자만 해당하고 후자는 해당하지 않습니다.**
|
||||
|
||||
이 한계를 실제로 완화하려면(이번 예제의 범위를 벗어나는 심화 주제입니다), 다음과 같은 방향을 고려할 수 있습니다:
|
||||
- RPC(또는 RPC 그룹)별로 **별도의 QUIC 스트림**을 열고, grpc-go 대신 자체 RPC 디스패치 로직을 구현(즉 §1에서 언급한 "진짜 HTTP/3" B안에 가까워짐).
|
||||
- 커넥션 풀링을 도입해 무거운 스트리밍 RPC and 가벼운 Unary RPC를 서로 다른 QUIC 스트림(혹은 별도 QUIC 커넥션)으로 분리.
|
||||
|
||||
**이 예제는 "QUIC 전송 위에서 gRPC 개발 경험을 그대로 유지하는 실용적 절충안"을 보여주는 것이 목적이며, 프로덕션 환경에서 QUIC의 HOL 블로킹 방지 이점을 온전히 활용하려면 위와 같은 추가 설계가 필요함을 명확히 인지해야 합니다.**
|
||||
|
||||
### 3.4 💡 Tip: Graceful Shutdown 시 QUIC 커넥션 누수 방지 (권장 개선안 — 현재 미반영)
|
||||
|
||||
* **간단 설명**: 현재 `quicListener.Close()`는 리스너 자체만 닫을 뿐, 이미 연결되어 있던 개별 QUIC 커넥션들은 닫지 않습니다. 그래서 서버를 정상 종료해도 이미 접속해 있던 클라이언트와의 물리적 QUIC 연결은 클라이언트가 스스로 끊거나 타임아웃이 될 때까지 좀비 상태로 남습니다. 이를 막으려면 리스너가 자신이 수락한 커넥션 목록을 직접 기억해 두었다가, 종료 시점에 일일이 닫아주어야 합니다.
|
||||
* **상세 설명**: `quic.Listener.Close()`는 (Go의 일반적인 `net.Listener` 관용구와 마찬가지로) **새로운 연결 수락을 중단시킬 뿐, 이미 `Accept()`된 개별 `*quic.Conn`에는 관여하지 않습니다.** `grpc.Server.GracefulStop()`도 자신이 인계받은 `net.Conn`(= QUIC 스트림)들만 정리하므로, 그 밑에 있는 물리적 QUIC 커넥션 자체는 아무도 명시적으로 닫아주지 않는 사각지대가 생깁니다. 장기 기동 서버에서 이 gRPC 컴포넌트만 반복적으로 그레이스풀 재시작하는 운영 시나리오라면, 포트는 매번 정상적으로 풀려도 프로세스 내부에 유효하지 않은 UDP 커넥션 핸들이 누적될 수 있습니다.
|
||||
|
||||
**현재 저장소의 `lib/grpc/http3/server.go`에는 아직 반영되어 있지 않은 권장 보강 코드**는 다음과 같습니다(수락된 `*quic.Conn`들을 맵으로 추적하고, `Close()` 시점에 순회하며 명시적으로 종료):
|
||||
|
||||
```go
|
||||
// 권장 보강안 — quicListener에 커넥션 추적 필드 추가
|
||||
type quicListener struct {
|
||||
lis *quic.Listener
|
||||
connChan chan net.Conn
|
||||
errChan chan error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
mu sync.Mutex
|
||||
conns map[*quic.Conn]struct{} // 추가: 현재 살아있는 QUIC 커넥션 추적
|
||||
}
|
||||
|
||||
func NewQuicListener(lis *quic.Listener) *quicListener {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ql := &quicListener{
|
||||
lis: lis,
|
||||
connChan: make(chan net.Conn, 100),
|
||||
errChan: make(chan error, 10),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
conns: make(map[*quic.Conn]struct{}), // 추가
|
||||
}
|
||||
go ql.listenLoop()
|
||||
return ql
|
||||
}
|
||||
|
||||
func (ql *quicListener) listenLoop() {
|
||||
for {
|
||||
qconn, err := ql.lis.Accept(ql.ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case ql.errChan <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
ql.mu.Lock()
|
||||
ql.conns[qconn] = struct{}{} // 추가: 커넥션 등록
|
||||
ql.mu.Unlock()
|
||||
go ql.acceptStreams(qconn)
|
||||
}
|
||||
}
|
||||
|
||||
func (ql *quicListener) acceptStreams(qconn *quic.Conn) {
|
||||
defer func() {
|
||||
ql.mu.Lock()
|
||||
delete(ql.conns, qconn) // 추가: 커넥션 종료 시 목록에서 제거
|
||||
ql.mu.Unlock()
|
||||
}()
|
||||
for {
|
||||
stream, err := qconn.AcceptStream(ql.ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ql.connChan <- &quicNetConn{Stream: stream, conn: qconn}
|
||||
}
|
||||
}
|
||||
|
||||
func (ql *quicListener) Close() error {
|
||||
ql.cancel()
|
||||
err := ql.lis.Close()
|
||||
|
||||
ql.mu.Lock()
|
||||
defer ql.mu.Unlock()
|
||||
for qconn := range ql.conns {
|
||||
qconn.CloseWithError(0, "server shutting down") // 추가: 잔여 커넥션 명시적 종료
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
`sync.Mutex`로 맵 접근을 보호하는 이유는 `listenLoop`(등록)과 `acceptStreams`(제거) 여러 고루틴이 동시에 같은 맵을 건드릴 수 있기 때문입니다. 이 보강을 적용하면 `ServerRun`이 반환하는 `cleanup` 함수(`server.GracefulStop(); qlis.Close()`)를 호출했을 때, 리스너뿐 아니라 그 시점까지 맺어져 있던 모든 QUIC 커넥션까지 확실하게 정리됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. TLS 1.3 강제와 자체 서명 인증서
|
||||
|
||||
### 4.1 QUIC에서 TLS는 선택이 아니다
|
||||
|
||||
* **간단 설명**: TCP는 TLS 없이도(평문으로) 연결할 수 있지만, QUIC은 프로토콜 설계 자체에 TLS 1.3이 포함되어 있어 **TLS 없는 QUIC 연결은 존재하지 않습니다**. 그래서 `lib/grpc/basic`에서 썼던 "완전 평문" 방식은 QUIC에서는 애초에 선택지가 아니었습니다.
|
||||
* **상세 설명**: `quic.ListenAddr`/`quic.DialAddr`는 `*tls.Config` 인자가 필수로 요구하며, `nil`을 넘기면 즉시 에러가 납니다. 로컬 데모/학습 환경에서는 공인 인증기관(CA)이 발급한 정식 인증서를 쓸 수 없으므로, **실행 시점에 자체 서명(self-signed) 인증서를 코드로 즉석 생성**하는 방식을 택했습니다.
|
||||
|
||||
### 4.2 자체 서명 인증서 즉석 생성
|
||||
|
||||
* **간단 설명**: `crypto/tls`, `crypto/x509` 같은 Go 표준 라이브러리만으로 "가짜 인증기관 없이 나 스스로 서명한 인증서"를 그 자리에서 만들어 서버에 장착합니다. 외부 파일이나 별도 도구가 전혀 필요 없습니다.
|
||||
* **상세 설명**:
|
||||
```go
|
||||
// lib/grpc/http3/server.go
|
||||
func generateTLSConfig() (*tls.Config, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"gRPC HTTP3 Canary"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
||||
}
|
||||
|
||||
// template을 자기 자신으로 서명(self-signed) — 별도의 상위 CA가 없음
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
...
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{"grpc-http3-canary"},
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
`x509.CreateCertificate(..., &template, &template, ...)`처럼 서명 대상과 서명자(issuer) 템플릿을 동일하게 넘기는 것이 "자체 서명"의 핵심입니다. `DNSNames`/`IPAddresses`에 로컬 접속에 쓰일 이름과 주소를 미리 등록해 두어야 TLS 핸드셰이크의 호스트 이름 검증(있을 경우)을 통과할 수 있습니다. `NextProtos`는 ALPN(Application-Layer Protocol Negotiation) 값으로, **서버와 클라이언트가 반드시 동일한 문자열**(`"grpc-http3-canary"`)을 사용해야 QUIC 핸드셰이크 단계에서 서로 호환되는 상위 프로토콜임을 확인하고 연결을 진행합니다.
|
||||
|
||||
### 4.3 클라이언트의 `InsecureSkipVerify`와 grpc-go의 `insecure.NewCredentials()` — 이름은 비슷해도 역할은 다르다
|
||||
|
||||
* **간단 설명**: 클라이언트 코드에 "insecure"라는 단어가 두 번 등장하는데, 하나는 "자체 서명 인증서라 진위를 검증할 CA가 없으니 검증을 생략한다"는 뜻이고, 다른 하나는 "QUIC이 이미 암호화를 다 했으니 grpc-go가 별도로 또 암호화할 필요는 없다"는 뜻입니다. 둘 다 "완전히 안전하지 않다"는 의미가 아닙니다.
|
||||
* **상세 설명**:
|
||||
```go
|
||||
// lib/grpc/http3/client.go
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: true, // (1) 자체 서명 인증서의 CA 체인 검증을 건너뜀
|
||||
NextProtos: []string{"grpc-http3-canary"},
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()), // (2) grpc-go 자체 레이어에서는 암호화를 하지 않음
|
||||
grpc.WithContextDialer(quicDialer(tlsConf)),
|
||||
)
|
||||
```
|
||||
(1) `tls.Config.InsecureSkipVerify`는 QUIC 핸드셰이크 단계에서 서버 인증서의 신뢰 체인을 검증하지 않겠다는 설정입니다. 실제 QUIC 연결 자체는 여전히 TLS 1.3으로 **암호화**되어 있으며, 다만 "이 인증서가 신뢰할 만한 CA가 발급한 것인지"는 확인하지 않는다는 뜻입니다(로컬 데모이므로 자체 서명 인증서를 신뢰하기 위한 실용적 선택).
|
||||
(2) `grpc.WithTransportCredentials(insecure.NewCredentials())`는 grpc-go 자신의 트랜스포트 레벨 보안 레이어를 비활성화하는 옵션입니다. 이는 grpc-go가 "내가 별도로 TLS 핸드셰이크를 또 하지 않겠다"는 의미일 뿐이며, 이미 QUIC이 그 아래에서 실제 암호화 채널을 제공하고 있으므로 이중으로 암호화할 필요가 없기 때문에 정확한 설정입니다. 이 둘을 혼동해 "이 코드는 완전히 안전하지 않은 통신을 한다"고 오해하지 않도록 주의가 필요합니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 포트 바인딩과 에러 진단
|
||||
|
||||
### 5.1 UDP 기반이라는 것의 함의
|
||||
|
||||
* **간단 설명**: QUIC은 TCP가 아니라 UDP 위에서 동작합니다. 그래서 `:8080` 같은 주소로 리슨할 때도 내부적으로는 TCP 소켓이 아니라 UDP 소켓을 엽니다. 에러 메시지에도 `tcp` 대신 `udp`가 등장합니다.
|
||||
* **상세 설명**: `quic.ListenAddr(addr, tlsConf, cfg)`는 내부적으로 `net.ListenPacket("udp", addr)`에 준하는 동작을 수행합니다. 따라서 포트가 이미 사용 중일 때 발생하는 에러 메시지도 `lib/grpc/basic`에서 익숙했던 `listen tcp :8080: bind: address already in use`가 아니라 **`listen udp :8080: bind: address already in use`** 형태로 나타납니다. 트러블슈팅 시 이 차이를 인지하지 못하면 "TCP 포트는 비어있는데 왜 에러가 나지?"라며 헤맬 수 있습니다.
|
||||
|
||||
### 5.2 실전 사례: 에러를 조용히 삼키던 버그와 그 수정
|
||||
|
||||
* **간단 설명**: `ServerRun` and `ClientRun`은 각각 에러를 반환하도록 설계되어 있지만, 이를 호출하는 쪽에서 반환값을 확인하지 않고 버려버리면 문제가 생겨도 **아무 메시지 없이 그냥 조용히 끝나버립니다**. 실제로 이 예제에서도 이런 실수가 있었고, 반환값을 제대로 확인하도록 고친 뒤에야 실패 원인이 화면에 보이기 시작했습니다.
|
||||
* **상세 설명**: `ServerRun(addr string) (*quic.Listener, func(), error)`과 `ClientRun(addr string) error`는 둘 다 `error`를 반환하는 관용적인(idiomatic) Go 시그니처입니다. 그런데 최초 버전의 `lib/main.go`는 다음처럼 반환값을 그대로 버렸습니다:
|
||||
```go
|
||||
// 수정 전 — 에러가 발생해도 아무 것도 출력되지 않음
|
||||
go http3.ServerRun(port)
|
||||
...
|
||||
http3.ClientRun(port)
|
||||
```
|
||||
포트가 이미 사용 중인 상태에서 이 코드를 실행하면, 프로그램은 **에러 메시지 없이 정상 종료(exit code 0)**됩니다 — 실패했다는 사실조차 알 수 없는 최악의 실패 모드입니다. 이를 다음과 같이 수정하여 두 함수의 에러를 모두 명시적으로 확인·출력하도록 했습니다:
|
||||
```go
|
||||
// 수정 후
|
||||
go func() {
|
||||
if _, _, err := http3.ServerRun(port); err != nil {
|
||||
fmt.Println("ServerRun error:", err)
|
||||
}
|
||||
}()
|
||||
...
|
||||
if err := http3.ClientRun(port); err != nil {
|
||||
fmt.Println("ClientRun error:", err)
|
||||
}
|
||||
```
|
||||
실제로 포트 충돌 상황을 인위적으로 재현해 검증한 결과, 수정 후에는 다음과 같이 원인을 즉시 진단할 수 있는 메시지가 출력됨을 확인했습니다:
|
||||
```text
|
||||
ServerRun error: listen udp :8080: bind: address already in use
|
||||
ClientRun error: unary ping 1 failed: rpc error: code = DeadlineExceeded desc = context deadline exceeded while waiting for connections to become ready
|
||||
```
|
||||
서버 쪽 에러(`listen udp ...`)가 근본 원인이고, 클라이언트 쪽 에러(`DeadlineExceeded ... waiting for connections to become ready`)는 그 결과로 연결이 아예 이루어지지 않아 타임아웃된 **연쇄 증상**입니다. 이처럼 두 에러가 함께 출력되면 "서버가 애초에 뜨지 못했다"는 진짜 원인을 클라이언트 쪽 에러 메시지만으로 오판하지 않고 빠르게 좁혀나갈 수 있습니다.
|
||||
|
||||
### 5.3 흔히 마주치는 에러 유형 요약
|
||||
|
||||
| 에러 메시지 패턴 | 원인 | 진단 포인트 |
|
||||
|---|---|---|
|
||||
| `listen udp :PORT: bind: address already in use` | 다른 프로세스(또는 이전 실습 세션)가 같은 UDP 포트를 점유 중 | `port` 변수 값을 바꾸거나 점유 프로세스 종료 |
|
||||
| `... waiting for connections to become ready` (클라이언트 측 `DeadlineExceeded`) | 서버가 아예 뜨지 못했거나, 서버·클라이언트의 TLS/ALPN(`NextProtos`) 설정이 어긋남 | 서버 쪽 로그(`ServerRun` 에러)를 먼저 확인, 양쪽의 `NextProtos` 문자열이 정확히 일치하는지 대조 |
|
||||
| `quic dial failed: ...` | QUIC 핸드셰이크 자체가 실패(네트워크 도달 불가, UDP 차단 등) | 로컬 방화벽/샌드박스 환경이 UDP를 허용하는지 확인 |
|
||||
| `failed to open stream: ...` | QUIC 연결은 맺어졌으나 스트림 개설이 실패(커넥션이 이미 닫히는 중 등) | 커넥션 유휴 타임아웃(`KeepAlivePeriod`) 설정과 서버 종료 타이밍 점검 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 마무리 체크리스트
|
||||
|
||||
향후 HTTP/3 기반 gRPC 예제를 새로 만들거나 확장할 때, 아래 항목을 순서대로 점검하면 이 문서에서 다룬 문제들을 다시 겪지 않을 수 있습니다.
|
||||
|
||||
- [ ] grpc-go를 그대로 쓸지, `quic-go/http3`의 순정 HTTP/3 서버로 갈아탈지 방향을 먼저 결정했는가 (§1)
|
||||
- [ ] QUIC 스트림/커넥션을 감싸는 `net.Conn` 어댑터에서 `LocalAddr`/`RemoteAddr`까지 모두 구현했는가 (§2.1)
|
||||
- [ ] 서버 리스너 어댑터의 `Accept()`가 **절대** `AcceptStream()`을 동기적으로 직접 호출하지 않고, 커넥션 수락과 스트림 수락이 독립된 고루틴+채널로 분리되어 있는가 (§3.2)
|
||||
- [ ] 검증 시 **하나의 연결 안에서 2회 이상 RPC**가 성공하는 케이스를 반드시 테스트했는가 (§3.2)
|
||||
- [ ] **(신규)** "이 구조가 QUIC의 스트림 단위 HOL 블로킹 방지 이점을 실제로 제공하지 않는다"는 제약을 문서·설계 리뷰에서 명확히 공유했는가, 프로덕션에서 이 이점이 정말 필요하다면 RPC별 스트림 분리 등 추가 설계를 검토했는가 (§3.3)
|
||||
- [ ] **(신규)** 서버 그레이스풀 셧다운 시 리스너뿐 아니라 이미 수락된 QUIC 커넥션까지 명시적으로 정리하는 로직이 있는가, 없다면 장기 기동/반복 재시작 환경에서 커넥션 누수 위험을 인지하고 있는가 (§3.4)
|
||||
- [ ] 서버·클라이언트 양쪽의 TLS `NextProtos`(ALPN) 문자열이 정확히 일치하는가 (§4.2)
|
||||
- [ ] `InsecureSkipVerify`/`insecure.NewCredentials()`가 각각 무엇을 생략하는 것인지 정확히 이해하고 사용했는가 (§4.3)
|
||||
- [ ] 포트 바인딩 에러 메시지가 `tcp`가 아닌 `udp`로 나타난다는 점을 팀 문서/트러블슈팅 가이드에 반영했는가 (§5.1)
|
||||
- [ ] `ServerRun`/`ClientRun` 등 에러를 반환하는 함수의 호출부에서 반환값을 빠짐없이 확인·출력하는가 (§5.2)
|
||||
|
||||
---
|
||||
|
||||
## 참고
|
||||
|
||||
* [gRPC-Go 공식 저장소](https://github.com/grpc/grpc-go)
|
||||
* [quic-go 공식 저장소](https://github.com/quic-go/quic-go)
|
||||
* 본 프로젝트 내 관련 코드: [`lib/grpc/http3/server.go`](../lib/grpc/http3/server.go), [`lib/grpc/http3/client.go`](../lib/grpc/http3/client.go), [`lib/grpc/http3/protoapi.proto`](../lib/grpc/http3/protoapi.proto)
|
||||
@@ -0,0 +1,61 @@
|
||||
# 1단계: JSON 데이터 다루기 상세 가이드
|
||||
|
||||
⬅ [학습 로드맵으로 돌아가기](MANUSCRIPT.md)
|
||||
|
||||
이 문서는 `grpccanary` 프로젝트의 **1단계: JSON 데이터 다루기**에 대한 이론적 배경과 코드 예시를 설명합니다.
|
||||
|
||||
Go 표준 라이브러리에는 JSON 데이터를 처리하기 위한 `encoding/json` 패키지가 포함되어 있습니다. Go는 구조체 태그(Struct Tag)를 사용하여 Go 구조체와 JSON 필드를 손쉽게 매핑할 수 있는 기능을 제공합니다. 이 태그는 Go 구조체를 JSON으로 변환하거나, JSON을 Go 구조체로 변환하는 과정을 제어합니다. 이 과정을 각각 **Marshaling**과 **Unmarshaling**이라고 부릅니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. `Marshal()`과 `Unmarshal()` 이해하기
|
||||
|
||||
Go 구조체로 JSON 데이터를 다룰 때 핵심적인 과정입니다.
|
||||
|
||||
* **Marshaling (마샬링)**: Go 구조체(메모리 상의 데이터)를 JSON 바이트 슬라이스(`[]byte`, 텍스트 데이터)로 변환하는 과정입니다. 주로 API 응답으로 JSON을 보내거나, 데이터를 파일로 저장할 때 사용됩니다.
|
||||
* **Unmarshaling (언마샬링)**: JSON 문자열/바이트 데이터를 Go 구조체 또는 맵 구조로 변환하는 과정입니다. 주로 외부 API로부터 받은 JSON 데이터를 다루거나 파일에서 데이터를 읽어올 때 사용됩니다.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **자주 발생하는 필드 노출(Exported) 에러**
|
||||
> `encoding/json` 패키지가 구조체의 필드에 접근하려면, 해당 필드는 **반드시 대문자로 시작해야 합니다 (Exported Field)**. 구조체 필드명이 소문자로 시작할 경우 JSON 파서가 이에 접근할 수 없어 Marshaling이나 Unmarshaling 시 값이 누락되는 문제가 발생합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 실습 코드 분석 (`lib/jsonexample/json_parser.go`)
|
||||
|
||||
저장소의 [json_parser.go](../lib/jsonexample/json_parser.go) 파일은 (1) `map[string]interface{}`와의 직렬화 및 (2) 구조체(`Person`)를 이용한 매핑 방식을 모두 다룹니다.
|
||||
|
||||
### 2.1 구조체 태그(Struct Tag) 정의
|
||||
```go
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
History []string `json:"history"`
|
||||
}
|
||||
```
|
||||
* 필드 옆의 `` `json:"..."` `` 부분을 **구조체 태그**라고 부릅니다.
|
||||
* 이 태그는 각 필드가 JSON 데이터에서 어떤 키(key) 이름과 1:1로 매핑되는지 매칭해 줍니다.
|
||||
* Go 내부적으로는 대문자 필드(`Name`)를 쓰고, 외부 JSON 통신 규약으로는 소문자 키(`name`)를 사용하도록 매핑합니다.
|
||||
|
||||
### 2.2 Go 맵(`map`) 데이터 마샬링
|
||||
```go
|
||||
obj := map[string]interface{}{
|
||||
"name": "홍길동",
|
||||
"age": 623,
|
||||
"history": []string{
|
||||
"1900-양반집을 털었다",
|
||||
"1910-왕에게 잡혀감",
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(obj)
|
||||
```
|
||||
* 구조체뿐만 아니라 동적 타입 맵인 `map[string]interface{}` 데이터도 `json.Marshal()`을 통해 쉽게 JSON 문자열로 변환할 수 있습니다.
|
||||
|
||||
### 2.3 구조체 언마샬링
|
||||
```go
|
||||
var p2 Person
|
||||
err = json.Unmarshal(b, &p2)
|
||||
```
|
||||
* `json.Unmarshal()` 함수는 바이트 슬라이스(`[]byte`) 형태의 JSON 데이터와, 결과를 받아올 변수의 **메모리 주소 포인터(`&p2`)**를 인자로 받습니다.
|
||||
* 포인터를 넘겨주어야 함수 내부에서 값을 정상적으로 갱신하여 반환할 수 있습니다.
|
||||
+21
-32
@@ -1,6 +1,18 @@
|
||||
# gRPC 튜토리얼 원고 (Manuscript)
|
||||
# gRPC 튜토리얼 통합 개념서 (MANUSCRIPT)
|
||||
|
||||
본 문서는 `grpccanary` 프로젝트의 개념적 학습 자료를 모아둔 문서입니다. JSON 데이터 포맷의 이해부터 HTTP, 그리고 마이크로서비스 환경에서 gRPC가 왜 필요한지에 대한 상세한 배경을 설명합니다.
|
||||
본 문서는 `grpccanary` 프로젝트의 전반적인 학습 경로와 각 단계별 통신 규약의 입문 배경을 설명하는 **통합 가이드북(GENERAL)**입니다.
|
||||
|
||||
각 장의 기초 개념을 습득한 후, 연결된 심화 학습 가이드를 통해 구체적인 코드 분석과 실습 가이드를 학습하실 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 학습 로드맵 및 가이드 바로가기
|
||||
|
||||
| 단계 | 실습 주제 | 심화 학습 가이드 링크 |
|
||||
| :--- | :--- | :--- |
|
||||
| **1단계** | JSON 데이터 다루기 | [JSON 상세 가이드 (JSON.md)](JSON.md) |
|
||||
| **2단계** | HTTP & Gin 웹 서버 | [HTTP 상세 가이드 (HTTP.md)](HTTP.md) |
|
||||
| **3단계** | gRPC 통신 구현 | [gRPC 상세 가이드 (GRPC.md)](GRPC.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -8,6 +20,8 @@
|
||||
|
||||
JSON(JavaScript Object Notation)은 데이터를 구조화하여 전송하기 위해 널리 사용되는 가볍고 읽기 쉬운 텍스트 기반의 데이터 포맷입니다. 대부분의 현대 프로그래밍 언어에서 기본적으로 지원하며, 특히 HTTP 기반 REST API의 데이터 교환 규격으로 오랫동안 사랑받아 왔습니다.
|
||||
|
||||
👉 **[1단계: JSON 데이터 다루기 상세 가이드 (JSON.md)](JSON.md)**
|
||||
|
||||
---
|
||||
|
||||
## 2. HTTP란?
|
||||
@@ -17,6 +31,8 @@ HTTP(Hypertext Transfer Protocol)는 웹 브라우저와 웹 서버 간에 데
|
||||
### `gin-gonic`을 이용한 http 서버 구현하기
|
||||
Go 언어에서는 전통적인 `net/http` 표준 라이브러리 외에도, 성능이 뛰어나고 라우팅 기능이 강력한 `gin-gonic/gin` 프레임워크를 널리 활용하여 RESTful 웹 API 서버를 구축합니다.
|
||||
|
||||
👉 **[2단계: HTTP & Gin 웹 서버 상세 가이드 (HTTP.md)](HTTP.md)**
|
||||
|
||||
---
|
||||
|
||||
## 3. gRPC란?
|
||||
@@ -40,35 +56,8 @@ REST와 JSON은 단순하고 사람이 읽기 쉽다는 훌륭한 장점이 있
|
||||
* **바이너리 프로토콜**: 텍스트가 아닌 이진 데이터 형식을 사용하여 통신 속도가 JSON 방식에 비해 훨씬 빠르고 가볍습니다.
|
||||
* **HTTP/2 기반**: 하나의 커넥션을 다중화(Multiplexing)하여 사용하므로 네트워크 리소스 효율성이 매우 높습니다.
|
||||
|
||||
---
|
||||
민우가 겪은 문제는 서비스 몇 개가 통신하는 MSA 환경에서도 발생했지만, 다수의 IoT 디바이스와 지능형 에이전트가 실시간으로 데이터를 주고받는 AIoT 멀티 에이전트 환경에서는 계약 불일치와 직렬화 비용의 영향이 훨씬 크게 증폭됩니다. 본 프로젝트가 gRPC를 사전 학습 주제로 채택한 이유도 여기에 있습니다.
|
||||
|
||||
## 4. gRPC 개요
|
||||
실제 gRPC 통신의 기술적 개념 요소(장단점, 프로토콜 버퍼) 및 소스코드 구현체 컴파일과 서버/클라이언트 개발 실습에 관한 상세 내용은 다른 페이지에서 나누어 다룹니다.
|
||||
|
||||
위와 같이 gRPC가 고안된 배경과 필요성을 바탕으로, 구체적인 특징과 개발 프로세스를 살펴보겠습니다.
|
||||
|
||||
gRPC 서버와 클라이언트를 개발하는 과정은 크게 세 단계로 나뉩니다:
|
||||
1. **인터페이스 정의 언어(IDL) 파일 생성**: 인터페이스를 계약(Contract)으로 정의합니다.
|
||||
2. **gRPC 서버 개발**: 이 정의를 기반으로 서비스를 구현합니다.
|
||||
3. **gRPC 클라이언트 개발**: 해당 서버와 통신하는 코드를 개발합니다.
|
||||
|
||||
### 4.1 장점
|
||||
gRPC의 주요 장점은 다음과 같습니다:
|
||||
* **빠른 데이터 교환**: 바이너리 데이터 형식을 사용하여 일반 텍스트 기반 서비스보다 훨씬 빠르게 데이터를 교환합니다.
|
||||
* **간편한 개발 도구**: 풍부한 명령줄 도구들을 제공하여 개발 작업을 더욱 간단하고 신속하게 만듭니다.
|
||||
* **쉬운 서버/클라이언트 생성**: gRPC 서비스의 함수와 메시지를 정의한 후에는 RESTful 서비스보다 서버와 클라이언트를 더 쉽게 생성할 수 있습니다.
|
||||
* **스트리밍 지원**: 스트리밍 서비스에 효과적으로 활용될 수 있습니다.
|
||||
* **세부 사항 자동 처리**: 데이터 교환의 복잡한 세부 사항을 gRPC가 자동으로 처리해주므로 개발자가 신경 쓸 필요가 없습니다.
|
||||
|
||||
> [!NOTE]
|
||||
> 이 장점 목록만 보고 gRPC가 모든 문제의 완벽한 해결책이라고 오해해서는 안 됩니다. 항상 현재 작업에 가장 적합한 도구나 기술을 선택하는 것이 중요합니다.
|
||||
|
||||
### 4.2 프로토콜 버퍼 (Protobuf)
|
||||
프로토콜 버퍼(Protobuf)는 구조화된 데이터를 효율적으로 직렬화하는 방법입니다. Protobuf는 IDL(인터페이스 정의 언어)의 일부로, 데이터 교환 시 바이너리 형식을 사용하기 때문에 일반 텍스트 기반 직렬화 형식보다 훨씬 적은 공간을 차지합니다. 하지만 데이터를 기계가 사용하고 사람이 읽을 수 있도록 하려면 각각 인코딩과 디코딩 과정이 필요합니다. Protobuf는 각 프로그래밍 언어에서 기본적으로 지원하는 데이터 타입으로 변환되는 자체 데이터 타입을 제공합니다.
|
||||
|
||||
일반적으로 IDL 파일은 모든 gRPC 서비스의 핵심입니다. 이는 데이터 교환 형식과 서비스 인터페이스를 정의하기 때문입니다. Protobuf 파일 없이는 gRPC 서비스를 구축할 수 없습니다. 더 정확히 말하면, Protobuf 파일에는 서비스 정의, 서비스 메서드, 그리고 교환될 메시지 형식이 모두 포함됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 참고 자료
|
||||
|
||||
* [gRPC와 REST의 차이점 (AWS)](https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/)
|
||||
👉 **[3단계: gRPC 통신 구현 상세 가이드 (GRPC.md)](GRPC.md)**
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# Go 언어와 JSON 다루기
|
||||
|
||||
Go 언어에서 JSON 데이터를 다루는 방법을 배워봅시다.
|
||||
|
||||
Go 표준 라이브러리에는 JSON 데이터를 처리하기 위한 `encoding/json` 패키지가 포함되어 있습니다. Go는 구조체 태그(Struct Tag)를 사용하여 Go 구조체와 JSON 필드를 손쉽게 매핑할 수 있는 기능을 제공합니다. 이 태그는 Go 구조체를 JSON으로 변환하거나, JSON을 Go 구조체로 변환하는 과정을 제어합니다. 이 과정을 각각 Marshaling과 Unmarshaling이라고 부릅니다.
|
||||
|
||||
## `Marshal()`과 `Unmarshal()` 이해하기
|
||||
|
||||
**Marshaling**과 **Unmarshaling**은 Go 구조체로 JSON 데이터를 다룰 때 핵심적인 과정입니다.
|
||||
|
||||
- **Marshaling**: Go 구조체(메모리 상의 데이터)를 JSON 문자열(텍스트 데이터)로 변환하는 과정입니다. 주로 API 응답으로 JSON을 보내거나, 데이터를 파일로 저장할 때 사용됩니다.
|
||||
- **Unmarshaling**: JSON 문자열을 Go 구조체로 변환하는 과정입니다. 주로 외부 API로부터 받은 JSON 데이터를 다루거나 파일에서 데이터를 읽어올 때 사용됩니다.
|
||||
|
||||
> **가장 흔히 겪는 문제**: JSON과 Go 구조체 간 변환 시 가장 흔한 버그는 구조체의 필드명을 소문자로 시작하여 발생하는 문제입니다. `encoding/json` 패키지가 구조체의 필드에 접근하려면, 해당 필드는 **반드시 대문자로 시작해야 합니다 (Exported field)**. Marshaling 또는 Unmarshaling이 제대로 동작하지 않는다면, 가장 먼저 구조체 필드명이 대문자로 시작하는지 확인해 보세요.
|
||||
|
||||
# 코딩 예제
|
||||
|
||||
아래 `encodeDecode.go` 코드는 간단한 예제를 통해 JSON 레코드의 Marshaling과 Unmarshaling 과정을 보여줍니다.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// UseAll 구조체는 JSON 데이터와 매핑됩니다.
|
||||
type UseAll struct {
|
||||
Name string `json:"username"`
|
||||
Surname string `json:"surname"`
|
||||
Year int `json:"created"`
|
||||
}
|
||||
```
|
||||
|
||||
구조체 필드 옆의 `` `json:"..."` `` 부분을 **구조체 태그**라고 부릅니다. 이 태그는 각 필드가 JSON 데이터에서 어떤 키(key)와 매핑되는지를 명시합니다.
|
||||
- `Name` 필드는 JSON에서 `username` 키와 매핑됩니다.
|
||||
- `Surname` 필드는 `surname` 키와 매핑됩니다.
|
||||
- `Year` 필드는 `created` 키와 매핑됩니다.
|
||||
|
||||
이 태그 정보는 Marshaling과 Unmarshaling 과정에서 사용되며, 이 외에는 `UseAll`을 일반적인 Go 구조체처럼 사용하면 됩니다.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
// Marshaling할 구조체 인스턴스 생성
|
||||
useall := UseAll{Name: "Mike", Surname: "Tsoukalos", Year: 2021}
|
||||
|
||||
// Go 구조체를 JSON 바이트 슬라이스로 Marshaling합니다.
|
||||
t, err := json.Marshal(&useall)
|
||||
}
|
||||
```
|
||||
|
||||
`json.Marshal()` 함수는 Go 데이터(주로 구조체 포인터)를 인자로 받아, JSON으로 인코딩된 `[]byte`와 `error`를 반환합니다.
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
// t는 []byte 타입이므로, 출력을 위해 문자열로 변환합니다.
|
||||
fmt.Printf("Value %s\n", t)
|
||||
}
|
||||
|
||||
// Unmarshaling할 JSON 문자열 데이터
|
||||
str := `{"username": "M.", "surname": "Ts", "created":2020}`
|
||||
```
|
||||
|
||||
JSON 데이터는 보통 문자열 형태로 다루어집니다.
|
||||
|
||||
```go
|
||||
// json.Unmarshal 함수는 바이트 슬라이스를 인자로 받으므로, 문자열을 변환합니다.
|
||||
jsonRecord := []byte(str)
|
||||
```
|
||||
|
||||
`json.Unmarshal()` 함수는 바이트 슬라이스 (`[]byte`)를 인자로 받기 때문에, 먼저 JSON 문자열을 `[]byte` 타입으로 변환해야 합니다.
|
||||
|
||||
```go
|
||||
// 변환된 JSON 데이터를 담을 구조체 변수를 선언합니다.
|
||||
var temp UseAll
|
||||
// JSON 바이트 슬라이스를 Go 구조체로 Unmarshaling합니다.
|
||||
err = json.Unmarshal(jsonRecord, &temp)
|
||||
```
|
||||
|
||||
`json.Unmarshal()` 함수는 JSON 데이터가 담긴 바이트 슬라이스와, 데이터를 채워 넣을 Go 구조체 변수의 **포인터**를 인자로 받습니다. 포인터를 사용하는 이유는 함수가 `temp` 변수의 값을 직접 수정해야 하기 때문입니다. 함수가 종료된 후에도 변경된 값이 유지되려면 이처럼 변수의 메모리 주소를 전달해야 합니다.
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Data type: %T with value %v\n", temp, temp)
|
||||
}
|
||||
```
|
||||
|
||||
`encodeDecode.go`를 실행하면 다음과 같은 결과가 출력됩니다.
|
||||
|
||||
```bash
|
||||
# go run encodeDecode.go
|
||||
Value {"username":"Mike","surname":"Tsoukalos","created":2021}
|
||||
Data type: main.UseAll with value {M. Ts 2020}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 전체 예제 코드
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UseAll struct {
|
||||
Name string `json:"username"`
|
||||
Surname string `json:"surname"`
|
||||
Year int `json:"created"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
useall := UseAll{Name: "Mike", Surname: "Tsoukalos", Year: 2021}
|
||||
|
||||
// Marshaling: Go 구조체 -> JSON
|
||||
t, err := json.Marshal(&useall)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Value %s\n", t)
|
||||
}
|
||||
|
||||
// Unmarshaling할 JSON 문자열
|
||||
str := `{"username": "M.", "surname": "Ts", "created":2020}`
|
||||
jsonRecord := []byte(str)
|
||||
|
||||
// 결과를 저장할 구조체 변수
|
||||
var temp UseAll
|
||||
// Unmarshaling: JSON -> Go 구조체
|
||||
err = json.Unmarshal(jsonRecord, &temp)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Data type: %T with value %v\n", temp, temp)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
# Working with JSON
|
||||
|
||||
Let’s learn how to work with JSON data.
|
||||
|
||||
The Go standard library includes `encoding/json`, which is for working with JSON data. Additionally, Go allows us to add support for JSON fields in Go structures using tags. Tags control the encoding and decoding of JSON records to and from Go structures. But first, we should talk about marshaling and unmarshaling JSON records.
|
||||
|
||||
## Using `Marshal()` and `Unmarshal()`
|
||||
|
||||
Both the marshaling and unmarshaling of JSON data are important procedures for working with JSON data using Go structures. **Marshaling** is the process of converting a Go structure into a JSON record. We usually want that for transferring JSON data via computer networks or for saving it on disk. **Unmarshaling** is the process of converting a JSON record given as a byte slice into a Go structure. We usually want that when receiving JSON data via computer networks or when loading JSON data from disk files.
|
||||
|
||||
> **Note:** The number one bug when converting JSON records into Go structures and vice versa is not making the required fields of our Go structures exported. When we have issues with marshaling and unmarshaling, begin our debugging process from there.
|
||||
|
||||
# Coding example
|
||||
|
||||
The code in `encodeDecode.go` illustrates both the marshaling and unmarshaling of JSON records using hardcoded data for simplicity:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UseAll struct {
|
||||
Name string `json:"username"`
|
||||
Surname string `json:"surname"`
|
||||
Year int `json:"created"`
|
||||
}
|
||||
```
|
||||
|
||||
What the previous metadata tells us is that the `Name` field of the `UseAll` structure is translated to `username` in the JSON record, and vice versa; the `Surname` field is translated to `surname`, and vice versa; and the `Year` structure field is translated to `created` in the JSON record, and vice versa. This information has to do with the marshaling and unmarshaling of JSON data. Other than this, we treat and use `UseAll` as a regular Go structure.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
useall := UseAll{Name: "Mike", Surname: "Tsoukalos", Year: 2021}
|
||||
|
||||
// Regular Structure
|
||||
// Encoding JSON data -> Convert Go Structure to JSON record with fields
|
||||
t, err := json.Marshal(&useall)
|
||||
}
|
||||
```
|
||||
|
||||
The `json.Marshal()` function requires a pointer to a structure variable—its real data type is an empty interface variable—and returns a byte slice with the encoded information and an `error` variable.
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Value %s\n", t)
|
||||
}
|
||||
|
||||
// Decoding JSON data given as a string
|
||||
str := `{"username": "M.", "surname": "Ts", "created":2020}`
|
||||
```
|
||||
|
||||
JSON data usually comes as a string.
|
||||
|
||||
```go
|
||||
// Convert string into a byte slice
|
||||
jsonRecord := []byte(str)
|
||||
```
|
||||
|
||||
However, as `json.Unmarshal()` requires a byte slice, we need to convert that string into a byte slice before passing it to `json.Unmarshal()`.
|
||||
|
||||
```go
|
||||
// Create a structure variable to store the result
|
||||
temp := UseAll{}
|
||||
err = json.Unmarshal(jsonRecord, &temp)
|
||||
```
|
||||
|
||||
The `json.Unmarshal()` function requires the byte slice with the JSON record and a pointer to the Go structure variable that is going to store the JSON record and returns an `error` variable.
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Data type: %T with value %v\n", temp, temp)
|
||||
}
|
||||
```
|
||||
|
||||
Running `encodeDecode.go` produces the next output:
|
||||
|
||||
```bash
|
||||
# go run encodeDecode.go
|
||||
Value {"username":"Mike","surname":"Tsoukalos","created":2021}
|
||||
Data type: main.UseAll with value {M. Ts 2020}
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UseAll struct {
|
||||
Name string `json:"username"`
|
||||
Surname string `json:"surname"`
|
||||
Year int `json:"created"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
useall := UseAll{Name: "Mike", Surname: "Tsoukalos", Year: 2021}
|
||||
|
||||
// Regular Structure
|
||||
// Encoding JSON data -> Convert Go Structure to JSON record with fields
|
||||
t, err := json.Marshal(&useall)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Value %s\n", t)
|
||||
}
|
||||
|
||||
// Decoding JSON data given as a string
|
||||
str := `{"username": "M.", "surname": "Ts", "created":2020}`
|
||||
// Convert string into a byte slice
|
||||
jsonRecord := []byte(str)
|
||||
// Create a structure variable to store the result
|
||||
temp := UseAll{}
|
||||
err = json.Unmarshal(jsonRecord, &temp)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Data type: %T with value %v\n", temp, temp)
|
||||
}
|
||||
}
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
@@ -1,222 +0,0 @@
|
||||
# 인터페이스 정의 언어(IDL) 파일 정의하기
|
||||
|
||||
인터페이스 정의 언어(IDL) 파일을 정의하는 방법을 배워보겠습니다.
|
||||
|
||||
우리가 개발할 gRPC 서비스는 다음 기능을 지원할 것입니다:
|
||||
|
||||
- 서버는 클라이언트에게 현재 날짜와 시간을 반환해야 합니다.
|
||||
- 서버는 클라이언트에게 주어진 길이의 무작위로 생성된 비밀번호를 반환해야 합니다.
|
||||
- 서버는 클라이언트에게 무작위 정수를 반환해야 합니다.
|
||||
|
||||
gRPC 클라이언트와 서버 개발을 시작하기 전에, IDL 파일을 먼저 정의해야 합니다. IDL 정의는 이 저장소 루트에 위치한 `protoapi.proto` 파일을 사용합니다.
|
||||
|
||||
## IDL 파일의 구조
|
||||
|
||||
다음은 `protoapi.proto`라는 IDL 파일의 내용입니다:
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
```
|
||||
|
||||
이 파일은 프로토콜 버퍼 언어의 **proto3** 버전을 사용합니다. 이전 버전인 **proto2**도 있으며, 약간의 문법적 차이가 있습니다. `proto3`를 명시하지 않으면, 프로토콜 버퍼 컴파일러는 **proto2**를 사용하는 것으로 간주합니다. 버전 정의는 `.proto` 파일의 첫 번째 비어있지 않은, 주석이 아닌 라인에 위치해야 합니다.
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
```
|
||||
|
||||
gRPC 도구들은 이 `.proto` 파일로부터 Go 코드를 생성할 것입니다. 위 라인은 생성될 Go 패키지의 이름이 `protoapi`임을 명시합니다. `./protoapi/`를 사용했기 때문에 출력 파일은 `protoapi/` 하위 디렉토리에 생성됩니다.
|
||||
|
||||
```proto
|
||||
service Random {
|
||||
rpc GetDate (RequestDateTime) returns (DateTime);
|
||||
rpc GetRandom (RandomParams) returns (RandomInt);
|
||||
rpc GetRandomPass (RequestPass) returns (RandomPass);
|
||||
}
|
||||
```
|
||||
|
||||
이 블록은 gRPC 서비스의 이름(`Random`)과 지원하는 메서드들을 명시합니다. 또한, 각 상호작용에 필요한 메시지들을 지정합니다. 예를 들어, `GetDate`의 경우 클라이언트는 `RequestDateTime` 메시지를 보내고 `DateTime` 메시지를 받기를 기대합니다.
|
||||
|
||||
이 메시지들은 동일한 `.proto` 파일에 정의되어 있습니다.
|
||||
|
||||
```proto
|
||||
// For random number
|
||||
```
|
||||
|
||||
모든 `.proto` 파일은 C와 C++ 스타일의 주석을 지원합니다. 즉, `// text`와 `/* text */` 형식의 주석을 사용할 수 있습니다.
|
||||
|
||||
```proto
|
||||
message RandomParams {
|
||||
int64 Seed = 1;
|
||||
int64 Place = 2;
|
||||
}
|
||||
```
|
||||
|
||||
난수 생성기는 시드(seed) 값으로 시작하며, 이 값은 클라이언트가 지정하여 `RandomParams` 메시지를 통해 서버로 전송됩니다. `Place` 필드는 무작위로 생성된 정수 시퀀스에서 반환될 난수의 위치를 지정합니다.
|
||||
|
||||
```proto
|
||||
message RandomInt {
|
||||
int64 Value = 1;
|
||||
}
|
||||
```
|
||||
|
||||
앞선 두 메시지는 `GetRandom` 메서드와 관련이 있습니다. `RandomParams`는 요청의 매개변수를 설정하는 데 사용되고, `RandomInt`는 서버가 생성한 난수를 저장하는 데 사용됩니다. 모든 메시지 필드는 `int64` 데이터 타입을 가집니다.
|
||||
|
||||
```proto
|
||||
message DateTime {
|
||||
string Value = 1;
|
||||
}
|
||||
|
||||
message RequestDateTime {
|
||||
string Value = 2;
|
||||
}
|
||||
```
|
||||
|
||||
위 두 메시지는 `GetDate` 메서드의 동작을 지원하기 위한 것입니다. `RequestDateTime` 메시지는 실질적인 데이터를 담고 있지 않은 더미 메시지입니다. 단지 클라이언트가 서버로 보내는 메시지가 필요할 뿐이며, `Value` 필드에는 어떤 종류의 정보든 저장할 수 있습니다. 서버가 반환하는 정보는 `DateTime` 메시지에 `string` 값으로 저장됩니다.
|
||||
|
||||
> **참고**: `RequestDateTime`의 `Value` 필드 번호가 `2`로 지정되어 있습니다. 프로토콜 버퍼에서 필드 번호는 태그 번호로 사용되며 고유한 번호라면 임의의 값을 가질 수 있지만, 일반적으로는 첫 필드에 `1`을 사용하는 것이 관례입니다.
|
||||
|
||||
```proto
|
||||
// For random password
|
||||
message RequestPass {
|
||||
int64 Seed = 1;
|
||||
int64 Length = 8;
|
||||
}
|
||||
|
||||
message RandomPass {
|
||||
string Password = 1;
|
||||
}
|
||||
```
|
||||
|
||||
마지막으로, 위 두 메시지는 `GetRandomPass`의 동작을 위한 것입니다.
|
||||
|
||||
요약하자면, IDL 파일은 다음을 수행합니다:
|
||||
- `proto3`를 사용함을 명시합니다.
|
||||
- 서비스의 이름이 `Random`임을 정의합니다.
|
||||
- 생성될 Go 패키지의 이름이 `protoapi`임을 명시합니다.
|
||||
- gRPC 서비스가 `GetDate`, `GetRandom`, `GetRandomPass` 세 가지 메서드를 지원함을 정의하고, 이 메서드 호출에서 교환될 메시지들의 이름을 정의합니다.
|
||||
- 데이터 교환에 사용될 여섯 가지 메시지의 형식을 정의합니다.
|
||||
|
||||
## Go에서 IDL 파일 사용하기
|
||||
|
||||
다음 중요한 단계는 이 파일을 Go에서 사용할 수 있는 형식으로 변환하는 것입니다. `protoapi.proto`나 다른 `.proto` 파일을 처리하여 관련된 Go `.pb.go` 파일을 생성하기 위해 몇 가지 추가 도구를 다운로드해야 합니다. 프로토콜 버퍼 컴파일러 바이너리의 이름은 `protoc`입니다. macOS에서는 `brew install protobuf` 명령을 사용하여 `protoc`를 설치해야 합니다. 마찬가지로, Homebrew를 사용하여 `protoc-gen-go-grpc`와 `protoc-gen-go` 패키지도 설치해야 합니다. 이 두 패키지는 Go와 관련이 있습니다.
|
||||
|
||||
Linux에서는 선호하는 패키지 관리자를 사용하여 `protobuf`를 설치하고, `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest` 명령을 사용하여 `protoc-gen-go`를 설치해야 합니다. 마찬가지로, `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest`를 실행하여 `protoc-gen-go-grpc` 실행 파일을 설치해야 합니다.
|
||||
|
||||
> 참고: Go 1.16부터는 모듈 모드에서 패키지를 빌드하고 설치하는 데 `go install`을 사용하는 것이 권장됩니다. `go get`의 사용은 더 이상 사용되지 않습니다. `go install`을 사용할 때는 최신 버전을 설치하기 위해 패키지 이름 뒤에 `@latest`를 추가하는 것을 잊지 마세요.
|
||||
|
||||
- protoc
|
||||
- protoc-gen-go
|
||||
- protoc-gen-go-grpc
|
||||
|
||||
변환 과정은 다음 단계를 필요로 합니다:
|
||||
|
||||
```bash
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \
|
||||
--go-grpc_opt=paths=source_relative protoapi.proto
|
||||
```
|
||||
|
||||
이 명령을 실행하면, 리포지토리 루트 하위의 `protoapi` 디렉토리에 `protoapi_grpc.pb.go`와 `protoapi.pb.go`라는 두 개의 파일이 생성됩니다. `protoapi.pb.go` 소스 코드 파일에는 메시지가 포함되어 있고, `protoapi_grpc.pb.go`에는 서비스가 포함되어 있습니다.
|
||||
|
||||
`protoapi_grpc.pb.go`의 첫 열 줄은 다음과 같습니다:
|
||||
|
||||
```go
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
|
||||
package protoapi
|
||||
```
|
||||
|
||||
앞서 논의했듯이, 패키지 이름은 `protoapi`입니다.
|
||||
|
||||
```go
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
```
|
||||
|
||||
이것은 `import` 블록입니다. `context "context"`가 있는 이유는 `context`가 예전에는 표준 Go 라이브러리의 일부가 아닌 외부 Go 패키지였기 때문입니다.
|
||||
|
||||
`protoapi.pb.go`의 첫 줄은 다음과 같습니다:
|
||||
|
||||
```go
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.33.0
|
||||
// protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
```
|
||||
|
||||
`protoapi_grpc.pb.go`와 `protoapi.pb.go`는 모두 `protoapi` Go 패키지의 일부이므로, 코드에서 한 번만 포함하면 됩니다.
|
||||
|
||||
## gRPC 서버 개발
|
||||
|
||||
IDL을 통해 생성된 Go 코드를 기반으로, 실제 비즈니스 로직을 수행할 gRPC 서버([server.go](./server.go))를 구현합니다.
|
||||
|
||||
### 1. 서비스 인터페이스 구현
|
||||
우리가 `.proto` 파일에 정의한 `Random` 서비스의 메서드들은 `RandomServer` 구조체 타입을 통해 구현됩니다. 이 구조체는 stub 코드의 `UnimplementedRandomServer`를 임베딩하여 기본 호환성을 확보합니다.
|
||||
|
||||
```go
|
||||
type RandomServer struct {
|
||||
protoapi.UnimplementedRandomServer
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 서비스 메서드 작성
|
||||
서버는 IDL에 기술된 세 가지 RPC 메서드인 `GetDate`, `GetRandom`, `GetRandomPass`를 각각 실제 동작 코드로 구현합니다.
|
||||
|
||||
* **`GetDate`**: 현재 날짜와 시간 정보를 반환합니다.
|
||||
* **`GetRandom`**: 전달받은 시드(`Seed`)와 위치(`Place`) 매개변수를 이용해 의사 난수를 생성하고 반환합니다.
|
||||
* **`GetRandomPass`**: 지정된 길이(`Length`)의 무작위 문자열 비밀번호를 빌드하여 반환합니다.
|
||||
|
||||
### 3. gRPC 서버 시작 (`ServerRun`)
|
||||
네트워크 포트 청취를 개시하고, gRPC 서버 객체를 인스턴스화한 후 서비스를 등록하여 대기 상태에 들어갑니다.
|
||||
|
||||
```go
|
||||
func ServerRun(addr string) {
|
||||
server := grpc.NewServer()
|
||||
var randomServer RandomServer
|
||||
protoapi.RegisterRandomServer(server, randomServer)
|
||||
|
||||
// 외부 CLI 도구(예: grpcurl)의 디버깅을 위해 리플렉션 등록
|
||||
reflection.Register(server)
|
||||
|
||||
listen, err := net.Listen("tcp", port)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Serving requests...")
|
||||
server.Serve(listen)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## gRPC 클라이언트 개발
|
||||
|
||||
서버로 RPC 요청을 전송하고 결과를 출력하는 gRPC 클라이언트([client.go](./client.go))의 흐름은 다음과 같습니다.
|
||||
|
||||
### 1. 서버 접속 채널 구축
|
||||
클라이언트는 보안 자격 증명 옵션을 지정하여 서버 네트워크 주소로 커넥션을 생성합니다. (본 예제에서는 로컬 테스트용으로 `insecure` 자격 증명을 이용해 평문 채널을 구축합니다.)
|
||||
|
||||
```go
|
||||
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
```
|
||||
|
||||
### 2. 클라이언트 인스턴스 및 호출 함수 정의
|
||||
채널 연결 완료 후 `protoapi.NewRandomClient(conn)`를 통해 클라이언트 객체를 생성하고, 개별 RPC 메서드들을 호출하는 래퍼 함수들을 정의해 서버에 값을 질의합니다.
|
||||
|
||||
* **`AskingDateTime`** -> `client.GetDate()` 호출
|
||||
* **`AskPass`** -> `client.GetRandomPass()` 호출
|
||||
* **`AskRandom`** -> `client.GetRandom()` 호출
|
||||
|
||||
### 3. 실행 엔트리포인트 (`ClientRun`)
|
||||
각 RPC 메서드를 호출하여 서버로부터 전달받은 시간 값, 난수 비밀번호, 무작위 난수들을 터미널 표준 출력(`fmt.Println`)으로 출력해 줍니다.
|
||||
@@ -1,80 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"grpccanary/protoapi"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func AskingDateTime(ctx context.Context, m protoapi.RandomClient) (*protoapi.DateTime, error) {
|
||||
request := &protoapi.RequestDateTime{
|
||||
Value: "Please send me the date and time",
|
||||
}
|
||||
|
||||
return m.GetDate(ctx, request)
|
||||
}
|
||||
|
||||
func AskPass(ctx context.Context, m protoapi.RandomClient, seed int64, length int64) (*protoapi.RandomPass, error) {
|
||||
request := &protoapi.RequestPass{
|
||||
Seed: seed,
|
||||
Length: length,
|
||||
}
|
||||
|
||||
return m.GetRandomPass(ctx, request)
|
||||
}
|
||||
|
||||
func AskRandom(ctx context.Context, m protoapi.RandomClient, seed int64, place int64) (*protoapi.RandomInt, error) {
|
||||
request := &protoapi.RandomParams{
|
||||
Seed: seed,
|
||||
Place: place,
|
||||
}
|
||||
|
||||
return m.GetRandom(ctx, request)
|
||||
}
|
||||
|
||||
func ClientRun(addr string) {
|
||||
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
fmt.Println("Dial:", err)
|
||||
return
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
seed := int64(rand.Intn(100))
|
||||
|
||||
client := protoapi.NewRandomClient(conn)
|
||||
r, err := AskingDateTime(context.Background(), client)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Server Date and Time:", r.Value)
|
||||
|
||||
length := int64(rand.Intn(20))
|
||||
p, err := AskPass(context.Background(), client, 100, length+1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Random Password:", p.Password)
|
||||
|
||||
place := int64(rand.Intn(100))
|
||||
i, err := AskRandom(context.Background(), client, seed, place)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Random Integer 1:", i.Value)
|
||||
|
||||
k, err := AskRandom(context.Background(), client, seed, place-1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Random Integer 2:", k.Value)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"grpccanary/protoapi"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
var min = 0
|
||||
var max = 100
|
||||
var port = ":8080"
|
||||
|
||||
func random(min, max int, src rand.Source) int {
|
||||
return rand.New(src).Intn(max-min) + min
|
||||
}
|
||||
|
||||
// Extra function for creating secure random numbers
|
||||
//
|
||||
// func randomSecure(min, max int) int {
|
||||
// v, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return min
|
||||
// }
|
||||
// fmt.Println("**", v, min, max)
|
||||
|
||||
// return min + int(v.Uint64())
|
||||
// }
|
||||
|
||||
func getString(len int64) string {
|
||||
temp := ""
|
||||
startChar := "!"
|
||||
var i int64 = 1
|
||||
for {
|
||||
// For getting valid ASCII characters
|
||||
myRand := random(0, 94, rand.NewSource(time.Now().UnixNano()))
|
||||
newChar := string(startChar[0] + byte(myRand))
|
||||
temp = temp + newChar
|
||||
if i == len {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
type RandomServer struct {
|
||||
protoapi.UnimplementedRandomServer
|
||||
}
|
||||
|
||||
func (RandomServer) GetDate(ctx context.Context, r *protoapi.RequestDateTime) (*protoapi.DateTime, error) {
|
||||
currentTime := time.Now()
|
||||
response := &protoapi.DateTime{
|
||||
Value: currentTime.String(),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (RandomServer) GetRandom(ctx context.Context, r *protoapi.RandomParams) (*protoapi.RandomInt, error) {
|
||||
src := rand.NewSource(r.GetSeed())
|
||||
place := r.GetPlace()
|
||||
temp := random(min, max, src)
|
||||
for {
|
||||
place--
|
||||
if place <= 0 {
|
||||
break
|
||||
}
|
||||
temp = random(min, max, src)
|
||||
}
|
||||
|
||||
response := &protoapi.RandomInt{
|
||||
Value: int64(temp),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (RandomServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*protoapi.RandomPass, error) {
|
||||
rand.Seed(r.GetSeed())
|
||||
temp := getString(r.GetLength())
|
||||
|
||||
response := &protoapi.RandomPass{
|
||||
Password: temp,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func ServerRun(addr string) {
|
||||
server := grpc.NewServer()
|
||||
var randomServer RandomServer
|
||||
protoapi.RegisterRandomServer(server, randomServer)
|
||||
|
||||
reflection.Register(server)
|
||||
|
||||
listen, err := net.Listen("tcp", port)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Serving requests...")
|
||||
server.Serve(listen)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
entity "grpccanary/examples/grpcentity"
|
||||
"grpccanary/examples/jsonexample"
|
||||
"time"
|
||||
)
|
||||
|
||||
var port = ":8080"
|
||||
|
||||
func main() {
|
||||
jsonexample.JsonParsingExample()
|
||||
}
|
||||
|
||||
func grpcSample() {
|
||||
go entity.ServerRun(port)
|
||||
|
||||
// Just to be sure that the server is running
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
fmt.Println("Client:")
|
||||
entity.ClientRun("localhost" + port)
|
||||
}
|
||||
@@ -3,41 +3,15 @@ module grpccanary
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/quic-go/quic-go v0.60.0
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
)
|
||||
|
||||
@@ -1,74 +1,23 @@
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
@@ -81,25 +30,16 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
@@ -108,7 +48,5 @@ google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# lib/grpc/basic 실습 설명서
|
||||
|
||||
본 디렉토리는 Go 언어를 활용한 gRPC 서버 및 클라이언트 실습 예제를 포함하고 있습니다.
|
||||
|
||||
## 📖 실습 상세 분석 및 가이드 안내
|
||||
|
||||
학습의 일관성을 위해, 이 실습의 상세 분석 및 개념 명세는 통합 교재의 gRPC 심화 가이드인 **[docs/GRPC.md](../../../docs/GRPC.md)**로 모듈화되어 있습니다. 전체 학습 로드맵은 [docs/MANUSCRIPT.md](../../../docs/MANUSCRIPT.md)를 참고하십시오.
|
||||
|
||||
[docs/GRPC.md](../../../docs/GRPC.md) 문서에서 다음 내용을 참고하실 수 있습니다:
|
||||
* **IDL ([protoapi.proto](./protoapi.proto)) 명세 및 필드 분석**
|
||||
* **Go에서의 `protoc` 설치 및 Stub 파일 컴파일 방법**
|
||||
* **gRPC 서버 코드 ([server.go](./server.go)) 구현 상세 분석**
|
||||
* **gRPC 클라이언트 코드 ([client.go](./client.go)) 커넥션 및 호출 흐름 분석**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 빠른 실행 방법
|
||||
|
||||
이 예제는 리포지토리 루트의 `lib/main.go`를 통해 실행됩니다.
|
||||
|
||||
1. 리포지토리 루트의 `lib/main.go`를 엽니다.
|
||||
2. `main()` 함수 내에서 `grpcSample()`의 주석을 해제합니다.
|
||||
3. 리포지토리 루트에서 다음 명령어를 실행합니다:
|
||||
```bash
|
||||
go run ./lib
|
||||
```
|
||||
@@ -0,0 +1,220 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"grpccanary/lib/grpc/basic/protoapi"
|
||||
"io"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func AskingDateTime(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.DateTime, error) {
|
||||
request := &protoapi.RequestDateTime{
|
||||
Value: "Please send me the date and time",
|
||||
}
|
||||
|
||||
return m.GetDate(ctx, request)
|
||||
}
|
||||
|
||||
func AskPass(ctx context.Context, m protoapi.IoTServiceClient, seed int64, length int64) (*protoapi.RandomPass, error) {
|
||||
request := &protoapi.RequestPass{
|
||||
Seed: seed,
|
||||
Length: length,
|
||||
}
|
||||
|
||||
return m.GetRandomPass(ctx, request)
|
||||
}
|
||||
|
||||
func AskUpdateSensingData(ctx context.Context, m protoapi.IoTServiceClient, deviceId string, temp float64, humid float64) (*protoapi.SensingResponse, error) {
|
||||
request := &protoapi.SensingData{
|
||||
DeviceId: deviceId,
|
||||
Temperature: temp,
|
||||
Humidity: humid,
|
||||
}
|
||||
|
||||
return m.UpdateSensingData(ctx, request)
|
||||
}
|
||||
|
||||
func AskUploadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string, fileData []byte) (*protoapi.UploadStatus, error) {
|
||||
stream, err := m.UploadFile(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 단위 청크
|
||||
totalBytes := len(fileData)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: fileName,
|
||||
Content: fileData[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return stream.CloseAndRecv()
|
||||
}
|
||||
|
||||
func AskListFiles(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.FileList, error) {
|
||||
return m.ListFiles(ctx, &protoapi.EmptyRequest{})
|
||||
}
|
||||
|
||||
func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string) ([]byte, error) {
|
||||
stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer []byte
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func AskSubscribeAlerts(ctx context.Context, m protoapi.IoTServiceClient, clientId string, topic string) {
|
||||
stream, err := m.SubscribeAlerts(ctx, &protoapi.AlertSubscription{
|
||||
ClientId: clientId,
|
||||
Topic: topic,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to subscribe alerts:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
alert, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
fmt.Println("Alert subscription stream closed by server.")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Printf("\n🔔 [ALERT RECEIVED] ID: %s | Device: %s | Msg: %s | Time: %s\n\n",
|
||||
alert.GetAlertId(), alert.GetDeviceId(), alert.GetMessage(),
|
||||
time.Unix(alert.GetTimestamp(), 0).Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
|
||||
func ClientRun(addr string) {
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
fmt.Println("NewClient error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := protoapi.NewIoTServiceClient(conn)
|
||||
|
||||
// 백그라운드에서 실시간 경보 구독 기동
|
||||
alertCtx, alertCancel := context.WithCancel(context.Background())
|
||||
defer alertCancel()
|
||||
go AskSubscribeAlerts(alertCtx, client, "client-app-01", "temperature_warnings")
|
||||
// 구독 리시버 채널 등록을 위해 50ms 슬립
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
r, err := AskingDateTime(context.Background(), client)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Server Date and Time:", r.Value)
|
||||
|
||||
length := int64(rand.Intn(20))
|
||||
p, err := AskPass(context.Background(), client, 100, length+1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Random Password:", p.Password)
|
||||
|
||||
res, err := AskUpdateSensingData(context.Background(), client, "sensor-room-01", 24.5, 52.3)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Sensing Update Success:", res.Success)
|
||||
fmt.Println("Sensing Update Message:", res.Message)
|
||||
|
||||
// 4단계: 파일 업로드 스트리밍 실행 예제
|
||||
dummyData := make([]byte, 10240) // 10KB 가상 더미 데이터
|
||||
for i := range dummyData {
|
||||
dummyData[i] = byte(rand.Intn(256))
|
||||
}
|
||||
fmt.Println("Uploading dummy file (10KB) via Client Streaming...")
|
||||
status, err := AskUploadFile(context.Background(), client, "firmware.bin", dummyData)
|
||||
if err != nil {
|
||||
fmt.Println("File upload failed:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Upload Success:", status.Success)
|
||||
fmt.Println("Upload Message:", status.Message)
|
||||
fmt.Printf("Uploaded Bytes: %d bytes\n", status.BytesUploaded)
|
||||
|
||||
// 5단계: 파일 리스트 조회 실행 예제
|
||||
fmt.Println("Querying uploaded files metadata from Server...")
|
||||
list, err := AskListFiles(context.Background(), client)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to list files:", err)
|
||||
return
|
||||
}
|
||||
for i, f := range list.GetFiles() {
|
||||
fmt.Printf("[%d] Name: %s, Size: %d bytes, UploadedAt: %s\n",
|
||||
i+1, f.GetFileName(), f.GetFileSize(), time.Unix(f.GetUploadedAt(), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// 6단계: 파일 다운로드 스트리밍 실행 예제
|
||||
fmt.Println("Downloading file 'firmware.bin' via Server Streaming...")
|
||||
downloadedData, err := AskDownloadFile(context.Background(), client, "firmware.bin")
|
||||
if err != nil {
|
||||
fmt.Println("Failed to download file:", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Download completed. Received %d bytes.\n", len(downloadedData))
|
||||
|
||||
// 데이터 정합성(Integrity) 검증
|
||||
isMatch := true
|
||||
if len(dummyData) != len(downloadedData) {
|
||||
isMatch = false
|
||||
} else {
|
||||
for i := range dummyData {
|
||||
if dummyData[i] != downloadedData[i] {
|
||||
isMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("Data Integrity Checked (Upload vs Download matches?): %t\n", isMatch)
|
||||
|
||||
// 7단계: 임계값 초과 온습도 전송을 통한 Pub/Sub 실시간 알림 유발 시뮬레이션
|
||||
fmt.Println("Sending abnormal high-temperature sensing data (45.8°C)...")
|
||||
|
||||
alertRes, err := AskUpdateSensingData(context.Background(), client, "sensor-room-01", 45.8, 60.1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Abnormal Sensing Update Success:", alertRes.Success)
|
||||
// 알림 이벤트가 비동기로 화면에 출력될 시간을 확보하기 위해 100ms 대기
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
service IoTService {
|
||||
rpc GetDate (RequestDateTime) returns (DateTime);
|
||||
rpc UpdateSensingData (SensingData) returns (SensingResponse);
|
||||
rpc GetRandomPass (RequestPass) returns (RandomPass);
|
||||
rpc UploadFile (stream FileChunk) returns (UploadStatus);
|
||||
rpc ListFiles (EmptyRequest) returns (FileList);
|
||||
rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
|
||||
rpc SubscribeAlerts (AlertSubscription) returns (stream AlertMessage);
|
||||
}
|
||||
|
||||
message FileChunk {
|
||||
string FileName = 1;
|
||||
bytes Content = 2;
|
||||
}
|
||||
|
||||
message UploadStatus {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
int64 BytesUploaded = 3;
|
||||
}
|
||||
|
||||
message SensingData {
|
||||
string DeviceId = 1;
|
||||
double Temperature = 2;
|
||||
double Humidity = 3;
|
||||
}
|
||||
|
||||
message SensingResponse {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
}
|
||||
|
||||
message DateTime {
|
||||
string Value = 1;
|
||||
}
|
||||
|
||||
message RequestDateTime {
|
||||
string Value = 2;
|
||||
}
|
||||
|
||||
message RequestPass {
|
||||
int64 Seed = 1;
|
||||
int64 Length = 8;
|
||||
}
|
||||
|
||||
message RandomPass {
|
||||
string Password = 1;
|
||||
}
|
||||
|
||||
message EmptyRequest {}
|
||||
|
||||
message FileMetadata {
|
||||
string FileName = 1;
|
||||
int64 FileSize = 2;
|
||||
int64 UploadedAt = 3;
|
||||
}
|
||||
|
||||
message FileList {
|
||||
repeated FileMetadata Files = 1;
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string FileName = 1;
|
||||
}
|
||||
|
||||
message AlertSubscription {
|
||||
string ClientId = 1;
|
||||
string Topic = 2;
|
||||
}
|
||||
|
||||
message AlertMessage {
|
||||
string AlertId = 1;
|
||||
string DeviceId = 2;
|
||||
string Message = 3;
|
||||
int64 Timestamp = 4;
|
||||
}
|
||||
@@ -0,0 +1,871 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type FileChunk struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
Content []byte `protobuf:"bytes,2,opt,name=Content,proto3" json:"Content,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileChunk) Reset() {
|
||||
*x = FileChunk{}
|
||||
mi := &file_protoapi_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileChunk) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileChunk) ProtoMessage() {}
|
||||
|
||||
func (x *FileChunk) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileChunk.ProtoReflect.Descriptor instead.
|
||||
func (*FileChunk) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *FileChunk) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FileChunk) GetContent() []byte {
|
||||
if x != nil {
|
||||
return x.Content
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UploadStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=Success,proto3" json:"Success,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
BytesUploaded int64 `protobuf:"varint,3,opt,name=BytesUploaded,proto3" json:"BytesUploaded,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UploadStatus) Reset() {
|
||||
*x = UploadStatus{}
|
||||
mi := &file_protoapi_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UploadStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UploadStatus) ProtoMessage() {}
|
||||
|
||||
func (x *UploadStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UploadStatus.ProtoReflect.Descriptor instead.
|
||||
func (*UploadStatus) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *UploadStatus) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *UploadStatus) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *UploadStatus) GetBytesUploaded() int64 {
|
||||
if x != nil {
|
||||
return x.BytesUploaded
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type SensingData struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DeviceId string `protobuf:"bytes,1,opt,name=DeviceId,proto3" json:"DeviceId,omitempty"`
|
||||
Temperature float64 `protobuf:"fixed64,2,opt,name=Temperature,proto3" json:"Temperature,omitempty"`
|
||||
Humidity float64 `protobuf:"fixed64,3,opt,name=Humidity,proto3" json:"Humidity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SensingData) Reset() {
|
||||
*x = SensingData{}
|
||||
mi := &file_protoapi_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SensingData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SensingData) ProtoMessage() {}
|
||||
|
||||
func (x *SensingData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SensingData.ProtoReflect.Descriptor instead.
|
||||
func (*SensingData) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *SensingData) GetDeviceId() string {
|
||||
if x != nil {
|
||||
return x.DeviceId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SensingData) GetTemperature() float64 {
|
||||
if x != nil {
|
||||
return x.Temperature
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SensingData) GetHumidity() float64 {
|
||||
if x != nil {
|
||||
return x.Humidity
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type SensingResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=Success,proto3" json:"Success,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SensingResponse) Reset() {
|
||||
*x = SensingResponse{}
|
||||
mi := &file_protoapi_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SensingResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SensingResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SensingResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SensingResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SensingResponse) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *SensingResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SensingResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DateTime struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DateTime) Reset() {
|
||||
*x = DateTime{}
|
||||
mi := &file_protoapi_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DateTime) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DateTime) ProtoMessage() {}
|
||||
|
||||
func (x *DateTime) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DateTime.ProtoReflect.Descriptor instead.
|
||||
func (*DateTime) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *DateTime) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RequestDateTime struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) Reset() {
|
||||
*x = RequestDateTime{}
|
||||
mi := &file_protoapi_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RequestDateTime) ProtoMessage() {}
|
||||
|
||||
func (x *RequestDateTime) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RequestDateTime.ProtoReflect.Descriptor instead.
|
||||
func (*RequestDateTime) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RequestPass struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"`
|
||||
Length int64 `protobuf:"varint,8,opt,name=Length,proto3" json:"Length,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RequestPass) Reset() {
|
||||
*x = RequestPass{}
|
||||
mi := &file_protoapi_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RequestPass) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RequestPass) ProtoMessage() {}
|
||||
|
||||
func (x *RequestPass) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RequestPass.ProtoReflect.Descriptor instead.
|
||||
func (*RequestPass) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *RequestPass) GetSeed() int64 {
|
||||
if x != nil {
|
||||
return x.Seed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RequestPass) GetLength() int64 {
|
||||
if x != nil {
|
||||
return x.Length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RandomPass struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Password string `protobuf:"bytes,1,opt,name=Password,proto3" json:"Password,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RandomPass) Reset() {
|
||||
*x = RandomPass{}
|
||||
mi := &file_protoapi_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RandomPass) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RandomPass) ProtoMessage() {}
|
||||
|
||||
func (x *RandomPass) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RandomPass.ProtoReflect.Descriptor instead.
|
||||
func (*RandomPass) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *RandomPass) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type EmptyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EmptyRequest) Reset() {
|
||||
*x = EmptyRequest{}
|
||||
mi := &file_protoapi_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EmptyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EmptyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *EmptyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EmptyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*EmptyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
type FileMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
FileSize int64 `protobuf:"varint,2,opt,name=FileSize,proto3" json:"FileSize,omitempty"`
|
||||
UploadedAt int64 `protobuf:"varint,3,opt,name=UploadedAt,proto3" json:"UploadedAt,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileMetadata) Reset() {
|
||||
*x = FileMetadata{}
|
||||
mi := &file_protoapi_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *FileMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*FileMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetFileSize() int64 {
|
||||
if x != nil {
|
||||
return x.FileSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetUploadedAt() int64 {
|
||||
if x != nil {
|
||||
return x.UploadedAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type FileList struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Files []*FileMetadata `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileList) Reset() {
|
||||
*x = FileList{}
|
||||
mi := &file_protoapi_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileList) ProtoMessage() {}
|
||||
|
||||
func (x *FileList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileList.ProtoReflect.Descriptor instead.
|
||||
func (*FileList) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *FileList) GetFiles() []*FileMetadata {
|
||||
if x != nil {
|
||||
return x.Files
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DownloadRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) Reset() {
|
||||
*x = DownloadRequest{}
|
||||
mi := &file_protoapi_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DownloadRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DownloadRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DownloadRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DownloadRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AlertSubscription struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ClientId string `protobuf:"bytes,1,opt,name=ClientId,proto3" json:"ClientId,omitempty"`
|
||||
Topic string `protobuf:"bytes,2,opt,name=Topic,proto3" json:"Topic,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) Reset() {
|
||||
*x = AlertSubscription{}
|
||||
mi := &file_protoapi_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AlertSubscription) ProtoMessage() {}
|
||||
|
||||
func (x *AlertSubscription) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AlertSubscription.ProtoReflect.Descriptor instead.
|
||||
func (*AlertSubscription) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) GetClientId() string {
|
||||
if x != nil {
|
||||
return x.ClientId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) GetTopic() string {
|
||||
if x != nil {
|
||||
return x.Topic
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AlertMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AlertId string `protobuf:"bytes,1,opt,name=AlertId,proto3" json:"AlertId,omitempty"`
|
||||
DeviceId string `protobuf:"bytes,2,opt,name=DeviceId,proto3" json:"DeviceId,omitempty"`
|
||||
Message string `protobuf:"bytes,3,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
Timestamp int64 `protobuf:"varint,4,opt,name=Timestamp,proto3" json:"Timestamp,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AlertMessage) Reset() {
|
||||
*x = AlertMessage{}
|
||||
mi := &file_protoapi_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AlertMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AlertMessage) ProtoMessage() {}
|
||||
|
||||
func (x *AlertMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AlertMessage.ProtoReflect.Descriptor instead.
|
||||
func (*AlertMessage) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetAlertId() string {
|
||||
if x != nil {
|
||||
return x.AlertId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetDeviceId() string {
|
||||
if x != nil {
|
||||
return x.DeviceId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetTimestamp() int64 {
|
||||
if x != nil {
|
||||
return x.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_protoapi_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_protoapi_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0eprotoapi.proto\"A\n" +
|
||||
"\tFileChunk\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName\x12\x18\n" +
|
||||
"\aContent\x18\x02 \x01(\fR\aContent\"h\n" +
|
||||
"\fUploadStatus\x12\x18\n" +
|
||||
"\aSuccess\x18\x01 \x01(\bR\aSuccess\x12\x18\n" +
|
||||
"\aMessage\x18\x02 \x01(\tR\aMessage\x12$\n" +
|
||||
"\rBytesUploaded\x18\x03 \x01(\x03R\rBytesUploaded\"g\n" +
|
||||
"\vSensingData\x12\x1a\n" +
|
||||
"\bDeviceId\x18\x01 \x01(\tR\bDeviceId\x12 \n" +
|
||||
"\vTemperature\x18\x02 \x01(\x01R\vTemperature\x12\x1a\n" +
|
||||
"\bHumidity\x18\x03 \x01(\x01R\bHumidity\"E\n" +
|
||||
"\x0fSensingResponse\x12\x18\n" +
|
||||
"\aSuccess\x18\x01 \x01(\bR\aSuccess\x12\x18\n" +
|
||||
"\aMessage\x18\x02 \x01(\tR\aMessage\" \n" +
|
||||
"\bDateTime\x12\x14\n" +
|
||||
"\x05Value\x18\x01 \x01(\tR\x05Value\"'\n" +
|
||||
"\x0fRequestDateTime\x12\x14\n" +
|
||||
"\x05Value\x18\x02 \x01(\tR\x05Value\"9\n" +
|
||||
"\vRequestPass\x12\x12\n" +
|
||||
"\x04Seed\x18\x01 \x01(\x03R\x04Seed\x12\x16\n" +
|
||||
"\x06Length\x18\b \x01(\x03R\x06Length\"(\n" +
|
||||
"\n" +
|
||||
"RandomPass\x12\x1a\n" +
|
||||
"\bPassword\x18\x01 \x01(\tR\bPassword\"\x0e\n" +
|
||||
"\fEmptyRequest\"f\n" +
|
||||
"\fFileMetadata\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName\x12\x1a\n" +
|
||||
"\bFileSize\x18\x02 \x01(\x03R\bFileSize\x12\x1e\n" +
|
||||
"\n" +
|
||||
"UploadedAt\x18\x03 \x01(\x03R\n" +
|
||||
"UploadedAt\"/\n" +
|
||||
"\bFileList\x12#\n" +
|
||||
"\x05Files\x18\x01 \x03(\v2\r.FileMetadataR\x05Files\"-\n" +
|
||||
"\x0fDownloadRequest\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName\"E\n" +
|
||||
"\x11AlertSubscription\x12\x1a\n" +
|
||||
"\bClientId\x18\x01 \x01(\tR\bClientId\x12\x14\n" +
|
||||
"\x05Topic\x18\x02 \x01(\tR\x05Topic\"|\n" +
|
||||
"\fAlertMessage\x12\x18\n" +
|
||||
"\aAlertId\x18\x01 \x01(\tR\aAlertId\x12\x1a\n" +
|
||||
"\bDeviceId\x18\x02 \x01(\tR\bDeviceId\x12\x18\n" +
|
||||
"\aMessage\x18\x03 \x01(\tR\aMessage\x12\x1c\n" +
|
||||
"\tTimestamp\x18\x04 \x01(\x03R\tTimestamp2\xcf\x02\n" +
|
||||
"\n" +
|
||||
"IoTService\x12&\n" +
|
||||
"\aGetDate\x12\x10.RequestDateTime\x1a\t.DateTime\x123\n" +
|
||||
"\x11UpdateSensingData\x12\f.SensingData\x1a\x10.SensingResponse\x12*\n" +
|
||||
"\rGetRandomPass\x12\f.RequestPass\x1a\v.RandomPass\x12)\n" +
|
||||
"\n" +
|
||||
"UploadFile\x12\n" +
|
||||
".FileChunk\x1a\r.UploadStatus(\x01\x12%\n" +
|
||||
"\tListFiles\x12\r.EmptyRequest\x1a\t.FileList\x12.\n" +
|
||||
"\fDownloadFile\x12\x10.DownloadRequest\x1a\n" +
|
||||
".FileChunk0\x01\x126\n" +
|
||||
"\x0fSubscribeAlerts\x12\x12.AlertSubscription\x1a\r.AlertMessage0\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_protoapi_proto_rawDescOnce sync.Once
|
||||
file_protoapi_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_protoapi_proto_rawDescGZIP() []byte {
|
||||
file_protoapi_proto_rawDescOnce.Do(func() {
|
||||
file_protoapi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)))
|
||||
})
|
||||
return file_protoapi_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
|
||||
var file_protoapi_proto_goTypes = []any{
|
||||
(*FileChunk)(nil), // 0: FileChunk
|
||||
(*UploadStatus)(nil), // 1: UploadStatus
|
||||
(*SensingData)(nil), // 2: SensingData
|
||||
(*SensingResponse)(nil), // 3: SensingResponse
|
||||
(*DateTime)(nil), // 4: DateTime
|
||||
(*RequestDateTime)(nil), // 5: RequestDateTime
|
||||
(*RequestPass)(nil), // 6: RequestPass
|
||||
(*RandomPass)(nil), // 7: RandomPass
|
||||
(*EmptyRequest)(nil), // 8: EmptyRequest
|
||||
(*FileMetadata)(nil), // 9: FileMetadata
|
||||
(*FileList)(nil), // 10: FileList
|
||||
(*DownloadRequest)(nil), // 11: DownloadRequest
|
||||
(*AlertSubscription)(nil), // 12: AlertSubscription
|
||||
(*AlertMessage)(nil), // 13: AlertMessage
|
||||
}
|
||||
var file_protoapi_proto_depIdxs = []int32{
|
||||
9, // 0: FileList.Files:type_name -> FileMetadata
|
||||
5, // 1: IoTService.GetDate:input_type -> RequestDateTime
|
||||
2, // 2: IoTService.UpdateSensingData:input_type -> SensingData
|
||||
6, // 3: IoTService.GetRandomPass:input_type -> RequestPass
|
||||
0, // 4: IoTService.UploadFile:input_type -> FileChunk
|
||||
8, // 5: IoTService.ListFiles:input_type -> EmptyRequest
|
||||
11, // 6: IoTService.DownloadFile:input_type -> DownloadRequest
|
||||
12, // 7: IoTService.SubscribeAlerts:input_type -> AlertSubscription
|
||||
4, // 8: IoTService.GetDate:output_type -> DateTime
|
||||
3, // 9: IoTService.UpdateSensingData:output_type -> SensingResponse
|
||||
7, // 10: IoTService.GetRandomPass:output_type -> RandomPass
|
||||
1, // 11: IoTService.UploadFile:output_type -> UploadStatus
|
||||
10, // 12: IoTService.ListFiles:output_type -> FileList
|
||||
0, // 13: IoTService.DownloadFile:output_type -> FileChunk
|
||||
13, // 14: IoTService.SubscribeAlerts:output_type -> AlertMessage
|
||||
8, // [8:15] is the sub-list for method output_type
|
||||
1, // [1:8] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_protoapi_proto_init() }
|
||||
func file_protoapi_proto_init() {
|
||||
if File_protoapi_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 14,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_protoapi_proto_goTypes,
|
||||
DependencyIndexes: file_protoapi_proto_depIdxs,
|
||||
MessageInfos: file_protoapi_proto_msgTypes,
|
||||
}.Build()
|
||||
File_protoapi_proto = out.File
|
||||
file_protoapi_proto_goTypes = nil
|
||||
file_protoapi_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
IoTService_GetDate_FullMethodName = "/IoTService/GetDate"
|
||||
IoTService_UpdateSensingData_FullMethodName = "/IoTService/UpdateSensingData"
|
||||
IoTService_GetRandomPass_FullMethodName = "/IoTService/GetRandomPass"
|
||||
IoTService_UploadFile_FullMethodName = "/IoTService/UploadFile"
|
||||
IoTService_ListFiles_FullMethodName = "/IoTService/ListFiles"
|
||||
IoTService_DownloadFile_FullMethodName = "/IoTService/DownloadFile"
|
||||
IoTService_SubscribeAlerts_FullMethodName = "/IoTService/SubscribeAlerts"
|
||||
)
|
||||
|
||||
// IoTServiceClient is the client API for IoTService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type IoTServiceClient interface {
|
||||
GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error)
|
||||
UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error)
|
||||
GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error)
|
||||
UploadFile(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[FileChunk, UploadStatus], error)
|
||||
ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error)
|
||||
DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error)
|
||||
SubscribeAlerts(ctx context.Context, in *AlertSubscription, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlertMessage], error)
|
||||
}
|
||||
|
||||
type ioTServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewIoTServiceClient(cc grpc.ClientConnInterface) IoTServiceClient {
|
||||
return &ioTServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *ioTServiceClient) GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DateTime)
|
||||
err := c.cc.Invoke(ctx, IoTService_GetDate_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *ioTServiceClient) UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SensingResponse)
|
||||
err := c.cc.Invoke(ctx, IoTService_UpdateSensingData_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *ioTServiceClient) GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RandomPass)
|
||||
err := c.cc.Invoke(ctx, IoTService_GetRandomPass_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *ioTServiceClient) UploadFile(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[FileChunk, UploadStatus], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &IoTService_ServiceDesc.Streams[0], IoTService_UploadFile_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[FileChunk, UploadStatus]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_UploadFileClient = grpc.ClientStreamingClient[FileChunk, UploadStatus]
|
||||
|
||||
func (c *ioTServiceClient) ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FileList)
|
||||
err := c.cc.Invoke(ctx, IoTService_ListFiles_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *ioTServiceClient) DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &IoTService_ServiceDesc.Streams[1], IoTService_DownloadFile_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[DownloadRequest, FileChunk]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_DownloadFileClient = grpc.ServerStreamingClient[FileChunk]
|
||||
|
||||
func (c *ioTServiceClient) SubscribeAlerts(ctx context.Context, in *AlertSubscription, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlertMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &IoTService_ServiceDesc.Streams[2], IoTService_SubscribeAlerts_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[AlertSubscription, AlertMessage]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_SubscribeAlertsClient = grpc.ServerStreamingClient[AlertMessage]
|
||||
|
||||
// IoTServiceServer is the server API for IoTService service.
|
||||
// All implementations must embed UnimplementedIoTServiceServer
|
||||
// for forward compatibility.
|
||||
type IoTServiceServer interface {
|
||||
GetDate(context.Context, *RequestDateTime) (*DateTime, error)
|
||||
UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error)
|
||||
GetRandomPass(context.Context, *RequestPass) (*RandomPass, error)
|
||||
UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error
|
||||
ListFiles(context.Context, *EmptyRequest) (*FileList, error)
|
||||
DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error
|
||||
SubscribeAlerts(*AlertSubscription, grpc.ServerStreamingServer[AlertMessage]) error
|
||||
mustEmbedUnimplementedIoTServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedIoTServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedIoTServiceServer struct{}
|
||||
|
||||
func (UnimplementedIoTServiceServer) GetDate(context.Context, *RequestDateTime) (*DateTime, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDate not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateSensingData not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) GetRandomPass(context.Context, *RequestPass) (*RandomPass, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRandomPass not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error {
|
||||
return status.Error(codes.Unimplemented, "method UploadFile not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) ListFiles(context.Context, *EmptyRequest) (*FileList, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListFiles not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error {
|
||||
return status.Error(codes.Unimplemented, "method DownloadFile not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) SubscribeAlerts(*AlertSubscription, grpc.ServerStreamingServer[AlertMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeAlerts not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) mustEmbedUnimplementedIoTServiceServer() {}
|
||||
func (UnimplementedIoTServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeIoTServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to IoTServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeIoTServiceServer interface {
|
||||
mustEmbedUnimplementedIoTServiceServer()
|
||||
}
|
||||
|
||||
func RegisterIoTServiceServer(s grpc.ServiceRegistrar, srv IoTServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedIoTServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&IoTService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _IoTService_GetDate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RequestDateTime)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IoTServiceServer).GetDate(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IoTService_GetDate_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IoTServiceServer).GetDate(ctx, req.(*RequestDateTime))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IoTService_UpdateSensingData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SensingData)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IoTServiceServer).UpdateSensingData(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IoTService_UpdateSensingData_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IoTServiceServer).UpdateSensingData(ctx, req.(*SensingData))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IoTService_GetRandomPass_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RequestPass)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IoTServiceServer).GetRandomPass(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IoTService_GetRandomPass_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IoTServiceServer).GetRandomPass(ctx, req.(*RequestPass))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IoTService_UploadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(IoTServiceServer).UploadFile(&grpc.GenericServerStream[FileChunk, UploadStatus]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_UploadFileServer = grpc.ClientStreamingServer[FileChunk, UploadStatus]
|
||||
|
||||
func _IoTService_ListFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(EmptyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IoTServiceServer).ListFiles(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IoTService_ListFiles_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IoTServiceServer).ListFiles(ctx, req.(*EmptyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IoTService_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(DownloadRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(IoTServiceServer).DownloadFile(m, &grpc.GenericServerStream[DownloadRequest, FileChunk]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_DownloadFileServer = grpc.ServerStreamingServer[FileChunk]
|
||||
|
||||
func _IoTService_SubscribeAlerts_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(AlertSubscription)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(IoTServiceServer).SubscribeAlerts(m, &grpc.GenericServerStream[AlertSubscription, AlertMessage]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_SubscribeAlertsServer = grpc.ServerStreamingServer[AlertMessage]
|
||||
|
||||
// IoTService_ServiceDesc is the grpc.ServiceDesc for IoTService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var IoTService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "IoTService",
|
||||
HandlerType: (*IoTServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetDate",
|
||||
Handler: _IoTService_GetDate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateSensingData",
|
||||
Handler: _IoTService_UpdateSensingData_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRandomPass",
|
||||
Handler: _IoTService_GetRandomPass_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListFiles",
|
||||
Handler: _IoTService_ListFiles_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "UploadFile",
|
||||
Handler: _IoTService_UploadFile_Handler,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "DownloadFile",
|
||||
Handler: _IoTService_DownloadFile_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "SubscribeAlerts",
|
||||
Handler: _IoTService_SubscribeAlerts_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "protoapi.proto",
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"grpccanary/lib/grpc/basic/protoapi"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var min = 0
|
||||
var max = 100
|
||||
var port = ":8080"
|
||||
|
||||
type UploadedFile struct {
|
||||
FileName string
|
||||
Content []byte
|
||||
UploadedAt int64
|
||||
}
|
||||
|
||||
var (
|
||||
fileStore = make(map[string]*UploadedFile)
|
||||
storeMu sync.RWMutex
|
||||
)
|
||||
|
||||
type AlertSubscriber struct {
|
||||
ClientId string
|
||||
Channel chan *protoapi.AlertMessage
|
||||
}
|
||||
|
||||
var (
|
||||
subscribers = make(map[string]*AlertSubscriber)
|
||||
subMu sync.Mutex
|
||||
)
|
||||
|
||||
func publishAlert(alert *protoapi.AlertMessage) {
|
||||
subMu.Lock()
|
||||
defer subMu.Unlock()
|
||||
for _, sub := range subscribers {
|
||||
select {
|
||||
case sub.Channel <- alert:
|
||||
default:
|
||||
fmt.Printf("Alert channel blocked for client %s, dropping event\n", sub.ClientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func random(min, max int, src rand.Source) int {
|
||||
return rand.New(src).Intn(max-min) + min
|
||||
}
|
||||
|
||||
// Extra function for creating secure random numbers
|
||||
//
|
||||
// func randomSecure(min, max int) int {
|
||||
// v, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return min
|
||||
// }
|
||||
// fmt.Println("**", v, min, max)
|
||||
|
||||
// return min + int(v.Uint64())
|
||||
// }
|
||||
|
||||
func getString(len int64, src rand.Source) string {
|
||||
temp := ""
|
||||
startChar := "!"
|
||||
var i int64 = 1
|
||||
for {
|
||||
// For getting valid ASCII characters
|
||||
myRand := random(0, 94, src)
|
||||
newChar := string(startChar[0] + byte(myRand))
|
||||
temp = temp + newChar
|
||||
if i == len {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
type IoTServer struct {
|
||||
protoapi.UnimplementedIoTServiceServer
|
||||
}
|
||||
|
||||
func (IoTServer) GetDate(ctx context.Context, r *protoapi.RequestDateTime) (*protoapi.DateTime, error) {
|
||||
currentTime := time.Now()
|
||||
response := &protoapi.DateTime{
|
||||
Value: currentTime.String(),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (IoTServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) {
|
||||
fmt.Printf("Received sensing data - Device: %s, Temp: %.2f°C, Humid: %.2f%%\n", r.GetDeviceId(), r.GetTemperature(), r.GetHumidity())
|
||||
|
||||
// 임계값 초과(40도 초과) 시 실시간 Pub/Sub 경보 메시지 발행
|
||||
if r.GetTemperature() > 40.0 {
|
||||
fmt.Printf("⚠️ Critical temperature detected: %.2f°C! Publishing warning...\n", r.GetTemperature())
|
||||
publishAlert(&protoapi.AlertMessage{
|
||||
AlertId: fmt.Sprintf("alert-%d", time.Now().UnixNano()),
|
||||
DeviceId: r.GetDeviceId(),
|
||||
Message: fmt.Sprintf("Critical high temperature: %.2f°C (Humidity: %.2f%%)", r.GetTemperature(), r.GetHumidity()),
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
response := &protoapi.SensingResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Sensing data updated successfully for device %s", r.GetDeviceId()),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (IoTServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*protoapi.RandomPass, error) {
|
||||
src := rand.NewSource(r.GetSeed())
|
||||
temp := getString(r.GetLength(), src)
|
||||
|
||||
response := &protoapi.RandomPass{
|
||||
Password: temp,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error {
|
||||
var totalBytes int64
|
||||
var fileName string
|
||||
var buffer []byte
|
||||
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
fmt.Printf("File upload completed. Received %d bytes for file '%s'\n", totalBytes, fileName)
|
||||
|
||||
if fileName != "" {
|
||||
storeMu.Lock()
|
||||
fileStore[fileName] = &UploadedFile{
|
||||
FileName: fileName,
|
||||
Content: buffer,
|
||||
UploadedAt: time.Now().Unix(),
|
||||
}
|
||||
storeMu.Unlock()
|
||||
}
|
||||
|
||||
return stream.SendAndClose(&protoapi.UploadStatus{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("File '%s' uploaded successfully.", fileName),
|
||||
BytesUploaded: totalBytes,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("File upload error:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if fileName == "" {
|
||||
fileName = chunk.GetFileName()
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
totalBytes += int64(len(chunk.GetContent()))
|
||||
}
|
||||
}
|
||||
|
||||
func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) {
|
||||
storeMu.RLock()
|
||||
defer storeMu.RUnlock()
|
||||
|
||||
var files []*protoapi.FileMetadata
|
||||
for _, f := range fileStore {
|
||||
files = append(files, &protoapi.FileMetadata{
|
||||
FileName: f.FileName,
|
||||
FileSize: int64(len(f.Content)),
|
||||
UploadedAt: f.UploadedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &protoapi.FileList{Files: files}, nil
|
||||
}
|
||||
|
||||
func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTService_DownloadFileServer) error {
|
||||
storeMu.RLock()
|
||||
f, exists := fileStore[r.GetFileName()]
|
||||
storeMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return status.Errorf(codes.NotFound, "file %s not found", r.GetFileName())
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 청크 단위
|
||||
totalBytes := len(f.Content)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: f.FileName,
|
||||
Content: f.Content[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (IoTServer) SubscribeAlerts(r *protoapi.AlertSubscription, stream protoapi.IoTService_SubscribeAlertsServer) error {
|
||||
clientId := r.GetClientId()
|
||||
ch := make(chan *protoapi.AlertMessage, 10)
|
||||
sub := &AlertSubscriber{
|
||||
ClientId: clientId,
|
||||
Channel: ch,
|
||||
}
|
||||
|
||||
subMu.Lock()
|
||||
subscribers[clientId] = sub
|
||||
subMu.Unlock()
|
||||
|
||||
fmt.Printf("Client %s subscribed to alerts on topic '%s'\n", clientId, r.GetTopic())
|
||||
|
||||
for {
|
||||
select {
|
||||
case alert := <-ch:
|
||||
err := stream.Send(alert)
|
||||
if err != nil {
|
||||
subMu.Lock()
|
||||
delete(subscribers, clientId)
|
||||
subMu.Unlock()
|
||||
fmt.Printf("Client %s alert subscription disconnected: %v\n", clientId, err)
|
||||
return err
|
||||
}
|
||||
case <-stream.Context().Done():
|
||||
subMu.Lock()
|
||||
delete(subscribers, clientId)
|
||||
subMu.Unlock()
|
||||
fmt.Printf("Client %s unsubscribed (context done)\n", clientId)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ServerRun(addr string) {
|
||||
server := grpc.NewServer()
|
||||
var iotServer IoTServer
|
||||
protoapi.RegisterIoTServiceServer(server, iotServer)
|
||||
|
||||
reflection.Register(server)
|
||||
|
||||
listen, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Serving requests...")
|
||||
server.Serve(listen)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"grpccanary/lib/grpc/http3/protoapi"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func quicDialer(tlsConf *tls.Config) func(context.Context, string) (net.Conn, error) {
|
||||
return func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
qconn, err := quic.DialAddr(ctx, addr, tlsConf, &quic.Config{
|
||||
KeepAlivePeriod: 10 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("quic dial failed: %w", err)
|
||||
}
|
||||
|
||||
stream, err := qconn.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
_ = qconn.CloseWithError(0, "failed to open stream")
|
||||
return nil, fmt.Errorf("failed to open stream: %w", err)
|
||||
}
|
||||
|
||||
return &quicNetConn{Stream: stream, conn: qconn}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ClientRun(addr string) error {
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"grpc-http3-canary"},
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(quicDialer(tlsConf)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create grpc client: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := protoapi.NewHttp3ServiceClient(conn)
|
||||
|
||||
// Call 1: First Unary Ping
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
res1, err := client.Ping(ctx, &protoapi.PingRequest{Message: "First Message"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unary ping 1 failed: %w", err)
|
||||
}
|
||||
fmt.Printf("[HTTP3 Client] Received Ping 1 Response: Message='%s', Transport='%s'\n", res1.GetMessage(), res1.GetTransport())
|
||||
|
||||
// Call 2: Second Unary Ping (to verify stream multiplexing / reuse over same QUIC connection)
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel2()
|
||||
res2, err := client.Ping(ctx2, &protoapi.PingRequest{Message: "Second Message"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unary ping 2 failed: %w", err)
|
||||
}
|
||||
fmt.Printf("[HTTP3 Client] Received Ping 2 Response: Message='%s', Transport='%s'\n", res2.GetMessage(), res2.GetTransport())
|
||||
|
||||
// Call 3: Bidirectional Streaming Ping
|
||||
streamCtx, streamCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer streamCancel()
|
||||
stream, err := client.StreamPing(streamCtx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open bi-directional stream: %w", err)
|
||||
}
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
msg := fmt.Sprintf("Stream Message %d", i)
|
||||
err := stream.Send(&protoapi.PingRequest{Message: msg})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send stream message %d: %w", i, err)
|
||||
}
|
||||
|
||||
res, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to receive stream response %d: %w", i, err)
|
||||
}
|
||||
fmt.Printf("[HTTP3 Client] Received Stream Response %d: Message='%s', Transport='%s'\n", i, res.GetMessage(), res.GetTransport())
|
||||
}
|
||||
|
||||
_ = stream.CloseSend()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHttp3ServerClient(t *testing.T) {
|
||||
// Start server on ephemeral port
|
||||
lis, cleanup, err := ServerRun("127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start server: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
addr := lis.Addr().String()
|
||||
|
||||
// Wait for server to be fully ready
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Run client against the server
|
||||
err = ClientRun(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("client run failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
service Http3Service {
|
||||
rpc Ping (PingRequest) returns (PingResponse);
|
||||
rpc StreamPing (stream PingRequest) returns (stream PingResponse);
|
||||
}
|
||||
|
||||
message PingRequest {
|
||||
string Message = 1;
|
||||
}
|
||||
|
||||
message PingResponse {
|
||||
string Message = 1;
|
||||
string Transport = 2; // Should return "quic"
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type PingRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Message string `protobuf:"bytes,1,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PingRequest) Reset() {
|
||||
*x = PingRequest{}
|
||||
mi := &file_protoapi_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PingRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PingRequest) ProtoMessage() {}
|
||||
|
||||
func (x *PingRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
|
||||
func (*PingRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PingRequest) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PingResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Message string `protobuf:"bytes,1,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
Transport string `protobuf:"bytes,2,opt,name=Transport,proto3" json:"Transport,omitempty"` // Should return "quic"
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PingResponse) Reset() {
|
||||
*x = PingResponse{}
|
||||
mi := &file_protoapi_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PingResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PingResponse) ProtoMessage() {}
|
||||
|
||||
func (x *PingResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead.
|
||||
func (*PingResponse) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *PingResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PingResponse) GetTransport() string {
|
||||
if x != nil {
|
||||
return x.Transport
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_protoapi_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_protoapi_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0eprotoapi.proto\"'\n" +
|
||||
"\vPingRequest\x12\x18\n" +
|
||||
"\aMessage\x18\x01 \x01(\tR\aMessage\"F\n" +
|
||||
"\fPingResponse\x12\x18\n" +
|
||||
"\aMessage\x18\x01 \x01(\tR\aMessage\x12\x1c\n" +
|
||||
"\tTransport\x18\x02 \x01(\tR\tTransport2b\n" +
|
||||
"\fHttp3Service\x12#\n" +
|
||||
"\x04Ping\x12\f.PingRequest\x1a\r.PingResponse\x12-\n" +
|
||||
"\n" +
|
||||
"StreamPing\x12\f.PingRequest\x1a\r.PingResponse(\x010\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_protoapi_proto_rawDescOnce sync.Once
|
||||
file_protoapi_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_protoapi_proto_rawDescGZIP() []byte {
|
||||
file_protoapi_proto_rawDescOnce.Do(func() {
|
||||
file_protoapi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)))
|
||||
})
|
||||
return file_protoapi_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_protoapi_proto_goTypes = []any{
|
||||
(*PingRequest)(nil), // 0: PingRequest
|
||||
(*PingResponse)(nil), // 1: PingResponse
|
||||
}
|
||||
var file_protoapi_proto_depIdxs = []int32{
|
||||
0, // 0: Http3Service.Ping:input_type -> PingRequest
|
||||
0, // 1: Http3Service.StreamPing:input_type -> PingRequest
|
||||
1, // 2: Http3Service.Ping:output_type -> PingResponse
|
||||
1, // 3: Http3Service.StreamPing:output_type -> PingResponse
|
||||
2, // [2:4] is the sub-list for method output_type
|
||||
0, // [0:2] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_protoapi_proto_init() }
|
||||
func file_protoapi_proto_init() {
|
||||
if File_protoapi_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_protoapi_proto_goTypes,
|
||||
DependencyIndexes: file_protoapi_proto_depIdxs,
|
||||
MessageInfos: file_protoapi_proto_msgTypes,
|
||||
}.Build()
|
||||
File_protoapi_proto = out.File
|
||||
file_protoapi_proto_goTypes = nil
|
||||
file_protoapi_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Http3Service_Ping_FullMethodName = "/Http3Service/Ping"
|
||||
Http3Service_StreamPing_FullMethodName = "/Http3Service/StreamPing"
|
||||
)
|
||||
|
||||
// Http3ServiceClient is the client API for Http3Service service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type Http3ServiceClient interface {
|
||||
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
|
||||
StreamPing(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PingRequest, PingResponse], error)
|
||||
}
|
||||
|
||||
type http3ServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewHttp3ServiceClient(cc grpc.ClientConnInterface) Http3ServiceClient {
|
||||
return &http3ServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *http3ServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PingResponse)
|
||||
err := c.cc.Invoke(ctx, Http3Service_Ping_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *http3ServiceClient) StreamPing(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PingRequest, PingResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &Http3Service_ServiceDesc.Streams[0], Http3Service_StreamPing_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[PingRequest, PingResponse]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Http3Service_StreamPingClient = grpc.BidiStreamingClient[PingRequest, PingResponse]
|
||||
|
||||
// Http3ServiceServer is the server API for Http3Service service.
|
||||
// All implementations must embed UnimplementedHttp3ServiceServer
|
||||
// for forward compatibility.
|
||||
type Http3ServiceServer interface {
|
||||
Ping(context.Context, *PingRequest) (*PingResponse, error)
|
||||
StreamPing(grpc.BidiStreamingServer[PingRequest, PingResponse]) error
|
||||
mustEmbedUnimplementedHttp3ServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedHttp3ServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedHttp3ServiceServer struct{}
|
||||
|
||||
func (UnimplementedHttp3ServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Ping not implemented")
|
||||
}
|
||||
func (UnimplementedHttp3ServiceServer) StreamPing(grpc.BidiStreamingServer[PingRequest, PingResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method StreamPing not implemented")
|
||||
}
|
||||
func (UnimplementedHttp3ServiceServer) mustEmbedUnimplementedHttp3ServiceServer() {}
|
||||
func (UnimplementedHttp3ServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeHttp3ServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to Http3ServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeHttp3ServiceServer interface {
|
||||
mustEmbedUnimplementedHttp3ServiceServer()
|
||||
}
|
||||
|
||||
func RegisterHttp3ServiceServer(s grpc.ServiceRegistrar, srv Http3ServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedHttp3ServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Http3Service_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Http3Service_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PingRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(Http3ServiceServer).Ping(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Http3Service_Ping_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(Http3ServiceServer).Ping(ctx, req.(*PingRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Http3Service_StreamPing_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(Http3ServiceServer).StreamPing(&grpc.GenericServerStream[PingRequest, PingResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Http3Service_StreamPingServer = grpc.BidiStreamingServer[PingRequest, PingResponse]
|
||||
|
||||
// Http3Service_ServiceDesc is the grpc.ServiceDesc for Http3Service service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Http3Service_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "Http3Service",
|
||||
HandlerType: (*Http3ServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Ping",
|
||||
Handler: _Http3Service_Ping_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "StreamPing",
|
||||
Handler: _Http3Service_StreamPing_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "protoapi.proto",
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"grpccanary/lib/grpc/http3/protoapi"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
// 1. self-signed certificate generation
|
||||
func generateTLSConfig() (*tls.Config, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"gRPC HTTP3 Canary"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{"grpc-http3-canary"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 2. net.Conn wrapper for quic.Stream
|
||||
type quicNetConn struct {
|
||||
*quic.Stream
|
||||
conn *quic.Conn
|
||||
}
|
||||
|
||||
func (c *quicNetConn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *quicNetConn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// 3. net.Listener wrapper for quic.Listener using non-blocking channels
|
||||
type quicListener struct {
|
||||
lis *quic.Listener
|
||||
connChan chan net.Conn
|
||||
errChan chan error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewQuicListener(lis *quic.Listener) *quicListener {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ql := &quicListener{
|
||||
lis: lis,
|
||||
connChan: make(chan net.Conn, 100),
|
||||
errChan: make(chan error, 10),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
go ql.listenLoop()
|
||||
return ql
|
||||
}
|
||||
|
||||
func (ql *quicListener) listenLoop() {
|
||||
for {
|
||||
qconn, err := ql.lis.Accept(ql.ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case ql.errChan <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
go ql.acceptStreams(qconn)
|
||||
}
|
||||
}
|
||||
|
||||
func (ql *quicListener) acceptStreams(qconn *quic.Conn) {
|
||||
for {
|
||||
stream, err := qconn.AcceptStream(ql.ctx)
|
||||
if err != nil {
|
||||
// Stop checking this connection when it closes
|
||||
return
|
||||
}
|
||||
ql.connChan <- &quicNetConn{Stream: stream, conn: qconn}
|
||||
}
|
||||
}
|
||||
|
||||
func (ql *quicListener) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn := <-ql.connChan:
|
||||
return conn, nil
|
||||
case err := <-ql.errChan:
|
||||
return nil, err
|
||||
case <-ql.ctx.Done():
|
||||
return nil, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
func (ql *quicListener) Close() error {
|
||||
ql.cancel()
|
||||
return ql.lis.Close()
|
||||
}
|
||||
|
||||
func (ql *quicListener) Addr() net.Addr {
|
||||
return ql.lis.Addr()
|
||||
}
|
||||
|
||||
// 4. Http3Service Server Implementation
|
||||
type Http3Server struct {
|
||||
protoapi.UnimplementedHttp3ServiceServer
|
||||
}
|
||||
|
||||
func (Http3Server) Ping(ctx context.Context, r *protoapi.PingRequest) (*protoapi.PingResponse, error) {
|
||||
return &protoapi.PingResponse{
|
||||
Message: "Pong: " + r.GetMessage(),
|
||||
Transport: "quic",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (Http3Server) StreamPing(stream protoapi.Http3Service_StreamPingServer) error {
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stream.Send(&protoapi.PingResponse{
|
||||
Message: "Pong Stream: " + req.GetMessage(),
|
||||
Transport: "quic",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ServerRun(addr string) (*quic.Listener, func(), error) {
|
||||
tlsConf, err := generateTLSConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
lis, err := quic.ListenAddr(addr, tlsConf, &quic.Config{
|
||||
KeepAlivePeriod: 10 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
qlis := NewQuicListener(lis)
|
||||
server := grpc.NewServer()
|
||||
protoapi.RegisterHttp3ServiceServer(server, Http3Server{})
|
||||
|
||||
reflection.Register(server)
|
||||
|
||||
go func() {
|
||||
fmt.Printf("[HTTP3 Server] Serving gRPC on UDP/QUIC %s...\n", addr)
|
||||
if err := server.Serve(qlis); err != nil {
|
||||
fmt.Println("[HTTP3 Server] Server closed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
cleanup := func() {
|
||||
server.GracefulStop()
|
||||
qlis.Close()
|
||||
}
|
||||
|
||||
return lis, cleanup, nil
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package modular
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"grpccanary/lib/grpc/modular/protoapi"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func AskingDateTime(ctx context.Context, m protoapi.CoreServiceClient) (*protoapi.DateTime, error) {
|
||||
request := &protoapi.RequestDateTime{
|
||||
Value: "Please send me the date and time",
|
||||
}
|
||||
return m.GetDate(ctx, request)
|
||||
}
|
||||
|
||||
func AskPass(ctx context.Context, m protoapi.CoreServiceClient, seed int64, length int64) (*protoapi.RandomPass, error) {
|
||||
request := &protoapi.RequestPass{
|
||||
Seed: seed,
|
||||
Length: length,
|
||||
}
|
||||
return m.GetRandomPass(ctx, request)
|
||||
}
|
||||
|
||||
func AskUpdateSensingData(ctx context.Context, m protoapi.CoreServiceClient, deviceId string, temp float64, humid float64) (*protoapi.SensingResponse, error) {
|
||||
request := &protoapi.SensingData{
|
||||
DeviceId: deviceId,
|
||||
Temperature: temp,
|
||||
Humidity: humid,
|
||||
}
|
||||
return m.UpdateSensingData(ctx, request)
|
||||
}
|
||||
|
||||
func AskUploadFile(ctx context.Context, m protoapi.FileTransferServiceClient, fileName string, fileData []byte) (*protoapi.UploadStatus, error) {
|
||||
stream, err := m.UploadFile(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunkSize := 1024
|
||||
totalBytes := len(fileData)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: fileName,
|
||||
Content: fileData[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return stream.CloseAndRecv()
|
||||
}
|
||||
|
||||
func AskListFiles(ctx context.Context, m protoapi.FileTransferServiceClient) (*protoapi.FileList, error) {
|
||||
return m.ListFiles(ctx, &protoapi.EmptyRequest{})
|
||||
}
|
||||
|
||||
func AskDownloadFile(ctx context.Context, m protoapi.FileTransferServiceClient, fileName string) ([]byte, error) {
|
||||
stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer []byte
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func AskSubscribeAlerts(ctx context.Context, m protoapi.AlertServiceClient, clientId string, topic string) {
|
||||
stream, err := m.SubscribeAlerts(ctx, &protoapi.AlertSubscription{
|
||||
ClientId: clientId,
|
||||
Topic: topic,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] Failed to subscribe alerts:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
alert, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
fmt.Println("[Modular Client] Alert subscription stream closed by server.")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
fmt.Printf("\n🔔 [Modular ALERT RECEIVED] ID: %s | Device: %s | Msg: %s | Time: %s\n\n",
|
||||
alert.GetAlertId(), alert.GetDeviceId(), alert.GetMessage(),
|
||||
time.Unix(alert.GetTimestamp(), 0).Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
|
||||
func ClientRun(addr string) {
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] NewClient error:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
coreClient := protoapi.NewCoreServiceClient(conn)
|
||||
fileClient := protoapi.NewFileTransferServiceClient(conn)
|
||||
alertClient := protoapi.NewAlertServiceClient(conn)
|
||||
|
||||
// Background Subscription
|
||||
alertCtx, alertCancel := context.WithCancel(context.Background())
|
||||
defer alertCancel()
|
||||
go AskSubscribeAlerts(alertCtx, alertClient, "modular-client-01", "temperature_warnings")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
r, err := AskingDateTime(context.Background(), coreClient)
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] GetDate error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("[Modular Client] Server Date and Time:", r.Value)
|
||||
|
||||
length := int64(rand.Intn(20))
|
||||
p, err := AskPass(context.Background(), coreClient, 100, length+1)
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] GetRandomPass error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("[Modular Client] Random Password:", p.Password)
|
||||
|
||||
res, err := AskUpdateSensingData(context.Background(), coreClient, "modular-sensor-01", 24.5, 52.3)
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] UpdateSensingData error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("[Modular Client] Sensing Update Success:", res.Success)
|
||||
|
||||
// File Upload
|
||||
dummyData := make([]byte, 10240)
|
||||
for i := range dummyData {
|
||||
dummyData[i] = byte(rand.Intn(256))
|
||||
}
|
||||
fmt.Println("[Modular Client] Uploading dummy file...")
|
||||
status, err := AskUploadFile(context.Background(), fileClient, "firmware_modular.bin", dummyData)
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] File upload failed:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("[Modular Client] Upload Success:", status.Success)
|
||||
|
||||
// File List
|
||||
list, err := AskListFiles(context.Background(), fileClient)
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] ListFiles failed:", err)
|
||||
return
|
||||
}
|
||||
for i, f := range list.GetFiles() {
|
||||
fmt.Printf("[Modular Client] File [%d]: %s, size: %d\n", i+1, f.GetFileName(), f.GetFileSize())
|
||||
}
|
||||
|
||||
// File Download & Verification
|
||||
downloadedData, err := AskDownloadFile(context.Background(), fileClient, "firmware_modular.bin")
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] Download failed:", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("[Modular Client] Downloaded %d bytes.\n", len(downloadedData))
|
||||
|
||||
isMatch := true
|
||||
if len(dummyData) != len(downloadedData) {
|
||||
isMatch = false
|
||||
} else {
|
||||
for i := range dummyData {
|
||||
if dummyData[i] != downloadedData[i] {
|
||||
isMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("[Modular Client] Data Integrity Checked: %t\n", isMatch)
|
||||
|
||||
// Trigger alert
|
||||
fmt.Println("[Modular Client] Triggering abnormal high-temperature...")
|
||||
_, err = AskUpdateSensingData(context.Background(), coreClient, "modular-sensor-01", 45.8, 60.1)
|
||||
if err != nil {
|
||||
fmt.Println("[Modular Client] UpdateSensingData (abnormal) error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package modular
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestModularServerClient(t *testing.T) {
|
||||
// Find an ephemeral port
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
addr := lis.Addr().String()
|
||||
lis.Close()
|
||||
|
||||
// Start server in background
|
||||
go ServerRun(addr)
|
||||
|
||||
// Wait for server to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Run client against the server
|
||||
ClientRun(addr)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service AlertService {
|
||||
rpc SubscribeAlerts (AlertSubscription) returns (stream AlertMessage);
|
||||
}
|
||||
|
||||
message AlertSubscription {
|
||||
string ClientId = 1;
|
||||
string Topic = 2;
|
||||
}
|
||||
|
||||
message AlertMessage {
|
||||
string AlertId = 1;
|
||||
string DeviceId = 2;
|
||||
string Message = 3;
|
||||
int64 Timestamp = 4;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
message DateTime {
|
||||
string Value = 1;
|
||||
}
|
||||
|
||||
message RequestDateTime {
|
||||
string Value = 2;
|
||||
}
|
||||
|
||||
message RequestPass {
|
||||
int64 Seed = 1;
|
||||
int64 Length = 8;
|
||||
}
|
||||
|
||||
message RandomPass {
|
||||
string Password = 1;
|
||||
}
|
||||
|
||||
message EmptyRequest {}
|
||||
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service CoreService {
|
||||
rpc GetDate (RequestDateTime) returns (DateTime);
|
||||
rpc UpdateSensingData (SensingData) returns (SensingResponse);
|
||||
rpc GetRandomPass (RequestPass) returns (RandomPass);
|
||||
}
|
||||
|
||||
message SensingData {
|
||||
string DeviceId = 1;
|
||||
double Temperature = 2;
|
||||
double Humidity = 3;
|
||||
}
|
||||
|
||||
message SensingResponse {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service FileTransferService {
|
||||
rpc UploadFile (stream FileChunk) returns (UploadStatus);
|
||||
rpc ListFiles (EmptyRequest) returns (FileList);
|
||||
rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
|
||||
}
|
||||
|
||||
message FileChunk {
|
||||
string FileName = 1;
|
||||
bytes Content = 2;
|
||||
}
|
||||
|
||||
message UploadStatus {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
int64 BytesUploaded = 3;
|
||||
}
|
||||
|
||||
message FileMetadata {
|
||||
string FileName = 1;
|
||||
int64 FileSize = 2;
|
||||
int64 UploadedAt = 3;
|
||||
}
|
||||
|
||||
message FileList {
|
||||
repeated FileMetadata Files = 1;
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string FileName = 1;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: alerts.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type AlertSubscription struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ClientId string `protobuf:"bytes,1,opt,name=ClientId,proto3" json:"ClientId,omitempty"`
|
||||
Topic string `protobuf:"bytes,2,opt,name=Topic,proto3" json:"Topic,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) Reset() {
|
||||
*x = AlertSubscription{}
|
||||
mi := &file_alerts_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AlertSubscription) ProtoMessage() {}
|
||||
|
||||
func (x *AlertSubscription) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alerts_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AlertSubscription.ProtoReflect.Descriptor instead.
|
||||
func (*AlertSubscription) Descriptor() ([]byte, []int) {
|
||||
return file_alerts_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) GetClientId() string {
|
||||
if x != nil {
|
||||
return x.ClientId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertSubscription) GetTopic() string {
|
||||
if x != nil {
|
||||
return x.Topic
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AlertMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AlertId string `protobuf:"bytes,1,opt,name=AlertId,proto3" json:"AlertId,omitempty"`
|
||||
DeviceId string `protobuf:"bytes,2,opt,name=DeviceId,proto3" json:"DeviceId,omitempty"`
|
||||
Message string `protobuf:"bytes,3,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
Timestamp int64 `protobuf:"varint,4,opt,name=Timestamp,proto3" json:"Timestamp,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AlertMessage) Reset() {
|
||||
*x = AlertMessage{}
|
||||
mi := &file_alerts_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AlertMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AlertMessage) ProtoMessage() {}
|
||||
|
||||
func (x *AlertMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alerts_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AlertMessage.ProtoReflect.Descriptor instead.
|
||||
func (*AlertMessage) Descriptor() ([]byte, []int) {
|
||||
return file_alerts_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetAlertId() string {
|
||||
if x != nil {
|
||||
return x.AlertId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetDeviceId() string {
|
||||
if x != nil {
|
||||
return x.DeviceId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertMessage) GetTimestamp() int64 {
|
||||
if x != nil {
|
||||
return x.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_alerts_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_alerts_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\falerts.proto\x1a\fcommon.proto\"E\n" +
|
||||
"\x11AlertSubscription\x12\x1a\n" +
|
||||
"\bClientId\x18\x01 \x01(\tR\bClientId\x12\x14\n" +
|
||||
"\x05Topic\x18\x02 \x01(\tR\x05Topic\"|\n" +
|
||||
"\fAlertMessage\x12\x18\n" +
|
||||
"\aAlertId\x18\x01 \x01(\tR\aAlertId\x12\x1a\n" +
|
||||
"\bDeviceId\x18\x02 \x01(\tR\bDeviceId\x12\x18\n" +
|
||||
"\aMessage\x18\x03 \x01(\tR\aMessage\x12\x1c\n" +
|
||||
"\tTimestamp\x18\x04 \x01(\x03R\tTimestamp2F\n" +
|
||||
"\fAlertService\x126\n" +
|
||||
"\x0fSubscribeAlerts\x12\x12.AlertSubscription\x1a\r.AlertMessage0\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_alerts_proto_rawDescOnce sync.Once
|
||||
file_alerts_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_alerts_proto_rawDescGZIP() []byte {
|
||||
file_alerts_proto_rawDescOnce.Do(func() {
|
||||
file_alerts_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_alerts_proto_rawDesc), len(file_alerts_proto_rawDesc)))
|
||||
})
|
||||
return file_alerts_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_alerts_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_alerts_proto_goTypes = []any{
|
||||
(*AlertSubscription)(nil), // 0: AlertSubscription
|
||||
(*AlertMessage)(nil), // 1: AlertMessage
|
||||
}
|
||||
var file_alerts_proto_depIdxs = []int32{
|
||||
0, // 0: AlertService.SubscribeAlerts:input_type -> AlertSubscription
|
||||
1, // 1: AlertService.SubscribeAlerts:output_type -> AlertMessage
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
0, // [0:1] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_alerts_proto_init() }
|
||||
func file_alerts_proto_init() {
|
||||
if File_alerts_proto != nil {
|
||||
return
|
||||
}
|
||||
file_common_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_alerts_proto_rawDesc), len(file_alerts_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_alerts_proto_goTypes,
|
||||
DependencyIndexes: file_alerts_proto_depIdxs,
|
||||
MessageInfos: file_alerts_proto_msgTypes,
|
||||
}.Build()
|
||||
File_alerts_proto = out.File
|
||||
file_alerts_proto_goTypes = nil
|
||||
file_alerts_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.21.12
|
||||
// source: alerts.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
AlertService_SubscribeAlerts_FullMethodName = "/AlertService/SubscribeAlerts"
|
||||
)
|
||||
|
||||
// AlertServiceClient is the client API for AlertService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AlertServiceClient interface {
|
||||
SubscribeAlerts(ctx context.Context, in *AlertSubscription, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlertMessage], error)
|
||||
}
|
||||
|
||||
type alertServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAlertServiceClient(cc grpc.ClientConnInterface) AlertServiceClient {
|
||||
return &alertServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *alertServiceClient) SubscribeAlerts(ctx context.Context, in *AlertSubscription, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlertMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &AlertService_ServiceDesc.Streams[0], AlertService_SubscribeAlerts_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[AlertSubscription, AlertMessage]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type AlertService_SubscribeAlertsClient = grpc.ServerStreamingClient[AlertMessage]
|
||||
|
||||
// AlertServiceServer is the server API for AlertService service.
|
||||
// All implementations must embed UnimplementedAlertServiceServer
|
||||
// for forward compatibility.
|
||||
type AlertServiceServer interface {
|
||||
SubscribeAlerts(*AlertSubscription, grpc.ServerStreamingServer[AlertMessage]) error
|
||||
mustEmbedUnimplementedAlertServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAlertServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAlertServiceServer struct{}
|
||||
|
||||
func (UnimplementedAlertServiceServer) SubscribeAlerts(*AlertSubscription, grpc.ServerStreamingServer[AlertMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeAlerts not implemented")
|
||||
}
|
||||
func (UnimplementedAlertServiceServer) mustEmbedUnimplementedAlertServiceServer() {}
|
||||
func (UnimplementedAlertServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAlertServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AlertServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAlertServiceServer interface {
|
||||
mustEmbedUnimplementedAlertServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAlertServiceServer(s grpc.ServiceRegistrar, srv AlertServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedAlertServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&AlertService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AlertService_SubscribeAlerts_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(AlertSubscription)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(AlertServiceServer).SubscribeAlerts(m, &grpc.GenericServerStream[AlertSubscription, AlertMessage]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type AlertService_SubscribeAlertsServer = grpc.ServerStreamingServer[AlertMessage]
|
||||
|
||||
// AlertService_ServiceDesc is the grpc.ServiceDesc for AlertService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AlertService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "AlertService",
|
||||
HandlerType: (*AlertServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "SubscribeAlerts",
|
||||
Handler: _AlertService_SubscribeAlerts_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "alerts.proto",
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: common.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type DateTime struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DateTime) Reset() {
|
||||
*x = DateTime{}
|
||||
mi := &file_common_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DateTime) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DateTime) ProtoMessage() {}
|
||||
|
||||
func (x *DateTime) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DateTime.ProtoReflect.Descriptor instead.
|
||||
func (*DateTime) Descriptor() ([]byte, []int) {
|
||||
return file_common_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *DateTime) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RequestDateTime struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) Reset() {
|
||||
*x = RequestDateTime{}
|
||||
mi := &file_common_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RequestDateTime) ProtoMessage() {}
|
||||
|
||||
func (x *RequestDateTime) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RequestDateTime.ProtoReflect.Descriptor instead.
|
||||
func (*RequestDateTime) Descriptor() ([]byte, []int) {
|
||||
return file_common_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RequestPass struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"`
|
||||
Length int64 `protobuf:"varint,8,opt,name=Length,proto3" json:"Length,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RequestPass) Reset() {
|
||||
*x = RequestPass{}
|
||||
mi := &file_common_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RequestPass) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RequestPass) ProtoMessage() {}
|
||||
|
||||
func (x *RequestPass) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RequestPass.ProtoReflect.Descriptor instead.
|
||||
func (*RequestPass) Descriptor() ([]byte, []int) {
|
||||
return file_common_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *RequestPass) GetSeed() int64 {
|
||||
if x != nil {
|
||||
return x.Seed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RequestPass) GetLength() int64 {
|
||||
if x != nil {
|
||||
return x.Length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RandomPass struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Password string `protobuf:"bytes,1,opt,name=Password,proto3" json:"Password,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RandomPass) Reset() {
|
||||
*x = RandomPass{}
|
||||
mi := &file_common_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RandomPass) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RandomPass) ProtoMessage() {}
|
||||
|
||||
func (x *RandomPass) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RandomPass.ProtoReflect.Descriptor instead.
|
||||
func (*RandomPass) Descriptor() ([]byte, []int) {
|
||||
return file_common_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *RandomPass) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type EmptyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EmptyRequest) Reset() {
|
||||
*x = EmptyRequest{}
|
||||
mi := &file_common_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EmptyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EmptyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *EmptyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_common_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EmptyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*EmptyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_common_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
var File_common_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_common_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\fcommon.proto\" \n" +
|
||||
"\bDateTime\x12\x14\n" +
|
||||
"\x05Value\x18\x01 \x01(\tR\x05Value\"'\n" +
|
||||
"\x0fRequestDateTime\x12\x14\n" +
|
||||
"\x05Value\x18\x02 \x01(\tR\x05Value\"9\n" +
|
||||
"\vRequestPass\x12\x12\n" +
|
||||
"\x04Seed\x18\x01 \x01(\x03R\x04Seed\x12\x16\n" +
|
||||
"\x06Length\x18\b \x01(\x03R\x06Length\"(\n" +
|
||||
"\n" +
|
||||
"RandomPass\x12\x1a\n" +
|
||||
"\bPassword\x18\x01 \x01(\tR\bPassword\"\x0e\n" +
|
||||
"\fEmptyRequestB\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_common_proto_rawDescOnce sync.Once
|
||||
file_common_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_common_proto_rawDescGZIP() []byte {
|
||||
file_common_proto_rawDescOnce.Do(func() {
|
||||
file_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)))
|
||||
})
|
||||
return file_common_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_common_proto_goTypes = []any{
|
||||
(*DateTime)(nil), // 0: DateTime
|
||||
(*RequestDateTime)(nil), // 1: RequestDateTime
|
||||
(*RequestPass)(nil), // 2: RequestPass
|
||||
(*RandomPass)(nil), // 3: RandomPass
|
||||
(*EmptyRequest)(nil), // 4: EmptyRequest
|
||||
}
|
||||
var file_common_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_common_proto_init() }
|
||||
func file_common_proto_init() {
|
||||
if File_common_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_common_proto_goTypes,
|
||||
DependencyIndexes: file_common_proto_depIdxs,
|
||||
MessageInfos: file_common_proto_msgTypes,
|
||||
}.Build()
|
||||
File_common_proto = out.File
|
||||
file_common_proto_goTypes = nil
|
||||
file_common_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: core.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type SensingData struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DeviceId string `protobuf:"bytes,1,opt,name=DeviceId,proto3" json:"DeviceId,omitempty"`
|
||||
Temperature float64 `protobuf:"fixed64,2,opt,name=Temperature,proto3" json:"Temperature,omitempty"`
|
||||
Humidity float64 `protobuf:"fixed64,3,opt,name=Humidity,proto3" json:"Humidity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SensingData) Reset() {
|
||||
*x = SensingData{}
|
||||
mi := &file_core_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SensingData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SensingData) ProtoMessage() {}
|
||||
|
||||
func (x *SensingData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_core_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SensingData.ProtoReflect.Descriptor instead.
|
||||
func (*SensingData) Descriptor() ([]byte, []int) {
|
||||
return file_core_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *SensingData) GetDeviceId() string {
|
||||
if x != nil {
|
||||
return x.DeviceId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SensingData) GetTemperature() float64 {
|
||||
if x != nil {
|
||||
return x.Temperature
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SensingData) GetHumidity() float64 {
|
||||
if x != nil {
|
||||
return x.Humidity
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type SensingResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=Success,proto3" json:"Success,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SensingResponse) Reset() {
|
||||
*x = SensingResponse{}
|
||||
mi := &file_core_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SensingResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SensingResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SensingResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_core_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SensingResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SensingResponse) Descriptor() ([]byte, []int) {
|
||||
return file_core_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *SensingResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SensingResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_core_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_core_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"core.proto\x1a\fcommon.proto\"g\n" +
|
||||
"\vSensingData\x12\x1a\n" +
|
||||
"\bDeviceId\x18\x01 \x01(\tR\bDeviceId\x12 \n" +
|
||||
"\vTemperature\x18\x02 \x01(\x01R\vTemperature\x12\x1a\n" +
|
||||
"\bHumidity\x18\x03 \x01(\x01R\bHumidity\"E\n" +
|
||||
"\x0fSensingResponse\x12\x18\n" +
|
||||
"\aSuccess\x18\x01 \x01(\bR\aSuccess\x12\x18\n" +
|
||||
"\aMessage\x18\x02 \x01(\tR\aMessage2\x96\x01\n" +
|
||||
"\vCoreService\x12&\n" +
|
||||
"\aGetDate\x12\x10.RequestDateTime\x1a\t.DateTime\x123\n" +
|
||||
"\x11UpdateSensingData\x12\f.SensingData\x1a\x10.SensingResponse\x12*\n" +
|
||||
"\rGetRandomPass\x12\f.RequestPass\x1a\v.RandomPassB\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_core_proto_rawDescOnce sync.Once
|
||||
file_core_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_core_proto_rawDescGZIP() []byte {
|
||||
file_core_proto_rawDescOnce.Do(func() {
|
||||
file_core_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_core_proto_rawDesc), len(file_core_proto_rawDesc)))
|
||||
})
|
||||
return file_core_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_core_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_core_proto_goTypes = []any{
|
||||
(*SensingData)(nil), // 0: SensingData
|
||||
(*SensingResponse)(nil), // 1: SensingResponse
|
||||
(*RequestDateTime)(nil), // 2: RequestDateTime
|
||||
(*RequestPass)(nil), // 3: RequestPass
|
||||
(*DateTime)(nil), // 4: DateTime
|
||||
(*RandomPass)(nil), // 5: RandomPass
|
||||
}
|
||||
var file_core_proto_depIdxs = []int32{
|
||||
2, // 0: CoreService.GetDate:input_type -> RequestDateTime
|
||||
0, // 1: CoreService.UpdateSensingData:input_type -> SensingData
|
||||
3, // 2: CoreService.GetRandomPass:input_type -> RequestPass
|
||||
4, // 3: CoreService.GetDate:output_type -> DateTime
|
||||
1, // 4: CoreService.UpdateSensingData:output_type -> SensingResponse
|
||||
5, // 5: CoreService.GetRandomPass:output_type -> RandomPass
|
||||
3, // [3:6] is the sub-list for method output_type
|
||||
0, // [0:3] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_core_proto_init() }
|
||||
func file_core_proto_init() {
|
||||
if File_core_proto != nil {
|
||||
return
|
||||
}
|
||||
file_common_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_core_proto_rawDesc), len(file_core_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_core_proto_goTypes,
|
||||
DependencyIndexes: file_core_proto_depIdxs,
|
||||
MessageInfos: file_core_proto_msgTypes,
|
||||
}.Build()
|
||||
File_core_proto = out.File
|
||||
file_core_proto_goTypes = nil
|
||||
file_core_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.21.12
|
||||
// source: core.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
CoreService_GetDate_FullMethodName = "/CoreService/GetDate"
|
||||
CoreService_UpdateSensingData_FullMethodName = "/CoreService/UpdateSensingData"
|
||||
CoreService_GetRandomPass_FullMethodName = "/CoreService/GetRandomPass"
|
||||
)
|
||||
|
||||
// CoreServiceClient is the client API for CoreService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type CoreServiceClient interface {
|
||||
GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error)
|
||||
UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error)
|
||||
GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error)
|
||||
}
|
||||
|
||||
type coreServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewCoreServiceClient(cc grpc.ClientConnInterface) CoreServiceClient {
|
||||
return &coreServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *coreServiceClient) GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DateTime)
|
||||
err := c.cc.Invoke(ctx, CoreService_GetDate_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *coreServiceClient) UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SensingResponse)
|
||||
err := c.cc.Invoke(ctx, CoreService_UpdateSensingData_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *coreServiceClient) GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RandomPass)
|
||||
err := c.cc.Invoke(ctx, CoreService_GetRandomPass_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CoreServiceServer is the server API for CoreService service.
|
||||
// All implementations must embed UnimplementedCoreServiceServer
|
||||
// for forward compatibility.
|
||||
type CoreServiceServer interface {
|
||||
GetDate(context.Context, *RequestDateTime) (*DateTime, error)
|
||||
UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error)
|
||||
GetRandomPass(context.Context, *RequestPass) (*RandomPass, error)
|
||||
mustEmbedUnimplementedCoreServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedCoreServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedCoreServiceServer struct{}
|
||||
|
||||
func (UnimplementedCoreServiceServer) GetDate(context.Context, *RequestDateTime) (*DateTime, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDate not implemented")
|
||||
}
|
||||
func (UnimplementedCoreServiceServer) UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateSensingData not implemented")
|
||||
}
|
||||
func (UnimplementedCoreServiceServer) GetRandomPass(context.Context, *RequestPass) (*RandomPass, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRandomPass not implemented")
|
||||
}
|
||||
func (UnimplementedCoreServiceServer) mustEmbedUnimplementedCoreServiceServer() {}
|
||||
func (UnimplementedCoreServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeCoreServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to CoreServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeCoreServiceServer interface {
|
||||
mustEmbedUnimplementedCoreServiceServer()
|
||||
}
|
||||
|
||||
func RegisterCoreServiceServer(s grpc.ServiceRegistrar, srv CoreServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedCoreServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&CoreService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _CoreService_GetDate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RequestDateTime)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CoreServiceServer).GetDate(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: CoreService_GetDate_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CoreServiceServer).GetDate(ctx, req.(*RequestDateTime))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _CoreService_UpdateSensingData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SensingData)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CoreServiceServer).UpdateSensingData(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: CoreService_UpdateSensingData_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CoreServiceServer).UpdateSensingData(ctx, req.(*SensingData))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _CoreService_GetRandomPass_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RequestPass)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CoreServiceServer).GetRandomPass(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: CoreService_GetRandomPass_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CoreServiceServer).GetRandomPass(ctx, req.(*RequestPass))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// CoreService_ServiceDesc is the grpc.ServiceDesc for CoreService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var CoreService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "CoreService",
|
||||
HandlerType: (*CoreServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetDate",
|
||||
Handler: _CoreService_GetDate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateSensingData",
|
||||
Handler: _CoreService_UpdateSensingData_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRandomPass",
|
||||
Handler: _CoreService_GetRandomPass_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "core.proto",
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: filetransfer.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type FileChunk struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
Content []byte `protobuf:"bytes,2,opt,name=Content,proto3" json:"Content,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileChunk) Reset() {
|
||||
*x = FileChunk{}
|
||||
mi := &file_filetransfer_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileChunk) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileChunk) ProtoMessage() {}
|
||||
|
||||
func (x *FileChunk) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filetransfer_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileChunk.ProtoReflect.Descriptor instead.
|
||||
func (*FileChunk) Descriptor() ([]byte, []int) {
|
||||
return file_filetransfer_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *FileChunk) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FileChunk) GetContent() []byte {
|
||||
if x != nil {
|
||||
return x.Content
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UploadStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=Success,proto3" json:"Success,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"`
|
||||
BytesUploaded int64 `protobuf:"varint,3,opt,name=BytesUploaded,proto3" json:"BytesUploaded,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UploadStatus) Reset() {
|
||||
*x = UploadStatus{}
|
||||
mi := &file_filetransfer_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UploadStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UploadStatus) ProtoMessage() {}
|
||||
|
||||
func (x *UploadStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filetransfer_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UploadStatus.ProtoReflect.Descriptor instead.
|
||||
func (*UploadStatus) Descriptor() ([]byte, []int) {
|
||||
return file_filetransfer_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *UploadStatus) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *UploadStatus) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *UploadStatus) GetBytesUploaded() int64 {
|
||||
if x != nil {
|
||||
return x.BytesUploaded
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type FileMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
FileSize int64 `protobuf:"varint,2,opt,name=FileSize,proto3" json:"FileSize,omitempty"`
|
||||
UploadedAt int64 `protobuf:"varint,3,opt,name=UploadedAt,proto3" json:"UploadedAt,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileMetadata) Reset() {
|
||||
*x = FileMetadata{}
|
||||
mi := &file_filetransfer_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *FileMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filetransfer_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*FileMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_filetransfer_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetFileSize() int64 {
|
||||
if x != nil {
|
||||
return x.FileSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetUploadedAt() int64 {
|
||||
if x != nil {
|
||||
return x.UploadedAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type FileList struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Files []*FileMetadata `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileList) Reset() {
|
||||
*x = FileList{}
|
||||
mi := &file_filetransfer_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileList) ProtoMessage() {}
|
||||
|
||||
func (x *FileList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filetransfer_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileList.ProtoReflect.Descriptor instead.
|
||||
func (*FileList) Descriptor() ([]byte, []int) {
|
||||
return file_filetransfer_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *FileList) GetFiles() []*FileMetadata {
|
||||
if x != nil {
|
||||
return x.Files
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DownloadRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) Reset() {
|
||||
*x = DownloadRequest{}
|
||||
mi := &file_filetransfer_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DownloadRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DownloadRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filetransfer_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DownloadRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DownloadRequest) Descriptor() ([]byte, []int) {
|
||||
return file_filetransfer_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_filetransfer_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_filetransfer_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x12filetransfer.proto\x1a\fcommon.proto\"A\n" +
|
||||
"\tFileChunk\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName\x12\x18\n" +
|
||||
"\aContent\x18\x02 \x01(\fR\aContent\"h\n" +
|
||||
"\fUploadStatus\x12\x18\n" +
|
||||
"\aSuccess\x18\x01 \x01(\bR\aSuccess\x12\x18\n" +
|
||||
"\aMessage\x18\x02 \x01(\tR\aMessage\x12$\n" +
|
||||
"\rBytesUploaded\x18\x03 \x01(\x03R\rBytesUploaded\"f\n" +
|
||||
"\fFileMetadata\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName\x12\x1a\n" +
|
||||
"\bFileSize\x18\x02 \x01(\x03R\bFileSize\x12\x1e\n" +
|
||||
"\n" +
|
||||
"UploadedAt\x18\x03 \x01(\x03R\n" +
|
||||
"UploadedAt\"/\n" +
|
||||
"\bFileList\x12#\n" +
|
||||
"\x05Files\x18\x01 \x03(\v2\r.FileMetadataR\x05Files\"-\n" +
|
||||
"\x0fDownloadRequest\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName2\x97\x01\n" +
|
||||
"\x13FileTransferService\x12)\n" +
|
||||
"\n" +
|
||||
"UploadFile\x12\n" +
|
||||
".FileChunk\x1a\r.UploadStatus(\x01\x12%\n" +
|
||||
"\tListFiles\x12\r.EmptyRequest\x1a\t.FileList\x12.\n" +
|
||||
"\fDownloadFile\x12\x10.DownloadRequest\x1a\n" +
|
||||
".FileChunk0\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_filetransfer_proto_rawDescOnce sync.Once
|
||||
file_filetransfer_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_filetransfer_proto_rawDescGZIP() []byte {
|
||||
file_filetransfer_proto_rawDescOnce.Do(func() {
|
||||
file_filetransfer_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_filetransfer_proto_rawDesc), len(file_filetransfer_proto_rawDesc)))
|
||||
})
|
||||
return file_filetransfer_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_filetransfer_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_filetransfer_proto_goTypes = []any{
|
||||
(*FileChunk)(nil), // 0: FileChunk
|
||||
(*UploadStatus)(nil), // 1: UploadStatus
|
||||
(*FileMetadata)(nil), // 2: FileMetadata
|
||||
(*FileList)(nil), // 3: FileList
|
||||
(*DownloadRequest)(nil), // 4: DownloadRequest
|
||||
(*EmptyRequest)(nil), // 5: EmptyRequest
|
||||
}
|
||||
var file_filetransfer_proto_depIdxs = []int32{
|
||||
2, // 0: FileList.Files:type_name -> FileMetadata
|
||||
0, // 1: FileTransferService.UploadFile:input_type -> FileChunk
|
||||
5, // 2: FileTransferService.ListFiles:input_type -> EmptyRequest
|
||||
4, // 3: FileTransferService.DownloadFile:input_type -> DownloadRequest
|
||||
1, // 4: FileTransferService.UploadFile:output_type -> UploadStatus
|
||||
3, // 5: FileTransferService.ListFiles:output_type -> FileList
|
||||
0, // 6: FileTransferService.DownloadFile:output_type -> FileChunk
|
||||
4, // [4:7] is the sub-list for method output_type
|
||||
1, // [1:4] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_filetransfer_proto_init() }
|
||||
func file_filetransfer_proto_init() {
|
||||
if File_filetransfer_proto != nil {
|
||||
return
|
||||
}
|
||||
file_common_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_filetransfer_proto_rawDesc), len(file_filetransfer_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_filetransfer_proto_goTypes,
|
||||
DependencyIndexes: file_filetransfer_proto_depIdxs,
|
||||
MessageInfos: file_filetransfer_proto_msgTypes,
|
||||
}.Build()
|
||||
File_filetransfer_proto = out.File
|
||||
file_filetransfer_proto_goTypes = nil
|
||||
file_filetransfer_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.21.12
|
||||
// source: filetransfer.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
FileTransferService_UploadFile_FullMethodName = "/FileTransferService/UploadFile"
|
||||
FileTransferService_ListFiles_FullMethodName = "/FileTransferService/ListFiles"
|
||||
FileTransferService_DownloadFile_FullMethodName = "/FileTransferService/DownloadFile"
|
||||
)
|
||||
|
||||
// FileTransferServiceClient is the client API for FileTransferService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type FileTransferServiceClient interface {
|
||||
UploadFile(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[FileChunk, UploadStatus], error)
|
||||
ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error)
|
||||
DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error)
|
||||
}
|
||||
|
||||
type fileTransferServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewFileTransferServiceClient(cc grpc.ClientConnInterface) FileTransferServiceClient {
|
||||
return &fileTransferServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *fileTransferServiceClient) UploadFile(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[FileChunk, UploadStatus], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &FileTransferService_ServiceDesc.Streams[0], FileTransferService_UploadFile_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[FileChunk, UploadStatus]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type FileTransferService_UploadFileClient = grpc.ClientStreamingClient[FileChunk, UploadStatus]
|
||||
|
||||
func (c *fileTransferServiceClient) ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FileList)
|
||||
err := c.cc.Invoke(ctx, FileTransferService_ListFiles_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *fileTransferServiceClient) DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &FileTransferService_ServiceDesc.Streams[1], FileTransferService_DownloadFile_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[DownloadRequest, FileChunk]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type FileTransferService_DownloadFileClient = grpc.ServerStreamingClient[FileChunk]
|
||||
|
||||
// FileTransferServiceServer is the server API for FileTransferService service.
|
||||
// All implementations must embed UnimplementedFileTransferServiceServer
|
||||
// for forward compatibility.
|
||||
type FileTransferServiceServer interface {
|
||||
UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error
|
||||
ListFiles(context.Context, *EmptyRequest) (*FileList, error)
|
||||
DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error
|
||||
mustEmbedUnimplementedFileTransferServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedFileTransferServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedFileTransferServiceServer struct{}
|
||||
|
||||
func (UnimplementedFileTransferServiceServer) UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error {
|
||||
return status.Error(codes.Unimplemented, "method UploadFile not implemented")
|
||||
}
|
||||
func (UnimplementedFileTransferServiceServer) ListFiles(context.Context, *EmptyRequest) (*FileList, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListFiles not implemented")
|
||||
}
|
||||
func (UnimplementedFileTransferServiceServer) DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error {
|
||||
return status.Error(codes.Unimplemented, "method DownloadFile not implemented")
|
||||
}
|
||||
func (UnimplementedFileTransferServiceServer) mustEmbedUnimplementedFileTransferServiceServer() {}
|
||||
func (UnimplementedFileTransferServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeFileTransferServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to FileTransferServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeFileTransferServiceServer interface {
|
||||
mustEmbedUnimplementedFileTransferServiceServer()
|
||||
}
|
||||
|
||||
func RegisterFileTransferServiceServer(s grpc.ServiceRegistrar, srv FileTransferServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedFileTransferServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&FileTransferService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _FileTransferService_UploadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(FileTransferServiceServer).UploadFile(&grpc.GenericServerStream[FileChunk, UploadStatus]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type FileTransferService_UploadFileServer = grpc.ClientStreamingServer[FileChunk, UploadStatus]
|
||||
|
||||
func _FileTransferService_ListFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(EmptyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(FileTransferServiceServer).ListFiles(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: FileTransferService_ListFiles_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FileTransferServiceServer).ListFiles(ctx, req.(*EmptyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _FileTransferService_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(DownloadRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(FileTransferServiceServer).DownloadFile(m, &grpc.GenericServerStream[DownloadRequest, FileChunk]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type FileTransferService_DownloadFileServer = grpc.ServerStreamingServer[FileChunk]
|
||||
|
||||
// FileTransferService_ServiceDesc is the grpc.ServiceDesc for FileTransferService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var FileTransferService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "FileTransferService",
|
||||
HandlerType: (*FileTransferServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "ListFiles",
|
||||
Handler: _FileTransferService_ListFiles_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "UploadFile",
|
||||
Handler: _FileTransferService_UploadFile_Handler,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "DownloadFile",
|
||||
Handler: _FileTransferService_DownloadFile_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "filetransfer.proto",
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package modular
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"grpccanary/lib/grpc/modular/protoapi"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type UploadedFile struct {
|
||||
FileName string
|
||||
Content []byte
|
||||
UploadedAt int64
|
||||
}
|
||||
|
||||
type AlertSubscriber struct {
|
||||
ClientId string
|
||||
Channel chan *protoapi.AlertMessage
|
||||
}
|
||||
|
||||
var (
|
||||
fileStore = make(map[string]*UploadedFile)
|
||||
storeMu sync.RWMutex
|
||||
subscribers = make(map[string]*AlertSubscriber)
|
||||
subMu sync.Mutex
|
||||
)
|
||||
|
||||
func publishAlert(alert *protoapi.AlertMessage) {
|
||||
subMu.Lock()
|
||||
defer subMu.Unlock()
|
||||
for _, sub := range subscribers {
|
||||
select {
|
||||
case sub.Channel <- alert:
|
||||
default:
|
||||
fmt.Printf("Alert channel blocked for client %s, dropping event\n", sub.ClientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func random(min, max int, src rand.Source) int {
|
||||
return rand.New(src).Intn(max-min) + min
|
||||
}
|
||||
|
||||
func getString(len int64, src rand.Source) string {
|
||||
temp := ""
|
||||
startChar := "!"
|
||||
var i int64 = 1
|
||||
for {
|
||||
myRand := random(0, 94, src)
|
||||
newChar := string(startChar[0] + byte(myRand))
|
||||
temp = temp + newChar
|
||||
if i == len {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
// 1. Core Server Implementation
|
||||
type CoreServer struct {
|
||||
protoapi.UnimplementedCoreServiceServer
|
||||
}
|
||||
|
||||
func (CoreServer) GetDate(ctx context.Context, r *protoapi.RequestDateTime) (*protoapi.DateTime, error) {
|
||||
currentTime := time.Now()
|
||||
return &protoapi.DateTime{
|
||||
Value: currentTime.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (CoreServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) {
|
||||
fmt.Printf("[Modular Core] Received sensing data - Device: %s, Temp: %.2f°C, Humid: %.2f%%\n", r.GetDeviceId(), r.GetTemperature(), r.GetHumidity())
|
||||
|
||||
if r.GetTemperature() > 40.0 {
|
||||
fmt.Printf("⚠️ [Modular Core] Critical temperature detected: %.2f°C! Publishing warning...\n", r.GetTemperature())
|
||||
publishAlert(&protoapi.AlertMessage{
|
||||
AlertId: fmt.Sprintf("alert-%d", time.Now().UnixNano()),
|
||||
DeviceId: r.GetDeviceId(),
|
||||
Message: fmt.Sprintf("Critical high temperature: %.2f°C (Humidity: %.2f%%)", r.GetTemperature(), r.GetHumidity()),
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
return &protoapi.SensingResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Sensing data updated successfully for device %s", r.GetDeviceId()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (CoreServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*protoapi.RandomPass, error) {
|
||||
src := rand.NewSource(r.GetSeed())
|
||||
temp := getString(r.GetLength(), src)
|
||||
return &protoapi.RandomPass{
|
||||
Password: temp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 2. File Transfer Server Implementation
|
||||
type FileTransferServer struct {
|
||||
protoapi.UnimplementedFileTransferServiceServer
|
||||
}
|
||||
|
||||
func (FileTransferServer) UploadFile(stream protoapi.FileTransferService_UploadFileServer) error {
|
||||
var totalBytes int64
|
||||
var fileName string
|
||||
var buffer []byte
|
||||
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
fmt.Printf("[Modular File] File upload completed. Received %d bytes for file '%s'\n", totalBytes, fileName)
|
||||
|
||||
if fileName != "" {
|
||||
storeMu.Lock()
|
||||
fileStore[fileName] = &UploadedFile{
|
||||
FileName: fileName,
|
||||
Content: buffer,
|
||||
UploadedAt: time.Now().Unix(),
|
||||
}
|
||||
storeMu.Unlock()
|
||||
}
|
||||
|
||||
return stream.SendAndClose(&protoapi.UploadStatus{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("File '%s' uploaded successfully.", fileName),
|
||||
BytesUploaded: totalBytes,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("[Modular File] File upload error:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if fileName == "" {
|
||||
fileName = chunk.GetFileName()
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
totalBytes += int64(len(chunk.GetContent()))
|
||||
}
|
||||
}
|
||||
|
||||
func (FileTransferServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) {
|
||||
storeMu.RLock()
|
||||
defer storeMu.RUnlock()
|
||||
|
||||
var files []*protoapi.FileMetadata
|
||||
for _, f := range fileStore {
|
||||
files = append(files, &protoapi.FileMetadata{
|
||||
FileName: f.FileName,
|
||||
FileSize: int64(len(f.Content)),
|
||||
UploadedAt: f.UploadedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &protoapi.FileList{Files: files}, nil
|
||||
}
|
||||
|
||||
func (FileTransferServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.FileTransferService_DownloadFileServer) error {
|
||||
storeMu.RLock()
|
||||
f, exists := fileStore[r.GetFileName()]
|
||||
storeMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return status.Errorf(codes.NotFound, "file %s not found", r.GetFileName())
|
||||
}
|
||||
|
||||
chunkSize := 1024
|
||||
totalBytes := len(f.Content)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: f.FileName,
|
||||
Content: f.Content[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. Alert Server Implementation
|
||||
type AlertServer struct {
|
||||
protoapi.UnimplementedAlertServiceServer
|
||||
}
|
||||
|
||||
func (AlertServer) SubscribeAlerts(r *protoapi.AlertSubscription, stream protoapi.AlertService_SubscribeAlertsServer) error {
|
||||
clientId := r.GetClientId()
|
||||
ch := make(chan *protoapi.AlertMessage, 10)
|
||||
sub := &AlertSubscriber{
|
||||
ClientId: clientId,
|
||||
Channel: ch,
|
||||
}
|
||||
|
||||
subMu.Lock()
|
||||
subscribers[clientId] = sub
|
||||
subMu.Unlock()
|
||||
|
||||
fmt.Printf("[Modular Alert] Client %s subscribed to alerts on topic '%s'\n", clientId, r.GetTopic())
|
||||
|
||||
for {
|
||||
select {
|
||||
case alert := <-ch:
|
||||
err := stream.Send(alert)
|
||||
if err != nil {
|
||||
subMu.Lock()
|
||||
delete(subscribers, clientId)
|
||||
subMu.Unlock()
|
||||
fmt.Printf("[Modular Alert] Client %s alert subscription disconnected: %v\n", clientId, err)
|
||||
return err
|
||||
}
|
||||
case <-stream.Context().Done():
|
||||
subMu.Lock()
|
||||
delete(subscribers, clientId)
|
||||
subMu.Unlock()
|
||||
fmt.Printf("[Modular Alert] Client %s unsubscribed (context done)\n", clientId)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ServerRun(addr string) {
|
||||
server := grpc.NewServer()
|
||||
|
||||
// Register multiple services on the same gRPC server
|
||||
protoapi.RegisterCoreServiceServer(server, CoreServer{})
|
||||
protoapi.RegisterFileTransferServiceServer(server, FileTransferServer{})
|
||||
protoapi.RegisterAlertServiceServer(server, AlertServer{})
|
||||
|
||||
reflection.Register(server)
|
||||
|
||||
listen, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[Modular Server] Listening on %s...\n", addr)
|
||||
server.Serve(listen)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# lib/httpentity 실습 설명서
|
||||
|
||||
본 디렉토리는 Go 언어 웹 프레임워크인 Gin(`gin-gonic`)을 활용한 HTTP 웹 API 서버 실습 예제를 포함하고 있습니다.
|
||||
|
||||
## 📖 실습 상세 분석 및 가이드 안내
|
||||
|
||||
이 실습에 대한 상세한 코드 구조 설명과 Gin 라우터 설계 이론은 심화 학습 문서인 **[docs/HTTP.md](../../docs/HTTP.md)**에서 상세히 기술되어 있습니다.
|
||||
|
||||
[docs/HTTP.md](../../docs/HTTP.md) 문서에서 다음 내용을 공부할 수 있습니다:
|
||||
* **HTTP 프로토콜 및 REST API 기본 구조**
|
||||
* **Gin 웹 프레임워크의 라우터 매핑 (`gin.Default()` vs `gin.New()`)**
|
||||
* **API 라우터와 정적 웹 리소스 서빙 우회 설계 패턴**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 빠른 실행 방법
|
||||
|
||||
이 예제는 리포지토리 루트의 `lib/main.go`를 통해 실행됩니다.
|
||||
|
||||
1. 리포지토리 루트 of `lib/main.go`를 엽니다.
|
||||
2. `main()` 함수 내에서 `httpentity` API 호출 주석을 해제합니다. (현재 주석 상태로, 추후 구현 완성을 위한 예제 뼈대 파일입니다.)
|
||||
3. 리포지토리 루트에서 다음 명령어를 실행합니다:
|
||||
```bash
|
||||
go run ./lib
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# lib/jsonexample 실습 설명서
|
||||
|
||||
본 디렉토리는 Go 언어 표준 라이브러리(`encoding/json`)를 활용한 JSON 데이터 직렬화 및 역직렬화 실습 예제를 포함하고 있습니다.
|
||||
|
||||
## 📖 실습 상세 분석 및 가이드 안내
|
||||
|
||||
이 실습에 대한 상세한 코드 해설과 구조체 태그 매핑 원리는 심화 학습 문서인 **[docs/JSON.md](../../docs/JSON.md)**에서 상세히 기술되어 있습니다.
|
||||
|
||||
[docs/JSON.md](../../docs/JSON.md) 문서에서 다음 내용을 공부할 수 있습니다:
|
||||
* **Go 구조체와 JSON 필드 매핑 및 필드 노출 대소문자 규칙**
|
||||
* **Go `Marshal` 및 `Unmarshal` 함수 동작 메커니즘**
|
||||
* **동적 맵 구조(`map[string]interface{}`)의 직렬화 실습 분석**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 빠른 실행 방법
|
||||
|
||||
이 예제는 리포지토리 루트의 `lib/main.go`를 통해 실행됩니다.
|
||||
|
||||
1. 리포지토리 루트의 `lib/main.go`를 엽니다.
|
||||
2. `main()` 함수 내에서 `jsonexample.JsonParsingExample()`의 주석을 해제합니다.
|
||||
3. 리포지토리 루트에서 다음 명령어를 실행합니다:
|
||||
```bash
|
||||
go run ./lib
|
||||
```
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"grpccanary/lib/grpc/http3"
|
||||
"time"
|
||||
)
|
||||
|
||||
var port = ":8080"
|
||||
|
||||
func main() {
|
||||
// 1단계: JSON 파싱 예제 실행
|
||||
// jsonexample.JsonParsingExample()
|
||||
|
||||
// 3단계: gRPC 통신 예제 실행 (필요 시 주석 제거하여 활성화 가능)
|
||||
grpcSample()
|
||||
}
|
||||
|
||||
func grpcSample() {
|
||||
fmt.Println("--- starting gRPC IoT Simulation ---")
|
||||
|
||||
// 1. gRPC 서버를 백그라운드 고루틴으로 구동
|
||||
go func() {
|
||||
if _, _, err := http3.ServerRun(port); err != nil {
|
||||
fmt.Println("ServerRun error:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. 서버 포트가 바인딩되어 통신 대기 상태에 들어갈 시간을 일시적으로 보장
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 3. gRPC 클라이언트를 구동하여 원격 프로시저(RPC) 기동 시뮬레이션 집행
|
||||
if err := http3.ClientRun(port); err != nil {
|
||||
fmt.Println("ClientRun error:", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# [보고서] MAM 위임 도구의 역할(Role) 지정 옵션 누락 이슈 분석
|
||||
|
||||
본 문서는 멀티 에이전트 오케스트레이션 프레임워크(`multi-agent-mux`)의 핵심 CLI 도구인 `multi-agent-mux-delegate-job`에서 세션의 역할(Role)을 지정할 수 있는 옵션이 누락되어 발생하는 정합성 충돌 문제와 이에 대한 원인 분석 및 해결 방안을 정의합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 문제가 발생한 정확한 상황 (Context)
|
||||
|
||||
프로젝트 개발을 오케스트레이션하는 과정에서 아래와 같은 에이전트 간 역할 분담을 적용하고자 했습니다.
|
||||
* **개발 팀장 (Antigravity)**: 실제 저장소의 문서 수정 및 구현 진행 (**Worker/Implementer**)
|
||||
* **리뷰 에이전트 (Claude)**: 문서 구조의 설계 및 계획안 수립 (**Planner**)
|
||||
|
||||
이 분담에 따라 Claude 세션(`canary-projects-grpccanary-creator-claude`)에 "문서 모듈화 계획 및 체크리스트 작성" 작업을 위임하기 위해 `multi-agent-mux-delegate-job` 도구로 비동기 작업을 요청했습니다.
|
||||
|
||||
그러나 자동 생성된 잡 지시서인 `.mam/jobs/<job_id>/brief.md` 파일의 메타데이터에 다음과 같이 **구현자의 역할이 `Worker`로 강제 지정**되어 나가는 상황이 발생했습니다:
|
||||
|
||||
```markdown
|
||||
# 📋 Brief: Job ed31b5fb Delegation
|
||||
|
||||
- **Job ID**: ed31b5fb
|
||||
- **Target Agent**: claude (session: tmux:canary-projects-grpccanary-creator-claude)
|
||||
- **Role**: Worker <-- [이슈 발생 지점: Planner가 아닌 Worker로 강제 지정됨]
|
||||
- **Timeout**: 3600 s (Idle: 120 s)
|
||||
```
|
||||
|
||||
이는 프로젝트 협업 규칙(`.agents/MULTI_AGENT_RULES.ko.md`)에 명시된 **"에이전트 역할 범위 준수 원칙(Role Suitability Check)"**에 위배되며, `claude`가 문서 작성이 아닌 파일 직접 수정을 시도할 위험이 있는 정합성 모순을 유발합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 문제 사유 (Root Cause)
|
||||
|
||||
이 문제의 근본적인 기술적 원인은 **CLI 인수 파싱 로직 및 지시서(Brief) 생성 템플릿의 하드코딩**에 있습니다.
|
||||
|
||||
1. **CLI 옵션 설계 누락**:
|
||||
* `multi-agent-mux-delegate-job submit` 명령어의 헬프 스펙을 확인한 결과, `--agent`, `--agent-session`, `--prompt` 등의 인수는 정의되어 있으나, 작업의 논리적 성격을 조율하는 **`--role <role_name>` 파라미터가 구현되어 있지 않습니다**.
|
||||
2. **템플릿 내부의 상수 고정**:
|
||||
* API를 통해 비동기 잡이 수임될 때 생성되는 `brief.md` 파일과 잡 레지스트리 JSON의 생성기 로직 내부에 `Role` 값이 **`Worker` 문자열 상수로 하드코딩**되어 동작하고 있습니다. 이로 인해 어떤 에이전트에 어떤 종류의 명령을 위임하더라도 메타데이터상으로는 항상 `Worker`로 바인딩됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 문제 해결 방법 (Remediation & Workarounds)
|
||||
|
||||
### 3.1 단기적 우회 방법 (Workaround)
|
||||
프레임워크 CLI 소스코드를 수정하기 어려운 제한적 상황에서는 **프롬프트 페이로드(Prompt Payload) 하드닝** 기법을 사용하여 에이전트의 오작동을 차단합니다.
|
||||
* **해결 원리**: brief.md의 메타데이터상 `Role: Worker` 지정을 덮어쓸 수 있도록, 프롬프트 문맥 내부에 **"너의 역할은 실제 문서를 수정하지 않고 계획만 수립하는 Planner이다. 절대 문서를 직접 수정하지 말라"**는 강력한 지시 제약(System-level Rule Override)을 포함하여 송신합니다.
|
||||
* **효과**: AI 에이전트는 메타데이터보다 프롬프트 지시어의 행위 제약을 우선 순위로 받아들이므로, 의도한 대로 설계서 및 계획안만 수립하는 Planner 동작을 정상 수행하게 됩니다.
|
||||
|
||||
### 3.2 근본적인 해결 방법 (Remediation)
|
||||
프레임워크의 CLI 래퍼인 `multi-agent-mux-delegate-job` 파일의 파싱 로직 및 brief.md 빌더 로직을 다음과 같이 수정합니다.
|
||||
|
||||
#### 1단계: CLI 인수 파서 수정 (`submit` 옵션 추가)
|
||||
스크립트의 인수 파싱 영역에 `--role` 파라미터를 식별할 수 있는 변수 및 분기 로직을 선언합니다.
|
||||
```bash
|
||||
# 옵션 분석 루프 예시
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--role)
|
||||
DELEGATE_ROLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
# ... 기존 옵션 파싱 ...
|
||||
esac
|
||||
done
|
||||
|
||||
# 기본값 정의
|
||||
DELEGATE_ROLE="${DELEGATE_ROLE:-Worker}"
|
||||
```
|
||||
|
||||
#### 2단계: `brief.md` 생성 템플릿 연동
|
||||
잡 디렉토리 내에 `brief.md`를 기입하여 내보내는 빌더 영역(Python 혹은 쉘 스크립트 에코 영역)을 다음과 같이 동적 변수와 연결합니다.
|
||||
```diff
|
||||
- echo "- **Role**: Worker" >> "$BRIEF_PATH"
|
||||
+ echo "- **Role**: ${DELEGATE_ROLE}" >> "$BRIEF_PATH"
|
||||
```
|
||||
|
||||
#### 3단계: 잡 레지스트리 JSON 메타데이터 갱신
|
||||
동일하게 생성되는 `.mam/jobs/<job_id>.json` 파일 등의 메타데이터 생성 객체 내에 `role: DELEGATE_ROLE` 매핑 키를 추가하여, 타 모니터링 도구(예: `reconcile.sh` 및 `status.sh`)에서도 해당 에이전트의 잡 실행 역할을 정확하게 대시보드에 모니터링할 수 있도록 보완합니다.
|
||||
@@ -0,0 +1,151 @@
|
||||
# 프로젝트 분석 보고서: grpccanary
|
||||
|
||||
## 1. 개요
|
||||
|
||||
`grpccanary`는 **Go 언어로 JSON 처리 → HTTP 서버 → gRPC 서버/클라이언트로 이어지는 학습 여정을 다루는 한국어 튜토리얼 저장소**입니다. 저장소 이름(`grpccanary`)과 달리 실제 내용은 gRPC 단일 주제가 아니라, "데이터 직렬화(JSON) → 웹 서버(HTTP/gin) → 원격 프로시저 호출(gRPC)"로 난이도를 높여가는 3단계 실습 커리큘럼이며, 현재는 **gRPC 파트가 가장 완성도 높게 구현**되어 있습니다.
|
||||
|
||||
저장소 루트에는 이 학습 프로젝트와는 성격이 다른 **`multi-agent-mux`라는 다중 에이전트(Claude Code) 오케스트레이션 인프라**가 함께 자리 잡고 있습니다(`.agents/`, `.mam/` 디렉토리). 이 인프라는 tmux 세션 + MQTT 메시지 브로커를 이용해 여러 AI 에이전트(팀장/리뷰어 역할)가 하나의 작업 저장소를 공유하며 협업하도록 설계된 별도의 도구 체계이며, 실제로 이 보고서를 작성하는 작업(`job 702ea1d8`) 자체도 이 인프라를 통해 위임되었습니다.
|
||||
|
||||
즉 이 저장소는 **"gRPC를 배우기 위한 Go 예제 코드"**와 **"그 예제 코드를 여러 AI 에이전트가 함께 작업하도록 돕는 협업 프레임워크"**가 한 저장소 안에 공존하는 구조입니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 저장소 구조
|
||||
|
||||
```
|
||||
grpccanary/
|
||||
├── README.md # 메인 튜토리얼 (JSON/HTTP/gRPC 개념 설명 + 실행 가이드, 한국어)
|
||||
├── AGENTS.md # LLM 코딩 행동 지침 (일반 원칙)
|
||||
├── go.mod / go.sum # Go 모듈 정의 (module grpccanary, go 1.25.4)
|
||||
├── protoapi.proto # gRPC 서비스 IDL(Protocol Buffers) 정의 원본
|
||||
├── protoapi/ # protoc로 생성된 Go stub 코드
|
||||
│ ├── protoapi.pb.go # 메시지 타입 (protoc-gen-go)
|
||||
│ └── protoapi_grpc.pb.go # 서비스/클라이언트 stub (protoc-gen-go-grpc)
|
||||
├── obj.json # JSON 예제용 샘플 데이터 파일
|
||||
├── lib/
|
||||
│ ├── main.go # 실행 진입점 (현재는 JSON 예제만 호출하도록 설정됨)
|
||||
│ ├── jsonexample/
|
||||
│ │ └── json_parser.go # encoding/json 마샬링·언마샬링 예제
|
||||
│ ├── httpentity/
|
||||
│ │ ├── server.go # gin 기반 HTTP 서버 (전체가 주석 처리된 미완성 스텁)
|
||||
│ │ └── client.go # 패키지 선언만 있는 빈 파일
|
||||
│ └── grpcentity/
|
||||
│ ├── server.go # gRPC 서버 구현 (Random 서비스)
|
||||
│ ├── client.go # gRPC 클라이언트 구현
|
||||
│ └── README.md # gRPC 서버/클라이언트 구현 상세 해설 (한국어)
|
||||
├── docs/
|
||||
│ └── Working with JSON/
|
||||
│ ├── README.md # JSON 관련 학습 자료 (영문)
|
||||
│ └── README-kr.md # JSON 관련 학습 자료 (한글)
|
||||
├── scripts/
|
||||
│ └── generate-env.sh # .env.example → .env 복사 스크립트 (멀티에이전트 인프라용)
|
||||
├── .env.example # 멀티에이전트 인프라(MQTT 브로커 등) 설정 템플릿
|
||||
├── .agents/ # 멀티에이전트 오케스트레이션 규칙·스킬 (아래 4절 참고)
|
||||
└── .mam/ # 멀티에이전트 잡(Job) 레지스트리 및 세션 상태 (런타임 산출물)
|
||||
```
|
||||
|
||||
빌드 확인 결과 `go build ./...` 및 `go vet ./...` 모두 오류 없이 통과했으며, 테스트 파일(`*_test.go`)은 저장소에 존재하지 않습니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 핵심 구성 요소 (학습용 Go 코드)
|
||||
|
||||
### 3.1 진입점 — `lib/main.go`
|
||||
- 프로젝트의 유일한 `main()` 함수. `var port = ":8080"`을 정의하고 있으며, 기본 상태에서는 `jsonexample.JsonParsingExample()`만 호출합니다.
|
||||
- gRPC 예제를 실행하려면 README.md 안내에 따라 `main()` 내부를 수동으로 편집해 `grpcSample()`을 호출하도록 바꿔야 합니다(미사용 import 오류 방지를 위해 `jsonexample` import를 주석 처리해야 함). 즉 **하나의 코드베이스 안에서 학습 단계별로 진입점을 수동 전환하는 방식**으로 설계되어 있습니다.
|
||||
- `grpcSample()` 함수는 `entity.ServerRun(port)`를 고루틴으로 백그라운드 실행한 뒤 1초 대기 후 `entity.ClientRun(...)`을 호출해, 같은 프로세스 안에서 서버·클라이언트가 통신하는 데모를 구성합니다.
|
||||
|
||||
### 3.2 JSON 파싱 예제 — `lib/jsonexample/json_parser.go`
|
||||
- `encoding/json` 표준 라이브러리를 이용해 (1) `map[string]interface{}` ↔ JSON 문자열 변환, (2) 구조체(`Person`) ↔ JSON 변환의 마샬링/언마샬링을 시연합니다.
|
||||
- 외부 의존성 없이 표준 라이브러리만 사용하는 가장 단순한 예제로, 커리큘럼의 1단계 역할을 합니다.
|
||||
|
||||
### 3.3 HTTP 서버 예제 — `lib/httpentity/`
|
||||
- `server.go`는 `gin-gonic/gin`을 이용한 REST API 서버(정적 파일 서빙 + `/api/randomNumber`, `/api/randomPassword`, `/api/randomDate` 라우트) 초안이 **전체 주석 처리**된 상태로만 존재합니다. 즉 코드는 작성되어 있으나 활성화되지 않은 미완성/보류 상태입니다.
|
||||
- `client.go`는 `package httpentity` 선언 한 줄만 있는 빈 파일입니다.
|
||||
- `go.mod`에는 `gin-gonic/gin` 의존성이 여전히 선언되어 있어, 이 파트가 완전히 폐기된 것이 아니라 추후 재개를 염두에 둔 진행 중(work-in-progress) 상태로 보입니다.
|
||||
|
||||
### 3.4 gRPC 서비스 정의 — `protoapi.proto`
|
||||
- `proto3` 문법으로 `Random`이라는 gRPC 서비스를 정의하며 3개의 RPC 메서드를 제공합니다.
|
||||
- `GetDate(RequestDateTime) returns (DateTime)` — 서버의 현재 날짜/시간 반환
|
||||
- `GetRandom(RandomParams) returns (RandomInt)` — 시드(Seed)와 위치(Place)를 기반으로 한 의사난수 생성
|
||||
- `GetRandomPass(RequestPass) returns (RandomPass)` — 시드와 길이(Length)를 받아 무작위 ASCII 비밀번호 생성
|
||||
- `option go_package = "./protoapi/;protoapi"`로 지정되어 있어, `protoc` 컴파일 시 `protoapi/` 디렉토리에 Go 패키지 `protoapi`가 생성됩니다.
|
||||
- 이미 컴파일된 stub(`protoapi/protoapi.pb.go`, `protoapi/protoapi_grpc.pb.go`)이 저장소에 커밋되어 있어 `protoc` 재설치 없이 바로 빌드/실행이 가능합니다.
|
||||
|
||||
### 3.5 gRPC 서버 구현 — `lib/grpcentity/server.go`
|
||||
- `RandomServer` 구조체가 `protoapi.UnimplementedRandomServer`를 임베딩하여 3개 RPC 메서드(`GetDate`, `GetRandom`, `GetRandomPass`)를 구현합니다.
|
||||
- `getString(len int64)`은 ASCII 코드 `!`(33)부터 94개 범위 내에서 문자를 뽑아 임의 문자열(비밀번호)을 생성하는 헬퍼입니다.
|
||||
- `ServerRun(addr string)`이 `grpc.NewServer()`로 서버를 만들고 `reflection.Register(server)`로 gRPC reflection(예: `grpcurl` 같은 외부 CLI 디버깅 도구 지원)을 활성화한 뒤 TCP 포트를 리슨합니다.
|
||||
- **주의(코드상 특이점)**: `ServerRun`은 인자로 받은 `addr`을 실제로 사용하지 않고 패키지 전역 변수 `port = ":8080"`으로 리슨합니다(`net.Listen("tcp", port)`). 따라서 현재 코드는 항상 `:8080`에서만 동작하며, 호출부에서 다른 포트를 넘겨도 무시됩니다.
|
||||
- `rand.Seed(r.GetSeed())`(패키지 전역 시드 설정, Go 1.20+에서는 deprecated)와 `rand.NewSource`를 혼용하고 있어 스레드 안전성이나 API 일관성 측면에서 다소 오래된 패턴을 보입니다(학습용 예제이므로 의도된 단순화로 판단됨).
|
||||
|
||||
### 3.6 gRPC 클라이언트 구현 — `lib/grpcentity/client.go`
|
||||
- `grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))`로 평문(TLS 미적용) 채널을 생성합니다(로컬 테스트 목적).
|
||||
- `AskingDateTime`, `AskPass`, `AskRandom` 세 개의 래퍼 함수가 각각 대응하는 RPC를 호출합니다.
|
||||
- `ClientRun(addr)`이 실행 엔트리포인트로, 날짜/시간 조회 1회, 비밀번호 생성 1회, 서로 다른 파라미터로 난수 생성 2회를 순차 호출하며 결과를 표준 출력으로 출력합니다.
|
||||
|
||||
### 3.7 문서 자료
|
||||
- **`README.md`(루트)**: 이야기체(스타트업 개발자 '민우'의 사례)로 gRPC 도입 배경(REST/JSON의 한계, Protobuf 계약, HTTP/2)을 설명한 뒤, Go 버전 요구사항(1.25.4+), 의존성 설치, `main.go` 수동 편집 방법, 실행 명령(`go run ./examples`), 기대 출력 예시, 트러블슈팅(포트 충돌, 모듈 로드 오류)까지 안내하는 실행 가이드로 구성되어 있습니다.
|
||||
- **`lib/grpcentity/README.md`**: `.proto` 파일 문법 요소별 상세 해설, `protoc`/`protoc-gen-go`/`protoc-gen-go-grpc` 설치 및 컴파일 절차, 서버·클라이언트 코드의 구현 단계별 설명을 담은 심화 가이드입니다.
|
||||
- **`docs/Working with JSON/`**: JSON 관련 별도 학습 자료(영/한 병기)가 준비되어 있으나, 루트 README와 직접 링크되어 있지는 않습니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 두 번째 레이어 — 멀티에이전트 오케스트레이션 인프라
|
||||
|
||||
저장소에는 학습 콘텐츠와 무관한 **AI 에이전트 협업 인프라**가 함께 포함되어 있습니다.
|
||||
|
||||
- **`AGENTS.md`**: 일반적인 LLM 코딩 행동 지침(가정하지 말 것, 최소 변경, 외과적 수정 등)을 규정합니다.
|
||||
- **`.agents/MULTI_AGENT_RULES.md`(+ 한국어판)**: MQTT 메시징 백플레인과 tmux 기반 다중 에이전트 협업 프로토콜을 정의합니다. 총괄 매니저(Orchestrator) → 팀장(개발/리뷰) → 작업 위임 및 리뷰 루프 → 완료 보고로 이어지는 워크플로우, Job 레지스트리(`.mam/jobs/<id>.json`, `fcntl` 파일 락), 세션 레지스트리(`.mam/agent-sessions.db`, SQLite WAL), HMAC-SHA256 기반 메시지 인증(PoC 모드에서는 비활성) 등을 상세히 규정합니다.
|
||||
- **`.agents/skills/`**: `multi-agent-mux-{create,delegate-job,loop,monitor,resume,status,stop}` 등 실제 세션 생성·작업 위임·모니터링·중지를 수행하는 스킬(스크립트) 모음입니다.
|
||||
- **`.mam/`**: 위 인프라의 런타임 상태 저장소로, `jobs/`(작업 레지스트리 및 브리프 파일), `agent-sessions.yaml`/`.db`(세션 상태), `agent_homes/`(에이전트별 메모리), `delegate_job_logs/`(위임 감사 로그)를 포함합니다. 이번 작업 브리프(`.mam/jobs/702ea1d8/brief.md`) 역시 이 구조를 통해 전달되었습니다.
|
||||
- **`scripts/generate-env.sh`, `.env.example`**: 이 인프라가 사용하는 MQTT 브로커 접속 정보, 경로 설정 등을 `.env`로 초기화하는 헬퍼입니다. 실제 비밀 값은 `.env`(git-ignored)에만 두고 `.env.example`에는 플레이스홀더만 커밋하도록 설계되어 있습니다.
|
||||
|
||||
**참고**: 새로 추가된 `.gitignore`는 `.agents/`, `.mam/`, `.env`, `AGENTS.md` 등을 앞으로 git 추적 대상에서 제외하도록 지정하고 있습니다. 다만 `git log`를 보면 `AGENTS.md`, `lib/grpcentity/README.md` 등은 과거 커밋(`init 251117`, `update 260609`, 이후 docs 커밋들)에서 이미 저장소에 커밋되어 있으므로, 이번 `.gitignore` 추가는 향후 신규/변경 파일이 실수로 다시 커밋되는 것을 막기 위한 조치로 보입니다(기존에 추적 중인 파일을 소급 제외하지는 않음).
|
||||
|
||||
---
|
||||
|
||||
## 5. 실행 방법
|
||||
|
||||
### 5.1 사전 준비
|
||||
```bash
|
||||
go version # Go 1.25.4 이상 필요
|
||||
go mod download # 의존성 다운로드
|
||||
```
|
||||
|
||||
### 5.2 JSON 예제 실행 (기본값, 별도 수정 불필요)
|
||||
```bash
|
||||
go run ./examples
|
||||
```
|
||||
`lib/main.go`가 기본적으로 `jsonexample.JsonParsingExample()`만 호출하므로, 별도 수정 없이 바로 JSON 마샬링/언마샬링 결과가 콘솔에 출력됩니다.
|
||||
|
||||
### 5.3 gRPC 예제 실행 (수동 편집 필요)
|
||||
1. `lib/main.go`를 열어 `jsonexample` import를 주석 처리하고 `main()` 안에서 `grpcSample()`을 호출하도록 변경.
|
||||
2. 저장 후 실행:
|
||||
```bash
|
||||
go run ./examples
|
||||
```
|
||||
3. 내부적으로 `:8080` 포트에서 gRPC 서버(고루틴)가 기동되고, 1초 후 클라이언트가 `GetDate`, `GetRandomPass`, `GetRandom`(2회) 순으로 RPC를 호출하며 결과를 출력합니다.
|
||||
4. 포트 충돌 시(`bind: address already in use`) `lib/grpcentity/server.go`의 `port` 전역 변수를 변경해야 합니다(3.5절에서 언급했듯 `ServerRun`의 `addr` 인자는 무시되므로, 포트 변경은 이 전역 변수 수정으로만 가능).
|
||||
|
||||
### 5.4 HTTP 예제
|
||||
`httpentity` 패키지는 서버 로직 전체가 주석 처리되어 있고 클라이언트 파일은 비어 있어, 현재 상태로는 실행 가능한 산출물이 없습니다. 향후 주석을 해제하고 `main.go`에 호출부를 추가해야 실습이 가능합니다.
|
||||
|
||||
### 5.5 `.proto` 재컴파일 (선택)
|
||||
`.proto` 정의를 수정하고 stub을 재생성하려면:
|
||||
```bash
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \
|
||||
--go-grpc_opt=paths=source_relative protoapi.proto
|
||||
```
|
||||
사전에 `protoc`, `protoc-gen-go`, `protoc-gen-go-grpc`가 설치되어 있어야 합니다(설치 방법은 `lib/grpcentity/README.md` 참고).
|
||||
|
||||
---
|
||||
|
||||
## 6. 주요 관찰 사항 및 특이점
|
||||
|
||||
1. **단일 진입점, 수동 전환 방식**: `lib/main.go`가 유일한 실행 지점이며, 학습 단계(JSON/HTTP/gRPC)를 전환하려면 코드를 직접 편집해야 합니다. 각 예제를 독립적으로 실행할 수 있는 별도 커맨드나 플래그는 없습니다.
|
||||
2. **`ServerRun(addr)`의 `addr` 인자 미사용**: gRPC 서버는 전달받은 인자 대신 패키지 전역 `port` 변수로 리슨하므로, 함수 시그니처와 실제 동작이 일치하지 않는 잠재적 혼동 요소입니다.
|
||||
3. **HTTP 파트 미완성**: `gin` 의존성은 `go.mod`에 존재하지만 실제 코드는 비활성 상태로, 커리큘럼상 "다음 실습 예정" 단계로 보입니다.
|
||||
4. **테스트 부재**: 저장소 전체에 자동화된 테스트(`*_test.go`)가 없어, 코드 정상 동작 여부는 수동 실행(`go run`)과 콘솔 출력 확인에 의존합니다. `go build ./...`, `go vet ./...`는 통과합니다.
|
||||
5. **레거시 API 사용**: `rand.Seed(...)`(전역 시드 설정) 등 Go 최신 버전에서 권장되지 않는(deprecated) 패턴이 사용되고 있으나, 학습용 예제의 단순성을 위한 의도적 선택으로 보입니다.
|
||||
6. **이중 성격의 저장소**: 순수 학습 콘텐츠(gRPC/JSON/HTTP 튜토리얼)와 AI 에이전트 협업 인프라(multi-agent-mux)가 한 저장소에 공존하며, 두 영역은 서로 기능적으로 독립적입니다. 협업 인프라(`.agents/`, `.mam/`)는 이 학습 프로젝트를 대상으로 여러 AI 에이전트가 작업을 위임받고 결과를 보고하는 용도로 사용되고 있습니다(본 보고서 작성 작업 자체가 그 예시).
|
||||
@@ -1,38 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./protoapi/;protoapi";
|
||||
|
||||
service Random {
|
||||
rpc GetDate (RequestDateTime) returns (DateTime);
|
||||
rpc GetRandom (RandomParams) returns (RandomInt);
|
||||
rpc GetRandomPass (RequestPass) returns (RandomPass);
|
||||
}
|
||||
|
||||
// For random number
|
||||
message RandomParams {
|
||||
int64 Seed = 1;
|
||||
int64 Place = 2;
|
||||
}
|
||||
|
||||
message RandomInt {
|
||||
int64 Value = 1;
|
||||
}
|
||||
|
||||
// For date time
|
||||
message DateTime {
|
||||
string Value = 1;
|
||||
}
|
||||
|
||||
message RequestDateTime {
|
||||
string Value = 2;
|
||||
}
|
||||
|
||||
// For random password
|
||||
message RequestPass {
|
||||
int64 Seed = 1;
|
||||
int64 Length = 8;
|
||||
}
|
||||
|
||||
message RandomPass {
|
||||
string Password = 1;
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.33.0
|
||||
// protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// For random number
|
||||
type RandomParams struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"`
|
||||
Place int64 `protobuf:"varint,2,opt,name=Place,proto3" json:"Place,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RandomParams) Reset() {
|
||||
*x = RandomParams{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_protoapi_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RandomParams) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RandomParams) ProtoMessage() {}
|
||||
|
||||
func (x *RandomParams) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RandomParams.ProtoReflect.Descriptor instead.
|
||||
func (*RandomParams) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *RandomParams) GetSeed() int64 {
|
||||
if x != nil {
|
||||
return x.Seed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RandomParams) GetPlace() int64 {
|
||||
if x != nil {
|
||||
return x.Place
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RandomInt struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Value int64 `protobuf:"varint,1,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RandomInt) Reset() {
|
||||
*x = RandomInt{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_protoapi_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RandomInt) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RandomInt) ProtoMessage() {}
|
||||
|
||||
func (x *RandomInt) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RandomInt.ProtoReflect.Descriptor instead.
|
||||
func (*RandomInt) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *RandomInt) GetValue() int64 {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// For date time
|
||||
type DateTime struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DateTime) Reset() {
|
||||
*x = DateTime{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_protoapi_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *DateTime) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DateTime) ProtoMessage() {}
|
||||
|
||||
func (x *DateTime) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DateTime.ProtoReflect.Descriptor instead.
|
||||
func (*DateTime) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *DateTime) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RequestDateTime struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) Reset() {
|
||||
*x = RequestDateTime{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_protoapi_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RequestDateTime) ProtoMessage() {}
|
||||
|
||||
func (x *RequestDateTime) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RequestDateTime.ProtoReflect.Descriptor instead.
|
||||
func (*RequestDateTime) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *RequestDateTime) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// For random password
|
||||
type RequestPass struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Seed int64 `protobuf:"varint,1,opt,name=Seed,proto3" json:"Seed,omitempty"`
|
||||
Length int64 `protobuf:"varint,8,opt,name=Length,proto3" json:"Length,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RequestPass) Reset() {
|
||||
*x = RequestPass{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_protoapi_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RequestPass) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RequestPass) ProtoMessage() {}
|
||||
|
||||
func (x *RequestPass) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RequestPass.ProtoReflect.Descriptor instead.
|
||||
func (*RequestPass) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *RequestPass) GetSeed() int64 {
|
||||
if x != nil {
|
||||
return x.Seed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RequestPass) GetLength() int64 {
|
||||
if x != nil {
|
||||
return x.Length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RandomPass struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Password string `protobuf:"bytes,1,opt,name=Password,proto3" json:"Password,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RandomPass) Reset() {
|
||||
*x = RandomPass{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_protoapi_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RandomPass) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RandomPass) ProtoMessage() {}
|
||||
|
||||
func (x *RandomPass) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RandomPass.ProtoReflect.Descriptor instead.
|
||||
func (*RandomPass) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *RandomPass) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_protoapi_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_protoapi_proto_rawDesc = []byte{
|
||||
0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x22, 0x38, 0x0a, 0x0c, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
|
||||
0x53, 0x65, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x05, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x22, 0x21, 0x0a, 0x09, 0x52, 0x61,
|
||||
0x6e, 0x64, 0x6f, 0x6d, 0x49, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x20, 0x0a,
|
||||
0x08, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22,
|
||||
0x27, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x39, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x65, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x4c,
|
||||
0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4c, 0x65, 0x6e,
|
||||
0x67, 0x74, 0x68, 0x22, 0x28, 0x0a, 0x0a, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x73,
|
||||
0x73, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x32, 0x84, 0x01,
|
||||
0x0a, 0x06, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x12, 0x26, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x44,
|
||||
0x61, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74,
|
||||
0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x09, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65,
|
||||
0x12, 0x26, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x12, 0x0d, 0x2e,
|
||||
0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x0a, 0x2e, 0x52,
|
||||
0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x49, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x52,
|
||||
0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x12, 0x0c, 0x2e, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x1a, 0x0b, 0x2e, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d,
|
||||
0x50, 0x61, 0x73, 0x73, 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_protoapi_proto_rawDescOnce sync.Once
|
||||
file_protoapi_proto_rawDescData = file_protoapi_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_protoapi_proto_rawDescGZIP() []byte {
|
||||
file_protoapi_proto_rawDescOnce.Do(func() {
|
||||
file_protoapi_proto_rawDescData = protoimpl.X.CompressGZIP(file_protoapi_proto_rawDescData)
|
||||
})
|
||||
return file_protoapi_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_protoapi_proto_goTypes = []interface{}{
|
||||
(*RandomParams)(nil), // 0: RandomParams
|
||||
(*RandomInt)(nil), // 1: RandomInt
|
||||
(*DateTime)(nil), // 2: DateTime
|
||||
(*RequestDateTime)(nil), // 3: RequestDateTime
|
||||
(*RequestPass)(nil), // 4: RequestPass
|
||||
(*RandomPass)(nil), // 5: RandomPass
|
||||
}
|
||||
var file_protoapi_proto_depIdxs = []int32{
|
||||
3, // 0: Random.GetDate:input_type -> RequestDateTime
|
||||
0, // 1: Random.GetRandom:input_type -> RandomParams
|
||||
4, // 2: Random.GetRandomPass:input_type -> RequestPass
|
||||
2, // 3: Random.GetDate:output_type -> DateTime
|
||||
1, // 4: Random.GetRandom:output_type -> RandomInt
|
||||
5, // 5: Random.GetRandomPass:output_type -> RandomPass
|
||||
3, // [3:6] is the sub-list for method output_type
|
||||
0, // [0:3] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_protoapi_proto_init() }
|
||||
func file_protoapi_proto_init() {
|
||||
if File_protoapi_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_protoapi_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RandomParams); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_protoapi_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RandomInt); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_protoapi_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DateTime); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_protoapi_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RequestDateTime); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_protoapi_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RequestPass); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_protoapi_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RandomPass); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_protoapi_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_protoapi_proto_goTypes,
|
||||
DependencyIndexes: file_protoapi_proto_depIdxs,
|
||||
MessageInfos: file_protoapi_proto_msgTypes,
|
||||
}.Build()
|
||||
File_protoapi_proto = out.File
|
||||
file_protoapi_proto_rawDesc = nil
|
||||
file_protoapi_proto_goTypes = nil
|
||||
file_protoapi_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v3.21.12
|
||||
// source: protoapi.proto
|
||||
|
||||
package protoapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Random_GetDate_FullMethodName = "/Random/GetDate"
|
||||
Random_GetRandom_FullMethodName = "/Random/GetRandom"
|
||||
Random_GetRandomPass_FullMethodName = "/Random/GetRandomPass"
|
||||
)
|
||||
|
||||
// RandomClient is the client API for Random service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type RandomClient interface {
|
||||
GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error)
|
||||
GetRandom(ctx context.Context, in *RandomParams, opts ...grpc.CallOption) (*RandomInt, error)
|
||||
GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error)
|
||||
}
|
||||
|
||||
type randomClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewRandomClient(cc grpc.ClientConnInterface) RandomClient {
|
||||
return &randomClient{cc}
|
||||
}
|
||||
|
||||
func (c *randomClient) GetDate(ctx context.Context, in *RequestDateTime, opts ...grpc.CallOption) (*DateTime, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DateTime)
|
||||
err := c.cc.Invoke(ctx, Random_GetDate_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *randomClient) GetRandom(ctx context.Context, in *RandomParams, opts ...grpc.CallOption) (*RandomInt, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RandomInt)
|
||||
err := c.cc.Invoke(ctx, Random_GetRandom_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *randomClient) GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RandomPass)
|
||||
err := c.cc.Invoke(ctx, Random_GetRandomPass_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RandomServer is the server API for Random service.
|
||||
// All implementations must embed UnimplementedRandomServer
|
||||
// for forward compatibility.
|
||||
type RandomServer interface {
|
||||
GetDate(context.Context, *RequestDateTime) (*DateTime, error)
|
||||
GetRandom(context.Context, *RandomParams) (*RandomInt, error)
|
||||
GetRandomPass(context.Context, *RequestPass) (*RandomPass, error)
|
||||
mustEmbedUnimplementedRandomServer()
|
||||
}
|
||||
|
||||
// UnimplementedRandomServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedRandomServer struct{}
|
||||
|
||||
func (UnimplementedRandomServer) GetDate(context.Context, *RequestDateTime) (*DateTime, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDate not implemented")
|
||||
}
|
||||
func (UnimplementedRandomServer) GetRandom(context.Context, *RandomParams) (*RandomInt, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRandom not implemented")
|
||||
}
|
||||
func (UnimplementedRandomServer) GetRandomPass(context.Context, *RequestPass) (*RandomPass, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRandomPass not implemented")
|
||||
}
|
||||
func (UnimplementedRandomServer) mustEmbedUnimplementedRandomServer() {}
|
||||
func (UnimplementedRandomServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeRandomServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to RandomServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeRandomServer interface {
|
||||
mustEmbedUnimplementedRandomServer()
|
||||
}
|
||||
|
||||
func RegisterRandomServer(s grpc.ServiceRegistrar, srv RandomServer) {
|
||||
// If the following call pancis, it indicates UnimplementedRandomServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Random_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Random_GetDate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RequestDateTime)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RandomServer).GetDate(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Random_GetDate_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RandomServer).GetDate(ctx, req.(*RequestDateTime))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Random_GetRandom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RandomParams)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RandomServer).GetRandom(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Random_GetRandom_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RandomServer).GetRandom(ctx, req.(*RandomParams))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Random_GetRandomPass_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RequestPass)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RandomServer).GetRandomPass(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Random_GetRandomPass_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RandomServer).GetRandomPass(ctx, req.(*RequestPass))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Random_ServiceDesc is the grpc.ServiceDesc for Random service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Random_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "Random",
|
||||
HandlerType: (*RandomServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetDate",
|
||||
Handler: _Random_GetDate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRandom",
|
||||
Handler: _Random_GetRandom_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRandomPass",
|
||||
Handler: _Random_GetRandomPass_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "protoapi.proto",
|
||||
}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# generate-env.sh — create a local .env from the committed .env.example template.
|
||||
#
|
||||
# Behaviour:
|
||||
# - .env absent → copy .env.example to .env, print the path.
|
||||
# - .env present → no-op (leaves your edits intact), exit 0.
|
||||
# - .env present --force → overwrite .env from .env.example (backs up to .env.bak).
|
||||
#
|
||||
# Paths are resolved relative to this script (repo root = parent of deploy/),
|
||||
# so it works regardless of the caller's cwd.
|
||||
#
|
||||
# Usage: deploy/generate-env.sh [--force] [-h|--help]
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SRC="$REPO_ROOT/.env.example"
|
||||
DST="$REPO_ROOT/.env"
|
||||
|
||||
FORCE=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force) FORCE=1; shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--force]"
|
||||
echo " Create .env from .env.example. --force overwrites an existing .env."
|
||||
exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; echo "Usage: $0 [--force]" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -f "$SRC" ] || { echo "ERROR: template not found: $SRC" >&2; exit 1; }
|
||||
|
||||
if [ -f "$DST" ] && [ "$FORCE" != "1" ]; then
|
||||
echo "no-op: $DST already exists (use --force to overwrite)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$DST" ] && [ "$FORCE" = "1" ]; then
|
||||
cp -p "$DST" "$DST.bak"
|
||||
echo "backed up existing .env -> $DST.bak"
|
||||
fi
|
||||
|
||||
cp "$SRC" "$DST"
|
||||
echo "created: $DST"
|
||||
echo "Next: edit $DST and fill in any secrets (look for 'replace_me')."
|
||||
Reference in New Issue
Block a user