2022
mv 2021/ 2022/
mv 2021/ 2022/
这里直接使用包管理器提供的版本,不过建议大家使用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
Nginx
报5XX错误uwsgi
的运行用户,务必保证你创建的socket
是Nginx
有权限读写的。uwsgi
报no 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
没有题头。
关于该部分,以后会详细阐述(大概,因为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;
(猫猫来源: 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;
可以看到属性是以,
分隔的。
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_reused | ssl 会话复用标识(复用"r"/未复用".") |
网上大部分是修改源码来实现,但我不喜欢折腾源码编译,所以使用了一些神奇的方法。
Nginx(Debian:nginx-full)中有两个神奇的指令,add_before_body
与add_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;">© 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>
不会正则,所以先替换再分割了一遍,旨在能用就行。使用时会一闪一下,正常。
一般情况下Nginx自带两个有关于鉴权的模块,一个是auth_basic
,另一个是auth_request
,本文主要介绍auth_request
。
(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;
}
(猫猫来源: 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久前遇到的问题,当时简单的解决了一下,现在在写一个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;
}
博客很少更了,我现在不像以前一样那么有动力,不过...
帮别人提出的解决方案(吹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();
如题,玩了这么久Minecraft,开了那么多服,总该要会写服务器插件吧。
JDK要求1.8,IDE随意,另外还要有Spigot的服务端,将服务端以库的形式导入到IDE即可。
比方说IDEA:文件 > 项目结构 > 项目设置 > 模块 来导入,导入后勾选导出选项。
现在我们随意取个名字,新建个类:
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为例:文件 > 项目结构 > 项目设置 > 工件 里添加。
并点击绿色箭头的加号,添加模块输出与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
如果要实时计算或产生效果,可以建个线程来跑。
摸了!
博客现在在缓慢更新中,没有嗝屁!