分类 编程/笔记 下的文章

如题,玩了这么久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

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

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

由MaxMind提供,有ASN,国家与市三种类型,支持IPv4与IPv6,使用mmdb或CSV格式分发。
注意:自2019年12月30起,需要注册账号下载。
https://dev.maxmind.com/geoip/geoip2/geolite2/

Python的使用

安装官方提供的模块:

pip3 install geoip2

示例:

import geoip2.database
gi = geoip2.database.Reader('GeoLite2-Country.mmdb')
gn = gi.country('1.1.1.1')
#city,country,asnisp
#print(gn)
print(gn.country.names['zh-CN'])
print(gn.country.iso_code)
#输出:
澳大利亚
AU

Nginx的使用

Nginx需要编译Geoip2模块(有Geoip模块,但只适用旧版的dat格式)。
https://github.com/leev/ngx_http_geoip2_module

(http)
geoip2 /etc/nginx/geoip2/GeoLite2-Country.mmdb {
    #自动重载
    auto_reload 5m;
    $geoip2_metadata_country_build metadata build_epoch;
    #国家代码
    $geoip2_country_code default=US country iso_code;
    #国家名称
    #$geoip2_country_name country names zh-CN;
}
if($geoip2_country_code != CN) {
    deny all;
}

引入

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lrsjng.jquery-qrcode/0.18.0/jquery-qrcode.min.js"></script>

快速使用

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" name="viewport" content="width=device-width,initial-scale=1" />
    <title>QRCode</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/lrsjng.jquery-qrcode/0.18.0/jquery-qrcode.min.js"></script>
</head>
<body>
    <div id="qrcode_demo"></div>
    <script>
        $(function(){
            $('#qrcode_demo').qrcode({text:"https://blog.yeziruo.cn/"});
        });
    </script>
</body>
</html>

参数

render: "table" 或 "canvas"(默认)
text: 文本内容
width: 256 宽
height: 256 高
background: "#ffffff" 背景颜色
foreground: "#66ccff" 前景颜色
typeNumber: -1 计算模式(暂不清楚作用)
correctLevel: QRErrorCorrectLevel.H 纠错等级(不建议指定,否则将不会渲染)
    QRErrorCorrectLevel.L,  (7%)
    QRErrorCorrectLevel.M, (15%)
    QRErrorCorrectLevel.Q, (25%)
    QRErrorCorrectLevel.H, (30%)

中文支持

使用该插件生成二维码,如果内容包含中文,则需要将Unicode(UTF-16)转为UTF-8。

function utf16to8(str) {
    var out, i, len, c;
    out = "";
    len = str.length;
    for(i = 0; i < len; i++) {
        c = str.charCodeAt(i);
        if ((c >= 0x0001) && (c <= 0x007F)) {
            out += str.charAt(i);
        } else if (c > 0x07FF) {
            out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
            out += String.fromCharCode(0x80 | ((c >>  6) & 0x3F));
            out += String.fromCharCode(0x80 | ((c >>  0) & 0x3F));
        } else {
            out += String.fromCharCode(0xC0 | ((c >>  6) & 0x1F));
            out += String.fromCharCode(0x80 | ((c >>  0) & 0x3F));
        }
    }
    return out;
}

网上有使用PyMouse,PyKeyboard,PyUserInput(前两者的整合,不活跃)的,但发现我并不适用,Pip都装不上,所以寻着PyUserInput的Readme文件找到了Pynput这个库。
这是一篇水文章。

pip install pynput

键盘

from pynput.keyboard import Key,Controller
keyboard = Controller()

#dir(Key) 功能键
#['__class__', '__doc__', '__members__', '__module__', 'alt', 'alt_l', 'alt_r', 'backspace', 'caps_lock', 'cmd', 'cmd_r', 'ctrl', 'ctrl_l', 'ctrl_r', 'delete', 'down', 'end', 'enter', 'esc', 'f1', 'f10', 'f11', 'f12', 'f13', 'f14', 'f15', 'f16', 'f17', 'f18', 'f19', 'f2', 'f20', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'home', 'insert', 'left', 'media_next', 'media_play_pause', 'media_previous', 'media_volume_down', 'media_volume_mute', 'media_volume_up', 'menu', 'num_lock', 'page_down', 'page_up', 'pause', 'print_screen', 'right', 'scroll_lock', 'shift', 'shift_r', 'space', 'tab', 'up']

#按下按键
keyboard.press(Key.space)
#释放按键
keyboard.release(Key.space)
#等待按下按键
with keyboard.pressed(Key.shift):
    keyboard.press('a')
    keyboard.release('a')

鼠标

from pynput.mouse import Button,Controller
mouse = Controller()
#读取位置
print(mouse.position)
#设置一个位置
mouse.position = (1926,2020)
#相对移动
mouse.move(88,-88)
#鼠标按键
mouse.press(Button.left)
mouse.release(Button.left)
#双击
mouse.click(Button.left,2)
#滚轮(向下滚动2格)
mouse.scroll(0,2)

另有监控键盘和鼠标的官方例程,不过我用不上,所以就不复制到文章里了:

https://pynput.readthedocs.io/en/latest/mouse.html#monitoring-the-mouse
https://pynput.readthedocs.io/en/latest/keyboard.html#monitoring-the-keyboard

应用

可以写一个Web宏键盘:
Screenshot_2020-07-25.jpg

Flask-Mail是Flask的邮件扩展,能让我们以简单的方式方式邮件。

安装

pip3 install flask_mail

配置

MAIL_SERVER : 服务器地址,默认为 ‘localhost’
MAIL_PORT : 服务器端口,默认为 25
MAIL_USE_TLS : 使用TLS,默认为 False
MAIL_USE_SSL : 使用SSL,默认为 False
MAIL_DEBUG : 是否打开Debug,默认为 app.debug
MAIL_USERNAME : 用户名,默认为 None
MAIL_PASSWORD : 密码(授权码),默认为 None
MAIL_DEFAULT_SENDER : 默认发送者,默认为 None
MAIL_MAX_EMAILS : 重新连接前发送邮件最大数目,默认为 None
MAIL_SUPPRESS_SEND : 启用发送(True为不发送),默认为 app.testing
MAIL_ASCII_ATTACHMENTS : 附件文件名转换为ASCII,默认为 False

示例(同步)

from flask import Flask
from flask_mail import Mail,Message
app = Flask(__name__)

#app.config['MAIL_DEBUG'] = True
app.config['MAIL_SUPPRESS_SEND'] = False
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = '你的邮箱'
app.config['MAIL_PASSWORD'] = '密码(授权码)'
app.config['MAIL_DEFAULT_SENDER'] = '发件人邮箱'
mail = Mail(app)

@app.route('/')
def index():
    msg = Message(subject="标题",sender="发送者邮箱",recipients=[接收者邮箱1,接收者邮箱2...])
    #发送HTML或文本信息
    msg.html = '<h1>hello</h1>'
    #msg.body = 'hello'
    #发送
    mail.send(msg)
    return 'index'

if __name__ == '__main__':
    app.run('0.0.0.0',8080)

异步解决

#省略上面的配置和路由
#(下面代码在路由中)
from threading import Thread
from flask import current_app
def send_mail(app,msg):
    #应用上下文管理器
    with app.app_context():
        mail.send(msg)
#启用线程来异步发送,传入app实例与msg对象
thr = Thread(target = send_mail, args = [app,msg])
thr.start()

在工厂模式下没有app实例怎么办,只需要在线程启动前添加这样一行来获取:

app = current_app._get_current_object()

关于current_app._get_current_object()的讨论:
https://segmentfault.com/q/1010000005865632/a-1020000005865704
Flask_Mail中文文档:
http://www.pythondoc.com/flask-mail/index.html

Select适用于类UNIX系统,可用于Socket,File等有文件描述符的动态轮询。

服务端

#Py3环境下,现在大部分迁移到Py3了
import socket
import select
#监听配置
s = socket.socket()
s.bind(('0.0.0.0',8500))
s.listen(1024)
#连接列表,包括服务器
conn_socket = [s,]
#存储可写连接
write_socket = []
while True:
    #可读列表,可写列表,错误列表,超时时间(单位秒,空为阻塞)
    read_list,write_list,error = select.select(conn_socket,write_socket,[],1)
    #轮询开始,将服务器句柄或客户句柄交给内核轮询,当所轮询句柄有变化时,会出现在read_list或error中
    #程序提交 -> 内核轮询 -> 程序操作
    print(conn_socket)
    for client in read_list:
            if client == s:
                    #接收一个新客户,存入连接列表
                    conn,addr = client.accept()
                    conn_socket.append(conn)
            else:
                    #接收客户数据
                    try:
                            msg = client.recv(1024)
                    except Exception as ex:
                            #客户端断线错误
                            conn_socket.remove(client)
                            write_socket.remove(client)
                    else:
                            #打印接收数据,并添加到可写列表
                            print(msg)
                            write_socket.append(client)
    for client in write_list:
            #向可写列表中的客户发送消息,并从可写列表移除,直到再次客户发送消息
            client.sendall(bytes(('2hello2').encode('utf-8')))
            write_socket.remove(client)
    for client in error:
            #移除发生错误的客户端
            write_socket.remove(client)
            conn_socket.remove(client)

客户端

import socket
s = socket.socket()
s.connect(('127.0.0.1', 8500))
while True:
    i = input('> ')
    s.sendall(bytes(i, encoding='utf-8'))
    rx = str(s.recv(1024),encoding='utf-8')
    print(rx)
s.close()

不足之处

  1. 最多1024的文件描述符(即1024个连接)
  2. 当连接客户增加时,轮询一次会消耗更多时间(程序空间与内核空间的copy,轮询的时间)

Select模块还有poll和epoll事件模型,一般选择epoll较优。

0716更新:

修复了客户端断开时服务器产生错误而崩溃:

import socket
import select
s = socket.socket()
s.bind(('0.0.0.0',8500))
s.listen(1024)
conn_socket = [s,]
write_socket = []
while True:
    read_list,write_list,error = select.select(conn_socket,write_socket,[])
    print('正在监听',conn_socket)
    for client in read_list:
            if client == s:
                    conn,addr = client.accept()
                    conn_socket.append(conn)
            else:
                    try:
                            msg = client.recv(1024)
                    except Exception as ex:
                            conn_socket.remove(client)
                    else:
                            if msg != b'':
                                    print(msg)
                                    if client not in write_socket:
                                            write_socket.append(client)
                            else:
                                    conn_socket.remove(client)
    for client in write_list:
            try:
                    client.sendall(bytes(('2hello2').encode('utf-8')))
                    write_socket.remove(client)
            except:
                    conn_socket.remove(client)
                    write_socket.remove(client)
    for client in error:
            write_socket.remove(client)
            conn_socket.remove(client)

共享全局变量

这是最简单的方法,也是最不线程安全的方法。
一个例子:

import time
import threading
i = 0
def test():
        global i
        while 1:
                i += 1
                #time.sleep(0.3)
def test2():
        global i
        while 1:
                i += 1
                #time.sleep(0.3)
if __name__ == '__main__':
        t1 = threading.Thread(target=test)
        t2 = threading.Thread(target=test2)
        t1.start()
        t2.start()
        for a in range(100):
                print(i)

结果好像好像出现了倒车。

OUTPUT: 42271 51380 51605 52942 53799 53823 55059 61042 63469 61928

由于线程速度太快,一个线程刚拷贝值,另一个已经赋值了,正要拷贝时,另一个线程赋值,拷贝到旧值,导致变量内容的回退。

Queue 队列

队列是线程安全的线程间的通信方式。队列有三种模式:先进先出(Queue),后进后出(LifoQueue),以及优先级(PriorityQueue)(权重) 。
一个例子:

import threading
from queue import Queue
def test(q):
        while 1:
                i = q.get() #得到压入的数据
                print(i)
                q.put(i + 1) #压入
def test2(q):
        while 1:
                i = q.get() #如果另一个线程已经取到数据,直到在它压入数据前,都会阻塞这里,除非设置了超时时间为0
                print(i)
                q.put(i + 1)
if __name__ == '__main__':
        q = Queue(1) #新建队列大小为1的一个先进先出队列
        q.put(0) #压入一个数值
        t1 = threading.Thread(target=test,args=(q,))
        t2 = threading.Thread(target=test2,args=(q,))
        t1.start()
        t2.start()

这是一个很蠢的例子,但它演示了队列的工作方式,你会看到它打印的数字是连续的。

参考

https://www.cnblogs.com/ArsenalfanInECNU/p/10022740.html

与同学合租服使用的简单工具。

const start_comm = ['-jar','minecraft_server.jar'];
const ws = require('nodejs-websocket');
// exec(),spawn(),fork()
// 参考:https://www.cnblogs.com/chyingp/p/node-learning-guide-child_process.html
const mcprocess = require('child_process').spawn('java',start_comm);
var client = null;
var process = true;
function mcprocess_log(log){
        console.log(String(log));
        if(client){
                client.sendText(String(log));
        }
}
mcprocess.stdout.on('data',mcprocess_log); // 输出/错误事件绑定
mcprocess.stdout.on('data',mcprocess_log);
mcprocess.on('exit',(code) => process = false); // 子进程退出事件
var server = ws.createServer(function(conn){
        client = conn;
        conn.on('text',function(str){ // 消息事件
                console.log(str);
                if(process){
                        conn.sendText(str + '\n');
                        mcprocess.stdin.write(str + '\n');
                }else{
                        conn.sendText('服务器未开启\n');
                }
        });
        conn.on('close',function(code,reason){ // 断开事件
                console.log('客户端断开连接');
                client = null;
        });
}).listen(8080);
console.log('start...');

Html还是和上几篇文章一样:

<title>WebSockets Test</title>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript">
var ws = new WebSocket("ws://server_ip:8080/");
ws.onmessage = function(msg){
        $('.message').append("<p>" + msg.data + "</p>");
}
ws.onclose = function(){
        $('.message').append("<p>Connection closed</p>");
}
function sendmsg(){
        var msg = $("#msg").val();
        ws.send(msg);
}
</script>
<div class='message'></div>
<textarea id="msg"></textarea>
<button type="but" onclick="sendmsg()">Send</button>

实际上服务器上使用的是我写的另外一个Python版本的,与Nodejs不同的是我需要新建一个线程去关注服务器核心的输出,所以简单尝试了Nodejs。

71_0.png