分類: 3C資訊

  • 徹底搞懂 netty 線程模型

    徹底搞懂 netty 線程模型

    編者注:Netty是Java領域有名的開源網絡庫,特點是高性能和高擴展性,因此很多流行的框架都是基於它來構建的,比如我們熟知的Dubbo、Rocketmq、Hadoop等。本文就netty線程模型展開分析討論下 : )

    IO模型

    • BIO:同步阻塞IO模型;
    • NIO:基於IO多路復用技術的“非阻塞同步”IO模型。簡單來說,內核將可讀可寫事件通知應用,由應用主動發起讀寫操作;
    • AIO:非阻塞異步IO模型。簡單來說,內核將讀完成事件通知應用,讀操作由內核完成,應用只需操作數據即可;應用做異步寫操作時立即返回,內核會進行寫操作排隊並執行寫操作。

    NIO和AIO不同之處在於應用是否進行真正的讀寫操作。

    reactor和proactor模型

    • reactor:基於NIO技術,可讀可寫時通知應用;
    • proactor:基於AIO技術,讀完成時通知應用,寫操作應用通知內核。

    netty線程模型

    netty的線程模型是基於Reactor模型的。

    netty單線程模型

    Reactor 單線程模型,是指所有的 I/O 操作都在同一個 NIO 線程上面完成的,此時NIO線程職責包括:接收新建連接請求、讀寫操作等。

    在一些小容量應用場景下,可以使用單線程模型(注意,Redis的請求處理也是單線程模型,為什麼Redis的性能會如此之高呢?因為Redis的讀寫操作基本都是內存操作,並且Redis協議比較簡潔,序列化/反序列化耗費性能更低)。但是對於高負載、大併發的應用場景卻不合適,主要原因如下:

    • 一個NIO線程同時處理成百上千的連接,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的編碼、解碼、讀取和發送。
    • 當NIO線程負載過重之後,處理速度將變慢,這會導致大量客戶端連接超時,超時之後往往會進行重發,這更加重了NIO線程的負載,最終會導致大量消息積壓和處理超時,成為系統的性能瓶頸。
    • 可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。

    Reactor多線程模型

    Rector 多線程模型與單線程模型最大的區別就是有一組 NIO 線程來處理連接讀寫操作,一個NIO線程處理Accept。一個NIO線程可以處理多個連接事件,一個連接的事件只能屬於一個NIO線程。

    在絕大多數場景下,Reactor 多線程模型可以滿足性能需求。但是,在個別特殊場景中,一個 NIO 線程負責監聽和處理所有的客戶端連接可能會存在性能問題。例如併發百萬客戶端連接,或者服務端需要對客戶端握手進行安全認證,但是認證本身非常損耗性能。在這類場景下,單獨一個 Acceptor 線程可能會存在性能不足的問題,為了解決性能問題,產生了第三種 Reactor 線程模型——主從Reactor 多線程模型。

    Reactor主從多線程模型

    主從 Reactor 線程模型的特點是:服務端用於接收客戶端連接的不再是一個單獨的 NIO 線程,而是一個獨立的 NIO 線程池。Acceptor 接收到客戶端 TCP連接請求並處理完成后(可能包含接入認證等),將新創建的 SocketChannel注 冊 到 I/O 線 程 池(sub reactor 線 程 池)的某個I/O線程上, 由它負責SocketChannel 的讀寫和編解碼工作。Acceptor 線程池僅僅用於客戶端的登錄、握手和安全認證,一旦鏈路建立成功,就將鏈路註冊到後端 subReactor 線程池的 I/O 線程上,由 I/O 線程負責後續的 I/O 操作。

    netty線程模型思考

    netty 的線程模型並不是一成不變的,它實際取決於用戶的啟動參數配置。通過設置不同的啟動參數,Netty 可以同時支持 Reactor 單線程模型、多線程模型。

    為了盡可能地提升性能,Netty 在很多地方進行了無鎖化的設計,例如在 I/O 線程內部進行串行操作,避免多線程競爭導致的性能下降問題。表面上看,串行化設計似乎 CPU 利用率不高,併發程度不夠。但是,通過調整 NIO 線程池的線程參數,可以同時啟動多個串行化的線程并行運行,這種局部無鎖化的串行線程設計相比一個隊列多個工作線程的模型性能更優。(小夥伴們後續多線程併發流程可參考該類實現方案

    Netty 的 NioEventLoop 讀取到消息之後,直接調用 ChannelPipeline 的fireChannelRead (Object msg)。 只要用戶不主動切換線程, 一直都是由NioEventLoop 調用用戶的 ChannelHandler,期間不進行線程切換。這種串行化處理方式避免了多線程操作導致的鎖的競爭,從性能角度看是最優的。

    Netty擁有兩個NIO線程池,分別是bossGroupworkerGroup,前者處理新建連接請求,然後將新建立的連接輪詢交給workerGroup中的其中一個NioEventLoop來處理,後續該連接上的讀寫操作都是由同一個NioEventLoop來處理。注意,雖然bossGroup也能指定多個NioEventLoop(一個NioEventLoop對應一個線程),但是默認情況下只會有一個線程,因為一般情況下應用程序只會使用一個對外監聽端口。

    這裏試想一下,難道不能使用多線程來監聽同一個對外端口么,即多線程epoll_wait到同一個epoll實例上?

    epoll相關的主要兩個方法是epoll_wait和epoll_ctl,多線程同時操作同一個epoll實例,那麼首先需要確認epoll相關方法是否線程安全:簡單來說,epoll是通過鎖來保證線程安全的, epoll中粒度最小的自旋鎖ep->lock(spinlock)用來保護就緒的隊列, 互斥鎖ep->mtx用來保護epoll的重要數據結構紅黑樹

    看到這裏,可能有的小夥伴想到了Nginx多進程針對監聽端口的處理策略,Nginx是通過accept_mutex機制來保證的。accept_mutex是nginx的(新建連接)負載均衡鎖,讓多個worker進程輪流處理與client的新連接。當某個worker進程的連接數達到worker_connections配置(單個worker進程的最大處理連接數)的最大連接數的7/8時,會大大減小獲取該worker獲取accept鎖的概率,以此實現各worker進程間的連接數的負載均衡。accept鎖默認打開,關閉它時nginx處理新建連接耗時會更短,但是worker進程之間可能連接不均衡,並且存在“驚群”問題。只有在使能accept_mutex並且當前系統不支持原子鎖時,才會用文件實現accept鎖。注意,accept_mutex加鎖失敗時不會阻塞當前線程,類似tryLock。

    現代linux中,多個socker同時監聽同一個端口也是可行的,nginx 1.9.1也支持這一行為。linux 3.9以上內核支持SO_REUSEPORT選項,允許多個socker bind/listen在同一端口上。這樣,多個進程可以各自申請socker監聽同一端口,當連接事件來臨時,內核做負載均衡,喚醒監聽的其中一個進程來處理,reuseport機制有效的解決了epoll驚群問題。

    再回到剛才提出的問題,java中多線程來監聽同一個對外端口,epoll方法是線程安全的,這樣就可以使用使用多線程監聽epoll_wait了么,當然是不建議這樣乾的,除了epoll的驚群問題之外,還有一個就是,一般開發中我們使用epoll設置的是LT模式(水平觸發方式,與之相對的是ET默認,前者只要連接事件未被處理就會在epoll_wait時始終觸發,後者只會在真正有事件來時在epoll_wait觸發一次),這樣的話,多線程epoll_wait時就會導致第一個線程epoll_wait之後還未處理完畢已發生的事件時,第二個線程也會epoll_wait返回,顯然這不是我們想要的,關於java nio的測試demo如下:

    public class NioDemo {
        private static AtomicBoolean flag = new AtomicBoolean(true);
        public static void main(String[] args) throws Exception {
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.socket().bind(new InetSocketAddress(8080));
            // non-block io
            serverChannel.configureBlocking(false);
            Selector selector = Selector.open();
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            // 多線程執行
            Runnable task = () -> {
                try {
                    while (true) {
                        if (selector.select(0) == 0) {
                            System.out.println("selector.select loop... " + Thread.currentThread().getName());
                            Thread.sleep(1);
                            continue;
                        }
    
                        if (flag.compareAndSet(true, false)) {
                            System.out.println(Thread.currentThread().getName() + " over");
                            return;
                        }
    
                        Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                        while (iter.hasNext()) {
                            SelectionKey key = iter.next();
    
                            // accept event
                            if (key.isAcceptable()) {
                                handlerAccept(selector, key);
                            }
    
                            // socket event
                            if (key.isReadable()) {
                                handlerRead(key);
                            }
    
                            /**
                             * Selector不會自己從已選擇鍵集中移除SelectionKey實例,必須在處理完通道時手動移除。
                             * 下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
                             */
                            iter.remove();
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
    
            List<Thread> threadList = new ArrayList<>();
            for (int i = 0; i < 2; i++) {
                Thread thread = new Thread(task);
                threadList.add(thread);
                thread.start();
            }
            for (Thread thread : threadList) {
                thread.join();
            }
            System.out.println("main end");
        }
    
        static void handlerAccept(Selector selector, SelectionKey key) throws Exception {
            System.out.println("coming a new client... " + Thread.currentThread().getName());
            Thread.sleep(10000);
            SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        }
    
        static void handlerRead(SelectionKey key) throws Exception {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
    
            int num = channel.read(buffer);
            if (num <= 0) {
                // error or fin
                System.out.println("close " + channel.getRemoteAddress());
                channel.close();
            } else {
                buffer.flip();
                String recv = Charset.forName("UTF-8").newDecoder().decode(buffer).toString();
                System.out.println("recv: " + recv);
    
                buffer = ByteBuffer.wrap(("server: " + recv).getBytes());
                channel.write(buffer);
            }
        }
    }

    netty線程模型實踐

    (1) 時間可控的簡單業務直接在 I/O 線程上處理

    時間可控的簡單業務直接在 I/O 線程上處理,如果業務非常簡單,執行時間非常短,不需要與外部網絡交互、訪問數據庫和磁盤,不需要等待其它資源,則建議直接在業務 ChannelHandler 中執行,不需要再啟業務的線程或者線程池。避免線程上下文切換,也不存在線程併發問題。

    (2) 複雜和時間不可控業務建議投遞到後端業務線程池統一處理

    複雜度較高或者時間不可控業務建議投遞到後端業務線程池統一處理,對於此類業務,不建議直接在業務 ChannelHandler 中啟動線程或者線程池處理,建議將不同的業務統一封裝成 Task,統一投遞到後端的業務線程池中進行處理。過多的業務ChannelHandler 會帶來開發效率和可維護性問題,不要把 Netty 當作業務容器,對於大多數複雜的業務產品,仍然需要集成或者開發自己的業務容器,做好和Netty 的架構分層。

    (3) 業務線程避免直接操作 ChannelHandler

    業務線程避免直接操作 ChannelHandler,對於 ChannelHandler,IO 線程和業務線程都可能會操作,因為業務通常是多線程模型,這樣就會存在多線程操作ChannelHandler。為了盡量避免多線程併發問題,建議按照 Netty 自身的做法,通過將操作封裝成獨立的 Task 由 NioEventLoop 統一執行,而不是業務線程直接操作,相關代碼如下所示:

    如果你確認併發訪問的數據或者併發操作是安全的,則無需多此一舉,這個需要根據具體的業務場景進行判斷,靈活處理。

    推薦閱讀

    歡迎小夥伴關注【TopCoder】閱讀更多精彩好文。

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

    【其他文章推薦】

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • 前端Leader你應該知道的NPM包管理機制

    前端Leader你應該知道的NPM包管理機制

    npm install 命令

    首先總結下npm 安裝一個模塊包的常用命令。

    /* 模塊依賴會寫入 dependencies 節點 */
    
    npm install moduleName
    
    npm install -save moduleName
    
    npm install -S moduleName 
    
    /* 模塊依賴會寫入 devDependencies 節點 */
    
    npm install -save-dev moduleName
    
    npm install -D moduleName
    
    /* 全局安裝模塊包 */
    
    npm install -g moduleName
    
    /* 安裝特定版本的包 */
    
    npm install 包名@版本號
    
    /* 通過地址安裝git倉庫 */
    
    npm install git+https://github.com/itwmike/axios.git
    
    npm install git+ssh://git@github.com:itwmike/axios.git
     
    /* 安裝特定分支或Tag的git倉庫 */
    
    npm install git+https://github.com/itwmike/axios.git#tag
    
    /* 通過用戶名安裝git倉庫 */ 
    
    npm install github:帳號/倉庫名 # npm install github:itwmike/axios
    
    npm install github:帳號/倉庫名
    

      

    npm 依賴包版本號

    npm 所有node包都使用語義化版本號,規則要求如下:  

    • 每個版本號都形如1.2.3,由三個部分組成,依次叫做“主版本號(major)”、“次版本號(minor)”和“修訂號(patch)” 。

    • 當新版本無法兼容基於前一版本的代碼時,則提高主版本號 。

    • 當新版本新增了功能與特性,但仍兼容前一版本的代碼時,則提高次版本號 。

    • 當新版本僅僅修正漏洞或者增強效率,仍然兼容前一版本代碼,則提高修訂號。

    默認使用 npm install -save 下載的都是最新版本,同時會在package.json 文件中登記一個最優版本號,如下形式:

    "dependencies": {
      "axios": "^0.19.0"
    },
    

      

    最優版本號前面會多出一個“標記”,這個標記有啥意義?它的寫法又有哪些?

     

    npm install 都做了哪些事?

    拿到一個node項目時首要做法是運行 npm install 命令,這個命令將 package.json 文件中的依賴包自動解析並安裝,這也是項目能夠本地運行的前置條件。那如此簡單的一條命令,npm 背後又做了哪些不為人知的事呢?

    Number One

    自 npm 5.0后,項目中如果沒有 package-lock.json 文件的時候,npm 會自動幫我們生成。該文件的主要作用是記錄依賴包之間的具體版本號,對包版本有一個鎖定的意義,項目開發中應該將此文件上傳到git等版本控制工具(博主為此經歷了血淋淋的慘痛教育)。

    Number Two

    檢測本地包是否已經下載。如果本地 node_modules 下已經存在和 package-lock.json 中版本一致的包,則不會重新下載。

    Number Three

    下載依賴節點中對應的模板包。下載規則是:如果 package-lock.json 文件存在,則按照該文件中記錄的版本號下載對應的模塊包;如果文件不存在或文件中沒有該包的記錄,此時會按照版本號的標記(上面已講)規範下載並同時更新到 package-lock.json。

    了解了 package-lock.json 的作用后,筆者有個疑問:手動修改 package.json 中的包版本號后運行 npm install 命令會下載新包么?

    帶着這個疑問,筆者做了實驗,得出如下結論:

    • 如果新舊版本號差距較大,比如從 ^2.5.2 變為 2.6.0 ,那麼會下載最新包並且更新 package-lock.json 。

    • 如果新舊版本號差距較小,比如從 ^2.5.2 變為 2.5.4,那麼不會更新。

    總之是否更新要看特定情況,取決於 package.json中版本號的標記和 package-lock.json 是否一致。

    cnpm install 探索

    cnpm 是淘寶 npm 鏡像,在國內很受歡迎,雖然筆者並不喜歡使用。那 cnpm 和 npm 對包的管理是否一樣呢?

    • cnpm install 並不會生成 package-lock.json

    • cnpm install 並不受 package-lock.json 的約束,它會按照版本號標記規則下載依賴包

     

    由此可見,我們在項目中使用 cnpm 的時候一定要慎重,因為很可能團隊成員每個人使用的依賴包版本都不相同,造成打包后的結果也不同。

    如果團隊要使用 cnpm,請使用固定版本號的方式安裝依賴包如:cnpm install -E moduleName

     

    本文轉載自:

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

    【其他文章推薦】

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

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

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

    南投搬家前需注意的眉眉角角,別等搬了再說!

  • 微信作弊,為3款小遊戲引擎開掛增速,將給小遊戲帶來怎樣的利好?

    微信作弊,為3款小遊戲引擎開掛增速,將給小遊戲帶來怎樣的利好?

    11月14日小遊戲開發圈子,有一條重磅新聞:“微信小遊戲聯合遊戲引擎廠商,推出引擎插件功能,可為小遊戲提升0.5~2秒的啟動時間”。

    引擎插件是個什麼東西?

    昨天有不少人在問曉衡:“引擎插件到底是個什麼東西?”、“又要讓我學習新東西嗎?”、“引擎插件是怎麼加速的,不太明白?” …

    曉衡也在第一時間,將文檔通讀了一遍,並用自己的小遊戲工程做了測試,對微信小遊戲引擎插件算是有了一個簡單的認識,看下圖:

    普通模式,每一個使用遊戲引擎開發的小遊戲,都需要下載遊戲引擎代碼模塊。

    引擎插件模式,僅第一個遊戲需要下載引擎代碼,其它使用同類引擎的遊戲,可共享之前 A 遊戲下載過的遊戲引擎代碼,從而加速遊戲的啟動時間。

    從事小遊戲開發和運營的夥伴應該都很了解,H5、小遊戲注重啟動加載速度,它對新用戶的體驗和流失都至關重要。

    啟動概況分析

    估計有人會覺得引擎插件就加快了0.5~2秒有什麼用?眨個眼的時間而已。

    其實對使用 Cocos Creator 開發的休閑類的小遊戲來說,目前的微信小遊戲啟動速度已經很不錯了,首包含引擎的話,iOS 在4 ~ 6秒啟動,Android 大多可以在6~8秒左右打開首屏,並不像微信吹噓的1秒啟動,估計只有 引擎和資源全放子包的遊戲可以做到!

    下面我將自己個人開發的一款微信小遊戲,在微信公測的前後两天做了一個數據統計,想窺視一下啟動性能對留存的影響,下圖是曉衡的遊戲在8月7日 ~ 8月9日時的活躍情況:

    遊戲是在8月7日的晚上10:00點打開的微信公測,微信平台在24小時內持續導入5184的用戶,當天遊戲啟動8121次。不過圖片上的數據比較尷尬,公測一過就沒幾個玩家了,但它不是我們要講的重點,我們是用這個時間節點、用戶數量,來看微信小遊戲的啟動性能表現。

    iOS啟動概況

    Android啟動概況

    從圖中看,8月7日這天 iOS 的總啟動時間比 Android 快 3.88 秒,Android 的用戶流失比 iOS 要多 12.55%,這裏重點也不是說 iOS 和 Android 系統那個好,而是看遊戲的啟動時間對首屏打開留存的影響。

    這是另一個朋友的遊戲《周車勞盾》在9月14日微信公測4800+用戶,遊戲啟動7000+次,下面是它在9月13~9月15日的啟動概況:

    《周車勞盾》的 iOS 首屏打開留存率由於用戶數太少,不太好與 Android 對比,並且朋友說當時遊戲沒有做分包優化。在9月14日公測當天,由於新用戶多,iOS、Android 的啟動速度都不快,在 10 秒左右。從中也可以看出微信小遊戲用戶,以及微信導量用戶,以 Android 屬性為主。

    啟動流失分析

    下圖是曉衡的遊戲在8月8日公測時的 Android 手機用戶流失分佈情況,統計一共有 893 名流失用戶:

    從前面的啟動概況看到,小遊戲啟動進入首屏是在8.38s,我們以9秒為分界線,將上圖分成左右兩部分:

    • 右邊標註綠色線框,是已經進入遊戲后流失的人數,這部分的優化需要美術和策劃同學的幫助。
    • 左邊紅色線框中的用戶,是在遊戲啟動過程中流失掉的共計679人佔76%,而且前4秒流失的最多共543占 60%,如果不計算已經打開首屏的更是高達80%,因此前幾秒它才是我們關心的重點。

    曉衡根據平常使用微信的習慣,模擬分析一下前8秒的用戶是大概會是什麼情況走失的,需要注意的一個前題是,這些用戶都是微信導量進入的,絕大多是手滑不小心點到廣告,並不是目標用戶。

    • 第1秒:1秒流失用戶,手滑的機率最大,似乎經過專業訓練,眼、腦、手的速度都非常的快,遊戲是什麼都沒看清就閃人了;
    • 第2秒:2秒流失用戶,與1秒戶大概差不多,只是動作稍慢而已,此時遊戲圖標已經進入視覺系統,但估計比較模糊,瞬間閃人;
    • 第3秒:3秒流失用戶,不僅遊戲圖標已經從視覺系統進入大腦,遊戲名字估計也是能看清楚,但是沒有任何感覺,同樣是條件反射,快速點擊關閉;
    • 第4秒:4秒流失用戶,已經是把遊戲圖標、名字已經完全進入大腦神經迴路,給他反饋的信號是沒有愛,甚至是反感,迅速閃人了。第4秒很關鍵,因為用戶已經有了思考!
    • 第5~6秒:5~6秒流失用戶,認真看完遊戲圖標、名字,以及加載進度,經過大腦綜合反饋,這個遊戲不值得等待,88了!
    • 第7~8秒:7~8秒流失用戶,估計是盯到了遊戲的加載進度,在100%或某個数字上停止下一瞬間,實在是不耐煩了,什麼個鬼遊戲,半天進不去,走了!

    以上分析是曉衡的個人YY,僅供參考,這裏要說的是前 3 秒流失的用戶大多是條件反射,很難轉化。當用戶將遊戲圖標、遊戲名稱看清了后,大腦產生了思考,再離開的這對我們來說還有機會爭取,讓他們早點看到遊戲首屏,已經花了這5、6秒了,體驗一下再走!

    提升遊戲0.5 ~ 2秒的啟動速度是非常具有價值的,而且小遊戲絕大多數又是 Android 用戶,特別是對需要買量的遊戲來說,時間就是金錢,毫秒必爭。

    引擎插件帶來的好處

    下面我們再來看看,引擎插件具體在那些場景下會帶來比較明顯的性能提供,盡可能充分利用這個機制呢?

    微信公測

    對於個人開發者,使用微信公測功能免費送5000流量,一定要利用好這個機會。將首包資源做到盡量小巧,引擎裁剪、圖片壓縮一定要做足,同時盡量選擇使用量較多的引擎版本號(目前曉衡了解到的,使用較多 Cocos Creator 引擎版本號分佈在:2.0.8 ~ 2.0.10、2.1.3、2.2.0,不過還是要以微信或 Cocos 官方統計為準),這樣容易蹭上已經下載過的遊戲引擎,這對大多數遊戲來說都是適用的。

    中重度遊戲

    中重度遊戲,通常會依賴較多的引擎模塊,比如 RPG 遊戲中的:地圖、角色動畫,會使用TileMap、Spine、DragonBones、Animation 等模塊,還有一些遊戲會使用到物理引擎模塊、碰撞模塊等,完整引擎模塊高達 1.6M。

    隨着微信引擎插件的廣泛普及,以後構建遊戲完全時可以將引擎裁剪到最精簡狀態,大概在550K左右。甚至可以想像到,以後小遊戲平台完全不用上傳引擎代碼,構建時只用配置上使用的什麼引擎,引擎版本號即可。

    中重度遊戲利用引擎插件同樣可以快速進入首屏,首包僅保留炫麗的動效和初始界面,用分治的方式動態下載遊戲當前必要的內容,儘快讓用戶參与到遊戲中去。還有隨着 5G 的到來,中重度遊戲的遊戲資源下載劣勢也會得到改善,對小遊戲更是一件好事。

    遊戲矩陣

    單款小遊戲一般是很難有收益的,甚至是虧本買賣。微信平台,一個小遊戲可以支持10個遊戲的跳轉,目前絕大多數遊戲商廠,都會在小遊戲中集成其它遊戲的入口加大流量,優質的遊戲還會出售遊戲跳轉坑位,有的還價格不菲。個人開發者也意識到了遊戲間跳轉帶來的爆光機會,不少開發者會在微信公眾時,組織邀請好友建立鏈接。

    如果是自家開發的休閑小遊戲,利用引擎插件的啟動增速,再配合上自定義的啟動背景(頭條支持),讓玩家感受不出是在不同遊戲中切換,在矩陣中瞬間穿梭,這也將極大增加遊戲的曝光率,降低流失。

    小結

    劉潤老師說的好:“一切的商業價值,要看是否讓用戶獲益”。

    微信引擎插件不僅讓普通用戶能獲得更好的遊戲體驗,也能讓遊戲開發商能中從獲益。點開即玩的小遊戲,縮短了遊戲產品呈現在用戶手中的時間,極大優化了產品的傳遞價值。

    曉衡是一個搬運工,傳遞有價值的遊戲開發技術,如果覺得本文對你有用,感謝來看個再看或傳遞給朋友。感謝您的閱讀,願我們在前進的道路上“砥礪前行,共同成長!”

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

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 盤點2016年起實施的新能源汽車新政策

    盤點2016年起實施的新能源汽車新政策

    電動汽車充電介面及通信協定5項新國標  

    第一版電動汽車充電介面等4項國家標準曾在2011年發佈,並於2012年3月1日起實施,包括《電動汽車傳導充電用連接裝置第1部分:通用要求》、《電動汽車傳導充電用連接裝置第2部分:交流充電介面》、《電動汽車傳導充電用連接裝置第3部分:直流充電介面》《電動汽車非車載傳導式充電機與電池管理系統之間的通信協定》等。此次發佈的5項國家標準在4項標準的修訂基礎上新增《電動汽車傳導充電系統 第1部分:一般要求》,於2016年1月1日起正式實施。

    標準內容:新修訂的5項國家標準主要在提高電動汽車充電設施安全性及相容性方面做出更進一步的要求。

    交流充電部分,更新禁止採用存在安全隱患的直通電纜加普通家用插頭的連接方式,大於16安培的充電方式要求在車輛插座和供電插座安裝電子鎖和溫度感測器等規範。

     

    直流充電部分,更新在直流充電槍內要求安裝電子鎖,同時預留車輛插座加裝電子鎖的機械結構,要求車輛和設施必須具備檢測和告警功能等規範。

     

    新能源車補貼政策  

    2015年4月29日,新一輪新能源汽車補貼政策正式出臺,其中指出在2016-2020年,對消費者購買的進入國家新能源車目錄的純電動汽車、插電式混合動力汽車和燃料電池汽車繼續給予購車補貼。新標準規定新能源車的補貼將分階段退坡,到2020年補貼標準將在2016年基礎上下降40%。

    政策內容:新的補貼標準將依據節能減排效果,並綜合考慮生產成本、規模效應、技術進步等因素逐步下調。具體下調辦法是,2017-2018年補貼標準將在2016年基礎上下降20%,2019-2020年補貼標準在2016年基礎上下降40%。而燃料電池汽車的補貼將從2015年的18萬元/輛提升至20萬元/輛。

     對比2013—2015年新能源車補貼標準,2016年起執行的新補貼標準針對續駛里程大於等於80km小於150km的純電動車和增程式在內的插電式混合動力乘用車的補貼金額分別降低了6500元和1500元,而續駛里程大於等於250km的電動車和燃料電池乘用車補貼金額分別提高了1000元和2萬元,因而您在購買新能源車的時候,續駛里程將是非常關鍵的價格影響因素。

    新補貼標準對補助範圍內的新能源汽車產品技術的要求也有所提高,其中純電動乘用車的最低續駛里程由大於等於80km提升至100km,同時在行駛速度方面,純電動乘用車30分鐘最高車速應不低於100km/h。

    車船稅管理新規  

    規程內容:規程中指出,已經繳納車船稅的車船,因品質原因,車船被退回生產企業或者經銷商的,納稅人可以向納稅所在地的主管稅務機關申請退還自退貨月份起至該納稅年度終了期間的稅款,退貨月份以退貨發票所載日期的當月為准。

    此外,已完稅車輛被盜搶、報廢、滅失而申請車船稅退稅的,由納稅人納稅所在地的主管稅務機關按照有關規定辦理。

    而對不屬於車船稅徵稅範圍的純電動乘用車和燃料電池乘用車,應當積極獲取車輛的相關資訊予以判斷,對其徵收了車船稅的應當及時予以退稅。

    第四階段乘用車燃料消耗量限值  

    政策內容:2012年6月28日,國務院發佈《節能與新能源汽車產業發展規劃(2012-2020年)》,明確了我國汽車節能標準的整體目標,要求2020年當年乘用車新車平均燃料消耗量達到5.0 L/100km。目前,第四階段的GB 19578-2014《乘用車燃料消耗量限值》和GB 27999-2014《乘用車燃料消耗量評價方法及指標》已於2014年12月22日正式發佈,於2016年1月1日起實施,要求汽車生產企業2016年平均燃料消耗標準需滿足6.7L/100km。

    除了以上提到的自2016年1月1日起實施的政策外,還有一些和我們密切相關的政策和措施已在2015年的最後一天終止或完成,讓我們也來關注一下。

    節能惠民補貼政策到期取消

    政策內容:2013年,國家財政部、發展改革委、工業和資訊化部發佈《關於開展1.6升及以下節能環保汽車推廣工作的通知》,決定從2013年10月1日起至2015年12月31日,繼續實施1.6升及以下節能環保汽車(乘用車)推廣政策,對購買符合條件節能汽車的消費者給予3000元補貼。截止2015年末,國家共實施了3個階段的汽車節能補貼,從每一輪政策的推出可以發現,國家對享受節能補貼的車型要求在逐年提高,推廣車輛要達到產品綜合燃料消耗量標準也由2011年的6.9L調整到2013年的5.9L,且需符合“國V”排放標準。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

  • 賓士傳將開發四款純電動車

    世界級車商賓士(Mercedes-Benz)被報導正在開發全新的電動車平台,並將以此平台為基礎,推出四款純電動汽車。

    ETToday引述外媒《Car Magazine》的報導,表示賓士正以MRA平台為基礎開發一款名為「EVA」的電動車平台,可供後續開發C-Class到S-Class尺碼的各式車款,最高能搭載400公斤的電池組。而在動力方面,後驅車型可能搭載一具402匹馬力的電動馬達,四驅車行可能還會再加上一具120匹馬力或者201匹馬力的馬達。主動懸載、扭力分配、動能回收等功能也將面面俱到。

    該報導表示,賓士官方並未證實這項消息,但將在2018年推出C-Class與E-Class之間的電動轎車,並以此為基礎再行打造一款休旅車。此後還有S-Class和GLS等級的電動車款會問世。《Car Magazine》表示,賓士目標為每年銷售2萬輛電動車,入門車款售價為7萬英鎊起跳。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 吉利:到2020年新能源汽車銷量將實現90%以上的占比

    日前,吉利控股集團董事長李書福在接受《中國經營報》記者採訪時表示,以後我們開發的新產品基本是新能源汽車和智慧互聯汽車,傳統汽車就逐漸不生產了。

    此話背後,實際就是吉利於近日提出的“藍色吉利行動”中的重要一環,到2020年,其新能源汽車銷量將實現90%以上的銷量占比。

    具體來說,根據“藍色吉利行動”,吉利新能源將在技術上主打純電動、油電混動、插電式混合動力三種路線,並通過兩大平臺——FE(中高端)與PE(緊湊級)平臺來發展純電動車板塊。

    從以上路徑上看,吉利與其他車企並無二致,但其公佈的“五大承諾”卻足以讓業內熱議——第一,提前全面實現2020年國家第四階段每百公里5.0L的企業平均燃油消耗限值;第二,實現消費者用傳統汽車的購買成本購買插電式混動汽車的夢想;第三,實現到2020年新能源汽車銷量占吉利整體銷量的90%以上;第四,在氫燃料及金屬燃料電池汽車研發方面取得實質性成果;第五,實現新能源技術,智慧化、輕量化技術在行業的領先地位。

    雖然言之鑿鑿,但不得不說的是,90%以上的新能源銷量加上此前吉利發佈的“2020年實現120萬輛”的銷量目標,同年吉利新能源汽車銷量目標竟逾100萬輛。

    對此,吉利控股集團總裁CEO安聰慧表示:“吉利制定這樣的目標並不是為了和其他企業進行對比,而是結合自身發展提出來的。”

    據其介紹,在技術領域,吉利汽車將以與沃爾沃合作打造的CMA中高級車基礎模組架構為核心打造新能源車型,該方面的設計研發工作主要由吉利汽車歐洲研發中心承擔,該中心在瑞典哥德堡已有1200名工程師,中國杭州也有300名工程師,負責架構開發、上車體開發、核心部件開發開發,整車設計、工程製造及新技術的研發。CMA基礎模組架構可以實現電機+發動機等核心部件的批量生產。

    此外,2015年初,吉利與新大洋機電集團成立合資公司並推出知豆電動車,加之此前其子公司上海華普國潤與康迪車業成立的合資公司,署名吉利旗下的新能源車型並不止吉利品牌。雖然,吉利在新能源領域的開疆破土頗有“借力而為”之感,但不論如何,2015年1~11月,在乘聯會統計的自主品牌新能源車銷量占比情況中,吉利節節攀升,的確實現了在自主品牌領域的市占率穩增,也正因如此,加之政策的多重鼓勵,吉利才許下到2020年實現新能源汽車逾百萬的戰略目標。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

  • 北京公車採購案 八成為電動車

    北京公交集團持續落實首都空氣清淨規劃,2016年計畫採購2700輛新的公車,其中有81%將採購電動車款,以減少大眾運輸系統的碳排放量。

    北京霧霾問題嚴重,改用電動車來取代汽油車是減輕空氣汙染的方法之一。2015年間,北京公交集團共置換了2306輛汽油公車為新式環保公車,其中有一半以上是新能源電動車。同時,2015年改造超過8000輛柴油公車,減少60%的氮氧化合物排放量。

    在純電動車基礎設施方面,北京公交集團陸續興建吳癸電動車線路網、變電站、充電站網絡等,共有21個公車站可供純電動車充電。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

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

  • 北汽豪擲80億元佈局新能源汽車

    2015年12月28日,北京汽車集團有限公司就“北汽新能源汽車動力電池”、“北汽集團常州產業基地”兩個專案,與常州市政府簽約。

    此次北汽集團擬在常州建設的兩個項目總投資80億元,其中,北汽新能源汽車動力電池專案總投資約30億元,規劃動力電池產能達到5G瓦時,同時將以滆湖低碳濕地公園培訓中心為主體,打造北汽新能源綠色商學院,其主要目的是加強新體系電池基礎研究和關鍵技術開發,推進新一代鋰離子電池的工程化和產業化,實現對動力電池產業鏈核心環節資源掌控,以支撐北汽新能源業務需求。

    而北汽集團常州產業基地專案總投資50億元,總規劃年產30萬輛整車及配套零部件、物流專案,其中一期年產15萬輛SUV、MPV和輕型客車,二期重點生產新能源汽車,打造產業生態鏈。

    此前,北汽集團總投資100億元的新能源汽車和總投資50億元的通用航空兩個項目已經於今年4月和10月相繼落戶常州。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • Deepin 下 使用 Rider 開發 .NET Core

    Deepin 下 使用 Rider 開發 .NET Core

    目錄

    Deepin 下 使用 Rider 開發 .NET Core

    國產的 Deepin 不錯,安利一下。

    Deepin 用了也有一兩年,也只是玩玩,沒用在開發上面。後來 Win10 不太清真了,就想着能不能到 Deepin下擼碼。要搞開發,首先少不了 IDE,VS2019 用不來,Vs Code 太複雜、麻煩,後來發現了 Rider 這個神器,可是 Rider 是英文界面,筆者的英文是渣渣的。結果在 Windows 下 使用 Rider 開發一段時間后, 已經熟悉了 Rider ,於是計劃後面遷移到 Deepin 下開發 .NET Core 。筆者裝了雙系統 Windows10 + Deepin 15。

    安裝 Rider

    Rider 的Linux 下載地址

    下載壓縮包后,將壓縮包解壓,打開 bin 目錄,在目錄下打開終端,運行

    sh rider.sh

    或者直接點擊 rider.sh 文件,選擇執行即可。

    之後會彈出安裝界面。

    根據提示一步步安裝。

    最後會要求輸入賬號密碼或者激活碼激活 Rider 。

    我這個是高材生的福利~你們沒有的話就用 Github 開源項目免費申請使用,或者其他手段激活。

    安裝完畢后,點擊 New Solution ,發現只能創建 .NET Frameork 的項目(Mono)。

    先關閉 Rider ,接下來安裝 .NET Core

    安裝 .NET Core SDK

    有兩種安裝方法

    1. 自己下載二進制的 安裝包

    2. 使用軟件包形式安裝

      無論哪種方法,如果不把 SDK/Runtime 放到 /usr/share/dotnet 下,Rider 是無法識別的(默認路徑,可以進入Rider修改設置),下面兩種方法都是在 Linux 簡單二進制安裝 .NET Core SDK的方法。

    sudo ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet -f
    export DOTNET_ROOT=/usr/share/dotnet export 
    PATH=$PATH:/usr/share/dotnet

    推薦第一種方法,第二種方法的或,可以按照微軟的文檔自己試試。。。

    安裝完畢后就是這個樣子。

    如果要通過軟件包形式安裝,參考一下這裏 

    體驗開發

    想不到在 Deepin 下,Rider 竟然支持 Desktop Application(WPF)。

    不過這不是重點,我又不會 WPF,先試一下 ASP.NET Core ,晚一點再看看 WPF。

    運行的時候,報這個錯,是 Https 證書的問題,只需要任意位置打開終端,輸入下面的命令即可。

    dotnet dev-certs https

    不知道為什麼,瀏覽器打開 Blazor 應用一片空白。。。

    換成 MVC 試試。

    不知道為什麼 Blazor 打開會空白。不管了,試試 Desktop Application。

    創建 Wpf 項目后,提示要安裝插件,然後退出重新打開。

    不過最後重新打開項目還是報錯

    Project 'WpfApp1' load finished with warnings
                The imported project "/usr/share/dotnet/sdk/3.0.100/Sdks/Microsoft.NET.Sdk.WindowsDesktop/targets/Microsoft.WinFX.targets" was not found. Confirm that the path in the <Import> declaration is correct, and that the file exists on disk.  /usr/share/dotnet/sdk/3.0.100/Sdks/Microsoft.NET.Sdk.WindowsDesktop/targets/Microsoft.NET.Sdk.WindowsDesktop.targets at (26:3)
                Windows is required to build Windows desktop applications. at (59:5)

    算了~就這樣好了,反正我又不會 WPF ~

    本文是使用 Typora 寫的,很清真。

    好好學習唄~

    最後錄了個視頻玩,不知道說啥,看看內容界面就好~

    打不開的話,請點擊 

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

  • .NET Core 3.0 單元測試與 Asp.Net Core 3.0 集成測試

    .NET Core 3.0 單元測試與 Asp.Net Core 3.0 集成測試

    單元測試與集成測試

    測試必要性說明

    相信大家在看到單元測試與集成測試這個標題時,會有很多感慨,我們無數次的在實踐中提到要做單元測試、集成測試,但是大多數項目都沒有做或者僅建了項目文件。這裡有客觀原因,已經接近交付日期了,我們沒時間做白盒測試了。也有主觀原因,面對業務複雜的代碼我們不知道如何入手做單元測試,不如就留給黑盒測試吧。但是,當我們的代碼無法進行單元測試的時候,往往就是代碼開始散發出壞味道的時候。長此以往,將欠下技術債務。在實踐過程中,技術債務常常會存在,關鍵在於何時償還,如何償還。

    上圖說明了隨着時間的推移開發/維護難度的變化。

    測試框架選擇

    在 .NET Core 中,提供了 xUnit 、NUnit 、 MSTest 三種單元測試框架。

    MSTest UNnit xUnit 說明 提示
    [TestMethod] [Test] [Fact] 標記一個測試方法
    [TestClass] [TestFixture] n/a 標記一個 Class 為測試類,xUnit 不需要標記特性,它將查找程序集下所有 Public 的類
    [ExpectedException] [ExpectedException] Assert.Throws 或者 Record.Exception xUnit 去掉了 ExpectedException 特性,支持 Assert.Throws
    [TestInitialize] [SetUp] Constructor 我們認為使用 [SetUp] 通常來說不好。但是,你可以實現一個無參構造器直接替換 [SetUp]。 有時我們會在多個測試方法中用到相同的變量,熟悉重構的我們會提取公共變量,並在構造器中初始化。但是,這裏我要強調的是:在測試中,不要提取公共變量,這會破壞每個測試用例的隔離性以及單一職責原則。
    [TestCleanup] [TearDown] IDisposable.Dispose 我們認為使用 [TearDown] 通常來說不好。但是你可以實現 IDisposable.Dispose 以替換。 [TearDown] 和 [SetUp] 通常成對出現,在 [SetUp] 中初始化一些變量,則在 [TearDown] 中銷毀這些變量。
    [ClassInitialize] [TestFixtureSetUp] IClassFixture< T > 共用前置類 這裏 IClassFixture< T > 替換了 IUseFixture< T > ,
    [ClassCleanup] [TestFixtureTearDown] IClassFixture< T > 共用後置類 同上
    [Ignore] [Ignore] [Fact(Skip=”reason”)] 在 [Fact] 特性中設置 Skip 參數以臨時跳過測試
    [Timeout] [Timeout] [Fact(Timeout=n)] 在 [Fact] 特性中設置一個 Timeout 參數,當允許時間太長時引起測試失敗。注意,xUnit 的單位時毫秒。
    [DataSource] n/a [Theory], [XxxData] Theory(數據驅動測試),表示執行相同代碼,但具有不同輸入參數的測試套件 這個特性可以幫助我們少寫很多代碼。

    以上寫了 MSTest 、UNnit 、 xUnit 的特性以及比較,可以看出 xUnit 在使用上相對其它兩個框架來說提供更多的便利性。但是這裏最終實現還是看個人習慣以選擇。

    單元測試

    1. 新建單元測試項目

    2. 新建 Class

    3. 添加測試方法

              /// <summary>
              /// 添加地址
              /// </summary>
              /// <returns></returns>
              [Fact]
              public async Task Add_Address_ReturnZero()
              {
                  DbContextOptions<AddressContext> options = new DbContextOptionsBuilder<AddressContext>().UseInMemoryDatabase("Add_Address_Database").Options;
                  var addressContext = new AddressContext(options);
      
                  var createAddress = new AddressCreateDto
                  {
                      City = "昆明",
                      County = "五華區",
                      Province = "雲南省"
                  };
                  var stubAddressRepository = new Mock<IRepository<Domain.Address>>();
                  var stubProvinceRepository = new Mock<IRepository<Province>>();
                  var addressUnitOfWork = new AddressUnitOfWork<AddressContext>(addressContext);
      
                  var stubAddressService = new AddressServiceImpl.AddressServiceImpl(stubAddressRepository.Object, stubProvinceRepository.Object, addressUnitOfWork);
                  await stubAddressService.CreateAddressAsync(createAddress);
                  int addressAmountActual = await addressContext.Addresses.CountAsync();
                  Assert.Equal(1, addressAmountActual);
              }
      • 測試方法的名字包含了測試目的、測試場景以及預期行為。
      • UseInMemoryDatabase 指明使用內存數據庫。
      • 創建 createAddress 對象。
      • 創建 Stub 。在單元測試中常常會提到幾個概念 Stub , Mock 和 Fake ,那麼在應用中我們該如何選擇呢?
        • Fake – Fake 通常被用於描述 Mock 或 Stub ,如何判斷它是 Stub 還是 Mock 依賴於使用上下文,換句話說,Fake 即是 Stub 也是 Mock 。
        • Stub – Stub 是系統中現有依賴項的可控替代品。通過使用 Stub ,你可以不用處理依賴直接測試你的代碼。默認情況下, 偽造對象以stub 開頭。
        • Mock – Mock 對象是系統中的偽造對象,它決定單元測試是否通過或失敗。Mock 會以 Fake 開頭,直到被斷言為止。
      • Moq4 ,使用 Moq4 模擬我們在項目中依賴對象。
    4. 打開視圖 -> 測試資源管理器。

    5. 點擊運行,得到測試結果。

    6. 至此,一個單元測試結束。

    集成測試

    集成測試確保應用的組件功能在包含應用的基礎支持下是正確的,例如:數據庫、文件系統、網絡等。

    1. 新建集成測試項目。

    2. 添加工具類 Utilities 。

      using System.Collections.Generic;
      using AddressEFRepository;
      
      namespace Address.IntegrationTest
      {
          public static class Utilities
          {
              public static void InitializeDbForTests(AddressContext db)
              {
                  List<Domain.Address> addresses = GetSeedingAddresses();
                  db.Addresses.AddRange(addresses);
                  db.SaveChanges();
              }
      
              public static void ReinitializeDbForTests(AddressContext db)
              {
                  db.Addresses.RemoveRange(db.Addresses);
                  InitializeDbForTests(db);
              }
      
              public static List<Domain.Address> GetSeedingAddresses()
              {
                  return new List<Domain.Address>
                  {
                      new Domain.Address
                      {
                          City = "貴陽",
                          County = "測試縣",
                          Province = "貴州省"
                      },
                      new Domain.Address
                      {
                          City = "昆明市",
                          County = "武定縣",
                          Province = "雲南省"
                      },
                      new Domain.Address
                      {
                          City = "昆明市",
                          County = "五華區",
                          Province = "雲南省"
                      }
                  };
              }
          }
      }
    3. 添加 CustomWebApplicationFactory 類,

       using System;
       using System.IO;
       using System.Linq;
       using AddressEFRepository;
       using Microsoft.AspNetCore.Hosting;
       using Microsoft.AspNetCore.Mvc.Testing;
       using Microsoft.EntityFrameworkCore;
       using Microsoft.Extensions.Configuration;
       using Microsoft.Extensions.DependencyInjection;
       using Microsoft.Extensions.Logging;
      
       namespace Address.IntegrationTest
       {
           public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
           {
               protected override void ConfigureWebHost(IWebHostBuilder builder)
               {
                   string projectDir = Directory.GetCurrentDirectory();
                   string configPath = Path.Combine(projectDir, "appsettings.json");
                   builder.ConfigureAppConfiguration((context, conf) =>
                   {
                       conf.AddJsonFile(configPath);
                   });
      
                   builder.ConfigureServices(services =>
                   {
                       ServiceDescriptor descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AddressContext>));
      
                       if (descriptor != null)
                       {
                           services.Remove(descriptor);
                       }
      
                       services.AddDbContextPool<AddressContext>((options, context) =>
                       {
                           //var configuration = options.GetRequiredService<IConfiguration>();
                           //string connectionString = configuration.GetConnectionString("TestAddressDb");
                           //context.UseMySql(connectionString);
                           context.UseInMemoryDatabase("InMemoryDbForTesting");
      
                       });
      
                       // Build the service provider.
                       ServiceProvider sp = services.BuildServiceProvider();
                       // Create a scope to obtain a reference to the database
                       // context (ApplicationDbContext).
                       using IServiceScope scope = sp.CreateScope();
                       IServiceProvider scopedServices = scope.ServiceProvider;
                       var db = scopedServices.GetRequiredService<AddressContext>();
                       var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
      
                       // Ensure the database is created.
                       db.Database.EnsureCreated();
      
                       try
                       {
                           // Seed the database with test data.
                           Utilities.ReinitializeDbForTests(db);
                       }
                       catch (Exception ex)
                       {
                           logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message);
                       }
                   });
               }
           }
       }
      • 這裏為什麼要添加 CustomWebApplicationFactory 呢?
        WebApplicationFactory 是用於在內存中引導應用程序進行端到端功能測試的工廠。通過引入自定義 CustomWebApplicationFactory 類重寫 ConfigureWebHost 方法,我們可以重寫我們在 StartUp 中定義的內容,換句話說我們可以在測試環境中使用正式環境的配置,同時可以重寫,例如:數據庫配置,數據初始化等等。
      • 如何準備測試數據?
        我們可以使用數據種子的方式加入數據,數據種子可以針對每個集成測試做數據準備。
      • 除了內存數據庫,還可以使用其他數據庫進行測試嗎?
        可以。
    4. 添加集成測試 AddressControllerIntegrationTest 類。

       using System.Collections.Generic;
       using System.Linq;
       using System.Net.Http;
       using System.Threading.Tasks;
       using Address.Api;
       using Microsoft.AspNetCore.Mvc.Testing;
       using Newtonsoft.Json;
       using Xunit;
      
       namespace Address.IntegrationTest
       {
           public class AddressControllerIntegrationTest : IClassFixture<CustomWebApplicationFactory<Startup>>
           {
               public AddressControllerIntegrationTest(CustomWebApplicationFactory<Startup> factory)
               {
                   _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                   {
                       AllowAutoRedirect = false
                   });
               }
      
               private readonly HttpClient _client;
      
               [Fact]
               public async Task Get_AllAddressAndRetrieveAddress()
               {
                   const string allAddressUri = "/api/Address/GetAll";
                   HttpResponseMessage allAddressesHttpResponse = await _client.GetAsync(allAddressUri);
      
                   allAddressesHttpResponse.EnsureSuccessStatusCode();
      
                   string allAddressStringResponse = await allAddressesHttpResponse.Content.ReadAsStringAsync();
                   var addresses = JsonConvert.DeserializeObject<IList<AddressDto.AddressDto>>(allAddressStringResponse);
                   Assert.Equal(3, addresses.Count);
      
                   AddressDto.AddressDto address = addresses.First();
                   string retrieveUri = $"/api/Address/Retrieve?id={address.ID}";
                   HttpResponseMessage addressHttpResponse = await _client.GetAsync(retrieveUri);
      
                   // Must be successful.
                   addressHttpResponse.EnsureSuccessStatusCode();
      
                   // Deserialize and examine results.
                   string addressStringResponse = await addressHttpResponse.Content.ReadAsStringAsync();
                   var addressResult = JsonConvert.DeserializeObject<AddressDto.AddressDto>(addressStringResponse);
                   Assert.Equal(address.ID, addressResult.ID);
                   Assert.Equal(address.Province, addressResult.Province);
                   Assert.Equal(address.City, addressResult.City);
                   Assert.Equal(address.County, addressResult.County);
               }
           }
       }
    5. 在測試資源管理器中運行集成測試方法。

    6. 結果。

    7. 至此,集成測試完成。需要注意的是,集成測試往往耗時比較多,所以建議能使用單元測試時就不要使用集成測試。

    總結:當我們寫單元測試時,一般不會同時存在 Stub 和 Mock 兩種模擬對象,當同時出現這兩種對象時,表明單元測試寫的不合理,或者業務寫的太過龐大,同時,我們可以通過單元測試驅動業務代碼重構。當需要重構時,我們應盡量完成重構,不要留下欠下過多技術債務。集成測試有自身的複雜度存在,我們不要節約時間而打破單一職責原則,否則會引發不可預期後果。為了應對業務修改,我們應該在業務修改以後,進行回歸測試,回歸測試主要關注被修改的業務部分,同時測試用例如果有沒要可以重寫,運行整個和修改業務有關的測試用例集。

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!