ZigでUDPでtimeserverから現在時刻を取得するサンプルプログラム
本記事はZigアドベントカレンダー24日目の記事です。
前回の記事でZigでUDP通信ができるようになったので実際のサービスに使ってみることにしました。
Daytime protocol(RFC-867)
とっても短いRFCです。簡単に読める!
TCPまたはUDPでポート13に何か書き込むとその返事として現在時刻の文字列が返ってきます。
UDPのdaytimeサービスを有効にする
daytimeサービスは実用的なものではなく疎通テストなどにしか使用されないので、デフォルトでは無効になっています。以下の手順でこれを有効にします。
Ubuntu 22.04 x86_64 を使用しています。
daytimeサービスはxinetdから起動されるので、それが入っていることを確認します。
$ sudo apt install xinetd
/etc/xinetd.d/daytime-udp
のdisable = yes
をno
に変更します。
$ sudo sed -i.org '/disable/s/yes/no/' /etc/xinetd.d/daytime-udp
変更箇所の確認。
$ diff -u /etc/xinetd.d/daytime-udp{.org,}
--- /etc/xinetd.d/daytime-udp.org 2018-02-06 03:31:09.000000000 +0900
+++ /etc/xinetd.d/daytime-udp 2022-12-22 14:30:35.633606989 +0900
@@ -9,6 +9,6 @@
protocol = udp
user = root
wait = yes
- disable = yes
+ disable = no
port = 13
}
xinetdをリスタートします。
$ sudo systemctl restart xinetd
UDPのdaytimeサービスを利用するサンプルプログラム
const std = @import("std");
const os = std.os;
const log = std.log;
const time = std.time;
const UDP_PAYLOADSIZE = 65507;
const DAYTIME_PORT = 13;
fn askDaytime(ipaddr: []const u8, verbose:bool) ![]u8 {
var buf: [UDP_PAYLOADSIZE]u8 = .{};
const sockfd = try os.socket(os.AF.INET, os.SOCK.DGRAM | os.SOCK.CLOEXEC, 0);
defer os.closeSocket(sockfd);
const addr = try std.net.Address.resolveIp(ipaddr, DAYTIME_PORT);
try os.connect(sockfd, &addr.any, addr.getOsSockLen());
if (0 != try os.send(sockfd, "", 0)) unreachable; // send empty data
const recv_bytes = try os.recv(sockfd, &buf, 0);
if (verbose) {
log.info("{d}: recv_bytes={d}, [{s}]", .{time.milliTimestamp(), recv_bytes, buf[0..recv_bytes]});
}
return buf[0..recv_bytes];
}
pub fn main() !void {
// TODO: get parameters from command line options
const verbose = true;
const adr = "127.0.0.1";
const t = try askDaytime(adr, verbose);
// Zig std lib doesn't have date parser yet.
_ = t;
}
実行例
$ ./udp_daytime
info: 1671795853836: recv_bytes=26, [23 DEC 2022 20:44:13 JST
]
このRFCでは返ってくる時刻の文字列のフォーマットは決まっていないと書いてあります。
RFCの最後に書いてありました。
NOTE: For machine useful time use the Time Protocol (RFC-868).
というわけで、RFC-868も試してみることにします。
Time protocol(RFC-868)
こちらは時刻を文字列でなく、32bitの整数で返してくれます。
UDPのtimeサービスを有効にする
timeサービスもデフォルトでは無効になっているので、これを有効にします。
Ubuntu 22.04 x86_64 を使用しています。
/etc/xinetd.d/time-udp
のdisable
の項目をyes
からno
に変更します。
$ sudo sed -i.org '/disable/s/yes/no/' /etc/xinetd.d/time-udp
$ diff -u /etc/xinetd.d/time-udp{.org,}
--- /etc/xinetd.d/time-udp.org 2018-02-06 03:31:09.000000000 +0900
+++ /etc/xinetd.d/time-udp 2022-12-23 15:07:44.867016004 +0900
@@ -9,6 +9,6 @@
protocol = udp
user = root
wait = yes
- disable = yes
+ disable = no
port = 37
}
xinetdをリスタートします。
$ sudo systemctl restart xinetd
UDPのtimeサービスを利用するサンプルプログラム
const std = @import("std");
const os = std.os;
const log = std.log;
const time = std.time;
const UDP_PAYLOADSIZE = 65507;
const TIMESERVER_PORT = 37;
const UNIX_TIME_BASE = 2_208_988_800;
fn queryTime(ipaddr: []const u8, verbose:bool) !u32 {
var buf: [UDP_PAYLOADSIZE]u8 = .{};
const sockfd = try os.socket(os.AF.INET, os.SOCK.DGRAM | os.SOCK.CLOEXEC, 0);
defer os.closeSocket(sockfd);
const addr = try std.net.Address.resolveIp(ipaddr, TIMESERVER_PORT);
try os.connect(sockfd, &addr.any, addr.getOsSockLen());
if (0 != try os.send(sockfd, "", 0)) unreachable; // send empty data
if (4 != try os.recv(sockfd, &buf, 0)) unreachable;
const t = std.mem.readIntBig(u32, buf[0..4]);
if (verbose) {
log.info("{d}: t={d}", .{time.milliTimestamp(), t});
}
// timeserver returns the number of seconds since 00:00 (midnight) 1 January 1900 GMT
return t - UNIX_TIME_BASE;
}
pub fn main() !void {
// TODO: get parameters from command line options
const verbose = true;
const adr = "127.0.0.1";
const t = try queryTime(adr, verbose);
log.info("{d}", .{t});
}
バッファから4バイトのビッグエンディアンの整数を読みだすにはstd.mem.readIntBig()
を使うとすっきりいけます。
time protocolで得られる数値は1900年1月1日00:00 GMTからの秒数です。よく使われるUNIX時間は1970年からの秒数なので、2,208,988,800を引くことで変換しています。
実行例
$ ./udp_time
info: 1671795936570: t=3880784736
info: 1671795936
$ ./udp_time
info: 1671795943186: t=3880784743
info: 1671795943
$ ./udp_time
info: 1671795946606: t=3880784746
info: 1671795946
何度か間隔をおいて実行してみると、Zigのstd.time.milliTimestamp()
で得られるタイムスタンプと秒の部分の桁が一致しているので、正しく動作しているようです。
UNIX時間(エポック秒)から時刻文字列に変換
このようにUNIX時間(エポック秒)を取得することはできたので、そこから時刻文字列に変換して表示したいと思いました。しかし、現在のZigの標準ライブラリ(version 0.10.0)では std.time.epoch
にそれらしいものがあるのですが、どうもタイムゾーンを考慮したローカルタイムにはできないようです。std.lib.tz
にタイムゾーンの情報を扱うコードがあるようなのですがまだ開発途中のような感じでした。
@cInclude("time.h")
としてlibcの関数を呼び出すというアイディアもあるのですが、あとで試して記事にしたいと思います。
Zigのコードからスタティックリンクライブラリを作成してCから呼び出してみた
逆にこのZigのコードをライブラリにして、Cで書いたコードから呼ぶようにしてみました。そうすればlibcの中の関数でUNIX時間を文字列に変換して出力できます。
ビルド
Cコンパイラはデフォルトのcc
でもいけますが、せっかくですのでzig cc
にしてみました。
$ make
zig cc -c -o main.o main.c
zig build-lib -lc -O ReleaseSmall udp_time_lib.zig
zig cc -o main main.o libudp_time_lib.a
zig build-lib
に -O ReleaseSmall
をつけないとリンクのときに以下のエラーが出ました。
./udp_time_lib.zig:11: undefined reference to `__zig_probe_stack'
実行結果
$ ./main
Sat Dec 24 13:43:43 2022
実行ファイルのサイズなど。
$ file ./main
./main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
$ size ./main
text data bss dec hex filename
8500 1416 25 9941 26d5 ./main
$ ldd ./main
linux-vdso.so.1 (0x00007ffe7d5fa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa15bf29000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa15c160000)
Discussion