tjun月1日記

なんでもいいので毎月書きたい

GoogleAppEngine/GoでCloudStorageのファイルを配信する方法とハマりどころ

半年前くらいからGAE/Goを結構触っているので、ちょっと書いておく。 今回は、CloudStorage上のファイルをGAE/Goで配信する方法。

概要

GoogleCloudStorageにおいてるファイル(例えば画像)をWebサービス上でユーザに配信したいということがある。 画像ならブラウザ上で表示させるだけでいいかもしれないし、その他のファイルならダウンロードさせたいし、複数ファイルならzipで固めて配信したい。

イメージ f:id:taka-jun:20170301160414p:plain

cloud.google.com/go/storageのライブラリを利用する。 以下の例では、エラー処理などは省略する。また、普段はEchoを使ってるので以下のコードは試してないし正しく動かない部分があるかも。

コード例

画像を表示させる

画像をブラウザで表示させるケース。 特に難しい部分はなく、シンプルに書ける。 検索して出て来るサンプルだとioutil.ReadAllを使ってることもあるけど、io.Copyで 直接http.ResponseWriterに書き出した方がパフォーマンス的にいいと思う。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)
  cli, _ := storage.NewClient(ctx)
  reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
  defer reader.Close()

  res.Header().Set("Content-Type", reader.ContentType())
  res.WriteHeader(http.StatusOK)

  siz, _ := io.Copy(res, reader)
  return nil
}

ファイルをダウンロードさせる

基本的には上と同じだけど、Content-Dispositionヘッダを指定して画像でもダウンロードさせられるようになる。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)
  cli, _ := storage.NewClient(ctx)
  reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
  defer reader.Close()

  res.Header().Set("Content-Type", reader.ContentType())
  disposition := "attachment; filename=" + fileName
  res.Header().Set("Content-Disposition", disposition)
  res.WriteHeader(http.StatusOK)

  siz, _ := io.Copy(res, reader)
  return nil
}

zipで固めてダウンロードさせる

複数のファイルをダウンロードさせたい場合、ディレクトリごとダウンロードさせたい場合などに使える。 storageのReaderと httpのResponseWriterの間にzipWriterを挟む感じ。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)
  cli, _ := storage.NewClient(ctx)

  res.Header().Set("Content-Type", "application/octet-stream")
  disposition := "attachment; filename=" + fileName
  res.Header().Set("Content-Disposition", disposition)
  res.WriteHeader(http.StatusOK)

  zipWriter := zip.NewWriter(res)
  defer zipWriter.Close()

  for _, fname := range fileNames {
 path := Path(path.Join(dir), fname))
    reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
    defer reader.Close()

    zipFile, err := zipWriter.Create(fname)

    _, err = io.Copy(zipFile, reader)
  }
  return nil
}

エラー処理がないとすごくシンプルに見えるけど、実際は if err != nil {... というコードがあちこちに入ります。

ハマりどころ

クラウドストレージ上のファイルのサイズが 32MBを越えると、storage.NewReaderAPI error 3 (urlfetch: UNSPECIFIED_ERROR)というエラーになります。 また、複数のファイルをZIPする場合でも、合計のサイズが32MB超えるとResponse返すところでエラーになります。 CloudStorage -> AppEngine -> Client の2箇所とも32MB制限があります。 参考: https://cloud.google.com/appengine/quotas#Requests

エラーが分かりにくいし、32MBはちょっと少ない気がする。 しかもGAE/Goのドキュメントを読むと、Streaming Responseに対応してないので、ファイルを少しずつ読んでStreaming Responseで返すってこともできません。

参考: https://cloud.google.com/appengine/docs/standard/go/how-requests-are-handled#streaming_responses

32MB以上のファイルを扱う際には、BlobStoreを使います。 BlobStoreは以下のように使えます。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)

  bpath := path.Join("/gs", fpath)
  bkey, _ := blobstore.BlobKeyForFile(ctx, bpath)
  
  disposition := "attachment; filename=" + fileName
  res.Header().Set("Content-Disposition", disposition)

  blobstore.Send(res, bkey)

  return nil
}

参考: The blobstore package  |  App Engine standard environment for Go

recvmmsgについて

(過去の記事の転載)

recvmmsg というのは、recvmsg(2)を拡張して複数のメッセージをsocketから受け取れるシステムコールで、Linux の 2.6.33 から利用できます。 glibcの version 2.12 からサポートされてます。 これを使うと、要するに1回のシステムコールで複数回recvmsgをすることができるので、messageをどかどか受けるようなアプリケーションでパフォーマンスが上がることがあります。

実際に使ってみて効果が見られたのと、man読んだだけじゃ使い方分かりにくかったので、これを書いておく。

manをみれば使い方はなんとなく分かると思う。manにあるサンプルコードを一番下に貼りました。 複数個受けるだけではなくtimeoutを取れるのがrecvmsgと違うところで、manを読むとまあ普通のタイムアウトのように見えるんだけど、カーネルのソースを読むと厳密なタイムアウトにはなってないようです。(ということを教えてもらいました)

また、すぐに抜けてきて欲しいときに timeoutに0 (tvsec=0, tvnsec=0) をセットすると、1つrecvしただけで返ってきてしまうのも注意。 すぐに抜けて来て欲しいけど、そのときに取れるmessageは全て取りたい、という時には、flagに MSG_DONTWAIT をセットして、timeoutはNULLにして呼び出すと期待している動作になります。

ハマりやすいポイントみたいで、manが分かりにくいという意見もあるようです。

また、sendでも似たようなことをやるためのsendmmsgというのもあります。

以下に、manにあったサンプルコードを載せておく。

#define _GNU_SOURCE
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>

int  
main(void)  
{
#define VLEN 10
#define BUFSIZE 200
#define TIMEOUT 1
  int sockfd, retval, i;
  struct sockaddr_in sa;
  struct mmsghdr msgs[VLEN];
  struct iovec iovecs[VLEN];
  char bufs[VLEN][BUFSIZE+1];
  struct timespec timeout;

  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sockfd == -1) {
    perror("socket()");
    exit(EXIT_FAILURE);
  }

  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  sa.sin_port = htons(1234);
  if (bind(sockfd, (struct sockaddr *) &sa, sizeof(sa)) == -1) {
    perror("bind()");
    exit(EXIT_FAILURE);
  }

  memset(msgs, 0, sizeof(msgs));
  for (i = 0; i < VLEN; i++) {
    iovecs[i].iov_base = bufs[i];
    iovecs[i].iov_len = BUFSIZE;
    msgs[i].msg_hdr.msg_iov = &iovecs[i];
    msgs[i].msg_hdr.msg_iovlen = 1;
  }

  timeout.tv_sec = TIMEOUT;
  timeout.tv_nsec = 0;

  retval = recvmmsg(sockfd, msgs, VLEN, 0, &timeout);
  if (retval == -1) {
    perror("recvmmsg()");
    exit(EXIT_FAILURE);
  }

  printf("%d messages received\n", retval);
  for (i = 0; i < retval; i++) {
    bufs[i][msgs[i].msg_len] = 0;
    printf("%d %s", i+1, bufs[i]);
  }
  exit(EXIT_SUCCESS);
}

WebRTCに使われるP2Pの技術

f:id:taka-jun:20180130154309p:plain(この記事は過去の記事の転載です)

この記事は、HTML5 Advent Calendar 2013 - Adventarの15日目になります。

WebRTCが少しずつ広まってきて、あまりこの辺りの情報を日本語では見たことがないので書きました。 間違っている箇所、表現がおかしい箇所がありましたら、@tjunまで教えていただけると助かります。 もっと詳しく知りたいという人は一番最後のリンクにあるページを見るとよいと思います。

はじめに

WebRTCのおさらい

WebRTC、聞いたことがあるという人も増えていると思います。 WebRTCとは何かを一言でいうと、ブラウザ間でリアルタイムなやりとりをするための仕組み、ということになると思います。

ポイントは、「ブラウザ間」です。 サーバを経由せずにブラウザ間で通信することで、ブラウザ同士のやりとりも高速になりますし(もちろんネットワークの環境によります)、サーバ側でそれぞれの接続を管理する必要もなくなります。 WebRTCのRTC=Real Time Communicationで、「リアルタイムな通信」の方に気を取られるとWebSocketと似たようなものかと誤解されることがありますが、WebSocketは基本的にブラウザとサーバ間の通信のものであるのに対し、WebRTCはブラウザ同士の通信がターゲットです。 だから、よくあるデモアプリケーションとしてはビデオチャットなどになります。

f:id:taka-jun:20180130154345p:plain

ブラウザ間通信を実現するためには

一般的なブラウザ-Webサーバ間の通信ではサーバがパブリックなIPアドレスを持っているので、そのIPアドレスのポート80番を宛先として通信が開始できます。 このとき、基本的にブラウザはどこかのNATの下のネットワークにいて、プライベートなIPアドレスを持つマシンで動いています。 サーバからブラウザへの通信は、サーバへの問い合わせへの返答であることをNATが判断できるので、サーバからブラウザへのデータの通信もNATを経由して実現できます。 その仕組みについてはここでは説明しません。 一方、ブラウザ-ブラウザ間で通信しようと思っても、どちらもどこかのNATの下のプライベートなIPアドレスを持つマシンで動いているので、そのままでは相手がどこにいるのか分かりません。 サーバ-ブラウザ間の場合は、片方がパブリックなIPアドレスを持っていたので通信が開始できましたが、お互いが異なるNATの下にいる場合には、NATを通してその先のマシンへ通信を開始することができません。

f:id:taka-jun:20180130154405p:plain

そこで、まず通信するブラウザでお互いに、さあこれから通信をしましょう、とコミュニケーションして、通信に必要なメタデータの交換などを行う必要があります。このことをシグナリング、といいます。 次に、2つのクライアントが異なるNATの下にいる場合やfirewallがある場合、それらを越えて通信する仕組みが必要です。 以下では、シグナリングと、NATを越える技術について説明します。

シグナリング

ブラウザからWebRTCで通信するためには、当たり前ですがまず通信する相手が必要です。 いわゆるチャットサービスなら、友人の誰がオンラインか知る必要があります。 あるいは、知らない人同士がゲームの対戦をするサービスなら、どこかで対戦相手のマッチングをする必要があります。 どのようにして通信相手を決めるかはアプリケーションによりますが、必ず通信相手を見つけるための仕組みがサービス側にも必要です。 したがって、WebRTCを使ったサービスには、マッチングのためのサーバが必ず必要となります。

そこで、多くの場合このサービス用のサーバを利用してシグナリングを行います。 シグナリングのやり方は、アプリケーションによって変わってくるため、WebRTCでは決められていません。例えばXMPPSIPを使ってもよいですし、WebSocketを使ってブラウザサーバ間の通信を独自に実装することもあります。サービスを作る側が自由に選択することができます。

f:id:taka-jun:20180130154421p:plain

通信相手が決まれば、サービスのサーバを介してそれぞれのクライアントがお互いの情報を交換して、P2Pの通信へと移行します。 ここで交換する情報とは、上にも書きましたが、だいたい次のようなものです。

  • セッションのコントールメッセージ。接続の開始や終了を相手に知らせる。
  • エラーメッセージ
  • 接続で使用するメディアのメタデータ。例えば、コーデックや帯域、メディアの種類など。
  • セキュアな接続を構築するための、鍵情報
  • ネットワーク情報。例えばホストのIPアドレスとポート番号など。

シグナリングの大まかな流れは以上です。 詳しくは、what is signaling - HTML5 Rocks などを参照してください。

ICEを使ったNATとFirewall越え

シグナリングを使えば、通信相手とのP2P通信のきっかけとなるやりとりができることが分かりました。 そこで次はどのようにしてブラウザ同士が通信するかについて説明します。

しかしながら、最初に書いたように基本的にそれぞれのクライアントはNATの下側に存在していて、クライアントから別のクライアント直接データを送ることはできません。 そこでWebRTCでは、ブラウザ間の通信を実現するためにICEというフレームワークを使います。

ICEでは、STUNサーバとTURNサーバという新たな役割を持つサーバが利用されます。 STUNサーバは、クライアントに外側のネットワークアドレスを教える役割をもち、そしてTURNサーバは直接(P2Pの)通信ができなかった場合に、通信を中継する役割を持ちます。

またICEではそれぞれのクライアントが接続に必要な情報として、自分のIPアドレスとポート番号の情報を、シグナリングを使って相手に送ります。この情報のことをCandidate(候補)といいます。それぞれのクライアントが、自身のCandidateから相手のCandidateに対して接続を試みて、お互いにベストな通信経路を探し出し、2つのブラウザの通信を確立します。

Candidateには、IPアドレスとポート番号の他に、優先度などの情報が含まれます。

以下でICEの流れを順に説明していきます。 以下でアドレスと書いているところは、IPアドレスとポート番号であると読み替えてください。

  • まずICEを開始したそれぞれのクライアントは、1つ目のCandidateとしてOSから取得できるデバイス自身のアドレスを送ります。もし複数のインターフェース(有線と無線、など)があれば、複数のCandidateとなります。相手側も同様にローカルのアドレスを送ってくるので、それぞれの自身のローカルアドレスと相手側のローカルアドレスを使って接続を試します。同じネットワークにいればこれで繋がることができて、ここで終了、となる場合もあります。

    f:id:taka-jun:20180130154441p:plain

ここでダメな場合、次へいきます。

  • 次にそれぞれのクライアントは、STUNサーバを利用して外側のアドレスを取得します。 クライアントがSTUNサーバに通信するとSTUNサーバはそのときの送信元であるアドレスとポート番号を教えてくれます。 STUNサーバからはそのとき通ったNATが送信元に見えるため、外側のアドレスとはつまり、NATの外側アドレスになります。 (もしSTUNサーバとクライアントの間にNATがなければクライアントのアドレスになります) このようにしてSTUNを使って取得したアドレスが2つめのCandidateとなり、相手へと送られます。 相手からも2つめのCandidateが送られてくれば、1つめのCandidate(C1)と2つめのCandidate(C2)を使って接続を試します。

    f:id:taka-jun:20180130154504p:plain

    このとき、NATでは内側からその(アドレスと)ポートを使って内側から外側(相手のC2)へ通信をした、ということが分かるため、そのポートに対して外側(相手のC2)から入ってきた通信は、さっきの通信に対する返答であると判断されて送信元であるクライアントへ届きます。 これがいわゆるNATのHole Panching(穴あけ)です。

    f:id:taka-jun:20180130154543p:plain

    ここで上のように通信ができるかどうかは、ネットワークの環境次第です。 複数(多段)のNATがある環境もありますし、NATはそれぞれフィルタリング特性やアドレス・ポート変換特性を持つので、つながるかどうかは実際に試してみないとわかりません。
    (以前はNATの区別をするためにフルコーン、シンメトリックなどの分類が使われてましたが、その分類は不十分なのでちゃんとポートマッピングとフィルタリングの特性を見ましょう、ということがRFC4787で書かれています。)

  • STUNを使っても接続ができなかった場合は、TURNサーバを使って通信します。 それぞれのクライアントはTURNサーバへ接続し、TURNサーバのアドレスと、確保されたポート番号を取得します。 このTURNサーバのアドレスとポート番号を3つめのCandidate(C3)として相手へ教えます。

    f:id:taka-jun:20180130154612p:plain

    そしてC1, C2, C3を使って、相手へと通信します。TURNサーバを経由して通信ができれば、その後のやりとりもTURNサーバ経由となります。

    f:id:taka-jun:20180130154644p:plain

以上のように順番に試していくことで、2つのクライアントでベストな経路を決定します。 このようにしてクライアント間の接続が確立されると、サービスのための通信が開始できます。

また、それぞれのクライアントがどのSTUN/TURNサーバを使うのか決める仕組みは、サービスの実装によります。 アプリケーションで固定のサーバを決めていて使ってもよいですし、最初のネゴシエーションの時にサービスサーバから割り当ててもよいです。 2つのクライアントは異なるSTUNサーバやTURNサーバを使っても動作します。

おわりに

WebRTCでクライアント同士がどのように接続するか見てきました。 WebRTCを利用するサービスを作る場合には、STUNやTURNサーバを使う必要があります。 サーバを自前で用意する場合にはもちろんですが、最近発表されたSkyWayのようなサービスを利用する場合にも、どのような仕組みでどのようなサーバが使われるのか、ある程度は知っておくとよいと思います。

今回の記事はいろいろと省略しています、本当はもうちょっといろいろな仕組みがあります。

参考にしたページ

Shared Libraryでシンボルを隠す方法について

(過去の記事の転載) CやC++でShared Libraryを作るときのメモです。 外に出したいシンボルと、外に出したくないシンボルがあるとき、どのようにしたらよいのか知らなかったので。 常識かもしれないけど、CとC++が両方ある場合にどうすればいいのか、調べてもあんまり情報が出て来なかった。

以下のようなサンプルがあるとします。

hoge.h

#ifndef HOGE_H
#define HOGE

#ifdef __cplusplus
extern "C" {  
#endif

void hoge();

#ifdef __cplusplus
}
#endif

#endif

hoge.c

#include "hoge.h"

void hoge_local() {  
}

void hoge() {  
  hoge_local();
}

fuga.h

class Fuga {  
public:  
  Fuga();
  ~Fuga();
private:  
  int fugafuga();
};

fuga.cpp

#include "hoge.h"
#include "fuga.h"

Fuga::Fuga() {

}
Fuga::~Fuga() {  
}

int Fuga::fugafuga()  
{
  hoge();
  return 0;
}

このようにCとC++が混ざってる状況で、例えば、hoge_localは、内部でしか使わないのでライブラリにしたときに利用して欲しくない、という時があると思います。

コンパイル時にはfPICをつけます。

$ gcc -fPIC -c -o hoge.o hoge.c
$ g++ -fPIC -c -o fuga.o fuga.cpp

試しにそのままshared libraryを作って

g++ -shared -fPIC -o libhogefuga.so fuga.o hoge.o

これでシンボルをみてみると

$ nm -g -C -D libhogefuga2.so ~/dev/tmp2

w _Jv_RegisterClasses
00000000000006d0 T Fuga::fugafuga()
00000000000006bc T Fuga::Fuga()
00000000000006bc T Fuga::Fuga()
00000000000006c6 T Fuga::~Fuga()
00000000000006c6 T Fuga::~Fuga()
0000000000201020 A __bss_start
w __cxa_finalize
w __gmon_start__
0000000000201020 A _edata
0000000000201030 A _end
0000000000000738 T _fini
0000000000000598 T _init
00000000000006ee T hoge
00000000000006e8 T hoge_local

こんな感じでhoge_localもシンボルが残って外から見えてしまいます。

この状況で、hoge_localとfugafugaは隠したい。 そのようなときには、version-script というものを利用します。

先ほどの例で、hoge_localとFuga::fugafuga以外をexportしたいので、以下のように書くことができます。

hogefuga.vermap

{
  global:
    extern "C++" {      
      Fuga::Fuga*;
      Fuga::?Fuga*;
    };
    hoge;
  local:
      *;
};

globalの部分に公開するシンボルを記述して、localは*で残り全部、としてます。 C++ のマングルされたシンボルに対応するため、C++でexportしたいシンボルは extern "C++" {} で囲ってます。

上の例のようにワイルドカードが使えますが、チルダは使えないので、デストラクタのexportでは、"?"で何か1文字、ということを表して代用してます。

このversion-scriptを以下のようなオプションでリンカのオプションに渡して、shared libraryを作ります。

g++ -shared -fPIC -o libhogefuga2.so fuga.o hoge.o -Wl,--version-script=hogefuga.vermap

先ほどと同様にシンボルを見ると、

$ nm -g -C -D libhogefuga2.so ~/dev/tmp2

w _Jv_RegisterClasses
000000000000057c T Fuga::Fuga()
000000000000057c T Fuga::Fuga()
0000000000000586 T Fuga::~Fuga()
0000000000000586 T Fuga::~Fuga()
w __cxa_finalize
w __gmon_start__
00000000000005ae T hoge

とversion-scriptでglobalに書いた部分だけ残っていて、目的が達成されました。

参考にしたサイト

ACCU :: Working with GNU Export Maps