🕰️

ZigでUDPでtimeserverから現在時刻を取得するサンプルプログラム

2022/12/24に公開

本記事はZigアドベントカレンダー24日目の記事です。

前回の記事でZigでUDP通信ができるようになったので実際のサービスに使ってみることにしました。

Daytime protocol(RFC-867)

https://datatracker.ietf.org/doc/html/rfc867

とっても短いRFCです。簡単に読める!
TCPまたはUDPでポート13に何か書き込むとその返事として現在時刻の文字列が返ってきます。

UDPのdaytimeサービスを有効にする

daytimeサービスは実用的なものではなく疎通テストなどにしか使用されないので、デフォルトでは無効になっています。以下の手順でこれを有効にします。

Ubuntu 22.04 x86_64 を使用しています。
daytimeサービスはxinetdから起動されるので、それが入っていることを確認します。

$ sudo apt install xinetd

/etc/xinetd.d/daytime-udpdisable = yesnoに変更します。

$ 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サービスを利用するサンプルプログラム

udp_daytime.zig
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)

https://datatracker.ietf.org/doc/html/rfc868
こちらは時刻を文字列でなく、32bitの整数で返してくれます。

UDPのtimeサービスを有効にする

timeサービスもデフォルトでは無効になっているので、これを有効にします。
Ubuntu 22.04 x86_64 を使用しています。

/etc/xinetd.d/time-udpdisableの項目を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サービスを利用するサンプルプログラム

udp_time.zig
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