HTTP/HTTPSから使える時刻同期プロトコルを作ってみた

# はじめに
こんにちは！:wave:
ゆっくり実況者のだいちまるです！:v:

早速ですが、みなさんこんなこと思ったことはありませんか？
＿人人人人人人人人人人人人人人＿
＞　クソ楽に時刻同期したい！　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣
まぁ時刻同期でなくても取得とかでもいいです
要するに(?)クライアントサイドのJavaScriptから正確な時刻を取得したりするのって、APIとかないと難しいよね？！
特定のAPIに頼るのも不安だし誰でも使えるようプロトコル作っちゃおう！ってお話です。
この記事に書いてあるコードはご自由に使っていただいて構いません！

# 完成物 (EpochLink)
作るまでの流れより先に完成物だけおいておきます！
JSONを使用して時刻を取得できるプロトコルです。(誰でも作れるような簡単仕様)
HTTP/HTTPS通信で取得できるのでJavaScriptからも簡単に使用できます！(なんならこれのためだけに[ライブラリ](https://epochlink.daichimarukana.com/epochlink_js.html)も作りました)
**[EpochLink](https://epochlink.daichimarukana.com/)**
取得できるデータはこんな感じ↓
```json:EpochLinkの仕様で取得できるJSON
{
    "protocol": "EpochLink",
    "version": "1.0.0",
    "address": "https://elp.emptybox.win/",
    "time_zone": "UTC",
    "iso8601_time": "2024-05-22T17:01:35+00:00",
    "unix_time_ms": 1716397295633,
    "unix_time": 1716397295
}
```

# 仕様
EpochLinkはこんな仕様になっています。
```json:取得できるデータ
{
    "protocol": "EpochLink",
    "version": "1.0.0",
    "address": "https://elp.emptybox.win/",
    "time_zone": "UTC",
    "iso8601_time": "2024-05-22T17:01:35+00:00",
    "unix_time_ms": 1716397295633,
    "unix_time": 1716397295
}
```
<table>
    <tr>
        <th width="30%">Name</th>
        <th width="50%">Data</th>
        <th width="20%">Type</th>
    </tr>
    <tr>
        <td>protocol</td>
        <td>プロトコル名</td>
        <td>文字列/String</td>
    </tr>
    <tr>
        <td>version</td>
        <td>プロトコルのバージョン</td>
        <td>文字列/String</td>
    </tr>
    <tr>
        <td>address</td>
        <td>サーバーのアドレス</td>
        <td>文字列/String</td>
    </tr>
    <tr>
        <td>time_zone</td>
        <td>時刻のタイムゾーン</td>
        <td>文字列/String</td>
    </tr>
    <tr>
        <td>iso8601_time</td>
        <td>ISO8601に準拠した時刻</td>
        <td>文字列/String</td>
    </tr>
    <tr>
        <td>unix_time_ms</td>
        <td>ミリ秒を含むunix時刻</td>
        <td>数値型/int</td>
    </tr>
    <tr>
        <td>unix_time</td>
        <td>ミリ秒を含まないunix時刻</td>
        <td>数値型/int</td>
    </tr>
</table>

# 時刻同期(取得)のしくみ
EpochLinkではサーバーはISO8601に対応した時刻とUNIX時刻(ミリ秒もあるよ)を配信します！
で、クライアント側がサーバーにリクエストを送ってJSONをもらいます。
もらったJSONの中にUNIX時刻があるのでそれを元にその地域ごとの形式に変換して現在時刻として扱うわけです。
もちろん少しながらズレはありますけどね...

EpochLinkでは、現状NTPなどにある階層構造はありません。
また、サーバーも多分一つしかありません。(どなたでも構築いただけます)

### 時刻補正のしくみ
時刻をやりとりするにあたって、やはりタイムラグなどで時刻のずれはでてしまいます！
なので、EpochLinkでは時刻補正機能をクライアント側で実装することをおすすめしています。
サンプルコードとJSライブラリはすでに時刻補正機能実装済みです。

:::note warn
もし、めちゃくちゃ超高精度な時刻補正機能を使いたいのなら、NTPなどのより正確性に優れたプロトコルを使ってください！
:::

で、仕組みについてですが、こんな仕組みになっています(クライアントサイド)

    1. EpochLinkサーバーにリクエストする直前の時刻を記録
    2. EpochLinkからのレスポンスが返ってきた時刻を記録
    3. 2と3の差を求め、2で割る(往復分あるため2で割る)
    4. EpochLinkサーバーのレスポンスに含まれている時刻に3で求めた差を足す

もちろん正確性を保証するものではありませんが、これである程度の補正を行うことはできます！

![こんな感じ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3227805/a4e5ed8d-52c3-3466-f990-878d0f388dda.png)

### セキュリティ
現状EpochLink側で実装しているセキュリティ対策はありません。
完全にHTTPSにお任せしています。(将来的にはセキュリティ対策するかも？)

# サーバーの実装
サーバーの実装です。
サーバーの時計が狂っていると配信する時刻も狂ってしまいます！
サーバーの時計を定期的にあわせる必要がありますね

### Node.js
```javascript:Node.js
var port = 8080

var http = require('http');
var server = http.createServer(function(request, response){
    response.writeHead(200,{'Content-Type': 'application/json; charset=utf-8'},{'Access-Control-Allow-Origin': '*'});
    var now = new Date();
    time_json = {
        "protocol": "EpochLink",
        "version": "1.0.0",
        "address": request.protocol + '://' + request.headers.host + request.url,
        "time_zone": "UTC",
        "iso8601_time": now.toISOString(),
        "unix_time_ms": now.getTime(),
        "unix_time": Math.floor(now.getTime() / 1000)
    }
    response.end(JSON.stringify(time_json));
})
server.listen(port);
console.log("Serving on port "+port+"...");
```

### php
```php:php
<?php
header("Content-Type: application/json");
header("charset=utf-8");
header("Access-Control-Allow-Origin: *");

date_default_timezone_set('UTC');//Set UTC
$date = new DateTime();

$response = [
    "protocol" => "EpochLink",
    "version" => "1.0.0",
    "address" => (empty($_SERVER['HTTPS']) ? 'http://' : 'https://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
    "time_zone" => date_default_timezone_get(),
    "iso8601_time" => $date->format(DateTime::ATOM),
    "unix_time_ms" => $date->getTimestamp() * 1000 + round($date->format('u') / 1000),
    "unix_time" => (int)$date->format('U')
];

echo json_encode($response, JSON_UNESCAPED_UNICODE);
?>
```

### Python
```python:Python
from wsgiref.simple_server import make_server
import json
import datetime
import time

port = 8080

def timejson(port):
    now = datetime.datetime.now(datetime.timezone.utc)#UTC Date

    timedata = {
        "protocol": "EpochLink",
        "version": "1.0.0",
        "address": "http://localhost:"+str(port)+"/",
        "time_zone": "UTC",
        "iso8601_time": now.isoformat(timespec='seconds'),
        "unix_time_ms": int(time.time() * 1000),
        "unix_time": int(time.time())
    }
    return timedata

def app(environ, start_response):
    status = '200 OK'
    headers = [
        ('Content-type', 'application/json; charset=utf-8'),
        ('Access-Control-Allow-Origin', '*'),
    ]
    start_response(status, headers)

    return [json.dumps(timejson(port)).encode("utf-8")]

with make_server('', port, app) as httpd:
    print("Serving on port "+str(port)+"...")
    httpd.serve_forever()
```

# クライアントの実装
クライアントの実装です。
JSONをパースして時刻を取り出し、補正するだけなので、とても簡単な処理になります！

JavaScriptのは少し長いのでここには書きません。([EpochLink](https://epochlink.daichimarukana.com/)のEpochLink.jsってやつです。サンプルコードにも含まれてます)

### php
```php:php
<?php
date_default_timezone_set('UTC');//Set UTC

$start_get_dt = (int)substr(str_replace('.', '', microtime(true)),0,13);//Get Start Time

$url = "https://elp.example.com/";//EpochLinkServer Address
$json = file_get_contents($url);

$end_get_dt = (int)substr(str_replace('.', '', microtime(true)),0,13);//Get End Time

$data = json_decode($json, true);
$now_unix_ms = $data["unix_time_ms"] + (($end_get_dt - $start_get_dt) / 2);

echo date('Y/m/d H:i:s', (int)substr($now_unix_ms,0,10));
?>
```

### Python
```python:Python
import time
import requests

url = "https://elp.example.com/"#EpochLinkServer Address

start_get_dt = int(time.time() * 1000)

response = requests.get(url)
json_data = response.json()

end_get_dt = int(time.time() * 1000)

now_unix_ms = json_data["unix_time_ms"] + ((end_get_dt - start_get_dt) / 2)

print(time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(now_unix_ms // 1000)))
```

# おわりに
いかがでしたか？
超簡単なこの時刻同期プロトコルは、完全に自己満足の範囲で作成したものですが、NTPのstratum 1サーバーへの負荷を減らしたり、NTPが使用できない環境で簡単に時刻同期をするのに使用できると考えています。

正確なWeb時計サイトを作成したりするのにも使用できます

また、まだありませんが、複数のサーバーから時刻を取得して平均値を出したりすれば、より正確な時刻を求めることもできますし、様々な使い方があると思います！

もし興味を持っていただけたらこちらのサンプルコードやサーバーについてコードを配布しているサイトもご確認いただけると嬉しいです！
どなたでも自由に使っていただけるサーバーも公開しています。
[EpochLink](https://epochlink.daichimarukana.com/)
また、このサイトで配布しているコードはすべてNYSLライセンスで、どなたでもEpochLinkサーバーを立てていただくことも可能です。

最後までお読みいただきありがとうございました！
EpochLinkについて改善点などありましたら是非コメントなどしていただけると幸いです！
それではまた！