網絡編程-Netty-Reactor模型

目錄

  • # 摘要
  • 高性能服務器
  • Reactor模式
    • Reactor單線程模型設計
    • Reactor多線程模型設計
    • 主從Reactor多線程模型設計
    • Netty Reactor模型設計
  • 參考
  • 你的鼓勵也是我創作的動力
  • Posted by 微博@Yangsc_o
  • 原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

# 摘要

在前兩篇《快速理解Linux網絡I_O》、《java的I_O模型-BIO&NIO&AIO》兩邊中介紹了Linux下的I/O模型和java中的I/O模型,今天我們介紹Reactor模型,並探究Netty的實現

高性能服務器

在互聯網時代,我們使用的軟件基本上全是C/S架構,C/S架構的軟件一個明顯的好處就是:只要有網絡,你可以在任何地方干同一件事。C/S架構可以抽象為如下模型:

  • C就是Client(客戶端),上面的B是Browser(瀏覽器)
  • S就是Server(服務器):服務器管理某種資源,並且通過操作這種資源來為它的客戶端提供某種服務

那服務器如何能快速的處理用戶的請求呢?在我看來高性能服務器至少要滿足如下幾個需求:

  • 效率高:既然是高性能,那處理客戶端請求的效率當然要很高了
  • 高可用:不能隨便就掛掉了
  • 編程簡單:基於此服務器進行業務開發需要足夠簡單
  • 可擴展:可方便的擴展功能
  • 可伸縮:可簡單的通過部署的方式進行容量的伸縮,也就是服務需要無狀態

而滿足如上需求的一個基礎就是高性能的IO!

Reactor模式

什麼是Reactor模式?

兩種I/O多路復用模式:Reactor和Proactor,兩個與事件分離器有關的模式是Reactor和Proactor。Reactor模式採用同步IO,而Proactor採用異步IO。

在Reactor中,事件分離器負責等待文件描述符或socket為讀寫操作準備就緒,然後將就緒事件傳遞給對應的處理器,最後由處理器負責完成實際的讀寫工作。

在Proactor模式中,處理器–或者兼任處理器的事件分離器,只負責發起異步讀寫操作。IO操作本身由操作系統來完成。傳遞給操作系統的參數需要包括用戶定義的數據緩衝區地址和數據大小,操作系統才能從中得到寫出操作所需數據,或寫入從socket讀到的數據。事件分離器捕獲IO操作完成事件,然後將事件傳遞給對應處理器。

說人話的方式理解:

  • reactor:能收了你跟俺說一聲。
  • proactor: 你給我收十個字節,收好了跟俺說一聲。

Doug Lea是這樣類比的

  • Reactor通過調度適當的處理程序來響應IO事件;
  • 處理程序執行非阻塞操作
  • 通過將處理程序綁定到事件來管理;

Reactor單線程模型設計

單線程版本Java NIO的支持:

  • Channels:與支持非阻塞讀取的文件,套接字等的連接

  • Buffers:類似於數組的對象,可由Channels直接讀取或寫入

  • Selectors:通知一組通道中哪一個有IO事件

  • SelectionKeys:維護IO事件狀態和綁定

  • Reactor 代碼如下

public class Reactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocketChannel;

    public Reactor(int port) throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        key.attach(new Acceptor());
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    dispatch(selectionKey);
                }
                selectionKeys.clear();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void dispatch(SelectionKey selectionKey) {
        Runnable run = (Runnable) selectionKey.attachment();
        if (run != null) {
            run.run();
        }
    }

    class Acceptor implements Runnable {
        @Override
        public void run() {
            try {
                SocketChannel channel = serverSocketChannel.accept();
                if (channel != null) {
                    new Handler(selector, channel);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new Thread(
                new Reactor(1234)
        ).start();
    }

}
  • Handler代碼如下:
public class Handler implements Runnable{
    private final static int DEFAULT_SIZE = 1024;
    private final SocketChannel socketChannel;
    private final SelectionKey seletionKey;
    private static final int READING = 0;
    private static final int SENDING = 1;
    private int state = READING;

    ByteBuffer inputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);
    ByteBuffer outputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);

    public Handler(Selector selector, SocketChannel channel) throws IOException {
        this.socketChannel = channel;
        socketChannel.configureBlocking(false);
        this.seletionKey = socketChannel.register(selector, 0);
        seletionKey.attach(this);
        seletionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override
    public void run() {
        if (state == READING) {
            read();
        } else if (state == SENDING) {
            write();
        }
    }


    private void write() {
        try {
            socketChannel.write(outputBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (outIsComplete()) {
            seletionKey.cancel();
        }
    }

    private void read() {
        try {
            socketChannel.read(inputBuffer);
            if (inputIsComplete()) {
                process();
                System.out.println("接收到來自客戶端(" + socketChannel.socket().getInetAddress().getHostAddress()
                        + ")的消息:" + new String(inputBuffer.array()));
                seletionKey.attach(new Sender());
                seletionKey.interestOps(SelectionKey.OP_WRITE);
                seletionKey.selector().wakeup();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public boolean inputIsComplete() {
        return true;
    }
    public boolean outIsComplete() {
        return true;
    }


    public void process() {
        // do something...
    }

    class Sender implements Runnable {
        @Override
        public void run() {
            try {
                socketChannel.write(outputBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (outIsComplete()) {
                seletionKey.cancel();
            }
        }
    }

}

這個模型和上面的NIO流程很類似,只是將消息相關處理獨立到了Handler中去了!雖然說到NIO一個線程就可以支持所有的IO處理。但是瓶頸也是顯而易見的!如果這個客戶端多次進行請求,如果在Handler中的處理速度較慢,那麼後續的客戶端請求都會被積壓,導致響應變慢!所以引入了Reactor多線程模型!

Reactor多線程模型設計

Reactor多線程模型就是將Handler中的IO操作和非IO操作分開,操作IO的線程稱為IO線程,非IO操作的線程稱為工作線程!這樣的話,客戶端的請求會直接被丟到線程池中,客戶端發送請求就不會堵塞!

Reactor保持不變,僅需要改動Handler代碼:

public class Handler implements Runnable{
    private final static int DEFAULT_SIZE = 1024;
    private final SocketChannel socketChannel;
    private final SelectionKey seletionKey;
    private static final int READING = 0;
    private static final int SENDING = 1;
    private int state = READING;

    ByteBuffer inputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);
    ByteBuffer outputBuffer = ByteBuffer.allocate(DEFAULT_SIZE);

    private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private static final int PROCESSING = 3;
    private Selector selector;


    public Handler(Selector selector, SocketChannel channel) throws IOException {
        this.selector = selector;
        this.socketChannel = channel;
        socketChannel.configureBlocking(false);
        this.seletionKey = socketChannel.register(selector, 0);
        seletionKey.attach(this);
        seletionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override
    public void run() {
        if (state == READING) {
            read();
        } else if (state == SENDING) {
            write();
        }
    }

    private void write() {
        try {
            socketChannel.write(outputBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (outIsComplete()) {
            seletionKey.cancel();
        }
    }

    private void read() {
        try {
            socketChannel.read(inputBuffer);
            if (inputIsComplete()) {
                process();
                executorService.execute(new Processer());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public boolean inputIsComplete() {
        return true;
    }
    public boolean outIsComplete() {
        return true;
    }


    public void process() {
        // do something...
    }

    class Sender implements Runnable {
        @Override
        public void run() {
            try {
                socketChannel.write(outputBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (outIsComplete()) {
                seletionKey.cancel();
            }
        }
    }

    synchronized void processAndHandOff() {
        process();
        // or rebind attachment
        state = SENDING;
        seletionKey.interestOps(SelectionKey.OP_WRITE);
        selector.wakeup();
    }

    class Processer implements Runnable {
        @Override
        public void run() {
            processAndHandOff();
        }
    }

}

主從Reactor多線程模型設計

主從Reactor多線程模型是將Reactor分成兩部分,mainReactor負責監聽server socket,accept新連接,並將建立的socket分派給subReactor。subReactor負責多路分離已連接的socket,讀寫網絡數據,對業務處理功能,其扔給worker線程池完成。通常,subReactor個數上可與CPU個數等同:

Handler保持不變,僅需要改動Reactor代碼:

public class Reactor {
    // also create threads
    Selector[] selectors;
    AtomicInteger next = new AtomicInteger(0);
    final ServerSocketChannel serverSocketChannel;

    private static ExecutorService sunReactors = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private static final int PROCESSING = 3;

    public Reactor(int port) throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        selectors = new Selector[4];
        for (int i = 0; i < selectors.length; i++) {
            Selector selector = selectors[i];
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            serverSocketChannel.configureBlocking(false);
            SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            key.attach(new Acceptor());
            new Thread(()->{
                while (!Thread.interrupted()) {
                    try {
                        selector.select();
                        Set<SelectionKey> selectionKeys = selector.selectedKeys();
                        for (SelectionKey selectionKey : selectionKeys) {
                            dispatch(selectionKey);
                        }
                        selectionKeys.clear();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }



    private void dispatch(SelectionKey selectionKey) {
        Runnable run = (Runnable) selectionKey.attachment();
        if (run != null) {
            run.run();
        }
    }

    class Acceptor implements Runnable {
        @Override
        public void run() {
            try {
                SocketChannel channel = serverSocketChannel.accept();
                if (channel != null) {
                    sunReactors.execute(new Handler(selectors[next.getAndIncrement() % selectors.length], channel));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new Reactor(1234);
    }

}

以上是三種不同的設計思路,接下來看一下Netty這個一個高性能NIO框架,其是如何實現Reactor模型的!

Netty Reactor模型設計

  • 看一個最簡單的Netty服務端代碼
public final class EchoServer {
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
    public static void main(String[] args) throws Exception {
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(serverHandler);
                 }
             });
            ChannelFuture f = b.bind(PORT).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
  • Netty Server Handler
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

我們從Netty服務器代碼來看,與Reactor模型進行對應!

  • EventLoopGroup就相當於是Reactor,bossGroup對應主Reactor,workerGroup對應從Reactor
  • TimeServerHandler就是Handler
  • child開頭的方法配置的是客戶端channel,非child開頭的方法配置的是服務端channel

當然Netty的線程模型並不是固定的,它支持Reactor單線程模型、Reactor多線程模型、主從模型,上面的例子是一個主從模型的,下面進行詳細的分析,如圖所示:

服務啟動時,創建了兩個EventLoopGroup,它們實際上是兩個Reactor線程池,一個用於接收TCP連接、一個用於處理I/O相關的讀寫操作、或者執行系統task、定時task等;

  • Netty用於接收客戶端請求連接池職責如下:
    • 接收客戶端請求並初始化channel參數;
    • 講鏈路變更事件通知給ChannelPipiline;
  • Netty用於處理I/O連接池職責如下:
    • 異步讀取通信對端的數據報,發送讀事件到ChannelPipiline;
    • 異步發送消息對端的數據報,調用ChannelPipiline的消息發送接口;
    • 執行系統調用task;
    • 執行系統定時任務task,例如鏈路空閑狀態檢測定時任務;

參考

Scalable IO in Java

高性能Server—Reactor模型

NIO技術概覽

《Netty 權威指南》第二版 — 李林峰

你的鼓勵也是我創作的動力

打賞地址

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

python3 源碼閱讀-虛擬機運行原理

閱讀源碼版本python 3.8.3

參考書籍<<Python源碼剖析>>

參考書籍<<Python學習手冊 第4版>>

官網文檔目錄介紹

  1. Doc目錄主要是官方文檔的說明。
  2. Include:目錄主要包括了Python的運行的頭文件。
  3. Lib:目錄主要包括了用Python實現的標準庫。
  4. Modules: 該目錄中包含了所有用C語言編寫的模塊,比如random、cStringIO等。Modules中的模塊是那些對速度要求非常嚴格的模塊,而有一些對速度沒有太嚴格要求的模塊,比如os,就是用Python編寫,並且放在Lib目錄下的
  5. Objects:該目錄中包含了所有Python的內建對象,包括整數、list、dict等。同時,該目錄還包括了Python在運行時需要的所有的內部使用對象的實現。
  6. Parser:該目錄中包含了Python解釋器中的Scanner和Parser部分,即對Python源碼進行詞法分析和語法分析的部分。除了這些,Parser目錄下還包含了一些有用的工具,這些工具能夠根據Python語言的語法自動生成Python語言的詞法和語法分析器,將python文件編譯生成語法樹等相關工作。
  7. Programs目錄主要包括了python的入口函數。
  8. Python:目錄主要包括了Python動態運行時執行的代碼,裡面包括編譯、字節碼解釋器等工作。

1. 總體架構

  • Runtime Env:python運行時環境,初始化對象/類型系統(Object/Type structures),內存分配器(Memory Allocator) 和 運行時狀態信息 (Current state of Python)。運行時狀態維護了解釋器在執行字節碼時不同的狀態(如正常和異常)之間的切換動作,可以視為一個巨大而複雜的有窮狀態機。內存管理機制可參考另外一篇文章Python3 源碼閱讀 – 內存管理機制。

  • Python Core: 中間部分是python的核心—-解釋器(PyInterpreter), 也可以成為PVM。大致流程就是 先對.py程序進行此法分析,將文件輸入的源代碼或從命令行輸入的一行行python代碼切分一個個Token, 然後使用Parser進行語法分析,建立抽象語法樹(AST), Compiler根據AST生成字節碼指令集合,最後由Code Evaluator來執行這些字節碼。

  • File Groups: Python Lib庫和用戶自己的模塊包等源代碼文件

2. Run Python文件的啟動流程

Python啟動是由Programs下的python.c文件中的main函數開始執行

/* Minimal main program -- everything is loaded from the library */

#include "Python.h"
#include "pycore_pylifecycle.h"

#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
    return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
    return Py_BytesMain(argc, argv);
}
#endif
int
Py_Main(int argc, wchar_t **argv) {
    ...
    return pymian_main(&args);
}

static int
pymain_main(_PyArgv *args)
{
    PyStatus status = pymain_init(args);  // 初始化
    if (_PyStatus_IS_EXIT(status)) {
        pymain_free();
        return status.exitcode;
    }
    if (_PyStatus_EXCEPTION(status)) {
        pymain_exit_error(status);
    }

    return Py_RunMain();
}

2.1 初始化關鍵流程

  • 初始化一些與配置項 如:開啟utf-8模式,設置Python內存分配器
  • 初始化pyinit_core核心部分
    • 創建生命周期 pycore_init_runtime, 同時生成HashRandom
    • 初始化線程和解釋器並創建GIL鎖 pycore_create_interpreter
    • 初始化所有基礎類型,list, int, tuple等 pycore_init_types
    • 初始化sys模塊 _PySys_Create
    • 初始化內建函數或者對象,如map, None, True等 pycore_init_builtins
      • 其中包括內建的錯誤類型初始化 _PyBuiltins_AddExceptions

Python3.8 對Python解釋器的初始化做了重構PEP 587-Python初始化配置

2.2 run 相關源碼閱讀

int
Py_RunMain(void)
{
    int exitcode = 0;
	
    pymain_run_python(&exitcode);  //執行python腳本

	if (Py_FinalizeEx() < 0) {  // 釋放資源
        /* Value unlikely to be confused with a non-error exit status or
           other special meaning */
        exitcode = 120;
    }

    pymain_free();   // 釋放資源

    if (_Py_UnhandledKeyboardInterrupt) {
        exitcode = exit_sigint();
    }

    return exitcode;
}


static void
pymain_run_python(int *exitcode)
{   
    // 獲取一個持有GIL鎖的解釋器
    PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE();
    /* pymain_run_stdin() modify the config */
    ... // 添加sys_path等操作

    if (config->run_command) {
        // 命令行模式
        *exitcode = pymain_run_command(config->run_command, &cf); 
    }
    else if (config->run_module) {
        // 模塊名
        *exitcode = pymain_run_module(config->run_module, 1);
    }
    else if (main_importer_path != NULL) {
        *exitcode = pymain_run_module(L"__main__", 0);
    }
    else if (config->run_filename != NULL) {
        // 文件名
        *exitcode = pymain_run_file(config, &cf);
    }
    else {
        *exitcode = pymain_run_stdin(config, &cf);
    }

	...
}

/* Parse input from a file and execute it */ //Python/pythonrun.c
int
PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
                     PyCompilerFlags *flags)
{
    if (filename == NULL)
        filename = "???";
    if (Py_FdIsInteractive(fp, filename)) {
        int err = PyRun_InteractiveLoopFlags(fp, filename, flags);  // 是否是交互模式
        if (closeit)
            fclose(fp);
        return err;
    }
    else
        return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);   // 執行腳本
}

// 執行python .py文件
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{
    ...
    if (maybe_pyc_file(fp, filename, ext, closeit)) {
        FILE *pyc_fp;
        /* Try to run a pyc file. First, re-open in binary */
        ...
        v = run_pyc_file(pyc_fp, filename, d, d, flags);
    } else {
        /* When running from stdin, leave __main__.__loader__ alone */
        ...
        v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
                              closeit, flags);
    }
    ...
}

PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
                  PyObject *locals, int closeit, PyCompilerFlags *flags)
{
    ...
    // // 解析傳入的腳本,解析成AST
    mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
                                     flags, NULL, arena); 
    ...
    // 將AST編譯成字節碼然後啟動字節碼解釋器執行編譯結果
    ret = run_mod(mod, filename, globals, locals, flags, arena);
    ...
}

// 查看run_mode
static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
            PyCompilerFlags *flags, PyArena *arena)
{
    ...
    // 將AST編譯成字節碼
    co = PyAST_CompileObject(mod, filename, flags, -1, arena);  
    ...

    // 解釋執行編譯的字節碼
    v = run_eval_code_obj(co, globals, locals);
    Py_DECREF(co);
    return v;
}

2.3 字節碼查看案例

新建test.py

def show(a):
    return  a


if __name__ == "__main__":
    print(show(10))

執行命令: python3 -m dis test.py

λ ppython3 -m dis test.py
  3           0 LOAD_CONST               0 (<code object show at 0x000000E7FC89E270, file "test.py", line 3>)
              2 LOAD_CONST               1 ('show')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (show)

  7           8 LOAD_NAME                1 (__name__)
             10 LOAD_CONST               2 ('__main__')
             12 COMPARE_OP               2 (==)
             14 POP_JUMP_IF_FALSE       28

  8          16 LOAD_NAME                2 (print)
             18 LOAD_NAME                0 (show)
             20 LOAD_CONST               3 (10)
             22 CALL_FUNCTION            1
             24 CALL_FUNCTION            1
             26 POP_TOP
        >>   28 LOAD_CONST               4 (None)

左邊3, 7, 8表示 test.py中的第一行和第二行,右邊表示python byte code

Include/opcode.h 發現總共有 163 個 opcode, 所有的 python 源文件(Lib庫中的文件)都會被編譯器翻譯成由 opcode 組成的 pyx 文件,並緩存在執行目錄,下次啟動程序如果源代碼沒有修改過,則直接加載這個pyx文件,這個文件的存在可以加快 python 的加載速度。普通.py文件如我們的test.py 是直接進行編譯解釋執行的,不會生成.pyc文件,想生成test.pyc 需要使用python內置的py_compile模塊來編譯該文件,或者執行命令python3 -m test.py python生成.pyc文件

嚴格意義上來說: 只有文件導入import 的情況下字節碼.pyc文件才會保存下來,__pycache__ — 《python學習手冊(第四版) Page40》

2.4 python中的code對象

字節碼在python虛擬機中對應的是PyCodeObject對象, .pyc文件是字節碼在磁盤上的表現形式。python編譯的過程中,一個代碼塊就對應一個code對象,那麼如何確定多少代碼算是一個Code Block呢? 編譯過程中遇到一個新的命名空間或者作用域時就生成一個code對象,即類或函數都是一個代碼塊,一個code的類型結構就是PyCodeObject, 參考Junnplus

/* Bytecode object */
typedef struct {
    PyObject_HEAD
    int co_argcount;            /* #arguments, except *args */     // 位置參數的個數,
    int co_posonlyargcount;     /* #positional only arguments */  
    int co_kwonlyargcount;      /* #keyword only arguments */
    int co_nlocals;             /* #local variables */
    int co_stacksize;           /* #entries needed for evaluation stack */
    int co_flags;               /* CO_..., see below */
    int co_firstlineno;         /* first source line number */
    PyObject *co_code;          /* instruction opcodes */
    PyObject *co_consts;        /* list (constants used) */
    PyObject *co_names;         /* list of strings (names used) */
    PyObject *co_varnames;      /* tuple of strings (local variable names) */
    PyObject *co_freevars;      /* tuple of strings (free variable names) */
    PyObject *co_cellvars;      /* tuple of strings (cell variable names) */
    /* The rest aren't used in either hash or comparisons, except for co_name,
       used in both. This is done to preserve the name and line number
       for tracebacks and debuggers; otherwise, constant de-duplication
       would collapse identical functions/lambdas defined on different lines.
    */
    Py_ssize_t *co_cell2arg;    /* Maps cell vars which are arguments. */
    PyObject *co_filename;      /* unicode (where it was loaded from) */
    PyObject *co_name;          /* unicode (name, for reference) */
    PyObject *co_lnotab;        /* string (encoding addr<->lineno mapping) See
                                   Objects/lnotab_notes.txt for details. */
    void *co_zombieframe;       /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist;   /* to support weakrefs to code objects */
    /* Scratch space for extra data relating to the code object.
       Type is a void* to keep the format private in codeobject.c to force
       people to go through the proper APIs. */
    void *co_extra;

    /* Per opcodes just-in-time cache
     *
     * To reduce cache size, we use indirect mapping from opcode index to
     * cache object:
     *   cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1]
     */

    // co_opcache_map is indexed by (next_instr - first_instr).
    //  * 0 means there is no cache for this opcode.
    //  * n > 0 means there is cache in co_opcache[n-1].
    unsigned char *co_opcache_map;
    _PyOpcache *co_opcache;
    int co_opcache_flag;  // used to determine when create a cache.
    unsigned char co_opcache_size;  // length of co_opcache.
} PyCodeObject;
Field Content Type
co_argcount Code Block 的參數個數 PyIntObject
co_posonlyargcount Code Block 的位置參數個數 PyIntObject
co_kwonlyargcount Code Block 的關鍵字參數個數 PyIntObject
co_nlocals Code Block 中局部變量的個數 PyIntObject
co_stacksize Code Block 的棧大小 PyIntObject
co_flags N/A PyIntObject
co_firstlineno Code Block 對應的 .py 文件中的起始行號 PyIntObject
co_code Code Block 編譯所得的字節碼 PyBytesObject
co_consts Code Block 中的常量集合 PyTupleObject
co_names Code Block 中的符號集合 PyTupleObject
co_varnames Code Block 中的局部變量名集合 PyTupleObject
co_freevars Code Block 中的自由變量名集合 PyTupleObject
co_cellvars Code Block 中嵌套函數所引用的局部變量名集合 PyTupleObject
co_cell2arg N/A PyTupleObject
co_filename Code Block 對應的 .py 文件名 PyUnicodeObject
co_name Code Block 的名字,通常是函數名/類名/模塊名 PyUnicodeObject
co_lnotab Code Block 的字節碼指令於 .py 文件中 source code 行號對應關係 PyBytesObject
co_opcache_map python3.8新增字段,存儲字節碼索引與CodeBlock對象的映射關係 PyDictObject

2.4.1 LOAD_CONST

// Python\ceval.c
PREDICTED(LOAD_CONST);     -> line 943: #define PREDICTED(op)           PRED_##op:
FAST_DISPATCH();           -> line 876 #define FAST_DISPATCH() goto fast_next_opcode

額外收穫: c 語言中 ##和# 號 在marco 里的作用可以參考 這篇

在宏定義里, ## 被稱為連接符(concatenator) , a##b 表示將ab連接起來

a 表示把a轉換成字符串,即加雙引號,

所以LONAD_CONST這個指領根據宏定義展開如下:

case TARGET(LOAD_CONST): {
    PRED_LOAD_CONST:
    PyObject *value = GETITEM(consts, oparg); // 獲取一個PyObject* 指針對象
    Py_INCREF(value);  // 引用計數加1
    PUSH(value);     // 把剛剛創建的PyObject* push到當前的frame的stack上, 以便下一個指令從這個 stack 上面獲取
    goto fast_next_opcode;

2.5 main_loop

// Python\ceval.c
main_loop:
    for (;;) {
        ...
            
        switch (opcode) {
 
        /* BEWARE!
           It is essential that any operation that fails must goto error
           and that all operation that succeed call [FAST_]DISPATCH() ! */
 
        case TARGET(NOP): {
            FAST_DISPATCH();
        }
 
        case TARGET(LOAD_FAST): {
            PyObject *value = GETLOCAL(oparg);
            if (value == NULL) {
                format_exc_check_arg(PyExc_UnboundLocalError,
                                     UNBOUNDLOCAL_ERROR_MSG,
                                     PyTuple_GetItem(co->co_varnames, oparg));
                goto error;
            }
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }
 
        case TARGET(LOAD_CONST): {
            PREDICTED(LOAD_CONST);
            PyObject *value = GETITEM(consts, oparg);
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }
        ...
    }
}

在 python 虛擬機中,解釋器主要在一個很大的循環中,不停地讀入 opcode, 並根據 opcode 執行對應的指令,當執行完所有指令虛擬機退出,程序也就結束了

2.6 總結

過程描述:

  1. python先把代碼(.py文件)編譯成字節碼,交給字節碼虛擬機,然後虛擬機會從編譯得到的PyCodeObject對象中一條一條執行字節碼指令,並在當前的上下文環境中執行這條字節碼指令,從而完成程序的執行。Python虛擬機實際上是在模擬操作中執行文件的過程。PyCodeObject對象中包含了字節碼指令以及程序的所有靜態信息,但沒有包含程序運行時的動態信息——執行環境(PyFrameObject),後面會繼續記錄執行環境的閱讀。
  2. 從整體上看:OS中執行程序離不開兩個概念:進程和線程。python中模擬了這兩個概念,模擬進程和線程的分別是PyInterpreterStatePyTreadState。即:每個PyThreadState都對應着一個幀棧,python虛擬機在多個線程上切換(靠GIL實現線程之間的同步)。當python虛擬機開始執行時,它會先進行一些初始化操作,最後進入PyEval_EvalFramEx函數,內部實現了一個main_loop它的作用是不斷讀取編譯好的字節碼,並一條一條執行,類似CPU執行指令的過程。函數內部主要是一個switch結構,根據字節碼的不同執行不同的代碼

3. Python中的Frame

如上所說,PyCodeObject對象只是包含了字節碼指令集以及程序的相關靜態信息,虛擬機的執行還需要一個執行環境,即PyFrameObject,也就是對系統棧幀的模擬。

3.1 堆和棧的認識

堆中存的是對象。棧中存的是基本數據類型和堆中對象的引用。一個對象的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個對象只對應了一個4btye的引用(堆棧分離的好處)

內存中的堆棧和數據結構堆棧不是一個概念,可以說內存中的堆棧是真實存在的物理區,數據結構中的堆棧是抽象的數據存儲結構。

內存空間在邏輯上分為三部分:代碼區,靜態數據區和動態數據區,動態數據區有分為堆區和棧區

  • 代碼區:存儲的二進制代碼塊,高級調度(作業調度)、中級調度(內存調度)、低級調度(進程調度)控制代碼區執行代碼的切換
  • 靜態數據區:存儲全局變量,靜態變量,常量,系統自動分配和回收。
  • 動態數據區:
    • 棧區(stack):存儲運行方法的形參,局部變量,返回值,有編譯器自動分配和回收,操作類似數據結構中的棧
    • 堆區(heap):new一個對象的引用或者地址存儲在棧區,該地址指向指向對象存儲在堆區中的真實數據。如c中的malloc函數,python中的Pymalloc

3.2 PyFrameObject對象

typedef struct _frame{  
    PyObject_VAR_HEAD //"運行時棧"的大小是不確定的, 所以用可變長的對象
    struct _frame *f_back; //執行環境鏈上的前一個frame,很多個PyFrameObject連接起來形成執行環境鏈表  
    PyCodeObject *f_code; //PyCodeObject 對象,這個frame就是這個PyCodeObject對象的上下文環境  
    PyObject *f_builtins; //builtin名字空間  
    PyObject *f_globals;  //global名字空間  
    PyObject *f_locals;   //local名字空間  
    PyObject **f_valuestack; //"運行時棧"的棧底位置  
    PyObject **f_stacktop;   //"運行時棧"的棧頂位置  
    //...  
    int f_lasti;  //上一條字節碼指令在f_code中的偏移位置  
    int f_lineno; //當前字節碼對應的源代碼行  
    //...  
      
    //動態內存,維護(局部變量+cell對象集合+free對象集合+運行時棧)所需要的空間  
    PyObject *f_localsplus[1];    
} PyFrameObject; 

如果你想知道 PyFrameObject 中每個字段的意義, 請參考 Junnplus’ blog 或者直接閱讀源代碼,了解frame的執行過程可以參考zpoint’blog.

名字空間實際上是維護着變量名和變量值之間關係的PyDictObject對象。
f_builtins, f_globals, f_locals名字空間分別維護了builtin, global, local的name與對應值之間的映射關係。

每一個 PyFrameObject對象都維護了一個 PyCodeObject對象,這表明每一個 PyFrameObject中的動態內存空間對象都和源代碼中的一段Code相對應。

3.2.1 棧幀的獲取,工作中會用到

可以通過sys._getframe([depth]), 獲取指定深度的PyFrameObject對象

>>> import sys
>>> frame = sys._getframe()
>>> frame
<frame object at 0x103ab2d48>

3.2.2 python中變量名的解析規則 LEGB

Local -> Enclosed -> Global -> Built-In

  • Local 表示局部變量

  • Enclosed 表示嵌套的變量

  • Global 表示全局變量

  • Built-In 表示內建變量

如果這幾個順序都取不到,就會拋出 ValueError

可以在這個網站python執行可視化網站,觀察代碼執行流程,以及變量的轉換賦值情況。

4. 額外收穫

意外收穫: 之前知道pythonGIL , 遇到I/O阻塞時會釋放gil,現在從源碼中看到了對應的流程

if (_Py_atomic_load_relaxed(&ceval->gil_drop_request)) {
    /* Give another thread a chance */
    if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
        Py_FatalError("ceval: tstate mix-up");
    }
    drop_gil(ceval, tstate);

    /* Other threads may run now */

    take_gil(ceval, tstate);

    /* Check if we should make a quick exit. */
    exit_thread_if_finalizing(runtime, tstate);

    if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) {
        Py_FatalError("ceval: orphan tstate");
    }
}
/* Check for asynchronous exceptions. */

參考:

python 源碼分析 基本篇

python虛擬機運行原理

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

網頁設計最專業,超強功能平台可客製化

SpringSceurity(4)—短信驗證碼功能實現

SpringSceurity(4)—短信驗證碼功能實現

有關SpringSceurity系列之前有寫文章

1、SpringSecurity(1)—認證+授權代碼實現

2、SpringSecurity(2)—記住我功能實現

3、SpringSceurity(3)—圖形驗證碼功能實現

一、思考

1、設計思路

在獲取短信驗證碼功能和圖形驗證碼還是有很多相似的地方,所以這裡在設計獲取短信驗證的時候,將之前開發好的的圖形驗證碼進一步整合、抽象與重構。

在獲取驗證碼的時候,它們最大的不同在於: 圖形驗證碼是通過接口返回獲取給前端。而短信驗證碼而言是通過第三方API向我們手機推送

但是它們在登陸的時候就有很大的不同了,對於圖形驗證碼而言驗證通過之前就走 UsernamePasswordAuthenticationFilter 過濾器了開始校驗用戶名密碼了。

但對於短信登陸而言,確實也需要先現在短信驗證碼是否通過,但是一旦通過他是不走 UsernamePasswordAuthenticationFilter,而是通過其它方式查詢用戶信息來校驗

認證已經通過了。

這篇博客只寫獲取獲取短信驗證碼的功能,不寫通過短信驗證碼登陸的邏輯。

2、重構設計

這裏才是最重要的,如何去設計和整合短信驗證碼和圖形驗證碼的代碼,是我們最應該思考的。如何將相似部分抽離出來,然後去實現不相同的部分。

整理后發現不同點主要在於

 1、獲取驗證碼。因為對於圖形驗證碼需要有個畫布,而短信驗證碼並不需要,所以它們可以實現同一個接口,來完成不同的邏輯。
 2、發送驗證碼。對於圖形驗證碼來講只要把驗證碼返給前端就可以,而短信驗證碼而言是通過第三方API將驗證碼發到我們的手機上。
    所以這裏也可以通過實現統一接口來具體實現不同的方法。

相同部分我可以通過抽象類來完成實現,不同部分可以通過具體的實現類來實現。

AbstractValidateCodeProcessorService 抽象類是用來實現兩種驗證碼可以抽離的部分。ImageCodeProcessorServiceImpl

SmsCodeProcessorServiceImpl方法是來實現兩種驗證碼不同的發送方式。

在簡單看下時序圖可能會更加明白點。

一個接口只有一個方法(processor)就是處理驗證碼,它其實需要做三件事。

 1、獲取驗證碼。2、將驗證碼存入session。3、將驗證碼信息通過短信或者圖形驗證碼發送出去。

首先講生成獲取驗證碼,這裡有一個公共接口和兩個實現類

對於保存驗證碼信息而言,可以在直接在 AbstractValidateCodeProcessorService抽象類來完成,都不需要去實現。

對發送驗證碼信息而言,只需要實現AbstractValidateCodeProcessorService抽象類的send發送驗證碼接口即可。

整個大致接口設計就是這樣,具體的可以通過代碼來展示。

二、代碼實現

1、驗證碼屬性

短信驗證碼和圖形驗證后包含屬性有 codeexpireTime,短信驗證碼只有這兩個屬性,而圖形驗證碼還多一個BufferedImage實例對象屬性,所以將共同屬性進行抽取

,抽取為ValidateCode類,代碼如下:

ValidateCode實體

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ValidateCode {

    private String code;

    private LocalDateTime expireTime;

    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime);
    }

}

對於圖形驗證碼而言,除了需要code和過期時間還需要圖片的畫布,所以繼承ValidateCode之後再寫自己屬性

ImageCode實體

@Data
public class ImageCode extends ValidateCode {

    private BufferedImage image;

    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        super(code, expireTime);
        this.image = image;
    }

    public ImageCode(BufferedImage image, String code, int expireIn) {
        super(code, LocalDateTime.now().plusSeconds(expireIn));
        this.image = image;
    }
}

對於短信驗證碼而言,暫時不需要添加自己的屬性字段了。

SmsCode實體

public class SmsCode extends ValidateCode {

    public SmsCode(String code, LocalDateTime expireTime) {
        super(code, expireTime);
    }

    public SmsCode(String code, int expireIn) {
        super(code, LocalDateTime.now().plusSeconds(expireIn));
    }
}

2、ValidateCodeProcessor接口

ValidateCodeProcessor接口主要是完成 驗證碼的生成、保存與發送的完整流程,接口的主要設計如下所示:

ValidateCodeProcessorService接口

public interface ValidateCodeProcessorService {

    /**
     * 因為現在有兩種驗證碼,所以存放到seesion的key不能一樣,所以前綴+具體type
     */
    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
    /**
     * 通過也是 type+CODE_PROCESSOR獲取對於的bean
     */
    String CODE_PROCESSOR = "CodeProcessorService";

    /**
     * 這個接口要做三件事
     * 1、獲取驗證碼。
     * 2、將驗證碼存入session
     * 3、將驗證碼信息通過短信或者圖形驗證碼發送出去。
     * (將spring-boot-security-study-03接口裡的那三步進行里封裝)
     *
     */
    void processor(ServletWebRequest request) throws Exception;

由於圖片驗證碼和短信驗證碼的 生成和保存、發送等流程是固定的。所以這裏寫一個抽象類來實現ValidateCodeProcessor接口,來實現相似部分。

AbstractValidateCodeProcessorService抽象類

@Component
public abstract class AbstractValidateCodeProcessorService<C> implements ValidateCodeProcessorService {

    private static final String SEPARATOR = "/code/";

    /**
     * 操作session的工具集
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 這是Spring的一個特性,就是在項目啟動的時候會自動收集系統中 {@link ValidateCodeGeneratorService} 接口的實現類對象
     *
     * key為bean name
     */
    @Autowired
    private Map<String, ValidateCodeGeneratorService> validateCodeGeneratorMap;

    @Override
    public void processor(ServletWebRequest request) throws Exception {
        //第一件事
        C validateCode = generate(request);
        //第二件事
        save(request, validateCode);
        //第三件事
        send(request, validateCode);
    }

    /**
     * 生成驗證碼
     *
     */
    private C generate(ServletWebRequest request) {
        String type = getProcessorType(request);
        //這裏 image+CodeGenerator = imgCodeGenerator 對應的就是ImageCodeGeneratorServiceService
        ValidateCodeGeneratorService validateCodeGenerator = validateCodeGeneratorMap.get(type.concat(ValidateCodeGeneratorService.CODE_GENERATOR));
        return (C) validateCodeGenerator.generate(request);
    }

    /**
     * 保存驗證碼到session中
     */
    private void save(ServletWebRequest request, C validateCode) {
        //這裏也是封裝了一下
        sessionStrategy.setAttribute(request, SESSION_KEY_PREFIX.concat(getProcessorType(request).toUpperCase()), validateCode);
    }

    /**
     * 發送驗證碼 (只有發送驗證碼是需要自己去實現的。)
     */
    protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;

    /**
     * 獲取請求URL中具體請求的驗證碼類型
     *
     */
    private String getProcessorType(ServletWebRequest request) {
        // 獲取URI分割后的第二個片段 (/code/image 通過/code/ 切割后就只剩下 image
        return StringUtils.substringAfter(request.getRequest().getRequestURI(), SEPARATOR);
    }
}

簡單說明

1、這裏用到了Spring一個特性就是Map<String, ValidateCodeGeneratorService> validateCodeGeneratorMap 可以把ValidateCodeGeneratorService所以的實現類都放
到這個map中,key為bean的名稱。

2、抽象類中實現了 ValidateCodeProcessor接口的processor方法,它主要是完成了驗證碼的創建、保存和發送的功能。

3、generate 方法根據傳入的不同泛型而生成了特定的驗證碼。

4、save 方法是將生成的驗證碼實例對象存入到session中,兩種驗證碼的存儲方式一致,只是有個key不一致,所以代碼也是通用的。

5、send 方法一個抽象方法,分別由ImageCodeProcessorService和SmsCodeProcessorService來具體實現,也是根據泛型來判斷具體調用哪一個具體的實現類的send方法。

3、驗證碼的生成接口

上面說過驗證的生成應該也是通過實現類

ValidateCodeGeneratorService

public interface ValidateCodeGeneratorService {

    /**
     * 這個常量也是用來 type+CodeGeneratorService獲取對於bean對象
     */
    String CODE_GENERATOR = "CodeGeneratorService";

    /**
     * 生成驗證碼
     * 具體是圖片驗證碼 還是短信驗證碼就需要對應的實現類
     */
    ValidateCode generate(ServletWebRequest request);
}

圖形驗證碼具體實現類

mageCodeGeneratorServiceImpl

@Data
@Component("imageCodeGeneratorService")
public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {

    private static final String IMAGE_WIDTH_NAME = "width";
    private static final String IMAGE_HEIGHT_NAME = "height";
    private static final Integer MAX_COLOR_VALUE = 255;

    @Autowired
    private ValidateCodeProperties validateCodeProperties;

    @Override
    public ImageCode generate(ServletWebRequest request) {
        int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, validateCodeProperties.getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME,validateCodeProperties.getImage().getHeight());
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();

        Random random = new Random();

        // 生成畫布
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        // 生成数字驗證碼
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < validateCodeProperties.getImage().getLength(); i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        return new ImageCode(image, sRand.toString(), validateCodeProperties.getImage().getExpireIn());
    }

    /**
     * 生成隨機背景條紋
     *
     * @param fc 前景色
     * @param bc 背景色
     * @return RGB顏色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > MAX_COLOR_VALUE) {
            fc = MAX_COLOR_VALUE;
        }
        if (bc > MAX_COLOR_VALUE) {
            bc = MAX_COLOR_VALUE;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

短信驗證碼具體實現類

SmsCodeGeneratorServiceImpl

@Data
@Component("smsCodeGeneratorService")
public class SmsCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {

    @Autowired
    private ValidateCodeProperties validateCodeProperties;


    @Override
    public SmsCode generate(ServletWebRequest request) {
        //生成隨機數
        String code = RandomStringUtils.randomNumeric(validateCodeProperties.getSms().getLength());
        return new SmsCode(code, validateCodeProperties.getSms().getExpireIn());
    }
}

4、驗證碼的發送邏輯類

獲取的實現類寫好了,我們在寫發送具體的發送實現類,發送類的實現類是實現AbstractValidateCodeProcessorService抽象類的。

圖片發送實現類

ImageCodeProcessorServiceImpl

@Component("imageCodeProcessorService")
public class ImageCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService<ImageCode> {

    private static final String FORMAT_NAME = "JPEG";

    /**
     * 發送圖形驗證碼,將其寫到相應中
     *
     * @param request   ServletWebRequest實例對象
     * @param imageCode 驗證碼
     */
    @Override
    protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
        ImageIO.write(imageCode.getImage(), FORMAT_NAME, request.getResponse().getOutputStream());
    }
}

短信發送具體實現類。這裏只是後台輸出就好了,實際中只要接入對於的SDK就可以了。

SmsCodeProcessorServiceImpl

@Component("smsCodeProcessorService")
public class SmsCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService<SmsCode> {
    private static final String SMS_CODE_PARAM_NAME = "mobile";

    @Override
    protected void send(ServletWebRequest request, SmsCode smsCode) throws Exception {
        //這裡有一個參數也是前端需要傳來的 就是用戶的手機號
        String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), SMS_CODE_PARAM_NAME);
        // 這裏僅僅寫個打印,具體邏輯一般都是調用第三方接口發送短信
        System.out.println("向手機號為:" + mobile + "的用戶發送驗證碼:" + smsCode.getCode());
    }

整個大致就是這樣,我們再來測試一下。

三、測試

1、ValidateCodeController接口

獲取驗證碼接口

@RestController
@Slf4j
public class ValidateCodeController {


    @Autowired
    private  Map<String, ValidateCodeProcessorService> validateCodeProcessorMap;


    @RequestMapping("/code/{type}")
    public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
        if(!StringUtils.equalsAny(type, "image", "sms")){
            log.info("type類型錯誤 type={}",type);
            return;
        };

        //根據type獲取具體的實現類
        ValidateCodeProcessorService validateCodeProcessorService = validateCodeProcessorMap.get(type.concat(ValidateCodeProcessorService.CODE_PROCESSOR));
        validateCodeProcessorService.processor(new ServletWebRequest(request, response));

    }

}

2、獲得圖形驗證碼

獲取成功

3、獲取短信驗證碼

獲取短信驗證碼需要多傳一個參數就是mobile 手機號碼

因為這裏發送短信沒有接第三方SDK,而是直接在控制台輸出

參考

1、Spring Security技術棧開發企業級認證與授權(JoJo)

這一套架構設計真的非常的好,代碼可讀性和可用性都非常高,以後如果要接第三個驗證碼只要實現發送和獲取的接口來自定義實a現就好了。很受啟發!

別人罵我胖,我會生氣,因為我心裏承認了我胖。別人說我矮,我就會覺得好笑,因為我心裏知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(20)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

架構C02:商業模式與架構設計

商業模式與架構設計:A段架構與B段架構

《思考軟件創新設計:A段架構師思考技術》 A段架構師必須具備鮮活的創新思維,睿智的策略思考,犀利的洞察力和靈活的戰術才能把握稍縱即逝的商機                                                       

 

 

 

 

 

 

 

 

 

 

 

  

  段架構師 B段架構師
關注點 產品策略規劃 實踐策略,執行能力,技術變遷
協作對象 協助產品經理 協助研發經理生產經理
思維的差異 獲利思維,知彼才能在複雜商業環境生存 成本思維,知己才能在成本和收益做出合適選擇

 

                目前我們所接觸的大多是B段技術的架構,更關注“知己”,我們做研發改進,敏捷管理,技術重構,就是為了更好的平衡技術的成本和業務的收益  

 A段架構與商業模式:以不變應萬變

 

架構師需要考慮的商業要素

決策前(A段設計)—->決策點—>決策后(B段設計)  

商業思維三要素:商業模式,架構模式,創新產品

 

軟件是現實世界的映射和抽象

現實世界是複雜多變的,所以由需求就是複雜多變的,軟件也是複雜多變的, 所以現實中組織要發展就要面對變化的適合的變化,反應到軟件上也會隨需求的變化而變化,所以軟件本質上是一個演化的系統,是一個複雜的系統  

商業與技術的關係

商業維度,現實世界是複雜多變的組織需要不停的適應市場的變化, 從產品維度需要不停的創新滿足客戶和市場的需求, 而從技術和架構的維度來看,架構則希望更少的信息熵,用更少的技術元素來表述更多的業務結構,這也正是為什麼我們追求模型,模式,結構與算法  

 商業和產品做加法,架構設計做減法 

在複雜的現實中,用簡單的抽象來支撐商業的變化,用靈活的設計支持業務的創新   《深奧的簡潔》是一本科普讀物,裏面講述了碎行,自我組織,自我類似等等自然界好些美妙的規律 https://zh.wikipedia.org/wiki/%E5%88%86%E5%BD%A2#%E7%A4%BA%E4%BE%8B    

大樹的隱喻描述商業,架構,研發技術生產管理

大樹的上層是枝恭弘=叶 恭弘,要吸收陽光雨露,要開花結果,是對外界展示的活躍和生機的一面,這裏用來表述商業模式和創新產品 這些都是要變化的部分,而且收外部影響較明顯   再次是樹榦是中層A段架構,中層要求穩既要約束和輔助枝恭弘=叶 恭弘發展和繁榮又要保護下層樹根承受壓力   下層部分的話就是B段架構,生產,技術,管理,這些是看不見但是很重要的元素,是整個樹木生命繁榮的根本    

從複雜中抽象出簡單,用簡單和較少信息熵,應對複雜多變的商業和產品

簡單的有序的產品和架構設計,通過一定的約束組合可以形成一個富有活力的系統,底層元素的簡單又保證了它可以包容現實中的複雜變化,應對紛繁複雜的現實情況,支持商業的變革和產品的創新    

B段架構技術和業務的矛盾:用成本收益作為衡量標準

 

變的是需求和技術,不變的是成本與收益評估,是要創造價值的目標

  “你這個功能啥時候能上? ” “這個有難度目前不行,需要做重構,技術細節blablabla…” “提這麼多需求沒幾個有用的,根本不懂技術實現,你要覺的能行為啥你不上”   產品和技術的矛盾點: 1. 資源的搶佔 2. 成本的評估 3.內外部目標的差異 4.內部目標設定不合理  

解決問題:業務知識+成本核算

技術要了解業務背景,業務收益,要解決的問題是什麼?只有這樣才能解決問題,做出架構設計,做出模型設計,解決業務問題,幫助客戶解決現實場景的問題  

優秀的架構要融和技術與業務的平衡和成本收益的評估

  1. 清晰服務業務短期目標,明確技術定位,輔助實現當前階段業務訴求 2. 協調技術資源投入和分配 3. 進行成本與收益的評估,確定做哪些,不做那些,先做那些,怎麼做收益更大 4. 預留長期技術規劃和儲備  

我們是解決昨日之債務,還是準備迎接今日之挑戰?

衡量的標準就是做這件事的收益?   產品和業務做哪些收益更大:產品的願景和價值觀 本年度看做哪些收益更大(OKR) 本季度本月做哪些收益最大(月度發版路標規劃) 當天本周做哪些收益最大(周計劃)  

舊系統的改造 OR 新技術的引進?

技術儲備和技術棧規劃方面: 中小型創業型公司,非技術驅動的公司 關注中長期發展的技術與趨勢,不要太超前,不必做小白鼠   舊系統改造方面: 假如不能明顯的產生業務價值,單純的把報表生成把半小時優化到5分鐘,不如做一些其他更有業務價值的任務 假如沒有其他高附件值任務可以去做,假如報表生成佔用研發時間減少了質量保證時間,影響了交付質量也可以去做  

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

關於Graph Convolutional Network的初步理解

  為給之後關於圖卷積網絡的科研做知識積累,這裏寫一篇關於GCN基本理解的博客。GCN的本質是一個圖網絡中,特徵信息的交互+與傳播。這裏的圖指的不是圖片,而是數據結構中的圖,圖卷積網絡的應用非常廣泛 ,經常被舉到的典型例子就是一個空間中熱量的傳遞和交互,這裏不多作贅述。

一、圖卷積網絡與普通卷積網絡的應用範圍

  圖卷積網絡為什麼叫圖卷積網絡呢?圖卷積網絡,其實就是就是GCN,但GCN為什麼是圖神經網絡呢?小編也很疑惑。

  好了!開玩笑的話先打住,進入正題。首先複習一下卷積神經網絡的工作原理,以檢測圖片的過程為例,卷積神經網絡提取圖片特徵的過程,其實就是對每個像素周邊像素加權求和的過程,初始每個像素對應的卷積核的權重隨機,在通過反向傳遞、逐層梯度下降的優化之後才會得到合理的權重,根據這個權重得到的feature map便是特徵提取的結果。對於圖像等像素排列規整的結構來說,使用普通的卷積神經網絡提取特徵沒有任何問題,但對於某些形如交通網、電網等“不整齊”的結構,普通的CNN就沒有用武之地了,引用知乎大佬學術性的話講,就是“CNN在Non Euclidean Structure的數據上無法保持平移不變性”,翻譯成人話就是對於圖結構的數據,其每個點鄰接的數量各不相同,會給CNN提取特徵帶來很大的困難;要提取圖結構的空間特徵進行後續的機器學習,就需要使用圖卷積網絡。簡而言之,GCN是CNN的升級版,CNN做不到的,GCN可以做,GCN做的到的,CNN做不到。

二、圖卷積網絡提取空間特徵的方式

  提取拓撲圖空間特徵有兩種方法:空間領域與譜領域。這裏我只對譜領域的提取方法作總結概述(空間領域的沒學),通過定義圖上的傅里恭弘=叶 恭弘變換,圖的卷積方式得到表示方式,與深度學習結合得到最終的圖卷積網絡。在進行傅里恭弘=叶 恭弘和卷積的推導前先複習一下線代?一張圖的拉普拉斯矩陣一般為其度矩陣D減去其鄰接矩陣A,其他常見定義也有D-1LD與D-1L。

1.圖的特徵分解

  對圖的拉普拉斯矩陣進行譜分解,說的通俗易懂一點就是對角化。使用拉普拉斯矩陣進行運算的優勢在這裏體現:拉普拉斯矩陣滿足譜分解所需線性無關的條件。圖的拉普拉斯矩陣分解形式為UPU-1,其中U=[u1,u2,…,un],為列向量是單位特徵向量的矩陣;P為含有n個特徵值的對角矩陣。

2.含特徵向量與特徵值矩陣的傅里恭弘=叶 恭弘變換定義

  在瀏覽一些大佬的博客與知乎時我常常感到詫異:進行完矩陣分解后怎麼突然講到傅里恭弘=叶 恭弘變化了?理清思路后發現相關矩陣傅里恭弘=叶 恭弘變換的定義是最後卷積公式推導的基礎,由於兩函數的卷積是其函數傅立恭弘=叶 恭弘變換乘積的逆變換,即:

  為了能將針對圖的傅里恭弘=叶 恭弘變換類比代入上述公式,我們需要推廣傅里恭弘=叶 恭弘變換,把特徵函數 eiωt 變為拉普拉斯矩陣的特徵向量。

  由傅里恭弘=叶 恭弘變換的一般形式:

  類比特徵方程定義:

LV=λV

  L、V、λ分別代表拉普拉斯矩陣、特徵向量/函數、特徵值。將特徵向量與前面定義的u矩陣對應,得到最終圖的傅里恭弘=叶 恭弘變換定義為:

 

  其中f(i)對應圖的各個頂點,ux*(i)表示第x個特徵向量的第 i 個分量。那麼特徵值λx下F的圖傅里恭弘=叶 恭弘變換就是與λx對應的特徵向量ux進行內積運算。矩陣形式為:

  即f^=UT f。同時由傅里恭弘=叶 恭弘逆變換基本公式:

  得到傅里恭弘=叶 恭弘逆變換的矩陣形式:

  

  即f=Uf^

3.圖卷積推導  

  在定義完圖上的傅里恭弘=叶 恭弘變換之後,總算要開始讓夢想照進現實將卷積運算推廣到圖上了。由卷積定理:

     將對應圖上各點的f與卷積核h的傅里恭弘=叶 恭弘定義代入其中,卷積核在圖上的傅里恭弘=叶 恭弘變換被定義為:

  按卷積定理將兩者傅里恭弘=叶 恭弘變換形式相乘得到:

  最後乘U求得傅立恭弘=叶 恭弘變換乘積的逆變換,最終得到卷積:

  以上,GCN粗略的推導過程就整理完畢了。

 

  參考網站: https://www.zhihu.com/question/54504471?sort=created

        https://www.cnblogs.com/h2zZhou/p/8405717.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

地球生態亮紅燈!最深海溝驚見「新物種」 體內藏大量塑膠纖維

摘錄自2020年6月3日CTWANT報導

太平洋馬里亞納海溝(Mariana Trench)是全球最深的海溝,其海平面深度甚至大於聖母峰的高度,在如此神秘的海域中,卻發現了一項全新物種,經研究過後,竟發現牠的體內充滿了塑膠微粒,可見塑膠的禍害已經嚴重污染地球生態,就連海洋深處也遭受其害。

綜合外媒報導,英國紐卡索大學(Newcastle University)在海平面下近7公里處發現新物種,並採集4隻帶回研究分析,沒想到竟在其中1隻體內發現塑膠纖維,與寶特瓶材質「聚對苯二甲酸乙二酯(PET)」相似度高達60%,因此將這新物種命名為「塑膠鉤蝦(Eurythenes plasticus)」。

世界自然基金會(WWF)德國海洋計畫主任維斯普(HeikeVesper)強調,這項驚人的發現證明了「廢棄塑膠」已經嚴重污染海洋,儘管物質已變成細小微粒,但還是充斥在空氣、飲用水之中。這項研究已刊登在動物生物分類學科學期刊《Zootaxa》,警示人們應立即採取行動,阻止傷害繼續發生。

生態保育
海洋
公害污染
污染治理
生物多樣性
國際新聞
馬里亞納海溝
新物種
海洋汙染
廢棄物

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

南非野生動物交易黑市 高價賣往中國的真相:淪落動物園、實驗室、肉品市場

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS 編按:接續
上篇,近期一份報告「南非與中國間可恥的野生動物貿易」揭露南非政府放任野生動物活體市場、違反國際公約的真相。報告由南非非政府組織「EMS基金會」和「禁止動物貿易(Ban Animal Trading)」發表,他們指控,包括黑猩猩、孟加拉虎、狼、非洲野犬和獅子在內,數千種瀕臨滅絕的野生動物從南非銷往中國,法規和審核流程毫無作用、動物福利和自然保護原則遭到忽視,並提供確切犯罪證據。

動物福利問題

雖然瀕危物種貿易公約(CITES)應該保證出口的野生動物到達「適當和可接受的目的地」,但從南非銷往中國的野生動物最終可能成為寵物、古董、食品、傳統藥材、動物園展示動物或實驗動物。

報告指出,由於許多動物是由最初的進口商出售給未知的第三方,通常無法確定其最終目的地。

在中國,進口野生動物的飼養設施通常品質不良。以出售給北京野生動物公園的黑猩猩為例,牠們抵達中國時,籠舍等設施還沒完工,也沒有合格的員工來照顧牠們。

來自南非的黑猩猩被關在北京野生動物公園中狹窄的展示空間內。
報告截圖

這些動物中有許多是為了娛樂目的而運往中國成千上萬個公有或私有「野生動物園」、主題公園和馬戲團。根據該報告,其中有些機構涉嫌虐待動物、環境設備條件惡劣、訓練野生動物表演以及非法購買野生動物。

從南非出口的大多數非靈長類動物,包括數百隻狨屬猴子,是賣給中間商、批發商和繁殖場,最終進入會執行活體切片的生物醫學、化妝品和製藥實驗室。

道德破產的政策

報告中提供的證據清楚地顯示,南非的野生動物貿易沒有任何保育作用。加上牽涉到瀕危物種,中國的最終目的地大多很可疑以及貿易活動刺激需求成長,這條供應鏈對生物多樣性和物種生存的影響,比較可能是弊遠大於利。

這個產業的真正動機並不難發現。根據這份報告,一間南非公司出於所謂非商業目的向一間中國公司出口的18隻黑猩猩,價格超過750萬南非幣(約新台幣1287萬元),100隻狐獴的行情價格為60萬南非幣(約新台幣102萬元),57頭長頸鹿700萬南非幣(約新台幣1201萬元)和18隻非洲野犬100萬南非幣(約新台幣171萬元)。

多年來,南非政府一直在積極推動這個獲利導向的行業,無視CITES及其自己的出口法規,還大力推廣「永續利用」,將野生動物視為養殖和出口商品,將保育責任推給國際市場。

100隻狐獴的行情價格為60萬南非幣(3萬4460美元)。照片來源:Jarod Carruthers(CC BY-NC-ND 2.0)

EMS基金會負責人皮克佛(Michele Pickover)表示,這份報告的前一個版本已經寄給南非各相關政府部門和官員,裡面有清楚的非法獅骨貿易證據,「但我們從未收到任何人的任何回應,這不是無能,是無視。」

考慮到武漢肺炎(COVID-19)危機,忽視貿易問題將牽涉刑事責任。南非政府若繼續支持和合法化這個危害國內生態系統和生物多樣性的產業,就是使全世界暴露於新型人畜共通傳染病的風險,就應對相關生態破壞、人類健康和財務後果承擔某些責任。

作者呼籲南非政府放棄有爭議的野生動植物貿易政策,並禁止出口活體野生動物及其身體部位。

「報告明確指出,任何人工繁殖和貿易都使消費合法化和常態化,影響減少需求的呼籲互相矛盾,使野生物種面臨進一步開發的風險。」

作者呼籲用新的全球協議代替CITES,以「野生動物貿易不適當、適得其反、不道德和根本非永續」為基本指導原則。

South Africa’s Live Wild Animal Trade to China Exposed CAPE TOWN, South Africa, May 26, 2020(ENS)

Animal Welfare Concerns

While CITES rules are supposed to guarantee that exported live wild animals go to “appropriate and acceptable destinations,” those traded from South Africa to China may end up being used as pets, curios, food, ingredients in traditional medical practices, zoo exhibits or laboratory test subjects.

The report notes that since many animals are sold on to unknown third parties by the initial importers, their final destination is frequently impossible to ascertain.

Facilities housing imported wild animals in China are often of an inferior standard. In the case of the chimpanzees sold to the Beijing Wild Animal Park, for example, their accommodation was not yet completed on their arrival in the country and the facility did not have qualified staff to take care of them.

Many of the animals are destined for China’s thousands of government-run or privately-owned “safari parks,” zoos, theme parks and circuses for the sole purpose of entertainment. According to the report, several of these institutions have been exposed for animal abuse, poor conditions and facilities, training wild animals to perform for audiences and illegally buying wild-caught animals.

Most of the non-human primates exported from South Africa, including hundreds of marmosets, are sold to brokers, wholesalers and breeding farms, and many of them end up in laboratories conducting experiments, including vivisection, for the biomedical, cosmetic and pharmaceutical industries.

Morally Bankrupt Government Policies

The evidence presented in the report makes it abundantly clear that South Africa’s trade in live wild animals has no conservation value whatsoever. Given the involvement of endangered species, the frequently dubious final destinations in China and the fact that the trade stimulates growing demand, it’s more likely to have a detrimental impact on biodiversity and species survival.

The true motivation for this industry is not hard to find. The 18 chimps exported from a commercial entity in South Africa for supposedly non-commercial purposes to a commercial entity in China came at a cost of over R7.5 million (US$430,758). The report lists the going price for 100 meerkats at R600,000 (US$34,460), 57 giraffes at R7million (US$402,000) and 18 African Wild Dogs at R1 million (US$57,430).

The South African government has been actively enabling this profit-driven industry for years. It has done so through its laissez-faire disregard for CITES and its own export regulations and through its aggressive promotion of a “sustainable use” philosophy that treats wild animals as mere commodities to be bred and sold while leaving conservation concerns to the supposed benevolence of international markets.

According to Michele Pickover, director of the EMS Foundation, copies of an earlier and equally damaging EMS/Ban Animal Trading report on South Africa’s trade in lion bones clearly detailing illegal activities was sent to various domestic authorities and individuals in government.

“We never received any response from any of them,” Pickover says. “This is not a case of incompetence. They are ignoring us.”

Given the COVID-19 disaster, ignoring this issue amounts to criminal negligence. As long as the South African government continues to support and legitimise an industry that endangers the biodiversity of domestic ecosystems while exposing the entire world to novel zoonotic diseases, it bears some responsibility for the devastating ecological, human health and financial consequences it causes.

The authors call for the government to abandon its controversial wildlife trade policies and to ban the export of living wild animals and their body parts.

They write, “The report makes clear that any captive breeding and trade legitimises and normalises consumption, which renders demand reduction campaigns incoherent and ineffective, and puts wild species at further risk of exploitation.”

The authors call for the replacement of CITES with a different global agreement that would have as its fundamental guiding principle that “the trade in wild animals is inappropriate, counter-productive, unethical and fundamentally unsustainable.”

※ 全文及圖片詳見:ENS

出售野生動物
CITES
走私
活體動物
動物福利
道德
國際新聞
南非
美國
生態保育
生物多樣性

作者

姜唯

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

林大利

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

延伸閱讀

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

長灘島上幾乎沒有蝙蝠了 菲律賓度假勝地的生態危機:狐蝠族群消逝中

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

首度超車燃煤 再生能源躍美主要電力

摘錄自2020年6月1日工商時報報導

根據能源資訊管理局(EIA)的調查指出,2019年美國再生能源發電量創下1885年來首次超越燃煤紀錄,這也意味再生能源已成為美國主要電力來源。

EIA表示,去年燃煤發電量不僅較前一年減少15%至11.3千兆英制熱單位,創下1964年來最低,並且為連續第六季下滑。同時期再生能源發電量則是年增1%到11.5千兆英制熱單位,寫下紀錄新高。

當中又以風力與太陽能發電量出現最大成長,目前在美國再生能源最常被使用的風力發電,在去年已首度超越水力發電。

再生能源
能源議題
能源轉型
國際新聞
美國
發電量

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

自從開上了寶駿310,媽媽再也不擔心我養不起車!

寶駿的保養成本,每天只需1。9元對於第一輛車,每一個人都恨不得給予其最好的養護。對此,寶駿可謂善解人意地為寶駿310制定了極為低廉的養護費用標準,這無疑能夠讓每一個寶駿310的車主節省很大的一筆錢。到底有多省,我們按照寶駿310維修保養手冊的標準算一算便清晰可知。

誰又會想過一部IpHONE 7的價格能完成一輛車的首付。至少在寶駿310面世以前,沒有人去假設過這一可能性。3.68萬的起售價,4.98萬的頂配封頂,寶駿310讓“年輕人的第一輛車“的門檻降低到了讓人詫異的地步。而在頗有吸引力的價格背後,我們看到了年輕動感的設計、豐富多元的配置、寬敞舒適的空間,這也直接造就了寶駿310在11月收穫高達15006輛的銷量。“年輕人的第一輛車”這個稱號,頗有幾分份量。

不過很顯然,寶駿310希望把“年輕人的第一輛車”這個名號更徹底地貫徹下去。所以在用車成本這項決定着用戶是“用一輛車”還是“養一輛車”的問題上,寶駿310在整個產品的生命周期內為年輕人提供了幾乎零壓力的用車成本。對於尚處於奮鬥的年輕人來說:沒有比這更讓人愉快的事情了。

超長的保修政策!完全沒有後顧之憂!

保修期的重要性,我想每一個人都很清楚。小到手機,大到冰箱,再到汽車,無一例外。這不僅是關乎消費者對產品本身的信心,更重要的是:萬一產品因為質量而壞了,消費者能夠找到一個可靠的人來把它免費修好。而寶駿310,也堪稱豪爽地給出了2年或5萬公里的三包有效期以及5年10萬公里的整車保修期。

正所謂沒有對比就沒有傷害。本田飛度的整車保修期為3年或10萬公里;豐田卡羅拉的整車保修期為3年或10萬公里。寶駿310的整車保修期足足比這些主流的合資車型多出了2年的時間。2年的時間能夠做些什麼?或許你能夠成為老闆;或者結婚成為愛情奴;又或者把歐洲都走上一遍。而這兩年的時間,你壓根不用擔心你的寶駿310。

而更貼心的是,寶駿310的免費首保服務(3個月或5000公里)包含了機油更換、機油濾芯更換以及工時費在內的全部保養費用。首保,沒有任何一分的費用。

修不起車?!寶駿的保養成本,每天只需1.9元

對於第一輛車,每一個人都恨不得給予其最好的養護。對此,寶駿可謂善解人意地為寶駿310制定了極為低廉的養護費用標準,這無疑能夠讓每一個寶駿310的車主節省很大的一筆錢。到底有多省,我們按照寶駿310維修保養手冊的標準算一算便清晰可知。

按照寶駿310維修保養手冊的標準,寶駿310三年下來的正常養護費用只需要2128元(按每年行駛15000公里為標準)。倘若把筆養護費用按天來計算,呵呵,那真是嚇壞寶寶—1.9元。相信我,這不是電視購物的忽悠。

加不起油?!寶駿310的油費低到你不信

比起正常養護,或許油費更能引起大家的關注,畢竟加油的頻率要比養護要高太多。不過比起其他車主對於油價的膽顫心驚,寶駿310車主絕對要淡定許多。原因無他,寶駿310搭載了一台高效的1.2L自然吸氣四缸發動機(最大馬力82ps,最大扭矩116Nm),並且配備了一台5檔的手動變速箱。按照寶駿310在工信部的認定油耗來計算(5.3L/百公里,實際車主反映也符合這一數據),三年下來(每年行駛15000公里),寶駿310的油耗只需要14167元(92號汽油)。對比起那些油耗動輒8L、9L的轎車、SUV來說,寶駿310的油耗恐怕不是一個省字能夠概括。所以我想那些寶駿310的車主一定都是些喜歡四處亂逛的人,畢竟他們都不需要擔心油費。

媽媽再也不用擔心我買不起保險了

寶駿310的價格優勢除了直接拉低了年輕人的購買門檻以外,也使得年輕人許多負擔的車險費用大大降低。以售價4.98萬的寶駿310 【2016款 1.2L 手動豪華型】為例,在第三者50萬以及險種齊全的情況下,交強險加上商業險的總費用只需要5000元/一年。而且按照現在的保險法,假如車主出險的次數較少,這一商業險費用還有很大的下降空間。

沒有車比寶駿310更勝任“年輕人的第一台車”

很明顯,寶駿310是很能夠代表“年輕人的第一台車”的,不僅因為其低得讓人詫異的售價,更因為其自身擁有着匹配於年輕人經濟能力的用車成本。對於購買寶駿310的消費者來說,使用寶駿310是一種毫無壓力的出行方式,而不是給自身帶來額外的經濟壓力。而這正是現時許多年輕人所真真切切需要的。

吃一頓賣相稍好的快餐,買一張講求緣分的打折電影票,買一件摻雜着劣質尼龍面料的T恤。而現在,同等的價值你可以選擇駕駛寶駿310去各種你想去的地方,這才是年輕人該做的事。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準