Keepdata 開発Divの瀧田です。
開発担当としてそれっぽい記事を書こうと思い、 今回はREST APIと通信する際に、 普段何気なく使っているデータフォーマットである XML、JSON、Protocol Buffersの3つについて、 通信データのサイズとパース速度を比較してみました。
各データフォーマットの特徴
XML(1998〜)
- ・マークアップ言語であり、データ型記述言語ではない。
- ・3つの中では1番登場が早い。
- ・ウェブの通信フォーマット以外にも様々な場面で使われる。
- ・SOAP APIでも使える。
- ・ありとあらゆる言語で扱える。
- ・API公開した際に、利用する側の実装のハードルが低い
JSON(2006〜)
- ・登場してから急速に普及したデータ型記述言語。
- ・構造がシンプルで人間にもコンピュータにも分かりやすい。
- ・ありとあらゆる言語で扱える。
- ・API公開した際に、利用する側の実装のハードルが低い
Protocol Buffers(2008〜)
- ・Google製のデータ型記述言語
- ・データを文字列ではなくバイナリで送受信する。
- ・.proto(構造を定義する)ファイルが必要など、事前準備が多い。
- ・XMLやJSONに比べると扱える言語が少ない。
- ・APIを公開した際に、利用する側の実装のハードルが高い。
Protocol Buffersを利用する準備
先ほども書きましたが、使う為には多くの準備が必要となります。
ステップ1 protocコマンドのインストール
protocコマンドを使えるようにするため、公式サイトから、
自分の環境に合わせたコンパイル済みのバイナリを取得します。
下記のコマンドを全てインストールします。 githubからソースを取得します。 次にクローンしたディレクトリに移動して、下記のコマンドを順番に実行します。 ここ先ほどインストールしたコマンドの中に足りないものがあるとエラーが出ます。 最後にmake installを行います。 ./configureを実行すると、デフォルトでは/usr/localの配下にインストールされますが、
他の場所にしたい方は、--prefix=/path/toをオプションで指定し、
任意のディレクトリにインストール先を変えてください。ソースからmakeインストールする場合はこちら
$ git clone https://github.com/google/protobuf.git
$ cd protobuf
$ git submodule update --init --recursive
$ ./autogen.sh
$ ./configure
$ make
$ make check
$ sudo make install
$ sudo ldconfig
取得したprotocのバイナリを任意のディレクトリに配置してインストール完了です。
私の環境では、/usr/local/binの配下にprotocコマンドをインストールしました。
ステップ2 Go言語用のgenerateソースファイルを取得
今回はGo言語で実装するので、Go言語用のprotocol buffersを生成するソースをgithubから取得します。
$ go get -u github.com/golang/protobuf/proto $ go get -u github.com/golang/protobuf/protoc-gen-go
また、protoc-gen-goは先にインストールしたprotocと同じディレクトリにある必要があります。 したがって、protoc-gen-goを/usr/local/binの下にコピーします。
$ cp $GOPATH/bin/protoc-gen-go /usr/local/bin/
これで準備が整いました。あとは通信するデータの構造に合わせて、 protocol buffersの定義ファイルを書いてビルドするだけです。
サンプルデータの用意
今回はテスト用のデータをこちらのopenweathermap.orgから拝借し、 下記のサンプルを作成しました。
作成したサンプルはレスポンスデータとしてファイルを読み込み利用するため、 サーバ側に配置しています。
server/data/data.json
{
"city": {
"coord": {
"lat": 44.549999,
"lon": 34.283333
},
"country": "UA",
"id": 707860,
"name": "Hurzuf"
},
"data": [
{
"clouds": 53,
"deg": 44,
"dt": 1489395600,
"humidity": 0,
"pressure": 1006.85,
"rain": 0.43,
"speed": 3.2,
"temp": {
"day": 285.33,
"eve": 280.94,
"max": 285.33,
"min": 277.97,
"morn": 277.97,
"night": 279.36
},
"uvi": 3.09,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 76,
"deg": 60,
"dt": 1489482000,
"humidity": 83,
"pressure": 991.08,
"rain": 0.95,
"speed": 4.13,
"temp": {
"day": 281.66,
"eve": 279.71,
"max": 281.9,
"min": 277.1,
"morn": 277.28,
"night": 277.1
},
"uvi": 3.19,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 80,
"deg": 41,
"dt": 1489568400,
"humidity": 94,
"pressure": 993.96,
"rain": 0.25,
"speed": 3.76,
"temp": {
"day": 278.97,
"eve": 279.13,
"max": 279.95,
"min": 275.65,
"morn": 275.93,
"night": 275.65
},
"uvi": 3.01,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 92,
"deg": 244,
"dt": 1489654800,
"humidity": 97,
"pressure": 990.04,
"rain": 3.17,
"speed": 3.51,
"temp": {
"day": 279.79,
"eve": 278.73,
"max": 279.79,
"min": 276.01,
"morn": 276.86,
"night": 276.01
},
"uvi": 3.2,
"weather": [
{
"description": "moderate rain",
"icon": "10d",
"id": 501,
"main": "Rain"
}
]
},
{
"clouds": 30,
"deg": 317,
"dt": 1489741200,
"humidity": 0,
"pressure": 1012.47,
"rain": 0.33,
"speed": 2.05,
"temp": {
"day": 280.8,
"eve": 276.71,
"max": 280.8,
"min": 272.36,
"morn": 277.43,
"night": 272.36
},
"uvi": 2.79,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 21,
"deg": 242,
"dt": 1489827600,
"humidity": 0,
"pressure": 1010.58,
"rain": 0.48,
"speed": 2.29,
"temp": {
"day": 281.5,
"eve": 276.26,
"max": 281.5,
"min": 272.69,
"morn": 276.57,
"night": 272.69
},
"uvi": 3.36,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 1,
"deg": 166,
"dt": 1489914000,
"humidity": 0,
"pressure": 1008.61,
"speed": 2.27,
"temp": {
"day": 283.87,
"eve": 277.1,
"max": 283.87,
"min": 273.73,
"morn": 277.3,
"night": 273.73
},
"uvi": 3.3,
"weather": [
{
"description": "sky is clear",
"icon": "01d",
"id": 800,
"main": "Clear"
}
]
},
{
"clouds": 34,
"deg": 154,
"dt": 1490000400,
"humidity": 0,
"pressure": 1002.01,
"rain": 1.9,
"speed": 4.36,
"temp": {
"day": 285.22,
"eve": 280.1,
"max": 285.22,
"min": 278.05,
"morn": 278.05,
"night": 278.85
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 52,
"deg": 289,
"dt": 1490086800,
"humidity": 0,
"pressure": 995.72,
"rain": 14.44,
"speed": 1.86,
"temp": {
"day": 284.12,
"eve": 280.24,
"max": 284.12,
"min": 279.07,
"morn": 280.81,
"night": 279.07
},
"uvi": 3.3,
"weather": [
{
"description": "heavy intensity rain",
"icon": "10d",
"id": 502,
"main": "Rain"
}
]
},
{
"clouds": 5,
"deg": 279,
"dt": 1490173200,
"humidity": 0,
"pressure": 1013.61,
"rain": 0.32,
"speed": 3.34,
"temp": {
"day": 283.15,
"eve": 278.56,
"max": 283.15,
"min": 277.4,
"morn": 278.69,
"night": 277.4
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 14,
"deg": 276,
"dt": 1490259600,
"humidity": 0,
"pressure": 1013.07,
"rain": 0.38,
"speed": 2.15,
"temp": {
"day": 283.16,
"eve": 277.97,
"max": 283.16,
"min": 273.3,
"morn": 279.78,
"night": 273.3
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 68,
"deg": 65,
"dt": 1490346000,
"humidity": 0,
"pressure": 1013.2,
"speed": 2,
"temp": {
"day": 282.89,
"eve": 279.01,
"max": 282.89,
"min": 275.36,
"morn": 276.61,
"night": 275.36
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 5,
"deg": 58,
"dt": 1490432400,
"humidity": 0,
"pressure": 1008.32,
"speed": 3.8,
"temp": {
"day": 286.7,
"eve": 281.22,
"max": 286.7,
"min": 278.31,
"morn": 278.31,
"night": 278.44
},
"uvi": 3.3,
"weather": [
{
"description": "sky is clear",
"icon": "01d",
"id": 800,
"main": "Clear"
}
]
},
{
"clouds": 67,
"deg": 51,
"dt": 1490518800,
"humidity": 0,
"pressure": 1003.89,
"rain": 2.54,
"speed": 3.09,
"temp": {
"day": 286.07,
"eve": 282.58,
"max": 286.07,
"min": 279.97,
"morn": 279.97,
"night": 280.45
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 76,
"deg": 253,
"dt": 1490605200,
"humidity": 0,
"pressure": 1006.67,
"rain": 0.85,
"speed": 2.74,
"temp": {
"day": 284.9,
"eve": 281.56,
"max": 284.9,
"min": 279.04,
"morn": 281.65,
"night": 279.04
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 32,
"deg": 170,
"dt": 1490691600,
"humidity": 0,
"pressure": 1016.53,
"rain": 0.73,
"speed": 2.83,
"temp": {
"day": 287.83,
"eve": 283.6,
"max": 287.83,
"min": 279.65,
"morn": 280.4,
"night": 279.65
},
"uvi": 3.3,
"weather": [
{
"description": "light rain",
"icon": "10d",
"id": 500,
"main": "Rain"
}
]
},
{
"clouds": 7,
"deg": 113,
"dt": 1490778000,
"humidity": 0,
"pressure": 1019.41,
"speed": 2.04,
"temp": {
"day": 279.65,
"eve": 279.65,
"max": 279.65,
"min": 279.65,
"morn": 279.65,
"night": 279.65
},
"uvi": 3.3,
"weather": [
{
"description": "sky is clear",
"icon": "01ddd",
"id": 800,
"main": "Clear"
}
]
}
],
"time": 1489458708
}
こちらのサンプルデータを元に、weather.protoファイルを下記のように作成します。
weather.proto
syntax = "proto3";
package rest;
message City {
message Coord {
float lat = 1;
float lon = 2;
}
Coord coord = 1;
string country = 2;
int32 id = 3;
string name = 4;
}
message Data {
int32 clouds = 1;
int32 deg = 2;
int32 dt = 3;
int32 humidity = 4;
float pressure = 5;
float rain = 6;
float speed = 7;
message Temp {
float day = 1;
float eve = 2;
float max = 3;
float min = 4;
float morn = 5;
float night = 6;
}
float uvi = 8;
message Weather {
string description = 1;
string icon = 2;
int32 id = 3;
string main = 4;
}
Temp temp = 9;
repeated Weather weather = 10;
}
message CityWeatherBuf {
City city = 1;
repeated Data data = 2;
int64 time = 3;
}
作成したweather.protoファイルを下記のコマンドでビルドしてweather.pb.goを作成します。
$protoc --go_out=. weather.proto
作成されたweather.pb.goファイルをクライアント側・サーバ側両方に配置します。 今回はクライアント・サーバともにrestの配下に配置しました。
また、それ以外のクライアント側・サーバ側のソースは次のような実装です。
クライアント側
client/main.go
package main
import (
"client/rest"
"log"
"net/http"
)
func main() {
http.HandleFunc("/rest/json", rest.RequestJSON)
http.HandleFunc("/rest/xml", rest.RequestXML)
http.HandleFunc("/rest/protobuf", rest.RequestProtoBuf)
log.Fatal(http.ListenAndServe(":8888", nil))
}
client/rest/request.go
package rest
import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/golang/protobuf/proto"
)
//CityWeather struct
type CityWeather struct {
City struct {
Coord struct {
Lat float64 `xml:"lat" json:"lat"`
Lon float64 `xml:"lon" json:"lon"`
} `xml:"coord" json:"coord"`
Country string `xml:"country" json:"country"`
ID int `xml:"id" json:"id"`
Name string `xml:"name" json:"name"`
} `xml:"city" json:"city"`
Data []struct {
Clouds int `xml:"clouds" json:"clouds"`
Deg int `xml:"deg" json:"deg"`
Dt int `xml:"dt" json:"dt"`
Humidity int `xml:"humidity" json:"humidity"`
Pressure float64 `xml:"pressure" json:"pressure"`
Rain float64 `xml:"rain" json:"rain"`
Speed float64 `xml:"speed" json:"speed"`
Temp struct {
Day float64 `xml:"day" json:"day"`
Eve float64 `xml:"eve" json:"eve"`
Max float64 `xml:"max" json:"max"`
Min float64 `xml:"min" json:"min"`
Morn float64 `xml:"morn" json:"morn"`
Night float64 `xml:"night" json:"night"`
} `xml:"temp" json:"temp"`
Uvi float64 `xml:"uvi" json:"uvi"`
Weather []struct {
Description string `xml:"description" json:"description"`
Icon string `xml:"icon" json:"icon"`
ID int `xml:"id" json:"id"`
Main string `xml:"main" json:"main"`
} `xml:"weather" json:"weather"`
} `xml:"data" json:"data"`
Time int64 `xml:"time" json:"time"`
}
//RequestJSON JSON format.
func RequestJSON(w http.ResponseWriter, r *http.Request) {
resp, err := http.Get("http://localhost:9999/rest/json")
if err != nil {
fmt.Fprint(w, err)
return
}
defer resp.Body.Close()
j, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprint(w, err)
return
}
log.Println("JSON : ", len(j))
var jj CityWeather
start := time.Now()
json.Unmarshal(j, &jj)
log.Println("JSON : ", time.Now().Sub(start))
fmt.Fprintf(w, "%+v", jj)
}
//RequestXML XML format
func RequestXML(w http.ResponseWriter, r *http.Request) {
resp, err := http.Get("http://localhost:9999/rest/xml")
if err != nil {
fmt.Fprint(w, err)
return
}
defer resp.Body.Close()
x, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprint(w, err)
return
}
log.Println("XML : ", len(x))
var xx CityWeather
start := time.Now()
xml.Unmarshal(x, &xx)
log.Println("XML : ", time.Now().Sub(start))
fmt.Fprintf(w, "%+v", xx)
}
//RequestProtoBuf Protocol Buffers
func RequestProtoBuf(w http.ResponseWriter, r *http.Request) {
resp, err := http.Get("http://localhost:9999/rest/protobuf")
if err != nil {
fmt.Fprint(w, err)
return
}
defer resp.Body.Close()
p, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprint(w, err)
return
}
log.Println("Protobuf : ", len(p))
var pp CityWeatherBuf
start := time.Now()
proto.Unmarshal(p, &pp)
log.Println("Protobuf : ", time.Now().Sub(start))
fmt.Fprintf(w, "%+v", pp)
}
サーバ側
server/main.go
package main
import (
"log"
"net/http"
"server/rest"
)
func main() {
http.HandleFunc("/rest/json", rest.ResponseJSON)
http.HandleFunc("/rest/xml", rest.ResponseXML)
http.HandleFunc("/rest/protobuf", rest.ResponseProtoBuf)
log.Fatal(http.ListenAndServe(":9999", nil))
}
server/rest/response.go
package rest
import (
"encoding/json"
"encoding/xml"
"fmt"
"log"
"net/http"
"os"
"github.com/golang/protobuf/proto"
)
//CityWeather struct
type CityWeather struct {
City struct {
Coord struct {
Lat float64 `xml:"lat" json:"lat"`
Lon float64 `xml:"lon" json:"lon"`
} `xml:"coord" json:"coord"`
Country string `xml:"country" json:"country"`
ID int `xml:"id" json:"id"`
Name string `xml:"name" json:"name"`
} `xml:"city" json:"city"`
Data []struct {
Clouds int `xml:"clouds" json:"clouds"`
Deg int `xml:"deg" json:"deg"`
Dt int `xml:"dt" json:"dt"`
Humidity int `xml:"humidity" json:"humidity"`
Pressure float64 `xml:"pressure" json:"pressure"`
Rain float64 `xml:"rain" json:"rain"`
Speed float64 `xml:"speed" json:"speed"`
Temp struct {
Day float64 `xml:"day" json:"day"`
Eve float64 `xml:"eve" json:"eve"`
Max float64 `xml:"max" json:"max"`
Min float64 `xml:"min" json:"min"`
Morn float64 `xml:"morn" json:"morn"`
Night float64 `xml:"night" json:"night"`
} `xml:"temp" json:"temp"`
Uvi float64 `xml:"uvi" json:"uvi"`
Weather []struct {
Description string `xml:"description" json:"description"`
Icon string `xml:"icon" json:"icon"`
ID int `xml:"id" json:"id"`
Main string `xml:"main" json:"main"`
} `xml:"weather" json:"weather"`
} `xml:"data" json:"data"`
Time int64 `xml:"time" json:"time"`
}
func ReadResponseDataFile() *json.Decoder {
file, err := os.Open("data/data.json")
if err != nil {
return nil
}
decode := json.NewDecoder(file)
return decode
}
func ResponseData() CityWeather {
var cityWeather CityWeather
decode := ReadResponseDataFile()
decode.Decode(&cityWeather)
return cityWeather
}
func ResponseDataBuf() CityWeatherBuf {
var cityWeather CityWeatherBuf
decode := ReadResponseDataFile()
decode.Decode(&cityWeather)
return cityWeather
}
//ResponseJSON JSON format.
func ResponseJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
cityWeather := ResponseData()
j, err := json.Marshal(&cityWeather)
if err != nil {
fmt.Fprint(w, err)
}
writeLen, err := w.Write(j)
if err != nil {
log.Println(err)
} else {
log.Println(writeLen)
}
}
//ResponseXML XML format
func ResponseXML(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
cityWeather := ResponseData()
x, err := xml.Marshal(&cityWeather)
if err != nil {
fmt.Fprint(w, err)
}
writeLen, err := w.Write(x)
if err != nil {
log.Println(err)
} else {
log.Println(writeLen)
}
}
//ResponseProtoBuf Protocol Buffers
func ResponseProtoBuf(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/protobuf")
cityWeather := ResponseDataBuf()
p, err := proto.Marshal(&cityWeather)
if err != nil {
fmt.Fprint(w, err)
}
writeLen, err := w.Write(p)
if err != nil {
log.Println(err)
} else {
log.Println(writeLen)
}
}
最終的にクライアントとサーバの構成は次のようになりました。
クライアント側
- client/main.go
- client/rest/request.go
- client/rest/weather.pb.go
サーバ側
- server/main.go
- server/rest/response.go
- server/rest/weather.pb.go
- server/data/data.json
実行結果
それぞれプログラムを実行し、curlコマンド等でクライアントのAPIにリクエストを送り結果を確認します。
フォーマット | XML | JSON | ProtoBuf |
---|---|---|---|
処理時間1回目(μs) | 2382 | 683 | 86 |
処理時間2回目(μs) | 2318 | 1291 | 89 |
処理時間3回目(μs) | 2326 | 699 | 77 |
サイズ(Byte) | 6732 | 4760 | 1620 |
以上のような結果となりました。
protocol buffersのパース速度は圧倒的ですね。
jsonと比べても8~15倍、XMLとなら30倍ほどの速度差があります。
通信データのサイズも他と比べて25%~33%程度になっています。
ただ、使うまでの準備の大変さや、利用可能な言語の種類、
外部にAPIを公開した場合などを考えると、必ずしも最良というわけではないですね。
社内システムや、秒間のリクエストが多くなるようなシステムの場合ですと、
通信量を抑えられ、かつパフォーマンスも出て良さそうです。
この辺はやはり用途に応じて選択すべきなのだと思います。