安装

这里直接使用包管理器提供的版本,不过建议大家使用pip来安装,会少一些坑:

(Debian/Ubuntu) apt-get install uwsgi uwsgi-plugin-python3

或使用pip安装

pip3 install uwsgi

试试看

[demo.py]
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

终端执行:

uwsgi --http-socket 0.0.0.0:8088 --manage-script-name --mount /=demo:app --plugin python3
(如果你是通过pip安装的,可不加 --plugin python3)
uwsgi --http-socket 0.0.0.0:8088 --manage-script-name --mount /[path]=demo:app --plugin python3

访问:

http://localhost:8088/[path]

部署

通常情况下我们的应用是按照工厂模式所编写的,使用我们需要新建一个文件来暴露出app对象:

from application import create_app
app = create_app()
if __name__ == "__main__":
    app.run()

下面来编写一个配置文件,便于配置修改:

[config.ini]

[uwsgi]
# 使用http协议
# http = 0.0.0.0:8081
# 指定工作用户(组)
uid = www-data
gid = www-data
# 主进程,由本进程派生子进程
master = true
# 工作目录
chdir = /var/application
# 插件(使用pip安装的可省略)
plugins = python3
# 入口文件
wsgi-file = app.py
# 指定入口文件的Flask对象
callable = app
# 指定uwsgi的socket路径
socket = /tmp/application.sock
# 进程数
processes = 2
# 线程数
threads = 4
# 缓冲区大小
buffer-size = 32768

配置Nginx

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name _;

    # location = /[path] { rewrite ^ /[path]/; }
    location / { try_files $uri @uwsgi; }
    location @uwsgi {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/application.sock;
    }
}

uwsgi后台运行,你可以直接在命令后加-d,但我这里是新建了个服务:

[/etc/systemd/system/uwsgi.service]

[Unit]
Description=uwsgi application

[Service]
User=www-data
Group=www-data
Type=simple
WorkingDirectory=/var/application
ExecStart=/usr/bin/uwsgi /var/application/config.ini

[Install]
WantedBy=multi-user.target

接下来就是设置开机启动了:

(sudo) systemctl enable uwsgi
(启动)
(sudo) systemctl start uwsgi

常见问题

  1. Nginx报5XX错误
    检查你的uwsgi的运行用户,务必保证你创建的socketNginx有权限读写的。
  2. uwsgino app loaded. going in full dynamic mode
    这个用pip安装的不会出现,需要添加python3插件

更多

https://uwsgi-docs.readthedocs.io/en/latest/
http://nginx.org/en/docs/http/ngx_http_uwsgi_module.html

没有题头。

搭建CA

关于该部分,以后会详细阐述(大概,因为openssl有点难用),如果仍需接着实现本文内容,可以使用XCA这个图形化工具。

https://www.hohnstaedt.de/xca/

已更新,参见:

使用

只需要两行命令(http中):

# 假设你已经开启了https
# 客户端证书所属的根/中间CA证书
ssl_client_certificate /etc/nginx/ssl_cert/email_ca.crt;
# 开启客户端验证
ssl_verify_client on;
# 指定crl列表
ssl_crl /etc/nginx/ssl_cert/email_ca.crl;
#error_page 400 /req_cert.html;

400.jpg

(猫猫来源: https://http.cat/)

重启即可生效。不过只实现了简单的验证,后端无法知道客户端证书的主题信息,这时需要使用ngx_stream_ssl_module模块内置的一些变量,将它附加到头上传给后端即可。

#proxy_set_header ...;
# 客户端证书主题信息
add_header SSL_CLIENT_CERT_DN $ssl_client_s_dn;
# 判断验证状态(SUCCESS)
add_header SSL_CLINET_VERIFY $ssl_client_verify;

21082501.png

21082502.png

可以看到属性是以,分隔的。

ngx_stream_ssl_module模块变量表

(1.11.8起完全支持)

变量名说明
$ssl_cipher返回当前使用的加密套件
$ssl_ciphers返回使用客户端所支持的加密套件(已知名称列出,未知以十六进制显示)
$ssl_client_cert返回pem格式的客户端证书(除第一行外其余行末尾均有制表符)
$ssl_client_fingerprint返回客户端证书的sha1指纹
$ssl_client_i_dn返回客户端证书颁发者主题信息
$ssl_client_raw_cert返回pem格式的客户端证书
$ssl_client_s_dn返回客户端证书使用者主题信息
$ssl_client_serial返回客户端证书序列号
$ssl_client_v_end返回客户端证书截止日期
$ssl_client_v_remain返回客户端证书距离截止的天数
$ssl_client_v_start返回客户端证书颁发日期
$ssl_client_verify返回客户端证书验证状态("SUCCESS"/"FAILED:reason"/"NONE")
$ssl_curves返回客户端支持的ECC算法套件(已知名称列出,未知以十六进制显示)
$ssl_protocol返回已建立的ssl连接的协议
$ssl_server_name返回请求的sni名称
$ssl_session_id返回当前ssl会话标识符
$ssl_session_reusedssl会话复用标识(复用"r"/未复用".")

网上大部分是修改源码来实现,但我不喜欢折腾源码编译,所以使用了一些神奇的方法。

Nginx(Debian:nginx-full)中有两个神奇的指令,add_before_bodyadd_after_body,分别用来在响应内容前后插入内容,所以只要注入到末尾,用Javascript处理就行了。

location / {
    # ......
    add_after_body .uindex.html;
}

在目录下创建一个隐藏文件.uindex.html:

<!-- uindex  -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha512-Dop/vW3iOtayerlYAqCgkVr2aTr2ErwwTYOvRFUpzl2VhCMJyjQF0Q9TjUXIo6JhuM/3i0vVEt2e/7QQmnHQqw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script>
let dirlist = $("pre").html().split("\n");
function getFileInfo(line) {
    line = line.replace('    ', '|');
    line = line.replace('<a href="', '');
    line = line.replace('">', '|');
    line = line.replace('</a>', '|');
    line = line.split('|');
    temp = [];
    for(i in line) {
        if(line[i] != "") temp.push(line[i].replace(/(^\s*)|(\s*$)/g, ""));
    }
    line = null;
    //if(temp.length < 3) return [];
    return temp;
}
function outputHtml(dirlist) {
    let list = [];
    for(i in dirlist) {
        let temp = getFileInfo(dirlist[i]);
        if(temp) list.push(temp);
    }
    let temp = '';
    list.forEach((i) => {
        if(i.length >= 2) {
            if(i.length > 3) {
                temp += `<tr><td><a href="${i[0]}">${i[1]}</a></td><td>${i[2].replace(' -','')}</td><td>`;
                temp += `${i[3]}</td></tr>`;
            } else if(i.length == 2) {
                temp += `<tr><td><a href="${i[0]}">${i[1]}</a></td><td></td><td></td></tr>`;
            } else {
                temp += `<tr><td><a href="${i[0]}">${i[1]}</a></td><td>${i[2].replace(' -','')}</td><td>`;
                temp += `</td></tr>`;
            }
        }
    });
    return `<div class="container"><div class="row"><div class="col-md-12"><table class="table table-striped table-hover">
  <caption class="h3">${$("h1").text()}</caption>
  <thead><tr><th>fileName</th><th>date</th><th>size</th></tr></thead><tbody>${temp}</tbody></table></div></div></div><p style="text-align:center;font-size:14px;color:#dddddd;text-align:center;font-size:14px;color:#dddddd;">&copy; 2021 uindex.</p>`;
}
$(() => {
    //$("body").css({"display": "none"})
    let html = outputHtml(dirlist);
    $("h1").remove();
    $("hr").remove();
    $("pre").remove();
    $("body").append(html);
    //$("body").css({"display": "inline"})
});
</script>

不会正则,所以先替换再分割了一遍,旨在能用就行。使用时会一闪一下,正常。

21081701.png

一般情况下Nginx自带两个有关于鉴权的模块,一个是auth_basic,另一个是auth_request,本文主要介绍auth_request

流程

21080701.png

Nginx的配置

(server段中,也可以做成一个配置文件,通过include引用)

#设置401错误跳转,用于跳转到登录页
location @error401 {
    #这里的url方便我们登录成功的跳转
    return 302 https://auth.example.com/?url=https://$http_host$request_uri;
}
#内部路由
location /auth {
    internal;
    #鉴权服务器地址
    proxy_pass http://127.0.0.1:8888/auth;
    #不传递body内容,当然请求头会被传递
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

(location段中)
location / {
    #指定内部路由与返回401的处理
    auth_request /auth;
    error_page 401 = @error401;
}

鉴权验证

401.jpg
(猫猫来源: https://http.cat/)

Nginx会通过所返回的状态码来判断是否放行请求,返回2XX放行,返回4XX则拦截请求,我们可以通过设置cookie来保证登录态的持续:

(Python3)
from sanic import Sanic, html, json, redirect
app = Sanic(__name__)

password = "19260817"

@app.get("/")
async def index(request):
    return html("<form action="" method="POST"><input name="pw"><button type="submit">check</button></form>")

@app.post("/")
async def vindex(request):
    pw = request.form.get("pw", None)
    url = request.args.get("url", None)
    if pw == password:
        res = redirect(url)
        res.cookies["login"] = True
        res.cookies["login"]["httponly"] = True
        #全域cookie
        res.cookies["login"]["domain"] = ".example.com"
        return res
    return redirect("/")

@app.get("/auth")
async def auth(request):
    login = request.cookies.get("login", False)
    if login:
        return json({"code": 200}, 200)
    return json({"code": 401}, 401)

if __name__ == "__main__":
    app.run(port=8888)

N久以前

N久前遇到的问题,当时简单的解决了一下,现在在写一个StorageBucket,需要有一个Demo,所以又回过头来研究。
来看看之前是这么解决的:

let slice_size = 1024 * 1024; // 1MB 分块大小
let totalSliceNum = 0; //总分块数量
let now = 0; //当前分块
let file = document.getElementById("file").files[0];
totalSliceNum = Math.ceil(file.size / slice_size);

print("size|" + file.size);
print("name|" + file.name);
print("type|" + file.type);
print("slice|" + totalSliceNum);

while(now < totalSliceNum) {
   let s = now * slice_size;
   let e = (now + 1) * slice_size;
   if(e > file.size) {
       e = file.size;
   }
   let chunk = file.slice(s,e); //切取一块
   //jquery.ajax 发送文件块数据
   now += 1;
}

看起来没错,网上也大部分都是这么写的,按顺序一块一块上传,但实际上使用你会发现,浏览器卡爆了,页面完全不渲染,幸好是给同学用的,让他F12在控制台看进度。
我们浏览器中的js是单线程的,我们分片时会卡死在file.slice这里,因为它要从文件中读出一块,它卡死在这占据了整个线程,导致整个页面无法渲染。不过我们可以用FileReader来异步读取,而不是等他读取,卡到无法渲染,所以有了下面的版本:

let slice_size = 1024 * 1024; // 1MB 分块大小
let totalSliceNum = 0; //总分块数量
let now = 0; //当前分块
let file = document.getElementById("file").files[0];
totalSliceNum = Math.ceil(file.size / slice_size);

print("size|" + file.size);
print("name|" + file.name);
print("type|" + file.type);
print("slice|" + totalSliceNum);

while(now < totalSliceNum) {
   let s = now * slice_size;
   let e = (now + 1) * slice_size;
   if(e > file.size) {
       e = file.size;
   }
   let chunk = file.slice(s,e); //切取一块
   let reader = new FileReader();
   reader.readAsArrayBuffer(chunk);
   reader.onload = function() {
       //当块加载好了,会执行这个函数
       //console.log(new Blob([reader.result]));
       //jquery.ajax 发送文件块数据
       //这里要传递当前分块与总块数量,方便后端合成,因为块不再是顺序上传
   }
   now += 1;
}

效果

21072601.png

最后

博客很少更了,我现在不像以前一样那么有动力,不过...

???

帮别人提出的解决方案(吹B过头),临时突击翻文档学移动开发写的,虽然没有多少行。

得到歌曲名与作者

这里用到了监听器,得到通知栏的网易云的播放控件,读取歌曲名与作者。

来点权限:

<service android:name=".NotificationListener"
        android:label="DemoApp"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
        <intent-filter>
            <action android:name="android.service.notification.NotificationListenerService" />
        </intent-filter>
        <meta-data
            android:name="android.service.notification.default_filter_types"
            android:value="1,2">
        </meta-data>
        <meta-data
            android:name="android.service.notification.disabled_filter_types"
            android:value="2">
        </meta-data>
</service>

监听器类:

package com.test.demoapplication;

import android.app.Notification;
import android.content.Intent;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.TextView;

public class NotificationListener extends NotificationListenerService {
    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        //网易云
        if(!sbn.getPackageName().equals("com.netease.cloudmusic")) return;

        //去除一般性通知
        if(!sbn.getNotification().extras.getString(Notification.EXTRA_TEXT,"").equals("")) return;

        //取得通知栏组件
        //另: 网易云支持安卓的MediaBroswerService,这个以后再说
        ViewGroup view = (ViewGroup) sbn.getNotification().bigContentView.apply(this, null);

        String author = (String) ((TextView) (((ViewGroup) view.getChildAt(2))).getChildAt(1)).getText();
        String songName = (String) ((TextView) (((ViewGroup) view.getChildAt(2))).getChildAt(0)).getText();

        Log.i("Debug",songName + "|" + author);

        super.onNotificationPosted(sbn);
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        if(!sbn.getPackageName().equals("com.netease.cloudmusic")) return;
        super.onNotificationRemoved(sbn);
    }
}

控制音乐播放

这个用模拟按键来实现,是通用的,不过要注意开启线控。

//模拟按键要在线程中执行,否则会卡死
class UPressKey extends Thread {
    private int keyCode;
    public UPressKey(int keyCode) {
        this.keyCode = keyCode;
    }

    @Override
    public void run() {
        Instrumentation mInst = new Instrumentation();
        mInst.sendKeyDownUpSync(this.keyCode);
    }
}


//以下为控制按键
new UPressKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS).start();
new UPressKey(KeyEvent.KEYCODE_VOLUME_UP).start();
new UPressKey(KeyEvent.KEYCODE_MEDIA_PLAY).start();
new UPressKey(KeyEvent.KEYCODE_VOLUME_DOWN).start();
new UPressKey(KeyEvent.KEYCODE_MEDIA_NEXT).start();
new UPressKey(KeyEvent.KEYCODE_MEDIA_PAUSE).start();

实现

24 185911.jpg

如题,玩了这么久Minecraft,开了那么多服,总该要会写服务器插件吧。

环境部署

JDK要求1.8,IDE随意,另外还要有Spigot的服务端,将服务端以库的形式导入到IDE即可。
比方说IDEA:文件 > 项目结构 > 项目设置 > 模块 来导入,导入后勾选导出选项。
13160207.png

入口类

现在我们随意取个名字,新建个类:

package com.yeziruo.mc.test;

import org.bukkit.plugin.java.JavaPlugin;

public class MainClass extends JavaPlugin {
    //服务器开启时执行
    public void onEnable() {
        super.onEnable();
    }

    //服务器关闭时执行
    public void onDisable() {
        super.onDisable();
    }
}

接着新建一个叫plugin.yml的文件,填写如下内容:

//名称,入口点,版本(打包时请删除本行说明)
name: Test
main: com.yeziruo.mc.test.MainClass
version: 0.0.1

接着打包,不同IDE方式不同,以IDEA为例:文件 > 项目结构 > 项目设置 > 工件 里添加。
13161101.png
并点击绿色箭头的加号,添加模块输出与plugin.yml文件,之后点击构建菜单下的构建工件,等打包完成后扔进服务端测试即可。

监听器

上面的例子只会在控制台有有一行输出罢了,下面我们做一个简单的欢迎插件,当玩家进入服务器后向其发送欢迎信息。
新建一个类,名称随意。

package com.yeziruo.mc.test;

//颜色
import org.bukkit.ChatColor;
//玩家对象
import org.bukkit.entity.Player;
//监听器头
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
//玩家加入事件
import org.bukkit.event.player.PlayerJoinEvent;

public class Welcome implements Listener {
    @EventHandler
    //on加监听器名去Event
    //监听器均在org.bukkit.event下,具体用法请翻阅文档
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        player.sendMessage(ChatColor.GREEN + "欢迎加入," + player.getName() + "!");
    }
}

回到入口类,在onEnable中添加一行:

this.getServer().getPluginManager().registerEvents(new Welcome(), this);

第一个个命令

同样新建一个类,名称随意。

package com.yeziruo.mc.test;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class PlayerCommand implements CommandExecutor {
    public boolean onCommand(CommandSender commandSender, Command command, String s, String[] strings) {
        //commandSender 执行实体(比方说玩家) strings 参数
        //if(strings.length == 0) return false;
        commandSender.sendMessage("PONG!");
        //成功执行返回true,否则返回false
        return true;
    }
}

当然,同样要注册命令,首先在plugin.yml下面添加:

//记得删除注释
commands:
  //命令名
  ping:
    //介绍
    description: Return PONG!
    //用法
    usage: /ping

然后又回到入口类,在onEnable中添加一行:

//命令节点名与执行类
this.getCommand("ping").setExecutor(new PlayerCommand());

同样打包,在服务器中输入/ping即可看到回复。

最后

SpigotMC JavaDoc:
https://hub.spigotmc.org/javadocs/spigot/index.html

如果要实时计算或产生效果,可以建个线程来跑。

摸了!
博客现在在缓慢更新中,没有嗝屁!