For installation instructions, see Go’s Getting Started guide.
protoc
, version 3.For installation instructions, see Protocol Buffer Compiler Installation.
Install the protocol compiler plugins for Go using the following commands:
$ go install google.golang.org/protobuf/cmd/[email protected]
$ go install google.golang.org/grpc/cmd/[email protected]
Update your PATH
so that the protoc
compiler can find the plugins:
$ export PATH="$PATH:$(go env GOPATH)/bin"
前面我们用 protoc
来编译 .proto
文件为 go 语言, 为了支持编译为 go, 需要安装 protoc-gen-go
插件, C# 可以安装 protoc-gen-zsharp
插件。
需要注意的是, 转换 .proto
为编程语言, 不一定要安装 protoc
。
例如 C# 只需要把 .proto
文件放到项目中, 通过包管理器安装一个库, 就会自动转换为相应的代码。
回归正题, 聊一下 protoc
编译 .proto
文件的命令。
protoc
常用的参数如下:
--proto_path=. #指定proto文件的路径, 填写 . 表示就在当前目录下
--go_out=. #表示编译后的文件存放路径; 如果编译的是 csharp, 则 --csharp_out
--go_opt={xxx.proto}={xxx.proto的路径} # 示例: --go_opt=Mprotos/bar.proto=example.com/project/protos/foo
最简单的编译命令:
protoc --go_out=. *.proto
--{xxx}_out
指令是必须的, 因为要输出具体的编程语言代码。
这个输出文件的路径是执行命令的路径, 如果我们不在 .proto
文件目录下执行命令, 则输出的代码便不是相同位置了。为了解决这个问题, 我们可以使用:
--go_opt=paths=source_relative
这样在别的地方执行命令, 生成的代码会跟 .proto
文件放在相同的位置。
protoc-gen-go
is a plugin for the Google protocol buffer compiler to generate Go code. Install it by building this program and making it accessible within your PATH with the name:
protoc-gen-go
The ‘go’ suffix becomes part of the argument for the protocol compiler, such that it can be invoked as:
protoc --go_out=paths=source_relative:. path/to/file.proto
This generates Go bindings for the protocol buffer defined by file.proto
. With that input, the output will be written to:
path/to/file.pb.go
See the README
and documentation for protocol buffers to learn more:
https://developers.google.com/protocol-buffers/
Protocol Buffers 是一种轻便高效的结构化数据存储格式, 可以用于结构化数据串行化, 或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
优点:
缺点:
.proto
文件)https://github.com/protocolbuffers/protobuf/releases
原生的 protoc 是不支持 golang 输出的, 所以咱们需要安装这个插件, 用于生成 .go
文件, 即使用 protoc 的命令行 --go_out=...
。
$ go get github.com/golang/protobuf/protoc-gen-go
$ go get github.com/golang/protobuf/proto
.proto
文件$ protoc --go_out=. *.proto
结束后会生成相应的 .pb.go
文件, 以这个文件里面的定义好的 package 名, 在 $GOPATH
下面创建一个同名文件夹, 也就是创建一个新的 golang 包了。
gogoprotobuf 完全兼容 google protobuf, 它生成的代码质量和编解码性能均比 goprotobuf 高一些。
gogoprotobuf 两个插件可以使用:
//gogo
go get github.com/gogo/protobuf/protoc-gen-gogo
//gofast
go get github.com/gogo/protobuf/protoc-gen-gofast
go get github.com/gogo/protobuf/proto
go get github.com/gogo/protobuf/gogoproto // 这个不装也没关系
//gogo
protoc --gogo_out=. *.proto
//gofast
protoc --gofast_out=. *.proto
这里只是简单的用 go test 测试了一下:
//goprotobuf
"编码": 447ns/op
"解码": 422ns/op
//gogoprotobuf-go
"编码": 433ns/op
"解码": 427ns/op
//gogoprotobuf-fast
"编码": 112ns/op
"解码": 112ns/op
到这里为止, 我们只给出了一个简单的没有任何用处的例子。在实际应用中, 人们往往需要定义更加复杂的 Message。我们用"复杂"这个词, 不仅仅是指从个数上说有更多的 fields 或者更多类型的 fields, 而是指更加复杂的数据结构:
嵌套是一个神奇的概念, 一旦拥有嵌套能力, 消息的表达能力就会非常强大。
代码清单 4 给出一个嵌套 Message 的例子。
message Person {
required string name = 1;
required int32 id = 2; // Unique ID number for this person.
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
在 Message Person 中, 定义了嵌套消息 PhoneNumber, 并用来定义 Person 消息中的 phone 域。这使得人们可以定义更加复杂的数据结构。
在一个 .proto 文件中, 还可以用 Import 关键字引入在其他 .proto 文件中定义的消息, 这可以称做 Import Message, 或者 Dependency Message。
比如下例:
import common.header;
message youMsg{
required common.info_header header = 1;
required string youPrivateData = 2;
}
其中 ,common.info_header定义在common.header包内。
Import Message 的用处主要在于提供了方便的代码管理机制, 类似 C 语言中的头文件。您可以将一些公用的 Message 定义在一个 package 中, 然后在别的 .proto 文件中引入该 package, 进而使用其中的消息定义。
Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message, 从而让定义复杂的数据结构的工作变得非常轻松愉快。
$GOPATH
└── add_person
├── add_person.go
└── add_person_test.go
└── list_people
├── list_people.go
└── list_people_test.go
└── tutorial
├── addressbook.pb.go
└── addressbook.proto
add_person/add_person.go
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
pb "tutorial"
"github.com/golang/protobuf/proto"
)
func promptForAddress(r io.Reader) (*pb.Person, error) {
// A protocol buffer can be created like any struct.
p := &pb.Person{}
rd := bufio.NewReader(r)
fmt.Print("Enter person ID number:")
// An int32 field in the .proto file is represented as an int32 field
// in the generated Go struct.
if _, err := fmt.Fscanf(rd, "%d\n", &p.Id); err != nil {
return p, err
}
fmt.Print("Enter name:")
name, err := rd.ReadString('\n')
if err != nil {
return p, err
}
// A string field in the .proto file results in a string field in Go.
// We trim the whitespace because rd.ReadString includes the trailing
// newline character in its output.
p.Name = strings.TrimSpace(name)
fmt.Print("Enter email address (blank for none):")
email, err := rd.ReadString('\n')
if err != nil {
return p, err
}
p.Email = strings.TrimSpace(email)
for {
fmt.Print("Enter a phone number (or leave blank to finish):")
phone, err := rd.ReadString('\n')
if err != nil {
return p, err
}
phone = strings.TrimSpace(phone)
if phone == "" {
break
}
// The PhoneNumber message type is nested within the Person
// message in the .proto file. This results in a Go struct
// named using the name of the parent prefixed to the name of
// the nested message. Just as with pb.Person, it can be
// created like any other struct.
pn := &pb.Person_PhoneNumber{
Number: phone,
}
fmt.Print("Is this a mobile, home, or work phone?")
ptype, err := rd.ReadString('\n')
if err != nil {
return p, err
}
ptype = strings.TrimSpace(ptype)
// A proto enum results in a Go constant for each enum value.
switch ptype {
case "mobile":
pn.Type = pb.Person_MOBILE
case "home":
pn.Type = pb.Person_HOME
case "work":
pn.Type = pb.Person_WORK
default:
fmt.Printf("Unknown phone type %q. Using default.\n", ptype)
}
// A repeated proto field maps to a slice field in Go. We can
// append to it like any other slice.
p.Phones = append(p.Phones, pn)
}
return p, nil
}
// Main reads the entire address book from a file, adds one person based on
// user input, then writes it back out to the same file.
func main() {
if len(os.Args) != 2 {
log.Fatalf("Usage: %s ADDRESS_BOOK_FILE\n", os.Args[0])
}
fname := os.Args[1]
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("%s: File not found. Creating new file.\n", fname)
} else {
log.Fatalln("Error reading file:", err)
}
}
// [START marshal_proto]
book := &pb.AddressBook{}
// [START_EXCLUDE]
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
// Add an address.
addr, err := promptForAddress(os.Stdin)
if err != nil {
log.Fatalln("Error with address:", err)
}
book.People = append(book.People, addr)
// [END_EXCLUDE]
// Write the new address book back to disk.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
// [END marshal_proto]
}
add_person/add_person_test.go
package main
import (
"strings"
"testing"
pb "tutorial"
"github.com/golang/protobuf/proto"
)
func TestPromptForAddressReturnsAddress(t *testing.T) {
in := `12345
Example Name
[email protected]
123-456-7890
home
222-222-2222
mobile
111-111-1111
work
777-777-7777
unknown
`
got, err := promptForAddress(strings.NewReader(in))
if err != nil {
t.Fatalf("promptForAddress(%q) had unexpected error: %s", in, err.Error())
}
if got.Id != 12345 {
t.Errorf("promptForAddress(%q) got %d, want ID %d", in, got.Id, 12345)
}
if got.Name != "Example Name" {
t.Errorf("promptForAddress(%q) => want name %q, got %q", in, "Example Name", got.Name)
}
if got.Email != "[email protected]" {
t.Errorf("promptForAddress(%q) => want email %q, got %q", in, "[email protected]", got.Email)
}
want := []*pb.Person_PhoneNumber{
{Number: "123-456-7890", Type: pb.Person_HOME},
{Number: "222-222-2222", Type: pb.Person_MOBILE},
{Number: "111-111-1111", Type: pb.Person_WORK},
{Number: "777-777-7777", Type: pb.Person_MOBILE},
}
if len(got.Phones) != len(want) {
t.Errorf("want %d phone numbers, got %d", len(want), len(got.Phones))
}
phones := len(got.Phones)
if phones > len(want) {
phones = len(want)
}
for i := 0; i < phones; i++ {
if !proto.Equal(got.Phones[i], want[i]) {
t.Errorf("want phone %q, got %q", *want[i], *got.Phones[i])
}
}
}
list_people/list_people.go
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
pb "tutorial"
"github.com/golang/protobuf/proto"
)
func writePerson(w io.Writer, p *pb.Person) {
fmt.Fprintln(w, "Person ID:", p.Id)
fmt.Fprintln(w, "Name:", p.Name)
if p.Email != "" {
fmt.Fprintln(w, "E-mail address:", p.Email)
}
for _, pn := range p.Phones {
switch pn.Type {
case pb.Person_MOBILE:
fmt.Fprint(w, "Mobile phone #:")
case pb.Person_HOME:
fmt.Fprint(w, "Home phone #:")
case pb.Person_WORK:
fmt.Fprint(w, "Work phone #:")
}
fmt.Fprintln(w, pn.Number)
}
}
func listPeople(w io.Writer, book *pb.AddressBook) {
for _, p := range book.People {
writePerson(w, p)
}
}
// Main reads the entire address book from a file and prints all the
// information inside.
func main() {
if len(os.Args) != 2 {
log.Fatalf("Usage: %s ADDRESS_BOOK_FILE\n", os.Args[0])
}
fname := os.Args[1]
// [START unmarshal_proto]
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
// [END unmarshal_proto]
listPeople(os.Stdout, book)
}
list_people/list_people_test.go
package main
import (
"bytes"
"strings"
"testing"
pb "tutorial"
)
func TestWritePersonWritesPerson(t *testing.T) {
buf := new(bytes.Buffer)
// [START populate_proto]
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "[email protected]",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.Person_HOME},
},
}
// [END populate_proto]
writePerson(buf, &p)
got := buf.String()
want := `Person ID: 1234
Name: John Doe
E-mail address: [email protected]
Home phone #: 555-4321
`
if got != want {
t.Errorf("writePerson(%s) =>\n\t%q, want %q", p.String(), got, want)
}
}
func TestListPeopleWritesList(t *testing.T) {
buf := new(bytes.Buffer)
in := pb.AddressBook{People: []*pb.Person{
{
Name: "John Doe",
Id: 101,
Email: "[email protected]",
},
{
Name: "Jane Doe",
Id: 102,
},
{
Name: "Jack Doe",
Id: 201,
Email: "[email protected]",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-555-5555", Type: pb.Person_WORK},
},
},
{
Name: "Jack Buck",
Id: 301,
Email: "[email protected]",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-555-0000", Type: pb.Person_HOME},
{Number: "555-555-0001", Type: pb.Person_MOBILE},
{Number: "555-555-0002", Type: pb.Person_WORK},
},
},
{
Name: "Janet Doe",
Id: 1001,
Email: "[email protected]",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-777-0000"},
{Number: "555-777-0001", Type: pb.Person_HOME},
},
},
}}
listPeople(buf, &in)
want := strings.Split(`Person ID: 101
Name: John Doe
E-mail address: [email protected]
Person ID: 102
Name: Jane Doe
Person ID: 201
Name: Jack Doe
E-mail address: [email protected]
Work phone #: 555-555-5555
Person ID: 301
Name: Jack Buck
E-mail address: [email protected]
Home phone #: 555-555-0000
Mobile phone #: 555-555-0001
Work phone #: 555-555-0002
Person ID: 1001
Name: Janet Doe
E-mail address: [email protected]
Mobile phone #: 555-777-0000
Home phone #: 555-777-0001
`, "\n")
got := strings.Split(buf.String(), "\n")
if len(got) != len(want) {
t.Errorf(
"listPeople(%s) =>\n\t%q has %d lines, want %d",
in.String(),
buf.String(),
len(got),
len(want))
}
lines := len(got)
if lines > len(want) {
lines = len(want)
}
for i := 0; i < lines; i++ {
if got[i] != want[i] {
t.Errorf(
"listPeople(%s) =>\n\tline %d %q, want %q",
in.String(),
i,
got[i],
want[i])
}
}
}
tutorial/addressbook.proto
// See README.txt for information and build instructions.
//
// Note: START and END tags are used in comments to define sections used in
// tutorials. They are not part of the syntax for Protocol Buffers.
//
// To get an in-depth walkthrough of this file and the related examples, see:
// https://developers.google.com/protocol-buffers/docs/tutorials
// [START declaration]
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
// [END declaration]
// [START java_declaration]
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]
// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]
// [START messages]
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
// [END messages]
$GOPATH
└── proto
├── test.pb.go
└── test.proto
└── proto_client
└── client_protobuf.go
└── proto_server
└── server_protobuf.go
proto/test.proto
syntax = "proto3"; // 指定版本, 必须要写 (proto3、proto2)
package proto;
enum FOO
{
X = 0;
};
//message 是固定的。UserInfo 是类名, 可以随意指定, 符合规范即可
message UserInfo{
string message = 1; // 消息
int32 length = 2; // 消息大小
int32 cnt = 3; // 消息计数
}
proto_client/client_protobuf.go
package main
import (
"bufio"
"fmt"
"net"
"os"
stProto "proto"
"time"
//protobuf 编解码库, 下面两个库是相互兼容的, 可以使用其中任意一个
"github.com/golang/protobuf/proto"
//"github.com/gogo/protobuf/proto"
)
func main() {
strIP := "localhost:6600"
var conn net.Conn
var err error
// 连接服务器
for conn, err = net.Dial("tcp", strIP); err != nil; conn, err = net.Dial("tcp", strIP) {
fmt.Println("connect", strIP, "fail")
time.Sleep(time.Second)
fmt.Println("reconnect...")
}
fmt.Println("connect", strIP, "success")
defer conn.Close()
// 发送消息
cnt := 0
sender := bufio.NewScanner(os.Stdin)
for sender.Scan() {
cnt++
stSend := &stProto.UserInfo{
Message: sender.Text(),
Length: *proto.Int(len(sender.Text())),
Cnt: *proto.Int(cnt),
}
//protobuf 编码
pData, err := proto.Marshal(stSend)
if err != nil {
panic(err)
}
// 发送
conn.Write(pData)
if sender.Text() == "stop" {
return
}
}
}
proto_server/server_protobuf.go
package main
import (
"fmt"
"net"
"os"
stProto "proto"
//protobuf 编解码库, 下面两个库是相互兼容的, 可以使用其中任意一个
"github.com/golang/protobuf/proto"
//"github.com/gogo/protobuf/proto"
)
func main() {
// 监听
listener, err := net.Listen("tcp", "localhost:6600")
if err != nil {
panic(err)
}
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
fmt.Println("new connect", conn.RemoteAddr())
go readMessage(conn)
}
}
// 接收消息
func readMessage(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096, 4096)
for {
// 读消息
cnt, err := conn.Read(buf)
if err != nil {
panic(err)
}
stReceive := &stProto.UserInfo{}
pData := buf[:cnt]
//protobuf 解码
err = proto.Unmarshal(pData, stReceive)
if err != nil {
panic(err)
}
fmt.Println("receive", conn.RemoteAddr(), stReceive)
if stReceive.Message == "stop" {
os.Exit(1)
}
}
}
$ go run server_protobuf.go
$ go run client_protobuf.go
然后在客户端处输入任意字符, 在服务端可以显示出来。