XML、JSON、Protocol Buffersを比較してみました

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コマンドを使えるようにするため、公式サイトから、

自分の環境に合わせたコンパイル済みのバイナリを取得します。

ソースからmakeインストールする場合はこちら

下記のコマンドを全てインストールします。

  • ・autoconf
  • ・automake
  • ・libtool
  • ・make
  • ・g++
  • ・unzip

githubからソースを取得します。

$ git clone https://github.com/google/protobuf.git

次にクローンしたディレクトリに移動して、下記のコマンドを順番に実行します。

$ cd protobuf
$ git submodule update --init --recursive
$ ./autogen.sh

ここ先ほどインストールしたコマンドの中に足りないものがあるとエラーが出ます。

最後にmake installを行います。

./configureを実行すると、デフォルトでは/usr/localの配下にインストールされますが、 他の場所にしたい方は、--prefix=/path/toをオプションで指定し、 任意のディレクトリにインストール先を変えてください。

$ ./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を公開した場合などを考えると、必ずしも最良というわけではないですね。

社内システムや、秒間のリクエストが多くなるようなシステムの場合ですと、

通信量を抑えられ、かつパフォーマンスも出て良さそうです。

この辺はやはり用途に応じて選択すべきなのだと思います。