标签 unmarshal 下的文章

go protobuf v1败给了gogo protobuf,那v2呢?

近期的一个项目有对结构化数据进行序列化和反序列化的需求,该项目具有performance critical属性,因此我们在选择序列化库包时是要考虑包的性能的。

github上有一个有关Go序列化方法性能比较的repo:go_serialization_benchmarks,这个repo横向比较了数十种数据序列化方法的正确性、性能、内存分配等,并给出了一个结论:推荐gogo protobuf。对于这样一个粗选的结果,我们是直接笑纳的^_^。接下来就是进一步对gogo protobuf做进一步探究。

img{512x368}

一. go protobuf v1 vs. gogo protobuf

gogo protobuf是既go protobuf官方api之外的另一个go protobuf的api实现,它兼容go官方protobuf api(更准确的说是v1版本)。gogo protobuf提供了三种代码生成方式:protoc-gen-gogofast、protoc-gen-gogofaster和protoc-gen-gogoslick。究竟选择哪一个呢?这里我也写了一些benchmark来比较,并顺便将官方go protobuf api也一并加入比较了。

我们首先安装一下gogo protobuf实现的protoc的三个插件,用于生成proto文件对应的Go包源码文件:

go get github.com/gogo/protobuf/protoc-gen-gofast
go get github.com/gogo/protobuf/protoc-gen-gogofaster

go get github.com/gogo/protobuf/protoc-gen-gogoslick

安装后,我们在$GOPATH/bin下将看到这三个文件(protoc-gen-go是go protobuf官方实现的代码生成插件):

$ls -l $GOPATH/bin|grep proto
-rwxr-xr-x   1 tonybai  staff   6252344  4 24 14:43 protoc-gen-go*
-rwxr-xr-x   1 tonybai  staff   9371384  2 28 09:35 protoc-gen-gofast*
-rwxr-xr-x   1 tonybai  staff   9376152  2 28 09:40 protoc-gen-gogofaster*
-rwxr-xr-x   1 tonybai  staff   9380728  2 28 09:40 protoc-gen-gogoslick*

为了对采用不同插件生成的数据序列化和反序列化方法进行性能基准测试,我们建立了下面repo。在repo中,每一种方法生成的代码放入独立的module中:

$tree -L 2 -F
.
├── IDL/
│   └── submit.proto
├── Makefile
├── gogoprotobuf-fast/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
├── gogoprotobuf-faster/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
├── gogoprotobuf-slick/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
└── goprotobuf/
    ├── go.mod
    ├── go.sum
    ├── submit/
    └── submit_test.go

我们的proto文件如下:

$cat IDL/submit.proto
syntax = "proto3";

option go_package = ".;submit";

package submit;

message request {
        int64 recvtime = 1;
        string uniqueid = 2;
        string token = 3;
        string phone = 4;
        string content = 5;
        string sign = 6;
        string type = 7;
        string extend = 8;
        string version = 9;
}

我们还建立了Makefile,用于简化操作:

$cat Makefile

gen-protobuf: gen-goprotobuf gen-gogoprotobuf-fast gen-gogoprotobuf-faster gen-gogoprotobuf-slick

gen-goprotobuf:
    protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit

gen-gogoprotobuf-fast:
    protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit

gen-gogoprotobuf-faster:
    protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit

gen-gogoprotobuf-slick:
    protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

benchmark: goprotobuf-bench gogoprotobuf-fast-bench gogoprotobuf-faster-bench  gogoprotobuf-slick-bench

goprotobuf-bench:
    cd goprotobuf && go test -bench .

gogoprotobuf-fast-bench:
    cd gogoprotobuf-fast && go test -bench .

gogoprotobuf-faster-bench:
    cd gogoprotobuf-faster && go test -bench .

gogoprotobuf-slick-bench:
    cd gogoprotobuf-slick && go test -bench .

针对每一种方法,我们建立一个benchmark test。benchmark test代码都是一样的,我们以gogoprotobuf-fast为例:

// submit_test.go

package protobufbench

import (
    "fmt"
    "os"
    "testing"

    "github.com/bigwhite/protobufbench_gogoprotofast/submit"
    "github.com/gogo/protobuf/proto"
)

var request = submit.Request{
    Recvtime: 170123456,
    Uniqueid: "a1b2c3d4e5f6g7h8i9",
    Token:    "xxxx-1111-yyyy-2222-zzzz-3333",
    Phone:    "13900010002",
    Content:  "Customizing the fields of the messages to be the fields that you actually want to use removes the need to copy between the structs you use and structs you use to serialize. gogoprotobuf also offers more serialization formats and generation of tests and even more methods.",
    Sign:     "tonybaiXZYDFDS",
    Type:     "submit",
    Extend:   "",
    Version:  "v1.0.0",
}

var requestToUnMarshal []byte

func init() {
    var err error
    requestToUnMarshal, err = proto.Marshal(&request)
    if err != nil {
        fmt.Printf("marshal err:%s\n", err)
        os.Exit(1)
    }
}

func BenchmarkMarshal(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _, _ = proto.Marshal(&request)
    }
}
func BenchmarkUnmarshal(b *testing.B) {
    b.ReportAllocs()
    var request submit.Request
    for i := 0; i < b.N; i++ {
        _ = proto.Unmarshal(requestToUnMarshal, &request)

    }
}

func BenchmarkMarshalInParalell(b *testing.B) {
    b.ReportAllocs()

    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _, _ = proto.Marshal(&request)
        }
    })
}
func BenchmarkUnmarshalParalell(b *testing.B) {
    b.ReportAllocs()
    var request submit.Request
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _ = proto.Unmarshal(requestToUnMarshal, &request)
        }
    })
}

我们看到,对每种方法生成的代码,我们都会进行顺序和并行的marshal和unmarshal基准测试。

我们首先分别使用不同方式生成对应的go代码:

$make gen-protobuf
protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit
protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit
protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit
protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

然后运行基准测试(使用macos上的go 1.14):

$make benchmark
cd goprotobuf && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goproto
BenchmarkMarshal-8                  2437068           483 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                2262229           529 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7592120           162 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        5306744           225 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goproto    6.239s
cd gogoprotobuf-fast && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotofast
BenchmarkMarshal-8                  7186828           164 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4706794           251 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15107896            83.0 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6258507           179 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotofast    5.449s
cd gogoprotobuf-faster && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotofaster
BenchmarkMarshal-8                  7036842           166 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4666698           256 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15444961            83.2 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6936337           202 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotofaster    5.750s
cd gogoprotobuf-slick && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotoslick
BenchmarkMarshal-8                  6529311           176 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4737463           252 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15700746            81.8 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6528390           202 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotoslick    5.668s

在我的macpro(4核8线程)上,我们看到两点结论:

  • 官方go protobuf实现生成的代码性能的确弱于gogo protobuf生成的代码,在顺序测试中,差距还较大;

  • 针对我预置的proto文件中数据格式,gogo protobuf的三种生成方法产生的代码的性能差异并不大,选择protoc-gen-gofast生成的代码在性能上即可满足。

二. go protobuf v2

今年三月份初,Go官方发布了protobuf的新API版本,这个版本与原go protobuf并不兼容。新版API旨在使protobuf的类型系统与go类型系统充分融合,提供反射功能和自定义消息实现。那么该版本生成的序列/反序列化代码在性能上有提升吗?我们将其加入我们的benchmark。

我们先下载go protobuf v2的代码生成插件(注意:由于go protobuf v1和go protobuf v2的插件名称相同,需要先备份好原先已经安装的protoc-gen-go):

$  go get google.golang.org/protobuf/cmd/protoc-gen-go
go: found google.golang.org/protobuf/cmd/protoc-gen-go in google.golang.org/protobuf v1.21.0

然后将新安装的插件名称改为protoc-gen-gov2,这样$GOPATH/bin下的插件文件列表如下:

$ls -l $GOPATH/bin/|grep proto
-rwxr-xr-x   1 tonybai  staff   6252344  4 24 14:43 protoc-gen-go*
-rwxr-xr-x   1 tonybai  staff   9371384  2 28 09:35 protoc-gen-gofast*
-rwxr-xr-x   1 tonybai  staff   9376152  2 28 09:40 protoc-gen-gogofaster*
-rwxr-xr-x   1 tonybai  staff   9380728  2 28 09:40 protoc-gen-gogoslick*
-rwxr-xr-x   1 tonybai  staff   8716064  4 24 14:56 protoc-gen-gov2*

在Makefile中增加针对go protobuf v2的代码生成和Benchmark target:

gen-goprotobufv2:
        protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit

goprotobufv2-bench:
        cd goprotobufv2 && go test -bench .

由于go protobuf v2与v1版本不兼容,因此也无法与gogo protobuf兼容,我们需要修改一下go protobuf v2对应的submit_test.go,将导入的“github.com/gogo/protobuf/proto”包换为“google.golang.org/protobuf/proto”

重新生成代码:

$make gen-protobuf
protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit
protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit
protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit
protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit
protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

运行benchmark:

$make benchmark
cd goprotobuf && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goproto
BenchmarkMarshal-8                  2420620           485 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                2186240           538 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7334412           162 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        4537429           222 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goproto    6.052s
cd goprotobufv2 && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goprotov2
BenchmarkMarshal-8                  2404473           506 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                1901947           626 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        6629139           171 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8       panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x11d4956]

goroutine 196 [running]:
google.golang.org/protobuf/internal/impl.(*messageState).protoUnwrap(0xc00007e210, 0xc000010360, 0xc00008ce01)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:27 +0x26
google.golang.org/protobuf/internal/impl.(*messageState).Interface(0xc00007e210, 0xc00007e210, 0xc00012c000)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:24 +0x2b
google.golang.org/protobuf/proto.UnmarshalOptions.unmarshal(0x0, 0x12acc00, 0xc000010360, 0xc00012c000, 0x177, 0x177, 0x12b23e0, 0xc00007e210, 0xc000200001, 0x0, ...)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:71 +0x2c5
google.golang.org/protobuf/proto.Unmarshal(0xc00012c000, 0x177, 0x177, 0x12ac180, 0xc00007e210, 0x0, 0x0)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:48 +0x89
github.com/bigwhite/protobufbench_goprotov2.BenchmarkUnmarshalParalell.func1(0xc0004a8000)
    /Users/tonybai/test/go/protobuf/goprotobufv2/submit_test.go:65 +0x6a
testing.(*B).RunParallel.func1(0xc0000161b0, 0xc0000161a8, 0xc0000161a0, 0xc00010c700, 0xc00004a000)
    /Users/tonybai/.bin/go1.14/src/testing/benchmark.go:763 +0x99
created by testing.(*B).RunParallel
    /Users/tonybai/.bin/go1.14/src/testing/benchmark.go:756 +0x192
exit status 2
FAIL    github.com/bigwhite/protobufbench_goprotov2    4.878s
make: *** [goprotobufv2-bench] Error 1

我们看到go protobuf v2并未完成所有benchmark test,在运行并行unmarshal测试中panic了。目前go protobuf v2官方并未在github开通issue,因此尚不知道哪里去提issue。于是回到test代码,再仔细看一下submit_test.go中 BenchmarkUnmarshalParalell的代码:

func BenchmarkUnmarshalParalell(b *testing.B) {
        b.ReportAllocs()
        var request submit.Request
        b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                        _ = proto.Unmarshal(requestToUnMarshal, &request)
                }
        })
}

这里存在一个“问题”,那就是多goroutine会共享一个request。但在其他几个测试中同样的代码并未引发panic。我修改一下代码,将其放入for循环中:

func BenchmarkUnmarshalParalell(b *testing.B) {
        b.ReportAllocs()
        b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                        var request submit.Request
                        _ = proto.Unmarshal(requestToUnMarshal, &request)
                }
        })
}

再运行go protobuf v2的benchmark:

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goprotov2
BenchmarkMarshal-8                  2348630           509 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                1913904           627 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7133936           175 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        4841752           232 ns/op         576 B/op           8 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goprotov2    6.355s

看来的确是这个问题。

从Benchmark结果来看,即便是与go protobuf v1相比,go protobuf v2生成的代码性能也要逊色一些,更不要说与gogo protobuf相比了。

三. 小结

从性能角度考虑,如果要使用go protobuf api,首选gogo protobuf。

如果从功能角度考虑,显然go protobuf v2在成熟稳定了以后,会成为Go语言功能上最为强大的protobuf API。

本文涉及源码可以在这里下载。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go与SOAP

RESTRPC大行其道的今天,支持SOAP(简答对象访问协议)作为Web服务消息交换协议的情况是越来越少了。但在一些遗留系统中,尤其是采用微软技术栈的服务系统中,SOAP依然占有一席之地,比如在一些医院院内的IT系统中。

Go语言诞生后,主流的Web Service设计已经开始过渡到REST和RPC,Go相关开源项目也以对REST和RPC的支持为主。而对SOAP的支持则少而零散,社区里也没有对SOAP支持的重量级开源项目,在awesome go的各种list中也难觅有关SOAP的推荐项目的身影。

但Gopher世界还是有以client身份与SOAP service交互或是实现SOAP server的需求的。在这篇文章中,我就和大家一起来探索一下如何基于一些开源项目,使用Go实现SOAP client和SOAP Server的。

一. SOAP简介

如果你觉得SOAP这个协议很陌生也不奇怪,因为SOAP协议诞生于“遥远”的1998年,2000年才提交到标准化组织。SOAP是一种消息传递协议规范,用于在计算机网络的Web服务中实现交换结构化信息。其目的是促进可扩展性、中立性和独立性。它使用XML作为承载消息的格式,并依赖于应用层协议,通常是HTTP或SMTP(简单邮件传输协议),用于消息协商和传输。经过若干年的演进,其主要binding的协议是http,其支持SMTP Binding已经极少有应用了。现在,我们可以不严谨的说,SOAP可以理解为“xml over http”。并且从SOAP Body的形式来看,SOAP也像是一种使用XML作为序列化编码格式的RPC调用。

SOAP目前存在两个版本:1.1和1.2版本。一些比较old的SOAP服务仅支持1.1版本,而一些新的SOAP服务则两个版本都支持。

下面是SOAP协议的通用结构:

img{512x368}

基于这个结构,我们看看SOAP(over http)的Request和Response的样子:
img{512x368}

img{512x368}

关于SOAP协议的更多细节,可以参见SOAP协议规范,这里限于篇幅就不细说了。

二. 环境准备

本文中使用的Go语言版本为go 1.11.2

1. 获取wsdl文件

现在在互联网上要找到一个面向公共的、免费的SOAP服务着实困难。free-web-services.com上的很多服务已经不提供SOAP服务了,并且多数提供SOAP的服务也已经打不开页面了。在本文中,我们将使用www.dneonline.com/calculator.asmx这个calculator服务,至少目前它还是ready的(不过也不保证它在将来能一直ready)。

我们可以通过下面命令获得这个calculator服务的WSDL文件。

$cd /Users/tony/go/src/github.com/bigwhite/experiments/go-soap/pkg

$curl http://www.dneonline.com/calculator.asmx\?WSDL > calculator.wsdl

$cat calculator.wsdl

<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://tempuri.org/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" targetNamespace="http://tempuri.org/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">

... ...

  <wsdl:service name="Calculator">
    <wsdl:port name="CalculatorSoap" binding="tns:CalculatorSoap">
      <soap:address location="http://www.dneonline.com/calculator.asmx" />
    </wsdl:port>
    <wsdl:port name="CalculatorSoap12" binding="tns:CalculatorSoap12">
      <soap12:address location="http://www.dneonline.com/calculator.asmx" />
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

这个calculator.wsdl是后续实现soap client和soap server的基础。

2. 根据wsdl文件生成SOAP package

虽然Go语言标准库中拥有比较完善的XML操作package,但是我们也没有必要从头开始进行SOAP协议的封装和解包。github上面的gowsdl项目可以帮助我们基于calculator.wsdl自动生成实现SOAP Client和SOAP Server所要使用的各种方法和结构体,这也是我们后续实现SOAP Client和SOAP Server的基本原理。

$go get github.com/hooklift/gowsdl/...

$gowsdl -i calculator.wsdl
Reading file /Users/tony/go/src/github.com/bigwhite/experiments/go-soap/pkg/calculator.wsdl

Done

$tree
.
├── calculator.wsdl
└── myservice
    └── myservice.go

1 directory, 2 files

gowsdl根据calculator.wsdl生成了myservice.go,所有有关calculator soap service的结构体和方法都在这个Go源文件中。有了这个package,我们就可以来实现soap客户端了。

三. 实现SOAP客户端

我们实现一个SOAP客户端,用于调用www.dneonline.com/calculator服务中提供的Add方法来进行加法计算。

我们先在$GOPATH/src/github.com/bigwhite/experiments/go-soap下面建立client目录,进入client目录,创建client的main.go文件。

在前面根据calculator.wsdl生成的myservice.go文件中,我们找到了NewCalculatorSoap方法,该方法会返回一个到对应服务的client实例,通过该soap client实例,我们可以调用其包含的Add方法,我们来看一下main.go中的代码实现:

package main

import (
    "fmt"

    soap "github.com/bigwhite/experiments/go-soap/pkg/myservice"
)

func main() {
    c := soap.NewCalculatorSoap("", false, nil)
    r, err := c.Add(&soap.Add{
        IntA: 2,
        IntB: 3,
    })

    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(r.AddResult)
}

Add方法的参数为soap.Add结构体的实例,Add结构有两个字段IntA和IntB,分别代表了两个加数。我们来执行一下该client实现:

$go run main.go
2019/01/08 12:54:31 <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Body xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Add xmlns="http://tempuri.org/"><intA>2</intA><intB>3</intB></Add></Body></Envelope>
2019/01/08 12:54:31 <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><AddResponse xmlns="http://tempuri.org/"><AddResult>5</AddResult></AddResponse></soap:Body></soap:Envelope>
5

我们看到client输出了加法服务调用后的正确结果:5

四. 实现SOAP服务

下面我们再来实现一个类似www.dneonline.com/calculator的服务,由于只是demo,我们只实现Add方法,其他方法的“套路”是一样的。

我们在$GOPATH/src/github.com/bigwhite/experiments/go-soap下面建立server目录,进入server目录,创建server的main.go文件。pkg/myservice/myservice.go中只是SOAP层协议数据负荷的marshal和unmarshal操作,并没有网络层面的支持,因此我们需要自己建立http server框架,我们就使用Go标准库http server。代码结构如下:

package main

import (
    "encoding/xml"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "regexp"

    soap "github.com/bigwhite/experiments/go-soap/pkg/myservice"
)

func main() {
    s := NewSOAPServer("localhost:8080")
    log.Fatal(s.ListenAndServe())
}

func NewSOAPMux() *http.ServeMux {
    mux := http.NewServeMux()
    mux.HandleFunc("/", soapHandler)
    return mux
}

func NewSOAPServer(addr string) *http.Server {
    mux := NewSOAPMux()
    server := &http.Server{
        Handler: mux,
        Addr:    addr,
    }
    return server
}

func soapHandler(w http.ResponseWriter, r *http.Request) {

   ... ...

}

这个SOAP server的外层结构与普通http server并无太多差异。我们重点要关注的是soapHandler的实现逻辑。

func soapHandler(w http.ResponseWriter, r *http.Request) {
    rawBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    // match method
    var res interface{}
    m := regexp.MustCompile(`<Add xmlns=`)
    if m.MatchString(string(rawBody)) {
        res = processAdd(rawBody)
    } else {
        res = nil
        fmt.Println("the method requested is not available")
    }

    v := soap.SOAPEnvelope{
        Body: soap.SOAPBody{
            Content: res,
        },
    }
    w.Header().Set("Content-Type", "text/xml")
    x, err := xml.MarshalIndent(v, "", "  ")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(x)
    return
}

我们看到:

首先,我们从http body中读取出原始数据;

接下来,我们通过一个正则表达式去匹配原始数据,如果匹配到方法,则进入方法的处理函数processAdd;否则提示方法不存在;

最后将processAdd的返回结果marshall为SOAP格式后,返回给client端。

processAdd是真正执行服务算法的函数:

func processAdd(body []byte) *soap.AddResponse {
    envlop := &soap.SOAPEnvelope{
        Body: soap.SOAPBody{
            Content: &soap.Add{},
        },
    }
    err := xml.Unmarshal(body, envlop)
    if err != nil {
        fmt.Println("xml Unmarshal error:", err)
        return nil
    }

    fmt.Println(envlop.Body.Content)

    r, ok := envlop.Body.Content.(*soap.Add)
    if !ok {
        return nil
    } else {
        return &soap.AddResponse{
            AddResult: r.IntA + r.IntB,
        }
    }
}

processAdd首先将rawBody unmarshal到一个SOAPEnvelope结构体中,从而得到SOAP envelope中Body中的方法的输入参数的值:IntA和IntB。将两个加数的和赋值给AddResult,作为AddResponse的值返回。

我们启动一下该SOAP server,并修改一下前面client所要连接的soap server的地址,并让client向我们自己实现的soap server发起服务请求调用:

1.修改client main.go

$GOPATH/src/github.com/bigwhite/experiments/go-soap/client/main.go

    //c := soap.NewCalculatorSoap("", false, nil)
    c := soap.NewCalculatorSoap("http://localhost:8080/", false, nil)

2.启动soap server

$GOPATH/src/github.com/bigwhite/experiments/go-soap/server git:(master) ✗ $go run main.go

3. 启动client

$go run main.go
2019/01/08 14:55:20 <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Body xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Add xmlns="http://tempuri.org/"><intA>2</intA><intB>3</intB></Add></Body></Envelope>
2019/01/08 14:55:20 <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
  <Body xmlns="http://schemas.xmlsoap.org/soap/envelope/">
    <AddResponse xmlns="http://tempuri.org/">
      <AddResult>5</AddResult>
    </AddResponse>
  </Body>
</Envelope>
5

4. server console输出

&{{http://tempuri.org/ Add} 2 3}

我们看到,我们的client成功调用了我们实现的SOAP Server的服务方法,并获得了正确的结果。一个简单的SOAP server就这么实现了。不过明眼的朋友肯定已经看出代码中的问题了,那就是method match那块仅仅适用于demo,在真正的服务中,服务会有很多method,我们需要一种规范的、通用的匹配机制,一种可以通过SOAP Body匹配来做,另一种可以通过http header中的SOAP Action 来匹配(仅适用于SOAP 1.1,SOAP 1.2版本去掉了SOAP Action)。这里就留给大家自己去发挥吧。

五. 小结

如果不是碰到了基于SOAP的遗留系统,我想我是不会研究SOAP的,毕竟基于SOAP的系统正在逐渐消逝在大众的视野中。上面的demo应该可以让大家对如何用Go与SOAP系统交互有了一个粗浅的认识。这里算是一个不错的起点。如果大家对于Go与SOAP有更深刻地研究或者有更好的有关SOAP的开源项目,欢迎交流。

文中源码在这里可以找到。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我要发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats