衡阳派盒市场营销有限公司

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

現代的服務端技術棧:Golang/Protobuf/gRPC詳解

電子設計 ? 來源:電子設計 ? 作者:電子設計 ? 2020-12-25 17:32 ? 次閱讀

譯注:

并發與并行:并發是虛擬的并行,比如通過時間切片技術在單核CPU上運行多個任務,讓每個使用者“以為”自己在獨占這一CPU資源;并行是實際的同一時間多任務同時運行,多數是指在多核CPU的場景下。

隊列與雙端隊列:隊列遵循先入先出的原則,從一端存數,從另一端取數,雙端隊列支持從隊列的兩端存數和取數。

阻塞和非阻塞:阻塞和非阻塞描述了程序等待返回結果時的狀態,阻塞代表不返回結果就掛起,不進行任何操作;非阻塞是在沒返回結果時可以執行其他任務。

合作和搶占:高優先級任務可以打斷其他正在運行的低優先級任務,則調度器是搶占式的;反之,則是合作式的。

服務端編程的陣營中有很多新面孔,一水兒的谷歌血統。在谷歌開始將Golang應用于其產品系統后,Golang快速的吸引了大量的關注。隨著微服務架構的興起,人們開始關注一些現代的數據通信解決方案,如gRPC和Protobuf。在本文中,我們會對以上這些概念作一些簡要的介紹。

一、Golang

Golang又稱Go語言,是一個開源的、多用途的編程語言,由Google研發,并由于種種原因,正在日益流行。Golang已經有10年的歷史,并且據Google稱已經在生產環境中使用了接近7年的時間,這一點可能讓大多數人大跌眼鏡。

Golang的設計理念是,簡單、現代、易于理解和快速上手。Golang的創造者將Golang設計為一個普通的程序員可以在一個周末的時間就可以掌握,并達到使用Golang進行工作的程度。這一點我已經親身證實。Golang的創造者,都是C語言原始草案的專家組成員,可以說,Golang根紅苗正,值得信賴。

理都懂,但話說回來,為什么我們需要另一門編程語言呢

多數場景下,確實并不需要。事實上,Go語言并不能解決其他語言或工具無法解決的新問題。但在一些強調效率、優雅與直觀的場景下,人們通常會面臨一系列的相關問題,這正是Go所致力于解決的領域。Go的主要特點是:

一流的并發支持

內核十分簡單,語言優雅、現代

高性能

提供現代軟件開發所需要的原生工具支持

我將簡要介紹Go是如何提供上述的支持的。在Go語言的官網可以了解更多的特性和細節。

一流的并發支持

并發是多數服務端應用所需要考慮的主要問題之一,考慮到現代微處理器的特性,并發也成為編程語言的主要關切之一。Go語言引入了“goroutine”的理念。可以把“goroutine”理解為一個“輕量級的用戶空間線程”(現實中,當然遠比這要復雜得多,同一線程可能會附著多路的goroutine,但這樣的提法可以讓你有一個大致的概念)。所謂“輕量級”,可以這樣理解,由于采用了十分袖珍的堆棧,你可以同時啟動數以百萬計的goroutine,事實上這也是Go語言官方所推薦的方式。在Go語言中,任何函數或方法都可以生成一個goroutine。比如,只需要運行“go myAsyncTask()”就可以從“myAsyncTask”函數生成一個goroutine。示例代碼如下:

// This function performs the given task concurrently by spawing a goroutine

// for each of those tasks.

func performAsyncTasks(task []Task) {

for _, task := range tasks {

// This will spawn a separate goroutine to carry out this task.

// This call is non-blocking

go task.Execute()

goroutineExample.go hosted with ? by GitHub

(左右滑動查看全部代碼)

怎么樣,是不是很簡單?Go是一門簡單的語言,因此注定是以這樣的方式來解決問題。你可以為每個獨立的異步任務生成一個goroutine而不需要顧慮太多事情。如果處理器支持多核運行,Go語言運行時會自動的以并行的方式運行所有的goroutine。那么,goroutine之間是如何通信的呢,答案是channel。

“channel”也是Go語言的一個概念,用于進行goroutine之間的通信。通過channel,你可以向另一個goroutine傳遞各種信息(比如Go語言概念里的type或者struct甚至是channel)。一個channel大體上是一個“雙端阻塞隊列”(也可以單端的)。如果需要goroutine基于特定條件觸發下一步的行動,也可以利用channel來實現goroutine的協作阻塞任務模式。

在編寫異步或者并發的代碼時,goroutine和channel這兩個概念賦予了編程者大量的靈活性和簡便性。可以籍此很容易的建立其他很有用的庫,比如goroutine pool,舉個簡單的例子:

package executor

import (

"log"

"sync/atomic"

// The Executor struct is the main executor for tasks.

// 'maxWorkers' represents the maximum number of simultaneous goroutines.

// 'ActiveWorkers' tells the number of active goroutines spawned by the Executor at given time.

// 'Tasks' is the channel on which the Executor receives the tasks.

// 'Reports' is channel on which the Executor publishes the every tasks reports.

// 'signals' is channel that can be used to control the executor. Right now, only the termination

// signal is supported which is essentially is sending '1' on this channel by the client.

type Executor struct {

maxWorkers int64

ActiveWorkers int64

Tasks chan Task

Reports chan Report

signals chan int

// NewExecutor creates a new Executor.

// 'maxWorkers' tells the maximum number of simultaneous goroutines.

// 'signals' channel can be used to control the Executor.

func NewExecutor(maxWorkers int, signals chan int) *Executor {

chanSize := 1000

if maxWorkers > chanSize {

chanSize = maxWorkers

executor := Executor{

maxWorkers: int64(maxWorkers),

Tasks: make(chan Task, chanSize),

Reports: make(chan Report, chanSize),

signals: signals,

go executor.launch()

return &executor

// launch starts the main loop for polling on the all the relevant channels and handling differents

// messages.

func (executor *Executor) launch() int {

reports := make(chan Report, executor.maxWorkers)

for {

select {

case signal := <-executor.signals:

if executor.handleSignals(signal) == 0 {

return 0

case r := <-reports:

executor.addReport(r)

default:

if executor.ActiveWorkers < executor.maxWorkers && len(executor.Tasks) > 0 {

task := <-executor.Tasks

atomic.AddInt64(&executor.ActiveWorkers, 1)

go executor.launchWorker(task, reports)

// handleSignals is called whenever anything is received on the 'signals' channel.

// It performs the relevant task according to the received signal(request) and then responds either

// with 0 or 1 indicating whether the request was respected(0) or rejected(1).

func (executor *Executor) handleSignals(signal int) int {

if signal == 1 {

log.Println("Received termination request...")

if executor.Inactive() {

log.Println("No active workers, exiting...")

executor.signals <- 0

return 0

executor.signals <- 1

log.Println("Some tasks are still active...")

return 1

// launchWorker is called whenever a new Task is received and Executor can spawn more workers to spawn

// a new Worker.

// Each worker is launched on a new goroutine. It performs the given task and publishes the report on

// the Executor's internal reports channel.

func (executor *Executor) launchWorker(task Task, reports chan<- Report) {

report := task.Execute()

if len(reports) < cap(reports) {

reports <- report

} else {

log.Println("Executor's report channel is full...")

atomic.AddInt64(&executor.ActiveWorkers, -1)

// AddTask is used to submit a new task to the Executor is a non-blocking way. The Client can submit

// a new task using the Executor's tasks channel directly but that will block if the tasks channel is

// full.

// It should be considered that this method doesn't add the given task if the tasks channel is full

// and it is up to client to try again later.

func (executor *Executor) AddTask(task Task) bool {

if len(executor.Tasks) == cap(executor.Tasks) {

return false

executor.Tasks <- task

return true

// addReport is used by the Executor to publish the reports in a non-blocking way. It client is not

// reading the reports channel or is slower that the Executor publishing the reports, the Executor's

// reports channel is going to get full. In that case this method will not block and that report will

// not be added.

func (executor *Executor) addReport(report Report) bool {

if len(executor.Reports) == cap(executor.Reports) {

return false

executor.Reports <- report

return true

// Inactive checks if the Executor is idle. This happens when there are no pending tasks, active

// workers and reports to publish.

func (executor *Executor) Inactive() bool {

return executor.ActiveWorkers == 0 && len(executor.Tasks) == 0 && len(executor.Reports) == 0

executor.go hosted with ? by GitHub

(左右滑動查看全部代碼)

內核十分簡單,語言優雅、現代

與其他多數的現代語言不同,Golang本身并沒有提供太多的特性。事實上,嚴格限制特性集的范圍正是Go語言的顯著特征,且Go語言著意于此。Go語言的設計與Java的編程范式不同,也不支持如Python一樣的多語言的編程范式。Go只是一個編程的骨架結構。除了必要的特性,其他一無所有。

看過Go語言之后,第一感覺是其不遵循任何特定的哲學或者設計指引,所有的特性都是以引用的方式解決某一個特定的問題,不會畫蛇添足做多余的工作。比如,Go語言提供方法和接口但沒有類;Go語言的編譯器生成動態鏈接庫,但同時保留垃圾回收器;Go語言有嚴格的類型但不支持泛型;Go語言有一個輕量級的運行時但不支持異常。

Go的這一設計理念的主要用意在于,在表達想法、算法或者編碼的環節,開發者可以盡量少想或者不去想“在某種編程語言中處理此事的最佳方案”,讓不同的開發者可以更容易理解對方的代碼。不支持泛型和異常使得Go語言并不那么完美,也因此在很多場景下束手束腳 ,因此在“Go 2”版本中,官方加入了對這些必要特性的考慮。

高性能

單線程的執行效率并不足以評估一門語言的優劣,當語言本身聚焦于解決并發和并行問題的時候尤其如此。即便如此,Golang還是跑出了亮眼的成績,僅次于一些硬核的系統編程語言,如C/C++/Rust等等,并且Golang還在不斷的改進。考慮到Go是有垃圾回收機制的語言,這一成績實際上相當的令人印象深刻,這使得Go語言的性能可以應付幾乎所有的使用場景。

(Image Source: Medium)

提供現代軟件開發所需要的原生工具支持

是否采用一種新的語言或工具,直接取決于開發者體驗的好壞。就Go語言來說,其工具集是用戶采納的主要考量。同最小化的內核一樣,Go的工具集也采用了同樣的設計理念,最小化,但足夠應付需要。執行所有Go語言工具,都采用 go 命令及其子命令,并且全部是以命令行的方式。

Go語言中并沒有類似pip或者npm這類包管理器。但只需要下面的命令,就可以得到任何的社區包:

go get github.com/farkaskid/WebCrawler/blob/master/executor/executor.go

(左右滑動查看全部代碼)

是的,這樣就行。可以直接從Github或其他地方拉取所需要的包。所有的包都是源代碼文件的形態。

對于package.json這類的包,我沒有看到與 goget 等價的命令。事實上也沒有。在Go語言中,無須在一個單一文件中指定所有的依賴,可以在源文件中直接使用下面的命令:

import "github.com/xlab/pocketsphinx-go/sphinx"

(左右滑動查看全部代碼)

那么,當執行go build命令的時候,運行時會自動的運行 goget 來獲取所需要的依賴。完整的源碼如下:

package main

import (

"encoding/binary"

"bytes"

"log"

"os/exec"

"github.com/xlab/pocketsphinx-go/sphinx"

pulse "github.com/mesilliac/pulse-simple" // pulse-simple

var buffSize int

func readInt16(buf []byte) (val int16) {

binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &val)

return

func createStream() *pulse.Stream {

ss := pulse.SampleSpec{pulse.SAMPLE_S16LE, 16000, 1}

buffSize = int(ss.UsecToBytes(1 * 1000000))

stream, err := pulse.Capture("pulse-simple test", "capture test", &ss)

if err != nil {

log.Panicln(err)

return stream

func listen(decoder *sphinx.Decoder) {

stream := createStream()

defer stream.Free()

defer decoder.Destroy()

buf := make([]byte, buffSize)

var bits []int16

log.Println("Listening...")

for {

_, err := stream.Read(buf)

if err != nil {

log.Panicln(err)

for i := 0; i < buffSize; i += 2 {

bits = append(bits, readInt16(buf[i:i+2]))

process(decoder, bits)

bits = nil

func process(dec *sphinx.Decoder, bits []int16) {

if !dec.StartUtt() {

panic("Decoder failed to start Utt")

dec.ProcessRaw(bits, false, false)

dec.EndUtt()

hyp, score := dec.Hypothesis()

if score > -2500 {

log.Println("Predicted:", hyp, score)

handleAction(hyp)

func executeCommand(commands ...string) {

cmd := exec.Command(commands[0], commands[1:]...)

cmd.Run()

func handleAction(hyp string) {

switch hyp {

case "SLEEP":

executeCommand("loginctl", "lock-session")

case "WAKE UP":

executeCommand("loginctl", "unlock-session")

case "POWEROFF":

executeCommand("poweroff")

func main() {

cfg := sphinx.NewConfig(

sphinx.HMMDirOption("/usr/local/share/pocketsphinx/model/en-us/en-us"),

sphinx.DictFileOption("6129.dic"),

sphinx.LMFileOption("6129.lm"),

sphinx.LogFileOption("commander.log"),

dec, err := sphinx.NewDecoder(cfg)

if err != nil {

panic(err)

listen(dec)

client.go hosted with ? by GitHub

(左右滑動查看全部代碼)

上述的代碼將把所有的依賴聲明與源文件綁定在一起。

如你所見,Go語言是如此的簡單、最小化但仍足夠滿足需要并且十分優雅。Go語言提供了諸多的直接的工具支持,既可用于單元測試,也可以用于benchmark的火焰圖。誠然,正如前面所講到的特性集方面的限制,Go語言也有其缺陷。比如, goget 并不支持版本化,一旦源文件中引用了某個URL,就將鎖定于此。但是,Go也還在逐漸的演進,一些依賴管理的工具也正在涌現。

Golang最初是設計用來解決Google的一些產品問題,比如厚重的代碼庫,以及滿足編寫高效并發類應用的急迫需求。在需要利用現代處理器的多核特性的場景,Go語言使得在應用和庫文件的編程方面變得更加容易。并且,這些都不需要開發者來考慮。Go語言是一門現代的編程語言,簡單是其主旨,Go語言永遠不會考慮超過這一主旨的范疇。

二、Protobuf(Protocol Buffers)

Protobuf 或者說 Protocol Buffers是由Google研發的一種二進制通信格式,用以對結構化數據進行序列化。格式是什么意思?類似于JSON這樣?是的。Protobuf已經有10年的歷史,在Google內部也已經使用了一段時間。

既然已經有了JSON這種通信格式,并且得到了廣泛的應用,為什么需要Protobuf?

與Golang一樣,Protobuf實際上并有解決任何新的問題,只是在解決現有的問題方面更加高效,更加現代化。與Golang不同的是,Protobuf并不一定比現存的解決方案更加優雅。下面是Protobuf的主要特性:

Protobuf是一種二進制格式,不同于JSON和XML,后者是基于文本的也因此相對比較節省空間。

Protobuf提供了對于schema的精巧而直接的支持

Protobuf為生成解析代碼和消費者代碼提供直接的多語言支持。

Protobuf的二進制格式帶來的是傳輸速度方面的優化

那么Protobuf是不是真的很快?簡單回答,是的。根據Google Developer的數據,相對于XML來說,Protobuf在體積上只有前者的1/3到1/10,在速度上卻要快20到100倍。毋庸置疑的是,由于采用了二進制格式,序列化的數據對于人類來說是不可讀的。

(Image Source: Beating JSON performance with Protobuf)

相對其他傳輸協議格式來說,Protobuf采用了更有規劃性的方式。首先需要定義 .proto 文件,這種文件與schema類似,但更強大。在 .proto 文件中定義消息結構,哪些字段是必選的哪些是可選的,以及字段的數據類型等。接下來,Protobuf編譯器會生成用于數據訪問的類,開發者可以在業務邏輯中使用這些類來更方便的進行數據傳輸。

觀察某個服務的 .proto 文件,可以清晰的獲知通信的細節以及暴露的特性。一個典型的 .proto 文件類似如下:

message Person {

required string name = 1;

required int32 id = 2;

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;

protobufExample.proto hosted with ? by GitHub

(左右滑動查看全部代碼)

曝個料:Stack Overflow的大牛Jon Skeet也是Protobuf項目的主要貢獻者之一。

三、gRPC

gRPC,物如其名,是一種功能齊備的現代的RPC框架,提供了諸多內置支持的機制,如負載均衡、跟蹤、健康檢查和認證等。gRPC由Google在2015年開源,并由此日益火爆。

既然已經有了REST,還搞個RPC做什么?

在SOA架構的時代,有相當長的時間,基于WSDL的SOAP協議是系統間通信的解決方案。彼時,通信協議的定義是十分嚴格的,龐大的單體架構系統暴露大量的接口用于擴展。

隨著B/S理念的興起,服務器和客戶端開始解耦,在這樣的架構下,即使客戶端和服務端分別進行獨立的編碼,也不影響對服務的調用。客戶端想查詢一本書的信息,服務端會根據請求提供相關的列表供客戶端瀏覽。REST范式主要解決的就是這種場景下的問題,REST允許服務端和客戶端可以自由的通信,而不需要定義嚴格的契約以及獨有的語義。

從某種意義上講,此時的服務已經開始變得像是單體式架構系統一樣,對于某個特定的請求,會返回一坨毫無必要的數據,用以滿足客戶端的“瀏覽”需求。但這并不是所有場景下都會發生的情況,不是么?

跨入微服務的時代

采用微服務架構理由多多。最常提及的事實是,單體架構太難擴展了。以微服務架構設計大型系統,所有的業務和技術需求都傾向于實現成互相合作的組件,這些組件就是“微”服務。

微服務不需要以包羅萬象的信息響應用戶請求,而僅需要根據請求完成特定的任務并給出所需要的回應。理想情況下,微服務應該像一堆可以無縫組裝的函數。

使用REST做為此類服務的通信范式變得不那么有效。一方面,采用REST API確實可以讓服務的表達能力更強,但同時,如果這種表達的能力既非必要也并不出自設計者的本意,我們就需要根據不同的因素考慮其他范式了。

gRPC嘗試在如下的技術方面改進傳統的HTTP請求:

默認支持HTTP/2協議,并可以享受該協議帶來的所有好處

采用Protobuf格式用于機器間通信

得益于HTTP/2協議,提供了對流式調用的專有支持

對所有常用的功能提供了插件化的支持,如認證、跟蹤、負載均衡和健康檢查等。

當然,既然是RPC框架,仍舊會有服務定義和接口描述語言(DSL)的相關概念,REST世代的開發者可能會感覺這些概念有些格格不入,但是由于gRPC采用Protobuf做為通信格式,就不會顯得像以前那么笨拙。

Protobuf的設計理念使得其既是一種通信格式,又可以是一種協議規范工具,在此過程中無需做任何額外的工作。一個典型的gRPC服務定義類似如下:

service HelloService {

rpc SayHello (HelloRequest) returns (HelloResponse);

message HelloRequest {

string greeting = 1;

message HelloResponse {

string reply = 1;

serviceDefinition.proto hosted with ? by GitHub

(左右滑動查看全部代碼)

只需要為服務定義一個 .proto 文件,并在其中描述接口名稱,服務的需求,以及以Protobuf格式返回的消息即可。Protobuf編譯器會生成客戶端和服務端代碼。客戶端可以直接調用這些代碼,服務端可以用這些代碼實現API來填充業務邏輯。

四、結語

Golang,Protobuf和gRPC是現代服務端編程的后起之秀。Golang簡化了并發/并行應用的編程,gRPC和Protobuf的結合提供了更高效的通信并同時提供了更愉悅的開發者體驗。

審核編輯:符乾江
聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 代碼
    +關注

    關注

    30

    文章

    4825

    瀏覽量

    69043
  • 服務端
    +關注

    關注

    0

    文章

    66

    瀏覽量

    7056
收藏 人收藏

    評論

    相關推薦

    SSR與微服務架構的結合應用

    隨著互聯網技術的快速發展,前端技術不斷更新迭代,后端架構也經歷了從單體應用到微服務的變革。在這個過程中,服務端渲染(SSR)作為一種提升頁
    的頭像 發表于 11-18 11:34 ?396次閱讀

    【米爾NXP i.MX 93開發板試用評測】4、使用golang搭建Modbus 服務

    負責處理來自客戶(通常稱為Modbus客戶或從站)的請求,并根據請求提供相應的數據或執行操作。 快速開發modbus服務器 可以使用golang快速部署一個modbus
    發表于 09-21 22:51

    調試stm32的TCP服務端程序,會導致hard fault的原因?

    最近調試stm32的TCP服務端程序,遇到了想不明白的問題,具體過程如下: 使用的是rt-thread4.1.0的內核; 通過串口和esp32-c3通信; 啟用at_socket; 建立tcp
    發表于 09-13 06:59

    軟通動力數據庫全服務,助力企業數據庫體系全面升級

    。在企業節與"數博會"展區,軟通動力受邀分享數據庫專業服務解決方案,并重點展示以全服務為核心的數智化能力。 軟通動力高級數據庫服務
    的頭像 發表于 09-05 15:30 ?379次閱讀
    軟通動力數據庫全<b class='flag-5'>棧</b><b class='flag-5'>服務</b>,助力企業數據庫體系全面升級

    使用NS1串口服務器HTTP模式上傳服務器數據

    HTTP協議工作于客戶-服務端架構之上。瀏覽器作為HTTP客戶通過URL向HTTP服務端即Web服務器發送所有請求。Web
    的頭像 發表于 08-30 12:36 ?479次閱讀
    使用NS1串口<b class='flag-5'>服務</b>器HTTP模式上傳<b class='flag-5'>服務</b>器數據

    LwIP協議源碼詳解—TCP/IP協議的實現

    電子發燒友網站提供《LwIP協議源碼詳解—TCP/IP協議的實現.pdf》資料免費下載
    發表于 07-03 11:22 ?3次下載

    請問ESP32作為藍牙服務端如何修改MTU?

    我們的工程把esp32當作藍牙服務端讓電腦去連,由于一些老電腦上沒有藍牙,要用外置藍牙驅動,默認MTU只有23,但是說明上驅動是支持最大mtu的,所以有什么辦法可以通過服務端去修改mtu嗎
    發表于 06-27 07:47

    請問esp_local_ctrl中服務端如何主動發消息?

    請問,在wifi本地控制例程esp_local_ctrl中,設備作為服務端在客戶沒有請求的情況下,如何主動發送消息給客戶呢?
    發表于 06-06 06:11

    服務端測試包括什么類型

    服務端測試是確保軟件系統在服務器端正常運行和滿足性能要求的重要環節。本文將詳細介紹服務端測試的類型、方法和最佳實踐。 1. 服務端測試的定義 服務端
    的頭像 發表于 05-30 16:03 ?855次閱讀

    服務端測試是web測試嗎為什么

    服務端測試和Web測試是兩個不同的概念,但它們在軟件開發和測試過程中是相互關聯的。本文將詳細解釋這兩個概念以及它們之間的關系。 服務端測試 服務端測試主要關注服務器端的軟件組件,這些組
    的頭像 發表于 05-30 15:30 ?689次閱讀

    服務端測試和客戶測試區別在哪

    服務端測試和客戶測試是軟件開發過程中的兩個重要環節,它們分別針對服務器端和客戶的軟件進行測試。本文將詳細介紹服務端測試和客戶
    的頭像 發表于 05-30 15:27 ?3419次閱讀

    服務端的測試主要是測什么內容

    服務端測試是軟件開發過程中的一個重要環節,主要目的是確保服務端程序的穩定性、性能、安全性和可靠性。 功能測試 功能測試是服務端測試的基礎,主要驗證服務端程序是否按照需求實現了所有功能。
    的頭像 發表于 05-30 15:24 ?4280次閱讀

    智行者聯合清華完成國內首套全自動駕駛系統的開放道路測試

    近日,智行者與清華大學車輛學院李克強院士、李升波教授領導的研究團隊,完成了國內首套全自動駕駛系統的開放道路測試。
    的頭像 發表于 04-22 09:24 ?840次閱讀
    智行者聯合清華完成國內首套全<b class='flag-5'>棧</b>式<b class='flag-5'>端</b>到<b class='flag-5'>端</b>自動駕駛系統的開放道路測試

    PLC采用HTTP協議JSON文件對接MES等服務系統平臺

    文件的字段與PLC寄存器地址,配置URL即可。支持POST/GET/PUT等多種方法。智能網關IGT-DSER可同時作為HTTP協議的客戶服務端。作為客戶通訊時將JSON文件提交給HTTP
    發表于 03-25 14:25

    lwip stm407作為服務端 pc連接不上怎么解決?

    lwip stm407作為服務端 pc連接不上
    發表于 03-20 06:32
    大发888娱乐85战神版| 沙龙百家乐官网娱乐场| 澳门百家乐游戏皇冠网| 大发888娱乐城 真钱| 亚洲百家乐官网论坛| 龍城百家乐的玩法技巧和规则 | 顶级赌场 官方直营网| 澳门百家乐官网必赢技巧| 现场百家乐的玩法技巧和规则| 沙龙百家乐官网破解| 作弊百家乐赌具| 百家乐官网投注助手| 百家乐和| 百家乐官网足球投注网哪个平台网址测速最好 | 百家乐官网海滨网现场| 大发888官方网址| 百家乐官网保单机作弊| 大发888官方免费下载| 红桃K百家乐官网的玩法技巧和规则| 云鼎娱乐场网址| 百家乐官网群dmwd| 白凤凰博彩通| 百家乐玩揽法的论坛| 真人百家乐官网导航| 太阳城线上娱乐城| 优惠搏百家乐官网的玩法技巧和规则 | 百家乐官网不倒翁注码| 百家乐黏土筹码| 电子百家乐官网技巧| 金沙国际娱乐城| 百家乐娱乐城备用网址| 澳门百家乐官网娱乐开户| 大发8887s88| 百家乐下注稳赢法| 百家乐官网怎么注册| 威尼斯人娱乐网注册送38元彩金 | 百家乐牌机的破解法| 百家乐官网稳中一注法| 大发888娱乐代理| 七乐百家乐现金网| 百家乐官网庄闲筹码|