首頁>技術>

生命不止,繼續 go go go !!!

號外號外,插播一條廣告,透過部落格的uv可以看到週五,程式設計師是不怎麼幹活的:

本篇部落格,使用gRPC和Protobuf,實現所謂的高效能api。

protobuf

golang中的protobuf大家應該不會很陌生,之前也有部落格介紹過:Go實戰–go中使用google/protobuf(The way to go)

Protocol Buffers (a.k.a., protobuf) are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data. You can find protobuf’s documentation on the Google Developers site.

獲取:

go get -u github.com/golang/protobuf/protogo get -u github.com/golang/protobuf/protoc-gen-go12

Protobuf語法下面簡要介紹Protobuf語法:參考:https://segmentfault.com/a/1190000007917576官方:https://developers.google.com/protocol-buffers/https://developers.google.com/protocol-buffers/docs/gotutorial

Message定義一個message型別定義描述了一個請求或相應的訊息格式,可以包含多種型別欄位。例如定義一個搜尋請求的訊息格式,每個請求包含查詢字串、頁碼、每頁數目。

syntax = "proto3";package tutorial;12

首行宣告使用的protobuf版本為proto3

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;}// Our address book file is just one of these.message AddressBook {  repeated Person people = 1;}1234567891011121314151617181920212223

生成

protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto1

寫訊息

book := &pb.AddressBook{}// ...// 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)}1234567891011

讀訊息

// 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)}123456789
grpc

golang中的rpc大家也不會陌生,之前也有介紹過奧:Go實戰–go中使用rpc(The way to go)

什麼是rpcRPC是Remote Procedure CallProtocol的縮寫,即—遠端過程呼叫協議。

RPC是一個計算機通訊協議。該協議允許運行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計。如果涉及的軟體採用面向物件程式設計,那麼遠端過程呼叫亦可稱作遠端呼叫或遠端方法呼叫,資訊資料。透過它可以使函式呼叫模式網路化。在OSI網路通訊模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網路分散式多程式在內的應用程式更加容易。

net/rpcGo語言標準庫能夠自帶一個rpc框架還是非常給力的,這可以很大程度的降低寫後端網路通訊服務的門檻,特別是在大規模的分散式系統中,rpc基本是跨機器通訊的標配。rpc能夠最大程度遮蔽網路細節,讓開發者專注在服務功能的開發上面

什麼是grpcgRPC 是一個高效能、開源、通用的RPC框架,由Google推出,基於HTTP/2協議標準設計開發,預設採用Protocol Buffers資料序列化協議,支援多種開發語言。gRPC提供了一種簡單的方法來精確的定義服務,並且為客戶端和服務端自動生成可靠的功能庫。

在gRPC客戶端可以直接呼叫不同伺服器上的遠端程式,使用姿勢看起來就像呼叫本地程式一樣,很容易去構建分散式應用和服務。和很多RPC系統一樣,服務端負責實現定義好的介面並處理客戶端的請求,客戶端根據介面描述直接呼叫需要的服務。客戶端和服務端可以分別使用gRPC支援的不同語言實現。

grpc-gogithub地址:https://github.com/grpc/grpc-go

Star: 4402

文件地址:https://godoc.org/google.golang.org/grpc

獲取:go get -u google.golang.org/grpc

應用

檔案結構

proto檔案customer.proto

syntax = "proto3";package customer;// The Customer service definition.service Customer {     // Get all Customers with filter - A server-to-client streaming RPC.  rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {}  // Create a new Customer - A simple RPC   rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}}// Request message for creating a new customermessage CustomerRequest {  int32 id = 1;  // Unique ID number for a Customer.  string name = 2;  string email = 3;  string phone= 4;  message Address {    string street = 1;    string city = 2;    string state = 3;    string zip = 4;    bool isShippingAddress = 5;   }  repeated Address addresses = 5;}message CustomerResponse {  int32 id = 1;  bool success = 2;}message CustomerFilter {      string keyword = 1;}12345678910111213141516171819202122232425262728293031323334353637

生成customer.pb.go

protoc --go_out=plugins=grpc:. customer.proto1
// Code generated by protoc-gen-go. DO NOT EDIT.// source: customer.proto/*Package customer is a generated protocol buffer package.It is generated from these files:    customer.protoIt has these top-level messages:    CustomerRequest    CustomerResponse    CustomerFilter*/package customerimport proto "github.com/golang/protobuf/proto"import fmt "fmt"import math "math"import (    context "golang.org/x/net/context"    grpc "google.golang.org/grpc")// Reference imports to suppress errors if they are not otherwise used.var _ = proto.Marshalvar _ = fmt.Errorfvar _ = math.Inf// This is a compile-time assertion to ensure that this generated file// is compatible with the proto package it is being compiled against.// A compilation error at this line likely means your copy of the// proto package needs to be updated.const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package// Request message for creating a new customertype CustomerRequest struct {    Id        int32                      `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`    Name      string                     `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`    Email     string                     `protobuf:"bytes,3,opt,name=email" json:"email,omitempty"`    Phone     string                     `protobuf:"bytes,4,opt,name=phone" json:"phone,omitempty"`    Addresses []*CustomerRequest_Address `protobuf:"bytes,5,rep,name=addresses" json:"addresses,omitempty"`}func (m *CustomerRequest) Reset()                    { *m = CustomerRequest{} }func (m *CustomerRequest) String() string            { return proto.CompactTextString(m) }func (*CustomerRequest) ProtoMessage()               {}func (*CustomerRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }func (m *CustomerRequest) GetId() int32 {    if m != nil {        return m.Id    }    return 0}func (m *CustomerRequest) GetName() string {    if m != nil {        return m.Name    }    return ""}func (m *CustomerRequest) GetEmail() string {    if m != nil {        return m.Email    }    return ""}func (m *CustomerRequest) GetPhone() string {    if m != nil {        return m.Phone    }    return ""}func (m *CustomerRequest) GetAddresses() []*CustomerRequest_Address {    if m != nil {        return m.Addresses    }    return nil}type CustomerRequest_Address struct {    Street            string `protobuf:"bytes,1,opt,name=street" json:"street,omitempty"`    City              string `protobuf:"bytes,2,opt,name=city" json:"city,omitempty"`    State             string `protobuf:"bytes,3,opt,name=state" json:"state,omitempty"`    Zip               string `protobuf:"bytes,4,opt,name=zip" json:"zip,omitempty"`    IsShippingAddress bool   `protobuf:"varint,5,opt,name=isShippingAddress" json:"isShippingAddress,omitempty"`}func (m *CustomerRequest_Address) Reset()                    { *m = CustomerRequest_Address{} }func (m *CustomerRequest_Address) String() string            { return proto.CompactTextString(m) }func (*CustomerRequest_Address) ProtoMessage()               {}func (*CustomerRequest_Address) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} }func (m *CustomerRequest_Address) GetStreet() string {    if m != nil {        return m.Street    }    return ""}func (m *CustomerRequest_Address) GetCity() string {    if m != nil {        return m.City    }    return ""}func (m *CustomerRequest_Address) GetState() string {    if m != nil {        return m.State    }    return ""}func (m *CustomerRequest_Address) GetZip() string {    if m != nil {        return m.Zip    }    return ""}func (m *CustomerRequest_Address) GetIsShippingAddress() bool {    if m != nil {        return m.IsShippingAddress    }    return false}type CustomerResponse struct {    Id      int32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`    Success bool  `protobuf:"varint,2,opt,name=success" json:"success,omitempty"`}func (m *CustomerResponse) Reset()                    { *m = CustomerResponse{} }func (m *CustomerResponse) String() string            { return proto.CompactTextString(m) }func (*CustomerResponse) ProtoMessage()               {}func (*CustomerResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }func (m *CustomerResponse) GetId() int32 {    if m != nil {        return m.Id    }    return 0}func (m *CustomerResponse) GetSuccess() bool {    if m != nil {        return m.Success    }    return false}type CustomerFilter struct {    Keyword string `protobuf:"bytes,1,opt,name=keyword" json:"keyword,omitempty"`}func (m *CustomerFilter) Reset()                    { *m = CustomerFilter{} }func (m *CustomerFilter) String() string            { return proto.CompactTextString(m) }func (*CustomerFilter) ProtoMessage()               {}func (*CustomerFilter) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }func (m *CustomerFilter) GetKeyword() string {    if m != nil {        return m.Keyword    }    return ""}func init() {    proto.RegisterType((*CustomerRequest)(nil), "customer.CustomerRequest")    proto.RegisterType((*CustomerRequest_Address)(nil), "customer.CustomerRequest.Address")    proto.RegisterType((*CustomerResponse)(nil), "customer.CustomerResponse")    proto.RegisterType((*CustomerFilter)(nil), "customer.CustomerFilter")}// Reference imports to suppress errors if they are not otherwise used.var _ context.Contextvar _ grpc.ClientConn// This is a compile-time assertion to ensure that this generated file// is compatible with the grpc package it is being compiled against.const _ = grpc.SupportPackageIsVersion4// Client API for Customer servicetype CustomerClient interface {    // Get all Customers with filter - A server-to-client streaming RPC.    GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error)    // Create a new Customer - A simple RPC    CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error)}type customerClient struct {    cc *grpc.ClientConn}func NewCustomerClient(cc *grpc.ClientConn) CustomerClient {    return &customerClient{cc}}func (c *customerClient) GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error) {    stream, err := grpc.NewClientStream(ctx, &_Customer_serviceDesc.Streams[0], c.cc, "/customer.Customer/GetCustomers", opts...)    if err != nil {        return nil, err    }    x := &customerGetCustomersClient{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}type Customer_GetCustomersClient interface {    Recv() (*CustomerRequest, error)    grpc.ClientStream}type customerGetCustomersClient struct {    grpc.ClientStream}func (x *customerGetCustomersClient) Recv() (*CustomerRequest, error) {    m := new(CustomerRequest)    if err := x.ClientStream.RecvMsg(m); err != nil {        return nil, err    }    return m, nil}func (c *customerClient) CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error) {    out := new(CustomerResponse)    err := grpc.Invoke(ctx, "/customer.Customer/CreateCustomer", in, out, c.cc, opts...)    if err != nil {        return nil, err    }    return out, nil}// Server API for Customer servicetype CustomerServer interface {    // Get all Customers with filter - A server-to-client streaming RPC.    GetCustomers(*CustomerFilter, Customer_GetCustomersServer) error    // Create a new Customer - A simple RPC    CreateCustomer(context.Context, *CustomerRequest) (*CustomerResponse, error)}func RegisterCustomerServer(s *grpc.Server, srv CustomerServer) {    s.RegisterService(&_Customer_serviceDesc, srv)}func _Customer_GetCustomers_Handler(srv interface{}, stream grpc.ServerStream) error {    m := new(CustomerFilter)    if err := stream.RecvMsg(m); err != nil {        return err    }    return srv.(CustomerServer).GetCustomers(m, &customerGetCustomersServer{stream})}type Customer_GetCustomersServer interface {    Send(*CustomerRequest) error    grpc.ServerStream}type customerGetCustomersServer struct {    grpc.ServerStream}func (x *customerGetCustomersServer) Send(m *CustomerRequest) error {    return x.ServerStream.SendMsg(m)}func _Customer_CreateCustomer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {    in := new(CustomerRequest)    if err := dec(in); err != nil {        return nil, err    }    if interceptor == nil {        return srv.(CustomerServer).CreateCustomer(ctx, in)    }    info := &grpc.UnaryServerInfo{        Server:     srv,        FullMethod: "/customer.Customer/CreateCustomer",    }    handler := func(ctx context.Context, req interface{}) (interface{}, error) {        return srv.(CustomerServer).CreateCustomer(ctx, req.(*CustomerRequest))    }    return interceptor(ctx, in, info, handler)}var _Customer_serviceDesc = grpc.ServiceDesc{    ServiceName: "customer.Customer",    HandlerType: (*CustomerServer)(nil),    Methods: []grpc.MethodDesc{        {            MethodName: "CreateCustomer",            Handler:    _Customer_CreateCustomer_Handler,        },    },    Streams: []grpc.StreamDesc{        {            StreamName:    "GetCustomers",            Handler:       _Customer_GetCustomers_Handler,            ServerStreams: true,        },    },    Metadata: "customer.proto",}func init() { proto.RegisterFile("customer.proto", fileDescriptor0) }var fileDescriptor0 = []byte{    // 326 bytes of a gzipped FileDescriptorProto    0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xef, 0x4a, 0xc3, 0x30,    0x10, 0xc0, 0x97, 0x6e, 0xdd, 0x9f, 0x53, 0xea, 0x0c, 0x22, 0xb1, 0x9f, 0x6a, 0x3f, 0x15, 0x91,    0x21, 0xf3, 0xab, 0x20, 0x32, 0x70, 0xf8, 0xb5, 0x3e, 0x41, 0x6d, 0x0f, 0x17, 0xdc, 0xda, 0x9a,    0xcb, 0x90, 0xf9, 0x0a, 0xbe, 0x83, 0xcf, 0xe0, 0x23, 0x4a, 0xd2, 0x66, 0x03, 0xe7, 0xbe, 0xdd,    0xef, 0x72, 0x77, 0xf9, 0xe5, 0x08, 0x04, 0xf9, 0x9a, 0x74, 0xb5, 0x42, 0x35, 0xa9, 0x55, 0xa5,    0x2b, 0x3e, 0x74, 0x1c, 0xff, 0x78, 0x70, 0x32, 0x6b, 0x21, 0xc5, 0xf7, 0x35, 0x92, 0xe6, 0x01,    0x78, 0xb2, 0x10, 0x2c, 0x62, 0x89, 0x9f, 0x7a, 0xb2, 0xe0, 0x1c, 0x7a, 0x65, 0xb6, 0x42, 0xe1,    0x45, 0x2c, 0x19, 0xa5, 0x36, 0xe6, 0x67, 0xe0, 0xe3, 0x2a, 0x93, 0x4b, 0xd1, 0xb5, 0xc9, 0x06,    0x4c, 0xb6, 0x5e, 0x54, 0x25, 0x8a, 0x5e, 0x93, 0xb5, 0xc0, 0xef, 0x61, 0x94, 0x15, 0x85, 0x42,    0x22, 0x24, 0xe1, 0x47, 0xdd, 0xe4, 0x68, 0x7a, 0x39, 0xd9, 0x1a, 0xfd, 0xb9, 0x7d, 0xf2, 0xd0,    0x94, 0xa6, 0xbb, 0x9e, 0xf0, 0x8b, 0xc1, 0xa0, 0x4d, 0xf3, 0x73, 0xe8, 0x93, 0x56, 0x88, 0xda,    0x0a, 0x8e, 0xd2, 0x96, 0x8c, 0x64, 0x2e, 0xf5, 0xc6, 0x49, 0x9a, 0xd8, 0xe8, 0x90, 0xce, 0x34,    0x3a, 0x49, 0x0b, 0x7c, 0x0c, 0xdd, 0x4f, 0x59, 0xb7, 0x8a, 0x26, 0xe4, 0xd7, 0x70, 0x2a, 0xe9,    0x79, 0x21, 0xeb, 0x5a, 0x96, 0xaf, 0xed, 0x45, 0xc2, 0x8f, 0x58, 0x32, 0x4c, 0xf7, 0x0f, 0xe2,    0x3b, 0x18, 0xef, 0x9c, 0xa9, 0xae, 0x4a, 0xc2, 0xbd, 0x95, 0x09, 0x18, 0xd0, 0x3a, 0xcf, 0xcd,    0x1c, 0xcf, 0xce, 0x71, 0x18, 0x5f, 0x41, 0xe0, 0xba, 0x1f, 0xe5, 0x52, 0xa3, 0x32, 0xb5, 0x6f,    0xb8, 0xf9, 0xa8, 0x54, 0xd1, 0x3e, 0xc9, 0xe1, 0xf4, 0x9b, 0xc1, 0xd0, 0x15, 0xf3, 0x39, 0x1c,    0xcf, 0x51, 0x3b, 0x24, 0x2e, 0xf6, 0x57, 0xd8, 0x0c, 0x0c, 0x2f, 0x0e, 0x2e, 0x37, 0xee, 0xdc,    0x30, 0xfe, 0x04, 0xc1, 0x4c, 0x61, 0xa6, 0x71, 0x3b, 0xfa, 0x70, 0x43, 0x18, 0xfe, 0x77, 0xd4,    0x3c, 0x3a, 0xee, 0xbc, 0xf4, 0xed, 0x77, 0xba, 0xfd, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xde, 0x91,    0xd3, 0x62, 0x60, 0x02, 0x00, 0x00,}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344

server/main.go

package mainimport (    "log"    "net"    "strings"    "golang.org/x/net/context"    "google.golang.org/grpc"    pb "go_grpc_protobuf/customer")const (    port = ":50051")// server is used to implement customer.CustomerServer.type server struct {    savedCustomers []*pb.CustomerRequest}// CreateCustomer creates a new Customerfunc (s *server) CreateCustomer(ctx context.Context, in *pb.CustomerRequest) (*pb.CustomerResponse, error) {    s.savedCustomers = append(s.savedCustomers, in)    return &pb.CustomerResponse{Id: in.Id, Success: true}, nil}// GetCustomers returns all customers by given filterfunc (s *server) GetCustomers(filter *pb.CustomerFilter, stream pb.Customer_GetCustomersServer) error {    for _, customer := range s.savedCustomers {        if filter.Keyword != "" {            if !strings.Contains(customer.Name, filter.Keyword) {                continue            }        }        if err := stream.Send(customer); err != nil {            return err        }    }    return nil}func main() {    lis, err := net.Listen("tcp", port)    if err != nil {        log.Fatalf("failed to listen: %v", err)    }    // Creates a new gRPC server    s := grpc.NewServer()    pb.RegisterCustomerServer(s, &server{})    s.Serve(lis)}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354

client/main.go

package mainimport (    "io"    "log"    "golang.org/x/net/context"    "google.golang.org/grpc"    pb "go_grpc_protobuf/customer")const (    address = "localhost:50051")// createCustomer calls the RPC method CreateCustomer of CustomerServerfunc createCustomer(client pb.CustomerClient, customer *pb.CustomerRequest) {    resp, err := client.CreateCustomer(context.Background(), customer)    if err != nil {        log.Fatalf("Could not create Customer: %v", err)    }    if resp.Success {        log.Printf("A new Customer has been added with id: %d", resp.Id)    }}// getCustomers calls the RPC method GetCustomers of CustomerServerfunc getCustomers(client pb.CustomerClient, filter *pb.CustomerFilter) {    // calling the streaming API    stream, err := client.GetCustomers(context.Background(), filter)    if err != nil {        log.Fatalf("Error on get customers: %v", err)    }    for {        customer, err := stream.Recv()        if err == io.EOF {            break        }        if err != nil {            log.Fatalf("%v.GetCustomers(_) = _, %v", client, err)        }        log.Printf("Customer: %v", customer)    }}func main() {    // Set up a connection to the gRPC server.    conn, err := grpc.Dial(address, grpc.WithInsecure())    if err != nil {        log.Fatalf("did not connect: %v", err)    }    defer conn.Close()    // Creates a new CustomerClient    client := pb.NewCustomerClient(conn)    customer := &pb.CustomerRequest{        Id:    101,        Name:  "Shiju Varghese",        Email: "[email protected]",        Phone: "732-757-2923",        Addresses: []*pb.CustomerRequest_Address{            &pb.CustomerRequest_Address{                Street:            "1 Mission Street",                City:              "San Francisco",                State:             "CA",                Zip:               "94105",                IsShippingAddress: false,            },            &pb.CustomerRequest_Address{                Street:            "Greenfield",                City:              "Kochi",                State:             "KL",                Zip:               "68356",                IsShippingAddress: true,            },        },    }    // Create a new customer    createCustomer(client, customer)    customer = &pb.CustomerRequest{        Id:    102,        Name:  "Irene Rose",        Email: "[email protected]",        Phone: "732-757-2924",        Addresses: []*pb.CustomerRequest_Address{            &pb.CustomerRequest_Address{                Street:            "1 Mission Street",                City:              "San Francisco",                State:             "CA",                Zip:               "94105",                IsShippingAddress: true,            },        },    }    // Create a new customer    createCustomer(client, customer)    // Filter with an empty Keyword    filter := &pb.CustomerFilter{Keyword: ""}    getCustomers(client, filter)}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104

執行server,執行client,輸出:

2017/12/07 11:55:59 A new Customer has been added with id: 1012017/12/07 11:55:59 A new Customer has been added with id: 1022017/12/07 11:55:59 Customer: id:101 name:"Shiju Varghese" email:"[email protected]" phone:"732-757-2923" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" > addresses:<street:"Greenfield" city:"Kochi" state:"KL" zip:"68356" isShippingAddress:true >2017/12/07 11:55:59 Customer: id:102 name:"Irene Rose" email:"[email protected]" phone:"732-757-2924" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" isShippingAddress:true >1234

18
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Python GUI程式設計:製作一個文件圖片提取軟體