Windowsの名前付きパイプ
これまではUnix系OSの話でした。 Windowsでは、ストリーム型のUnixドメインソケットに近い使い方ができる仕組みとして、名前付きパイプというものがあります。 なお、名前付きパイプという概念はUnixにもありますが、ややこしいことにWindowsとは意味が違う別物です。 ここで解説するのは、Windowsの名前付きパイプです。
Windowsの名前付きパイプを使うときも、Unixドメインソケットと同じように、ファイルシステムにマッピングします。 ただし定義できる場所は限定されており、「\\\\pipe\\パイプ名
」という特別なファイル名でパイプを定義して、他のプロセスはこのファイル名を使ってアクセスします。 Windowsの名前付きパイプはコンピュータ間の通信にも使えますが、通信速度はTCP/UDPよりも劣ります。
Go言語の標準ライブラリではWindowsの名前付きパイプの機能は提供されていません。 筆者は次のパッケージでWindowsの名前付きパイプを利用しています。
次のようにすればパッケージをインストールできます。
$ go get gopkg.in/natefinch/npipe.v2 ⏎
npipeの使い方は、今まで紹介してきたストリーム型のコードと同じです。 Dial()
とListen()
にプロトコル名を入力する必要がない程度の差しかありません。
// サーバ
ln, err := npipe.Listen(`\\.\pipe\mypipename`)
if err != nil {
}
for {
conn, err := ln.Accept()
if err != nil {
// エラー処理
continue
}
go handleConnection(conn)
}
// クライアント
conn, err := npipe.Dial(`\\.\pipe\mypipename`)
if err != nil {
// エラー処理
}
fmt.Fprintf(conn, "Hi server!\n")
msg, err := bufio.NewReader(conn).ReadString('\n')
サーバー系のミドルウェアによっては、POSIX環境ではUnixドメインソケット、Windowsでは名前付きパイプを使い分けるものがあります。 Oracle、MySQLやPostgreSQLのようなリレーショナルデータベース管理システムでは、Unixドメインソケットと名前付きパイプの両方をサポートしています。 Redis、Memcachedなど、Unixドメインソケットしかサポートしていないツールもあります。
GUIフレームワークのQtは、同一コンピュータ内で高速なソケット通信を行うためのクラスとして QLocalServer
1、QLocalSocket
2を提供しています。 このクラスは、Windowsでは名前付きパイプを「\\\\pipe\\
」以下に作成し、それ以外のOSではUnixドメインソケットをTEMPフォルダ以下に作成して通信します。 このQtのクラスと同じ挙動をするGo言語のパッケージを筆者が開発して公開しているので、興味があったら覗いてみてください。
UnixドメインソケットとTCPのベンチマーク
冒頭で「Unixドメインソケットはインターネットを超えて行う通信よりも速い」と紹介しましたが、 実際にどの程度速いのか、実験してみましょう。
下記のソースコードをすべて同一のフォルダに入れて、次のようにコマンドを起動すると、ベンチマークが行えます。
$ go test -bench . ⏎
testing: warning: no tests to run
BenchmarkTCPServer-8 1000 7989037 ns/op
BenchmarkUDSStreamServer-8 20000 91136 ns/op
このベンチマークは、Go言語の標準ライブラリのテスティングフレームワークの一部として提供されている、ベンチマーク測定のフレームワークを利用しています。
何度か実行すると、多少の変動はありますが、Unixドメインソケットの方が80倍から90倍高速なことが分かります。 Unixドメインソケットの場合はほぼ、カーネル内のバッファにデータをコピーして、そこからサーバプロセス(の裏側のカーネル内)のバッファに書き込む程度の負荷しかかかりません。 第五回のシステムコールの説明で、システムコールにはGo言語のタスクスケジュールを切り替えてカーネルの仕事が完了するのを非同期で待機するsyscall.Syscall
と、そのまま待機するsyscall.RawSyscall
の2種類あると紹介しました。 ファイルI/OやTCPソケットの場合はタスク切り替えを行って待ちを入れる前者を使いますが、Unixドメインソケットは高速なシステムコール専用の後者を使います。 もちろん、これはHTTPのスループットだけを計測するマイクロベンチマークで、TCPには比較的不利なベンチマークとなっています。 リアルなアプリケーションで同じ結果になるとは限りませんので注意してください。
TCPソケットは8ミリ秒ほど1回のHTTPアクセスにかかっていますが、1回に1リクエストしか受け付けておらず、決してGoのウェブサーバが秒間120アクセスしかさばけないわけではありません。並列して負荷をさばく余力を多分に残しています。
package main
import (
"bufio"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"strings"
)
func TCPServer() {
listener, err := net.Listen("tcp", "localhost:18888")
if err != nil {
panic(err)
}
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
go func() {
// リクエストを読み込む
request, err := http.ReadRequest(bufio.NewReader(conn))
if err != nil {
panic(err)
}
_, err = httputil.DumpRequest(request, true)
if err != nil {
panic(err)
}
// レスポンスを書き込む
response := http.Response{
StatusCode: 200,
ProtoMajor: 1,
ProtoMinor: 0,
Body: ioutil.NopCloser(strings.NewReader("Hello World\n")),
}
response.Write(conn)
conn.Close()
}()
}
}
package main
import (
"bufio"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"os"
"path/filepath"
"strings"
)
func UnixDomainSocketStreamServer() {
path := filepath.Join(os.TempDir(), "bench-unixdomainsocket-stream")
os.Remove(path)
listener, err := net.Listen("unix", path)
if err != nil {
panic(err)
}
for {
conn, err := listener.Accept()
go func() {
// リクエストを読み込む
request, err := http.ReadRequest(bufio.NewReader(conn))
if err != nil {
panic(err)
}
_, err = httputil.DumpRequest(request, true)
if err != nil {
panic(err)
}
// レスポンスを書き込む
response := http.Response{
StatusCode: 200,
ProtoMajor: 1,
ProtoMinor: 0,
Body: ioutil.NopCloser(strings.NewReader("Hello World\n")),
}
response.Write(conn)
conn.Close()
}()
}
}
package main
import (
"bufio"
"net"
"net/http"
"net/http/httputil"
"os"
"path/filepath"
"testing"
)
func BenchmarkTCPServer(b *testing.B) {
for i := 0; i < b.N; i++ {
conn, err := net.Dial("tcp", "localhost:18888")
if err != nil {
panic(err)
}
request, err := http.NewRequest("get", "http://localhost:18888", nil)
if err != nil {
panic(err)
}
request.Write(conn)
response, err := http.ReadResponse(bufio.NewReader(conn), request)
if err != nil {
panic(err)
}
_, err = httputil.DumpResponse(response, true)
if err != nil {
panic(err)
}
}
}
func BenchmarkUDSStreamServer(b *testing.B) {
for i := 0; i < b.N; i++ {
conn, err := net.Dial("unix",
filepath.Join(os.TempDir(), "bench-unixdomainsocket-stream"))
if err != nil {
panic(err)
}
request, err := http.NewRequest("get", "http://localhost:18888", nil)
if err != nil {
panic(err)
}
request.Write(conn)
response, err := http.ReadResponse(bufio.NewReader(conn), request)
if err != nil {
panic(err)
}
_, err = httputil.DumpResponse(response, true)
if err != nil {
panic(err)
}
}
}
func TestMain(m *testing.M) {
// init
go UnixDomainSocketStreamServer()
go TCPServer()
time.Sleep(time.Second)
// run test
code := m.Run()
// exit
os.Exit(code)
}
ソケットのシステムコール小話
データグラム型のソケットでリプライを送信するとエラーになってしまう挙動は、Go言語よりもC言語レベルのシステムコールの方が少しシンプルに考えられます。 Go言語ではシンプルな関数やメソッドで通信の確立ができますが、C言語の方は少し多くの関数を呼び出しています。
用途 | Go言語の実装 | C言語レベルのシステムコール |
---|---|---|
ストリーム型サーバ作成 | net.Listen() |
socket() , bind() , listen() |
ストリーム型クライアント作成 | net.Dial() |
socket() , connect() |
データグラム型サーバ作成 | net.Listen() |
socket() , bind() , listen() |
データグラム型クライアント作成 | net.Dial() |
socket() |
これを見ると、Listen()
とDial()
に対応している関数が一部重複しています。 サーバとクライアントで、Go言語のコードの見た目は大きく違いますが、C言語はそこまで違いはありません。 今回のコードに関して言えば、bind()
さえ読んでしまえば、クライアント側のコードはほとんど変更することなくそのまま行けました。
なお、下記のメソッドはほぼ1対1で対応しています。
用途 | Go言語の実装 | C言語レベルのシステムコール |
---|---|---|
ストリーム型サーバ・クライアント接続受付 | net.Listener.Accept() |
accept() |
ストリーム型送信 | net.Conn.Write() |
write() |
ストリーム型受信 | net.Conn.Read() |
read() |
データグラム型送信 | net.PacketConn.WriteTo() |
sendto() |
データグラム型受信 | net.PacketConn.ReadFrom() |
recvfrom() |
ソケットを閉じる | net.Conn.Close() , net.Listener.Close() |
close() |
まとめと次回予告
4回に分けてソケット通信のAPIを見てきました。 最初はTCPソケットでしたが、ソケットのAPIの使い方というよりも、HTTPの仕組みの再実装を通して、効率の良い通信の実装について学びました。 次にUDPソケットを学びましたが、主にTCPソケットと比較し、何が省かれているのかを説明しました。 最後にUnixドメインソケットについて学びました。 少しトリッキーなソケットで、1つのソケットでモードが複数ありましたが、高速なため使い所がハマればアプリケーションの性能向上に役立つでしょう。
次回からはファイルシステムについて学びます。
脚注
この連載の記事
-
第20回
プログラミング+
Go言語とコンテナ -
第19回
プログラミング+
Go言語のメモリ管理 -
第18回
プログラミング+
Go言語と並列処理(3) -
第17回
プログラミング+
Go言語と並列処理(2) -
第16回
プログラミング+
Go言語と並列処理 -
第15回
プログラミング+
Go言語で知るプロセス(3) -
第14回
プログラミング+
Go言語で知るプロセス(2) -
第13回
プログラミング+
Go言語で知るプロセス(1) -
第12回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(3) -
第11回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(2) -
第10回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(1) - この連載の一覧へ