grpc的使用

grpc 整理(nodejs)

gRPC 是什么肛冶?

在 gRPC 里客戶端應(yīng)用可以像調(diào)用本地對象一樣直接調(diào)用另一臺(tái)不同的機(jī)器上服務(wù)端應(yīng)用的方法停士,使得您能夠更容易地創(chuàng)建分布式應(yīng)用和服務(wù)周伦。與許多 RPC 系統(tǒng)類似,gRPC 也是基于以下理念:定義一個(gè)服務(wù)扔茅,指定其能夠被遠(yuǎn)程調(diào)用的方法(包含參數(shù)和返回類型)奕翔。在服務(wù)端實(shí)現(xiàn)這個(gè)接口,并運(yùn)行一個(gè) gRPC 服務(wù)器來處理客戶端調(diào)用先改。在客戶端擁有一個(gè)存根能夠像服務(wù)端一樣的方法疚察。

圖1

gRPC 有什么好處以及在什么場景下需要用 gRPC

既然是 server/client 模型,那么我們直接用 restful api 不是也可以滿足嗎仇奶,為什么還需要 RPC 呢貌嫡?下面我們就來看看 RPC 到底有哪些優(yōu)勢

gRPC vs. Restful API

gRPC 和 restful API 都提供了一套通信機(jī)制,用于 server/client 模型通信,而且它們都使用 http 作為底層的傳輸協(xié)議(嚴(yán)格地說, gRPC 使用的 http2.0岛抄,而 restful api 則不一定)别惦。不過 gRPC 還是有些特有的優(yōu)勢,如下:

  • gRPC 可以通過 protobuf 來定義接口夫椭,從而可以有更加嚴(yán)格的接口約束條件步咪。
  • 另外,通過 protobuf 可以將數(shù)據(jù)序列化為二進(jìn)制編碼益楼,這會(huì)大幅減少需要傳輸?shù)臄?shù)據(jù)量猾漫,從而大幅提高性能。
  • gRPC 可以方便地支持流式通信(理論上通過 http2.0 就可以使用 streaming 模式, 但是通常 web 服務(wù)的 restful api 似乎很少這么用感凤,通常的流式數(shù)據(jù)應(yīng)用如視頻流悯周,一般都會(huì)使用專門的協(xié)議如 HLS,RTMP 等陪竿,這些就不是我們通常 web 服務(wù)了禽翼,而是有專門的服務(wù)器應(yīng)用。

使用場景

  • 需要對接口進(jìn)行嚴(yán)格約束的情況族跛,比如我們提供了一個(gè)公共的服務(wù)闰挡,很多人,甚至公司外部的人也可以訪問這個(gè)服務(wù)礁哄,這時(shí)對于接口我們希望有更加嚴(yán)格的約束长酗,我們不希望客戶端給我們傳遞任意的數(shù)據(jù),尤其是考慮到安全性的因素桐绒,我們通常需要對接口進(jìn)行更加嚴(yán)格的約束夺脾。這時(shí) gRPC 就可以通過 protobuf 來提供嚴(yán)格的接口約束。
  • 對于性能有更高的要求時(shí)茉继。有時(shí)我們的服務(wù)需要傳遞大量的數(shù)據(jù)咧叭,而又希望不影響我們的性能,這個(gè)時(shí)候也可以考慮 gRPC 服務(wù)烁竭,因?yàn)橥ㄟ^ protobuf 我們可以將數(shù)據(jù)壓縮編碼轉(zhuǎn)化為二進(jìn)制格式菲茬,通常傳遞的數(shù)據(jù)量要小得多,而且通過 http2 我們可以實(shí)現(xiàn)異步的請求派撕,從而大大提高了通信效率婉弹。

基本概念

gRPC 是一個(gè)高性能、開源和通用的 RPC 框架腥刹,面向移動(dòng)和 HTTP/2 設(shè)計(jì)马胧。目前提供 C、Java 和 Go 語言版本衔峰,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.

gRPC 基于 HTTP/2 標(biāo)準(zhǔn)設(shè)計(jì),帶來諸如雙向流、流控垫卤、頭部壓縮威彰、單 TCP 連接上的多復(fù)用請求等特。這些特性使得其在移動(dòng)設(shè)備上表現(xiàn)更好穴肘,更省電和節(jié)省空間占用歇盼。

服務(wù)定義

正如其他 RPC 系統(tǒng),gRPC 基于如下思想:定義一個(gè)服務(wù)评抚, 指定其可以被遠(yuǎn)程調(diào)用的方法及其參數(shù)和返回類型豹缀。gRPC 默認(rèn)使用 protocol buffers 作為接口定義語言,來描述服務(wù)接口和有效載荷消息結(jié)構(gòu)慨代。

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  required string greeting = 1;
}

message HelloResponse {
  required string reply = 1;
}

gRPC 允許定義四類服務(wù)方法:

單項(xiàng) RPC邢笙,即客戶端發(fā)送一個(gè)請求給服務(wù)端,從服務(wù)端獲取一個(gè)應(yīng)答侍匙,就像一次普通的函數(shù)調(diào)用氮惯。

rpc SayHello(HelloRequest) returns (HelloResponse){
}

服務(wù)端流式 RPC,即客戶端發(fā)送一個(gè)請求給服務(wù)端想暗,可獲取一個(gè)數(shù)據(jù)流用來讀取一系列消息妇汗。客戶端從返回的數(shù)據(jù)流里一直讀取直到?jīng)]有更多消息為止说莫。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

客戶端流式 RPC杨箭,即客戶端用提供的一個(gè)數(shù)據(jù)流寫入并發(fā)送一系列消息給服務(wù)端。一旦客戶端完成消息寫入储狭,就等待服務(wù)端讀取這些消息并返回應(yīng)答告唆。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

雙向流式 RPC,即兩邊都可以分別通過一個(gè)讀寫數(shù)據(jù)流來發(fā)送一系列消息晶密。這兩個(gè)數(shù)據(jù)流操作是相互獨(dú)立的擒悬,所以客戶端和服務(wù)端能按其希望的任意順序讀寫,例如:服務(wù)端可以在寫應(yīng)答前等待所有的客戶端消息稻艰,或者它可以先讀一個(gè)消息再寫一個(gè)消息懂牧,或者是讀寫相結(jié)合的其他方式。每個(gè)數(shù)據(jù)流里消息的順序會(huì)被保持尊勿。

生命周期

單項(xiàng) rpc

客戶端發(fā)出單個(gè)請求僧凤,獲得單個(gè)響應(yīng)。

  • 客戶端發(fā)起調(diào)用元扔,服務(wù)端收到調(diào)用信息
  • 此時(shí)服務(wù)端還未收到數(shù)據(jù)信息躯保,但是已經(jīng)可以應(yīng)答(默認(rèn)不應(yīng)答)
  • 服務(wù)端收到客戶端信息,處理數(shù)據(jù)澎语,向客戶端應(yīng)答途事,這個(gè)應(yīng)答會(huì)和包含狀態(tài)碼以及可選的狀態(tài)信息等狀態(tài)明細(xì)及可選的追蹤信息
  • 若是狀態(tài) OK验懊,客戶端收到數(shù)據(jù),結(jié)束調(diào)用
流式 RPC

服務(wù)端流式 RPC 除了在得到客戶端請求信息后發(fā)送回一個(gè)應(yīng)答流之外尸变,與單項(xiàng) rpc 一樣义图。在發(fā)送完所有應(yīng)答后,服務(wù)端的狀態(tài)詳情(狀態(tài)碼和可選的狀態(tài)信息)和可選的跟蹤元數(shù)據(jù)被發(fā)送回客戶端召烂,以此來完成服務(wù)端的工作碱工。客戶端在接收到所有服務(wù)端的應(yīng)答后也完成了工作

客戶端流式 RPC

客戶端流式 RPC 也基本與單項(xiàng) rpc 一樣奏夫,區(qū)別在于客戶端通過發(fā)送一個(gè)請求流給服務(wù)端怕篷,取代了原先發(fā)送的單個(gè)請求。服務(wù)端通常(但并不必須)會(huì)在接收到客戶端所有的請求后發(fā)送回一個(gè)應(yīng)答酗昼,其中附帶有它的狀態(tài)詳情和可選的跟蹤數(shù)據(jù)廊谓。

截至?xí)r間

gRPC 允許客戶端在調(diào)用一個(gè)遠(yuǎn)程方法前指定一個(gè)最后期限值。這個(gè)值指定了在客戶端可以等待服務(wù)端多長時(shí)間來應(yīng)答仔雷,超過這個(gè)時(shí)間值 RPC 將結(jié)束并返回DEADLINE_EXCEEDED錯(cuò)誤蹂析。

RPC 終止

在 gRPC 里,客戶端和服務(wù)端對調(diào)用成功的判斷是獨(dú)立的碟婆、本地的电抚,他們的結(jié)論可能不一致。這意味著竖共,比如你有一個(gè) RPC 在服務(wù)端成功結(jié)束("我已經(jīng)返回了所有應(yīng)答!")蝙叛,到那時(shí)在客戶端可能是失敗的("應(yīng)答在最后期限后才來到!")。也可能在客戶端把所有請求發(fā)送完前公给,服務(wù)端卻判斷調(diào)用已經(jīng)完成了借帘。

安全認(rèn)證

安全認(rèn)證

在 nodejs 中的使用

定義服務(wù)

//簡單服務(wù)
rpc GetFeature(Point) returns (Feature) {}

//服務(wù)端流式服務(wù)
rpc ListFeatures(Rectangle) returns (stream Feature) {}

//客戶端流式服務(wù)
rpc RecordRoute(stream Point) returns (RouteSummary) {}

//雙向流式服務(wù)
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

創(chuàng)建服務(wù)端(創(chuàng)建服務(wù))

方法實(shí)現(xiàn)(簡單 rpc)
function checkFeature(point) {
    var feature
    // Check if there is already a feature object for the given point
    for (var i = 0; i < feature_list.length; i++) {
        feature = feature_list[i]
        if (
            feature.location.latitude === point.latitude &&
            feature.location.longitude === point.longitude
        ) {
            return feature
        }
    }
    var name = ''
    feature = {
        name: name,
        location: point
    }
    return feature
}
function getFeature(call, callback) {
    callback(null, checkFeature(call.request))
}
方法實(shí)現(xiàn)(流式 rpc)
function listFeatures(call) {
    var lo = call.request.lo
    var hi = call.request.hi
    var left = _.min([lo.longitude, hi.longitude])
    var right = _.max([lo.longitude, hi.longitude])
    var top = _.max([lo.latitude, hi.latitude])
    var bottom = _.min([lo.latitude, hi.latitude])
    // For each feature, check if it is in the given bounding box
    _.each(feature_list, function(feature) {
        if (feature.name === '') {
            return
        }
        if (
            feature.location.longitude >= left &&
            feature.location.longitude <= right &&
            feature.location.latitude >= bottom &&
            feature.location.latitude <= top
        ) {
            call.write(feature)
        }
    })
    call.end()
}
啟動(dòng)服務(wù)器
var server = new grpc.Server()
server.addService(hello_proto.Greeter.service, { sayHello: sayHello })
server.bind('localhost:50051', grpc.ServerCredentials.createInsecure())
server.start()

創(chuàng)建客戶端(創(chuàng)建調(diào)用)

簡單 rpc
var point = {latitude: 409146138, longitude: -746188906};
stub.getFeature(point, function(err, feature) {
  if (err) {
    // process error
  } else {
    // process feature
  }
});
流式 rpc
var call = client.listFeatures(rectangle);
  call.on('data', function(feature) {
      console.log('Found feature called "' + feature.name + '" at ' +
          feature.location.latitude/COORD_FACTOR + ', ' +
          feature.location.longitude/COORD_FACTOR);
  });
  call.on('end', function() {
    // The server has finished sending
  });
  call.on('status', function(status) {
    // process status
  });

Demo

第一行代碼 Hello World

helloworld.proto

?```
syntax = "proto3";

package helloworld;
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

server.js

var PROTO_PATH = __dirname + '/helloworld.proto'

var grpc = require('grpc')
var protoLoader = require('@grpc/proto-loader')
var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true, //保留字段名稱,默認(rèn)將下劃線處理為駝峰
    longs: String, //long類型自動(dòng)轉(zhuǎn)為string
    enums: String, //枚舉類型轉(zhuǎn)為string
    defaults: true, //在輸出對象上設(shè)置默認(rèn)值
    oneofs: true //虛擬屬性設(shè)置為當(dāng)前字段名稱
})
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld

function sayHello(call, callback) {
    callback(null, { message: 'Hello ' + call.request.name })
}

function main() {
    var server = new grpc.Server()
    server.addService(hello_proto.Greeter.service, { sayHello: sayHello })
    server.bind('localhost:50051', grpc.ServerCredentials.createInsecure())
    server.start()
}

main()

client.js

var PROTO_PATH = __dirname + '/helloworld.proto'

var grpc = require('grpc')
var protoLoader = require('@grpc/proto-loader')
var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
})
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld

function main() {
    var client = new hello_proto.Greeter(
        'localhost:50051',
        grpc.credentials.createInsecure()
    )
    var user
    if (process.argv.length >= 3) {
        user = process.argv[2]
    } else {
        user = 'world'
    }
    client.sayHello({ name: user }, function(err, response) {
        console.log('Greeting:', response.message)
    })
}
main()

有關(guān) grpc 接口(簡單接口)和普通 http 接口(express 實(shí)現(xiàn))的性能測試

服務(wù)器環(huán)境(單核淌铐,1G肺然,1Mbps)

http 請求

const http = require('http')
const taskList = []
console.log('請求數(shù)據(jù)中...')
const start = new Date().getTime()
let count = 0
let success = 0
let error = 0
let times = 3000
for (let i = 0; i < times; i++) {
    taskList[i] = new Promise((resolve, reject) => {
        http.get('http://39.100.197.67:3000/list', function(req, res) {
            let stream = ''
            req.on('data', function(data) {
                stream += data
            })
            req.on('error', function() {
                count++
                error++
                resolve({ count, success, error })
            })
            req.on('end', function() {
                count++
                success++
                resolve({ count, success, error })
            })
        })
    })
}
Promise.all(taskList)
    .then(result => {
        console.log('count:' + count)
        console.log('success:' + success)
        console.log('error:' + error)
        const end = new Date().getTime()
        console.log('time:' + (end - start))
    })
    .catch(err => {
        console.log(err)
    })
const express = require('express')
const app = express()
app.get('/', (req, res) =>{
    res.send('HellowWorld')
})
app.get('/list', (req, res) => {
    let result = {
        err: 0,
        msg: 'ok',
        data: {
            name: 'hello world',
            id: req.query.id
        }
    }
    if (req.query.id !== 1) {
        result.data.name = 'hello grpc'
    }
    res.send(result)
})
const server = app.listen(3000, function() {
    console.log('runing 3000...')
})

grpc 請求

const PROTO_PATH = __dirname + '/helloworld.proto'

const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
})
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld
const client = new hello_proto.Greeter(
    '39.100.197.67:50051',
    grpc.credentials.createInsecure()
)

const taskList = []
console.log('請求數(shù)據(jù)中...')
const start = new Date().getTime()
let count = 0
let success = 0
let error = 0
let times = 3000
for (let i = 0; i < times; i++) {
    taskList[i] = new Promise((resolve, reject) => {
        client.sayHello({ id: 1 }, function(err, response) {
            count++
            if (err) {
                error++
                resolve()
            } else {
                success++
                resolve()
            }
        })
    })
}
Promise.all(taskList)
    .then(result => {
        console.log('count:' + count)
        console.log('success:' + success)
        console.log('error:' + error)
        const end = new Date().getTime()
        console.log('time:' + (end - start))
    })
    .catch(err => {
        console.log(err)
    })
const PROTO_PATH = __dirname + '/helloworld.proto';
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    });
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
const sayHello = (call, callback) => {
    const data = { name: 'hello world', id: +call.request.id };
    if (call.request.id !== 1) {
        data.name = 'hello grpc'
    }
    callback(null, { message: JSON.stringify({ err: 0, msg: 'ok', data }) })
}

const main = () => {
    var server = new grpc.Server();
    server.addService(hello_proto.Greeter.service, { sayHello: sayHello });
    server.bind('localhost:50051', grpc.ServerCredentials.createInsecure());
    server.start();
};
main();

測試結(jié)果

請求量 成功量 失敗量 響應(yīng)時(shí)間(ms) 請求類型
500 500 0 298 grpc
500 500 0 1344 http
請求量 成功量 失敗量 響應(yīng)時(shí)間(ms) 請求類型
1000 1000 0 362 grpc
1000 1000 0 5251 http
請求量 成功量 失敗量 響應(yīng)時(shí)間(ms) 請求類型
5000 5000 0 3291 grpc
5000 5000 0 33571 http
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腿准,隨后出現(xiàn)的幾起案子际起,更是在濱河造成了極大的恐慌,老刑警劉巖吐葱,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件街望,死亡現(xiàn)場離奇詭異,居然都是意外死亡弟跑,警方通過查閱死者的電腦和手機(jī)灾前,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來孟辑,“玉大人哎甲,你說我怎么就攤上這事蔫敲。” “怎么了烧给?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵燕偶,是天一觀的道長喝噪。 經(jīng)常有香客問我础嫡,道長,這世上最難降的妖魔是什么酝惧? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任榴鼎,我火速辦了婚禮,結(jié)果婚禮上晚唇,老公的妹妹穿的比我還像新娘巫财。我一直安慰自己,他們只是感情好哩陕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布平项。 她就那樣靜靜地躺著,像睡著了一般悍及。 火紅的嫁衣襯著肌膚如雪闽瓢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天心赶,我揣著相機(jī)與錄音扣讼,去河邊找鬼。 笑死缨叫,一個(gè)胖子當(dāng)著我的面吹牛椭符,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播耻姥,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼销钝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了琐簇?” 一聲冷哼從身側(cè)響起蒸健,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸽嫂,沒想到半個(gè)月后纵装,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡据某,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年橡娄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片癣籽。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挽唉,死狀恐怖滤祖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瓶籽,我是刑警寧澤匠童,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站塑顺,受9級(jí)特大地震影響汤求,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜严拒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一扬绪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裤唠,春花似錦挤牛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至航瞭,卻和暖如春诫硕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沧奴。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工痘括, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滔吠。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓纲菌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疮绷。 傳聞我的和親對象是個(gè)殘疾皇子翰舌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • ?gRPC 學(xué)習(xí)筆記,記錄gprc一些基本概念. ?gRPC正如其他 RPC 系統(tǒng),gRPC 基于如下思想:定義一...
    Jancd閱讀 1,982評(píng)論 1 7
  • Fabric的節(jié)點(diǎn)通過grpc向內(nèi)部或外部提供接口冬骚,在學(xué)習(xí)源碼之前椅贱,需要對grpc的基本使用有所了解,并了解如何在...
    史圣杰閱讀 869評(píng)論 0 1
  • 1.簡介 在gRPC中只冻,客戶端應(yīng)用程序可以直接調(diào)用不同計(jì)算機(jī)上的服務(wù)器應(yīng)用程序上的方法庇麦,就像它是本地對象一樣,使您...
    第八共同體閱讀 1,878評(píng)論 0 6
  • GRPC是基于protocol buffers3.0協(xié)議的. 本文將向您介紹gRPC和protocol buffe...
    二月_春風(fēng)閱讀 17,992評(píng)論 2 28
  • 夢想總會(huì)實(shí)現(xiàn) 奇跡就在身邊 是天意吧喜德,讓我今生遇見你 認(rèn)識(shí)你是我一生的榮幸 希望在以后的日子里我們共同成長 探索永...
    老張小時(shí)候閱讀 533評(píng)論 0 7