简易Web QoTD服务

简单地说,我想在我的网页上随机显示一句话,于是便顺手搞了一个这东西。之所以不用公共服务,是因为公共服务的内容无法控制——万一我想循环播放毛主席语录呢。

使用

虽然估计没人用。

API

  • 地址:https://scsfdx.org/api/qotd
  • 方法:随便
  • 参数:没有
  • 返回:纯文本形式的一行语录
  • 示例:如下
user@localhost:~$ curl https://scsfdx.org/api/qotd
千芳茉花丛雨行,恋春芦姿荡旅莹。万情何待穗织齐?花舞娜飞蕾挥尽。
user@localhost:~$

随便用,要加语录请找站长。代码什么的还没来得及收拾,好久开个Gitea或者上Github再说。

自建

  1. 先把下面的代码全部复制粘贴,编译一个jar出来,设为jqotd.jar
  2. 然后以UTF-8新建文本文件,设为qoutes.txt,一行一句语录,存盘。
  3. 想一个端口,设为17,java -jar jqotd.jar 17 quotes.txt
  4. 打开nginx配置文件,找到你想要添加API的网站或者说server。
    proxy_pass http://localhost:17;
  5. 好了。

设计

最早打算直接AJAX从标准QoTD服务器上加载内容。然而那东西没HTTP头,估计只有IE5可以这样玩(我试过类似的),我不得不自己实现一个HTTP服务。有了服务端,剩下的事情就是AJAX。

服务端设计

最早是拿PHP顺手糊的,但是PHP没法持久化,每读一次要遍历一次文件,太消耗了。然后我考虑用C,结果呢,全机房就我不会耍指针了,算了算了。再然后考虑C#,我的确把C#的标准QoTD乃至echo, discard, daytime那一套都写出来了,甚至还是全异步实现——可是这小破服务器不仅是Linux,.NET Core还谜之没法用。算了算了,Java走起,真香。服务端代码如下:

// Main.java
package org.scsfdx;

public class Main {
    private static void help() {
        System.err.println("Usage: java -jar " + System.getProperty("sun.java.command") + " PORT QUOTE_PATH");
        System.exit(255);
    }

    public static void main(String[] args) {
        if (args.length < 2) help();
        Quotes.Load(args[1]);
        try {
            Server.start(Integer.parseInt(args[0]));
        } catch (NumberFormatException e) {
            help();
        }
        try {
            while (true) Thread.sleep(Integer.MAX_VALUE);
        } catch (Exception e) {
        }
    }
}
// Quotes.java
package org.scsfdx;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Scanner;

class Quotes {
    private static List<byte[]> quotes = new ArrayList<byte[]>();
    private static int sz = 0;
    private static Random r;

    static void Load(String file) {
        r = new Random();
        sz = 0;
        try {
            InputStream in = new FileInputStream(file);
            Scanner sc = new Scanner(in);
            while (sc.hasNext()) {
                quotes.add((sc.nextLine() + "\r\n").getBytes());
                sz++;
            }
        } catch (FileNotFoundException e) {
            quotes.add("无可奉告".getBytes());
        }
    }

    static byte[] getString() {
        return quotes.get(r.nextInt(sz));
    }
}
// Server.java
package org.scsfdx;

import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class Server implements Runnable {

    private static byte[] HTTPHDR = (
            "HTTP/1.0 200 OK\r\n" +                                     // Standard header
                    "Server: JQotd/0.1\r\n" +                                   // Server name
                    "Content-Type: text/plain; charset=utf-8\r\n" +             // We use UTF8
                    "Cache-Control: no-cache, no-store, must-revalidate\r\n" +  // Disable cache
                    "Pragma: no-cache\r\n" +
                    "Expires: 0\r\n" +
                    "Connection: close\r\n" +                                   // Of course.
                    "\r\n"                                                      // Write empty line to end http header
    ).getBytes();

    private Socket client;

    private Server(Socket s) {
        this.client = s;
    }

    public static void start(int port) {
        try {
            ServerSocket ss = new ServerSocket(port, 256, InetAddress.getLoopbackAddress());
            while (true) {
                Socket c = ss.accept();
                c.setSoLinger(true, 1); // Fast shutdown
                new Thread(new Server(c)).start();
            }
        } catch (Exception e) {

        }
    }

    @Override
    public void run() {
        try {
            OutputStream cos = client.getOutputStream();
            client.shutdownInput();         // Close unused down link
            cos.write(HTTPHDR);             // Send header
            cos.write(Quotes.getString());  // Send Quote
            client.shutdownOutput();        // Close up link
            client.close();                 // Close whole socket
        } catch (Exception e) {             // Ignore all exception
        }
    }
}

实现HTTP的办法比较暴力:对于任何传入连接,忽略传入内容,先写HTTP协议头,然后写一句话,然后关连接。考虑到这个服务只为HTTP客户端设计,那么这个服务也就只需要返回HTTP形式的内容。在这种情况下,客户端的输入是无所谓的。当然,为了实现绑域名套SSL这种需求,我在这服务前面套了一个nginx,nginx按照HTTP反代的一般配法来就是。

客户端设计

原理就那么回事,AJAX走起。主要问题是我不好意思引入jQuery。以前WordPress 2017主题内嵌一个jQuery,可以蹭一下。新版本主题没了,只有原生JS走起。同样,上代码:

<div id="_my_qotd"></div>
<script>
(() => {
    const _qotd_div = document.getElementById("_my_qotd");
    const _qotd_ajax = async (url) => {
        let xhr = new XMLHttpRequest();
        let resolve;
        let reject;
        let pm = new Promise((_resolve, _reject) => {
            resolve = _resolve;
            reject = _reject;
        });
        if (!xhr) reject(new Error("No AJAX support"));
        xhr.onreadystatechange = () => {
            if (xhr.readyState === XMLHttpRequest.DONE)
                (xhr.status === 200) ? resolve(xhr.responseText) : reject("AJAX fail");
        }
        xhr.open("GET", url);
        xhr.send();
        return pm;
    }
    const _update_qotd = async () => _qotd_div.innerHTML = await _qotd_ajax("api/qotd/");
    _update_qotd();
    _qotd_div.addEventListener("click", _update_qotd);
})()
</script>

比较值得一提的是async await方式的AJAX,其他的都几乎是IE6时代传下来的祖宗之法。

简单的说,在AJAX函数内部new一个Promise,然后按照祖宗之法来,成功的时候调用Promise内部的resolve,失败了调reject。最后直接返回那个Promise。然后你就可以let text = await asyncajax(url)了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注