4. 彤哥說netty系列之Java NIO實現群聊(自己跟自己聊上癮了)

你好,我是彤哥,本篇是netty系列的第四篇。

歡迎來我的公從號彤哥讀源碼系統地學習源碼&架構的知識。

簡介

上一章我們一起學習了Java中的BIO/NIO/AIO的故事,本章將帶着大家一起使用純純的NIO實現一個越聊越上癮的“群聊系統”。

業務邏輯分析

首先,我們先來分析一下群聊的功能點:

(1)加入群聊,並通知其他人;

(2)發言,並通知其他人;

(3)退出群聊,並通知其他人;

一個簡單的群聊系統差不多這三個功能足夠了,為了方便記錄用戶信息,當用戶加入群聊的時候自動給他分配一個用戶ID。

業務實現

上代碼:

// 這是一個內部類
private static class ChatHolder {
    // 我們只用了一個線程,用普通的HashMap也可以
    static final Map<SocketChannel, String> USER_MAP = new ConcurrentHashMap<>();

    /**
     * 加入群聊
     * @param socketChannel
     */
    static void join(SocketChannel socketChannel) {
        // 有人加入就給他分配一個id,本文來源於公從號“彤哥讀源碼”
        String userId = "用戶"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
        send(socketChannel, "您的id為:" + userId + "\n\r");

        for (SocketChannel channel : USER_MAP.keySet()) {
            send(channel, userId + " 加入了群聊" + "\n\r");
        }

        // 將當前用戶加入到map中
        USER_MAP.put(socketChannel, userId);
    }

    /**
     * 退出群聊
     * @param socketChannel
     */
    static void quit(SocketChannel socketChannel) {
        String userId = USER_MAP.get(socketChannel);
        send(socketChannel, "您退出了群聊" + "\n\r");
        USER_MAP.remove(socketChannel);

        for (SocketChannel channel : USER_MAP.keySet()) {
            if (channel != socketChannel) {
                send(channel, userId + " 退出了群聊" + "\n\r");
            }
        }
    }

    /**
     * 擴散說話的內容
     * @param socketChannel
     * @param content
     */
    public static void propagate(SocketChannel socketChannel, String content) {
        String userId = USER_MAP.get(socketChannel);
        for (SocketChannel channel : USER_MAP.keySet()) {
            if (channel != socketChannel) {
                send(channel, userId + ": " + content + "\n\r");
            }
        }
    }

    /**
     * 發送消息
     * @param socketChannel
     * @param msg
     */
    static void send(SocketChannel socketChannel, String msg) {
        try {
            ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
            writeBuffer.put(msg.getBytes());
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服務端代碼

服務端代碼直接使用上一章NIO的實現,只不過這裏要把上面實現的業務邏輯適時地插入到相應的事件中。

(1)accept事件,即連接建立的時候,說明加入了群聊;

(2)read事件,即讀取數據的時候,說明有人說話了;

(3)連接斷開的時候,說明退出了群聊;

OK,直接上代碼,為了與上一章的代碼作區分,彤哥特意加入了一些標記:

public class ChatServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 將accept事件綁定到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 阻塞在select上
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍歷selectKeys
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 如果是accept事件
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = ssc.accept();
                    System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    // 加入群聊,本文來源於公從號“彤哥讀源碼”
                    ChatHolder.join(socketChannel);
                } else if (selectionKey.isReadable()) {
                    // 如果是讀取事件
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 將數據讀入到buffer中
                    int length = socketChannel.read(buffer);
                    if (length > 0) {
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        // 將數據讀入到byte數組中
                        buffer.get(bytes);

                        // 換行符會跟着消息一起傳過來
                        String content = new String(bytes, "UTF-8").replace("\r\n", "");
                        if (content.equalsIgnoreCase("quit")) {
                            // 退出群聊,本文來源於公從號“彤哥讀源碼”
                            ChatHolder.quit(socketChannel);
                            selectionKey.cancel();
                            socketChannel.close();
                        } else {
                            // 擴散,本文來源於公從號“彤哥讀源碼”
                            ChatHolder.propagate(socketChannel, content);
                        }
                    }
                }
                iterator.remove();
            }
        }
    }
}

測試

打開四個XSHELL客戶端,分別連接telnet 127.0.0.1 8080,然後就可以開始群聊了。

彤哥發現,自己跟自己聊天也是會上癮的,完全停不下來,不行了,我再去自聊一會兒^^

總結

本文彤哥跟着大家一起實現了“群聊系統”,去掉註釋也就100行左右的代碼,是不是非常簡單?這就是NIO網絡編程的魅力,我發現寫網絡編程也上癮了^^

問題

這兩章我們都沒有用NIO實現客戶端,你知道怎麼實現嗎?

提示:服務端需要監聽accept事件,所以需要有一個ServerSocketChannel,而客戶端是直接去連服務器了,所以直接用SocketChannel就可以了,一個SocketChannel就相當於一個Connection。

最後,也歡迎來我的公從號彤哥讀源碼系統地學習源碼&架構的知識。

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

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

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

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

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

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?

併發編程-深入淺出AQS

AQS是併發編程中非常重要的概念,它是juc包下的許多併發工具類,如CountdownLatch,CyclicBarrier,Semaphore 和鎖, 如ReentrantLock, ReaderWriterLock的實現基礎,提供了一個基於int狀態碼和隊列來實現的併發框架。本文將對AQS框架的幾個重要組成進行簡要介紹,讀完本文你將get到以下幾個點:

  1. AQS進行併發控制的機制是什麼

  2. AQS獨佔和共享模式是如何實現的

  3. 同步隊列和條件等待隊列的區別,和數據出入隊原則

一,AQS基本概念

AQS(AbstractQueuedSynchronizer)是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量來表示狀態,通過內置的FIFO(first in,first out)隊列來完成資源獲取線程的排隊工作。

隊列可分為兩種,一種是同步隊列,是程序執行入口出處的等待隊列;而另一種則是條件等待隊列,隊列中的元素是在程序執行時在某個條件上發生等待。

1.1 獨佔or共享模式

AQS支持兩種獲取同步狀態的模式既獨佔式和共享式。顧名思義,獨佔式模式同一時刻只允許一個線程獲取同步狀態,而共享模式則允許多個線程同時獲取。

1.2 同步隊列

當一個線程嘗試獲取同步狀態失敗時,同步器會將這個線程以及等待狀態等信息構造成一個節點加入到等待隊列中,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試重複獲取同步隊列。

1.3 條件隊列

AQS內部類ConditionObject來實現的條件隊列,當一個線程獲取到同步狀態,但是卻通過Condition調用了await相關的方法時,會將該線程封裝成Node節點並加入到條件隊列中,它的結構和同步隊列相同。

二,獨佔or共享模式

AQS框架中,通過維護一個int類型的狀態,來進行併發控制,線程通常通過修改此狀態信息來表明當前線程持有此同步狀態。AQS則是通過保存修改狀態線程的引用來實現獨佔和共享模式的。

/**
 * 獲取同步狀態
 */
public final void acquire(int arg) {
    //嘗試獲取同步狀態, 如果嘗試獲取到同步狀態失敗,則加入到同步隊列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
/**
 * 嘗試獲取同步狀態【子類中實現】,因為aqs基於模板模式,僅提供基於狀態和同步隊列的實 
 * 現思路,具體的實現由子類決定
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 如果當前狀態值為0,並且等待隊列中沒有元素,執行修改狀態值操作
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 修改狀態值成功,記錄當前持有同步狀態的線程信息
            setExclusiveOwnerThread(current);
            return true;
        }
        // 如果當前線程已經持有同步狀態,繼續修改同步狀態【重入鎖實現原理】
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

/**
 * 根據傳入的模式以及當前線程信息創建一個隊列的節點並加入到同步隊列尾部
 */
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
/**
 * 同步隊列中節點,嘗試獲取同步狀態
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋(死循環)
        for (;;) {
            // 只有當前節點的前驅節點是頭節點時才會嘗試執行獲取同步狀態操作
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

獨佔式是如何控製得?

當修改狀態信息成功后,如果執行的是獨佔式操作,AQS的具體實現類中會保存當前線程的信息來聲明同步狀態已被當前線程佔用,此時其他線程再嘗試獲取同步狀態會返回false。

三,同步隊列

3.1 隊列中保存那些信息?

同步隊列節點中主要保存着線程的信息以及模式(共享or獨佔)。

3.2 何時執行入隊操作?

/**
 * 獲取同步狀態
 */
public final void acquire(int arg) {
    //嘗試獲取同步狀態, 如果嘗試獲取到同步狀態失敗,則加入到同步隊列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

復用上文中的代碼,不難看出再獲取同步狀態失敗后,會執行入隊操作。

3.3 何時執行出隊操作?

當線程獲取同步狀態失敗時,會被封裝成Node節點加入到等待隊列中,此時所有節點都回進入自旋過程,首先判斷自己prev是否時頭節點,如果是則嘗試獲取同步狀態。
被阻塞線程的喚醒主要以靠前驅節點的出隊或阻塞線程被中斷來實現。

/**
 * 同步隊列中節點,嘗試獲取同步狀態
 * 
 * 1. 當一個線程獲取到同步狀態時,會將當前線程構造程Node並設置為頭節點
 * 2. 並將原始的head節點設置為null,以便於垃圾回收
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

四,條件等待隊列

條件變量(ConidtionObject)是AQS中的一個內部類,用來實現同步隊列機制。同步隊列復用了等待隊列中Node節點,所以同步隊列到等待隊列中不需要進行額外的轉換。

4.1 什麼時候執行入隊操作?

當線程獲取到同步狀態,但是在臨界區中調用了await()方法,此時該線程會被加入到對應的條件隊列匯總。
ps: 臨界區,加鎖和釋放鎖之間的代碼區域

/**
 * ConditionObject中的await方法,調用后使得當前執行線程加入條件等待隊列
 */
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    // -----省略代碼------
}
/**
 * 添加等待線程
 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // -----省略代碼------
    // 將當前線程構造程條件隊列節點,並加入到隊列中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

4.2 什麼時候執行出隊操作?

當對應的Conditioni調用signial/signalAll()方法時回選擇從條件隊列中出隊列,同步隊列是通過自旋的方式獲取同步狀態,而條件隊列中的節點則通過通知的方式出隊。條件隊列中的節點被喚醒後會加入到入口等待隊列中。

/**
 * 喚醒當前條件等到隊列中的所有等待線程
 */
public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}
/**
 * 遍歷隊列,將元素從條件隊列 加入到 同步隊列
 */
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
final boolean transferForSignal(Node node) {
    // -----省略代碼------
    // 執行入隊操作,將node添加到同步隊列中
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

五,總結

  1. 使用Node實現的FIFO隊列,可以用於構建鎖或者其他同步裝置的基礎框架
  2. 利用一個int類型的屬性表示狀態
  3. 使用模板方法模式,子類可以通過繼承它來管理狀態實現各種併發工具
  4. 可以同時實現獨佔和共享模式

本文對AQS的基本原理進行的簡要的描述,對於子類的公平性和非公平行實現,中斷,隊列中節點的等待狀態,cas等操作沒有進行探討,感興趣的小夥伴可以進行源碼閱讀或者查閱相關資料。

六,Q&A

Question1: 在java中通常使用synchronized來實現方法同步,AQS中通過CAS保證了修改同步狀態的一致性問題,那麼對比synchronized,cas有什麼優勢不同與優勢呢?你還知道其他無鎖併發的策略嗎?

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

【其他文章推薦】

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

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

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

※大陸寄台灣空運注意事項

※大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

憂瀕危山地大猩猩染疫 剛果關閉園區

摘錄自2020年3月24日公視報導

部份科學家認為許多病毒是以「野生動物」做為媒介,傳到人類身上,但其實人類也可能把疾病傳染給動物。非洲剛果民主共和國的「維龍加國家公園」,就為了保護園裡瀕臨絕種的山地大猩猩,決定關閉到6月1號,免得入山的遊客帶來毀滅性的災難。

園區擔心武漢肺炎會經由與人接觸傳染給大猩猩,已經下令這處非洲第一個成立的國家公園,即日起關閉到6月1號。保育人員之所以這麼緊張,是因為一個尋常的感冒,就可能使山地大猩猩死亡。疫情一旦在這個群體擴散,很可能讓全球僅剩1000隻左右的山地大猩猩瀕臨滅絕。

為了保護山地大猩猩,這處園區原本規定,遊客與猩猩必須保持七公尺以上的距離,但這樣的規範執行起來有實際困難,有時猩猩也會主動靠近人類。因此維龍加國家公園才會忍痛做出閉園的決定,不過也將因此失去可觀的觀光收入,鄰近的盧安達和烏干達國家公園則決定先以加強防疫因應。

生態保育
國際新聞
剛果
大猩猩保育
國際疫情

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

生魚片注意!海獸胃線蟲暴增283倍

摘錄自2020年3月23日Yahoo!新聞報導

華盛頓大學研究團隊於近日指出,寄生在魚類的線蟲「海獸胃線蟲」 (Anisakis),自1978年至2015年的這段時間暴增283倍。

對此,威斯康辛大學水海洋學教授伍德(Chelsea Wood)表示,儘管對人類健康風險低,但對於海豚、鯨魚、海報等海洋哺乳動物,可能產生有害影響,內容提到若人類吃到活生生的海獸胃線蟲,蟲子會入侵腸壁,進而引發類似食物中毒的症狀,例如噁心嘔吐腹瀉等等,但蟲子幾乎會在幾天後就死亡,症狀隨即消失,因此大多數人都認為只是單純的食物中毒而很難被查覺到。

雖然海獸胃線蟲不能在人類體內存活太久,不過卻可以在海洋哺乳動物中生活並繁殖,伍德表示,他們仍不確定線蟲大量爆增的原因,氣候變遷、肥料、洋流等等所產生的養分,以及同其海洋哺乳動物的數量增加,都可能是潛在原因。

食品安全
糧食
生活環境
永續發展
國際新聞
生魚片
線蟲

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?

日本男童「免農藥除草大作戰」 農夫大讚:非常有效

摘錄自2020年3月24日中時報導

一名日本網友在推特表示,她在家中庭院內種植秋葵,但只要一到夏季,雜草就會生長得特別快,而她就讀小學的兒子看到這種情況,便主動說要以「除草」為主題,當作暑假作業來做研究,而且要「不使用農藥」。

兒子在三年間嘗試過多種方法,都無法完全清除雜草。某一天他突然靈機一動,發現土壤的軟硬度是導致雜草生長的關鍵,於是他決定每天在土壤上跑步。從今年的1月5日起到3月1日,不論晴雨,他每天都在土壤上奔跑,每次跑30分鐘,沒想到短短2個月後,雜草就不再生長了。

也有農民認同此方法「身為一名農民,這是非常有效的除草方法,可在雜草種子發芽時挪動土壤,在雜草生根前徹底將其消滅。」

環境經濟
農林漁牧業
國際新聞
日本
除草

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

【其他文章推薦】

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

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

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

※大陸寄台灣空運注意事項

※大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

10種改變 看懂武漢肺炎正對能源、氣候變遷造成什麼影響

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

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

※大陸寄台灣空運注意事項

※大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

吉利總投資80億元新能源整車項目落戶杭州 首期年產10萬輛

4月20日,總投資 80億元吉利新能源整車項目成功簽約落戶杭州大江東。

吉利新能源整車專案計畫投資80億元,用地600畝,首期年產10萬輛AMA平臺新能源汽車整車,達產後年銷售收入不少於150億元。專案包括了衝壓、焊裝、塗裝、總裝四大工藝以及與之配套的試車跑道、停車場、動力站房、管理辦公中心、生活配套區等協助工具設施。據悉,AMA平臺是吉利集團歷時三年專門為為新能源汽車打造的全新平臺,確保了整車項目的工藝技術水準、設備裝備水準達到國內領先、國際一流的先進水準。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

上汽集團150億定增用於投資新能源汽車、車聯網等

上汽集團4月20日晚間公告稱,中國證監會發行審核委員會於4月20日對公司非公開發行A股股票的申請進行了審核。根據會議審核結果,公司本次非公開發行A股股票的申請獲得審核通過。

根據調整後的定增預案,上汽集團擬以不低於15.56元/股非公開發行不超過9.64億股,募集資金不超過150億元用於投資新能源汽車、智慧化大規模定制、前瞻技術與車聯網、汽車服務與汽車金融領域內多個項目,其中公司控股股東上汽總公司擬認購不超過30億元,公司員工持股計畫擬認購不超過11.6745億元。

定增募集資金主要投向新興領域:其中新能源相關專案72億、智慧定制化專案20億、前瞻技術(燃料電池/智慧汽車)及車聯網19億、汽車服務與金融專案39億。

戰略定位由汽車製造轉為綜合服務:研發製造業環節,資源更多的向新能源汽車、智慧汽車傾斜;同時更加注重汽車服務領域的潛力挖掘,包括汽車金融、維修服務等。

公司為業務轉型已做大量的準備和調整:1、組織架構調整,新成立服務貿易事業部、金融事業部,用於整合內部相關業務;2、廣泛儲備技術,公司先後投入60億用於新能源產品開發,是國內少有的推出插電式車型的車企;3、積極跨界合作,公司已與阿裡巴巴、中國石油等企業在車聯網、汽車後市場方面建立戰略合作;4、激勵機制調整,本次增發實現更大額度的持股,同時在部分新業務上還實現了更大幅度的員工持股。

公司新業務發展目標:1、新能源汽車領域,完善供應鏈體系,有效降低成本,加快推出產品,2016、2020年分別實現2.6、20萬銷量;2、智慧汽車領域,2016年下半年推出與阿裡巴巴合作開發的首款互聯網汽車;3、服務貿易領域,以車享網/車享家為服務平臺,建立涵蓋(自建1500家)1萬家店的服務體系,至2020年,實現收入3000億(含物流/維修/金融)。

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?

三菱再爆電動車里程造假疑雲

日商三菱汽車日前自爆旗下汽車油耗測試造假,如今連電動車款也出了問題。日媒指出,三菱在海外銷售的電動車i-MiEV也涉及謊報里程耗電數據問題。

德國福斯汽車去年爆出排放廢氣造假醜聞,引發各界譁然。今年四月20日,日商三菱汽車也自爆迷你車款油耗測試造假,並開始研擬補償機制。但一波未平、一波又起,日媒產經新聞指出,三菱的外銷款電動車i-MiEV在里程耗電數據方面也有造假情事,可能與三菱迷你車款一樣,用竄改空氣與輪胎阻力、抑或是採用時間較短的美國「高速慣性行駛法」測試,讓油耗/電耗數據較實際狀況降低5~10%。

若三菱電動車確實也牽涉里程造假,則旗下有問題的車款已包括:RVR、Outlander、Pajero、Minicab MiEV、i-MiEV等。日本相關當局要求三菱在27日之前繳交報告。

自三菱自爆油耗造假醜聞已來,三菱的股票持續重挫,直到4月22日止,市值已蒸發3,600億日圓左右。

(照片來源:)

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

【其他文章推薦】

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

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

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

※大陸寄台灣空運注意事項

※大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

020.掌握Pod-Pod基礎使用

一 Pod定義詳解

1.1 完整Pod定義文件

  1 apiVersion: v1			#必選,版本號,例如v1,版本號必須可以用 kubectl api-versions 查詢到
  2 kind: Pod				#必選,Pod
  3 metadata:				#必選,元數據
  4   name: string			#必選,Pod名稱,需符合RFC 1035規範
  5   namespace: string			#必選,Pod所屬的命名空間,默認為"default"
  6   labels:				#自定義標籤
  7     - name: string			#自定義標籤名字
  8   annotations:			#自定義註釋列表
  9     - name: string
 10 spec:				#必選,Pod中容器的詳細定義
 11   containers:			#必選,Pod中容器列表
 12   - name: string			#必選,容器名稱,需符合RFC 1035規範
 13     image: string			#必選,容器的鏡像名稱
 14     imagePullPolicy: [ Always|Never|IfNotPresent ]	#獲取鏡像的策略,Alawys表示每次都嘗試下載鏡像,IfnotPresent表示優先使用本地鏡像,否則下載鏡像,Nerver表示僅使用本地鏡像
 15     command: [string]		#容器的啟動命令列表,如不指定,使用打包時使用的啟動命令
 16     args: [string]			#容器的啟動命令參數列表
 17     workingDir: string		#容器的工作目錄
 18     volumeMounts:			#掛載到容器內部的存儲卷配置
 19     - name: string			#引用pod定義的共享存儲卷的名稱,需用volumes[]部分定義的的卷名
 20       mountPath: string		#存儲卷在容器內mount的絕對路徑,應少於512字符
 21       readOnly: boolean		#是否為只讀模式,默認為讀寫模式
 22     ports:				#需要暴露的端口庫號列表
 23     - name: string			#端口的名稱
 24       containerPort: int		#容器需要監聽的端口號
 25       hostPort: int		        #容器所在主機需要監聽的端口號,默認與Container相同
 26       protocol: string		#端口協議,支持TCP和UDP,默認TCP
 27     env:				#容器運行前需設置的環境變量列表
 28     - name: string			#環境變量名稱
 29       value: string		        #環境變量的值
 30     resources:			#資源限制和請求的設置
 31       limits:			#資源限制的設置
 32         cpu: string		        #CPU的限制,單位為core數,將用於docker run --cpu-shares參數
 33         memory: string		#內存限制,單位可以為Mib/Gib,將用於docker run --memory參數
 34       requests:			#資源請求的設置
 35         cpu: string		        #CPU請求,容器啟動的初始可用數量
 36         memory: string		#內存請求,容器啟動的初始可用數量
 37     livenessProbe:			#對Pod內各容器健康檢查的設置,當探測無響應幾次后將自動重啟該容器,檢查方法有exec、httpGet和tcpSocket,對一個容器只需設置其中一種方法即可
 38       exec:			        #對Pod容器內檢查方式設置為exec方式
 39         command: [string]		#exec方式需要制定的命令或腳本
 40       httpGet:			#對Pod內個容器健康檢查方法設置為HttpGet,需要制定Path、port
 41         path: string
 42         port: number
 43         host: string
 44         scheme: string
 45         HttpHeaders:
 46         - name: string
 47           value: string
 48       tcpSocket:			#對Pod內個容器健康檢查方式設置為tcpSocket方式
 49          port: number
 50        initialDelaySeconds: 0	#容器啟動完成后首次探測的時間,單位為秒
 51        timeoutSeconds: 0		#對容器健康檢查探測等待響應的超時時間,單位秒,默認1秒
 52        periodSeconds: 0		#對容器監控檢查的定期探測時間設置,單位秒,默認10秒一次
 53        successThreshold: 0
 54        failureThreshold: 0
 55        securityContext:
 56          privileged: false
 57     restartPolicy: [Always | Never | OnFailure]	#Pod的重啟策略,Always表示一旦不管以何種方式終止運行,kubelet都將重啟,OnFailure表示只有Pod以非0退出碼退出才重啟,Nerver表示不再重啟該Pod
 58     nodeSelector: obeject		#設置NodeSelector表示將該Pod調度到包含這個label的node上,以key:value的格式指定
 59     imagePullSecrets:		#Pull鏡像時使用的secret名稱,以key:secretkey格式指定
 60     - name: string
 61     hostNetwork: false		#是否使用主機網絡模式,默認為false,如果設置為true,表示使用宿主機網絡
 62     volumes:			#在該pod上定義共享存儲卷列表
 63     - name: string			#共享存儲卷名稱 (volumes類型有很多種)
 64       emptyDir: {}			#類型為emtyDir的存儲卷,與Pod同生命周期的一個臨時目錄。為空值
 65       hostPath: string		#類型為hostPath的存儲卷,表示掛載Pod所在宿主機的目錄
 66         path: string		#Pod所在宿主機的目錄,將被用於同期中mount的目錄
 67       secret:			#類型為secret的存儲卷,掛載集群與定義的secre對象到容器內部
 68         scretname: string
 69         items:
 70         - key: string
 71           path: string
 72       configMap:			#類型為configMap的存儲卷,掛載預定義的configMap對象到容器內部
 73         name: string
 74         items:
 75         - key: string
 76           path: string

二 Pod的基本用法

2.1 創建Pod


Pod可以由1個或多個容器組合而成,通常對於緊耦合的兩個應用,應該組合成一個整體對外提供服務,則應該將這兩個打包為一個pod。

屬於一個Pod的多個容器應用之間相互訪問只需要通過localhost即可通信,這一組容器被綁定在一個環境中。

  1 [root@k8smaster01 study]# vi frontend-localredis-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: redis-php
  6   label:
  7     name: redis-php
  8 spec:
  9   containers:
 10   - name: frontend
 11     image: kubeguide/guestbook-php-frontend:localredis
 12     ports:
 13     - containersPort: 80
 14   - name: redis-php
 15     image: kubeguide/redis-master
 16     ports:
 17     - containersPort: 6379
 18 
 19 [root@k8smaster01 study]# kubectl create -f frontend-localredis-pod.yaml
 20 


2.2 查看Pod

  1 [root@k8smaster01 study]# kubectl get pods	                #READY為2/2,表示此Pod中運行了yaml定義的兩個容器
  2 NAME        READY   STATUS    RESTARTS   AGE
  3 redis-php   2/2     Running   0          7m45s
  4 [root@k8smaster01 study]# kubectl describe pod redis-php	#查看詳細信息
  5 


三 靜態Pod

3.1 靜態Pod概述


靜態pod是由kubelet進行管理的僅存在於特定Node的Pod上,他們不能通過API Server進行管理,無法與ReplicationController、Deployment或者DaemonSet進行關聯,並且kubelet無法對他們進行健康檢查。靜態Pod總是由kubelet進行創建,並且總是在kubelet所在的Node上運行。

創建靜態Pod有兩種方式:配置文件或者HTTP方式。

3.2 配置文件方式創建

  1 [root@k8snode01 ~]# mkdir -p /etc/kubelet.d
  2 [root@k8snode01 ~]# vi /etc/kubelet.d/static-web.yaml
  3 apiVersion: v1
  4 kind: Pod
  5 metadata:
  6   name: static-web
  7   label:
  8     name: static-web
  9 spec:
 10   containers:
 11   - name: static-web
 12     image: nginx
 13     ports:
 14     - name: web
 15       containersPort: 80
 16 
 17 [root@k8snode01 ~]# vi /etc/systemd/system/kubelet.service
 18 ……
 19   --config=/etc/kubelet.d/ \·				#加入此參數
 20 ……
 21 [root@k8snode01 ~]# systemctl daemon-reload
 22 [root@k8snode01 ~]# systemctl restart kubelet.service	#重啟kubelet
 23 [root@k8snode01 ~]# docker ps				#查看創建的pod



提示:由於靜態pod不能通過API Server進行管理,因此在Master節點執行刪除操作後會變為Pending狀態,且無法刪除。刪除該pod只能在其運行的node上,將定義POD的yaml刪除。

3.3 HTTP方式


通過設置kubelet的啟動參數–mainfest-url,會定期從該URL下載Pod的定義文件,並以.yaml或.json文件的格式進行解析,從而創建Pod。

四 Pod容器共享Volume

4.1 共享Volume


在同一個Pod中的多個容器能夠共享Pod級別的存儲就Volume。Volume可以被定義為各種類型,多個容器各自進行掛載操作,將一個Volume掛載為容器內部需要的目錄。


示例1:

Pod級別設置Volume “app-logs”,同時Pod包含兩個容器,Tomcat向該Volume寫日誌,busybox讀取日誌文件。

  1 [root@k8smaster01 study]# vi pod-volume-applogs.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: volume-pod
  6 spec:
  7   containers:
  8   - name: tomcat
  9     image: tomcat
 10     ports:
 11     - containerPort: 8080
 12     volumeMounts:
 13     - name: app-logs
 14       mountPath: /usr/local/tomcat/logs
 15   - name: logreader
 16     image: busybox
 17     command: ["sh","-c","tail -f /logs/catalina*.log"]
 18     volumeMounts:
 19     - name: app-logs
 20       mountPath: /logs
 21   volumes:
 22   - name: app-logs
 23     emptyDir: {}

解釋:

Volume名:app-logs;

emptyDir:為Pod分配到Node的時候創建。無需指定宿主機的目錄文件,為Kubernetes自動分配的目錄。

  1 [root@k8smaster01 study]# kubectl create -f pod-volume-applogs.yaml	#創建
  2 [root@k8smaster01 study]# kubectl get pods				#查看
  3 [root@k8smaster01 study]# kubectl logs volume-pod -c busybox	#讀取log




  1 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- ls /usr/local/tomcat/logs
  2 catalina.2019-06-29.log      localhost_access_log.2019-06-29.txt
  3 host-manager.2019-06-29.log  manager.2019-06-29.log
  4 localhost.2019-06-29.log
  5 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- tail /usr/local/tomcat/logs/catalina.2019-06-29.log



提示:通過tomcat容器可查看日誌,對比busybox通過共享Volume查看的日誌是否一致。

五 Pod配置管理

5.1 Pod配置概述


應用部署的一個最佳實踐是將應用所需的配置信息與程序進行分離,使程序更加靈活。將相應的應用打包為鏡像,可以通過環境變量或者外掛volume的方式在創建容器的時候進行配置注入,從而實現更好的復用。

Kubernetes提供一種統一的應用配置管理方案:ConfigMap。

5.2 ConfigMap概述


ConfigMap供容器使用的主要場景:

  • 生成容器內部的環境變量;
  • 設置容器的啟動命令的參數(需設置為環境變量);
  • 以volume的形式掛載為容器內部的文件或者目錄。


ConfigMap以一個或多個key:value的形式定義。value可以是string也可以是一個文件內容,可以通過yaml配置文件或者通過kubectl create configmap 的方式創建configMap。

5.3 創建ConfigMap資源對象——yaml方式

  1 [root@k8smaster01 study]# vi cm-appvars.yaml
  2 apiVersion: v1
  3 kind: ConfigMap
  4 metadata:
  5   name: cm-appvars
  6 data:
  7   apploglevel: info
  8   appdatadir: /var/data
  9 
 10 [root@k8smaster01 study]# kubectl create -f cm-appvars.yaml
 11 configmap/cm-appvars created
 12 [root@k8smaster01 study]# kubectl get configmaps
 13 NAME         DATA   AGE
 14 cm-appvars   2      8s
 15 [root@k8smaster01 study]# kubectl describe configmaps cm-appvars



  1 [root@k8smaster01 study]# kubectl get configmaps cm-appvars -o yaml


5.4 創建ConfigMap資源對象——命令行方式


語法1

  1 # kubectl create configmap NAME --from-file=[key=]source --from-file=[key=]source



解釋:通過–from-file參數從文件中創建,可以指定key名稱,也可以制定多個key。

語法2

  1 # kubectl create configmap NAME --from-file=config-files-dir



解釋:通過–from-file參數從目錄中創建,該目錄下的每個配置文件名都被設置為key,文件的內容被設置為value。

語法3

  1 # kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2



解釋:通過–from-literal參數從文本中創建,直接將指定的key#=value#創建為ConfigMap的內容。

5.5 Pod使用ConfigMap


容器應用使用ConfigMap有兩種方式:

  • 通過環境變量獲取ConfigMap中的內容;
  • 通過Volume掛載的方式將ConfigMap中的內容掛載為容器內容的文件或目錄。

  1 [root@k8smaster01 study]# vi cm-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: cm-test-pod
  6 spec:
  7   containers:
  8   - name: cm-test
  9     image: busybox
 10     command: ["/bin/sh","-c","env|grep APP"]	#容器里執行查看環境變量的命令
 11     env:
 12     - name: APPLOGLEVEL				#定義容器環境變量名稱
 13       valueFrom:
 14         configMapKeyRef:			#環境變量的值來自ConfigMap
 15           name: cm-appvars			#指定來自cm-appvars的ConfigMap
 16           key: apploglevel			#key為apploglevel
 17     - name: APPDATADIR
 18       valueFrom:
 19         configMapKeyRef:
 20           name: cm-appvars
 21           key: appdatadir
 22 
 23 [root@k8smaster01 study]# kubectl create -f cm-test-pod.yaml
 24 [root@k8smaster01 study]# kubectl get pods
 25 NAME          READY   STATUS      RESTARTS   AGE
 26 cm-test-pod   0/1     Completed   2          24s



【掛載形式-待補充】

5.6 ConfigMap限制


  • Configmap必須在pod創建之間創建;
  • ConfigMap受到namespace的限制,只有同一個命名空間下才能引用;
  • ConfigMap暫時無法配置配額;
  • 靜態的pod無法使用ConfigMap;
  • 在使用volumeMount掛載的時候,configMap基於items創建的文件會整體的將掛載數據卷的容器的目錄下的文件全部覆蓋。

六 Pod獲取自身信息

6.1 Downward API


pod擁有唯一的名字、IP地址,並且處於某個Namespace中。pod的容器內獲取pod的信息科通過Downward API實現。具體有以下兩種方式:

  • 環境變量:用於單個變量,可以將pod信息和container信息注入容器內部;
  • volume掛載:將數組類信息生成為文件,掛載至容器內部。


舉例1:通過Downward API將Pod的IP、名稱和所在的Namespace注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       command: [ "/bin/sh", "-c", "env" ]
 11       env:
 12         - name: MY_POD_NAME
 13           valueFrom:
 14             fieldRef:
 15               fieldPath: metadata.name
 16         - name: MY_POD_NAMESPACE
 17           valueFrom:
 18             fieldRef:
 19               fieldPath: metadata.namespace
 20         - name: MY_POD_IP
 21           valueFrom:
 22             fieldRef:
 23               fieldPath: status.podIP
 24   restartPolicy: Never



提示:Downward API提供如下變量:

metadata.name:Pod的名稱,當Pod通過RC生成時,其名稱是RC隨機產生的唯一名稱;

status.podIP:Pod的IP地址,POd的IP屬於狀態數據,而非元數據;

metadata.namespace:Pod所在的namespace。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod | grep MY_POD
  3 MY_POD_NAMESPACE=default
  4 MY_POD_IP=172.30.240.4
  5 MY_POD_NAME=dapi-test-pod
  6 



舉例2:通過Downward API將Container的自願請求和限制信息注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod-container-vars.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-container-vars
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       imagePullPolicy: Never
 11       command: [ "/bin/sh", "-c" ]
 12       args:
 13       - while true; do
 14           echo -en '\n';
 15           printenv MY_CPU_REQUEST MY_CPU_LIMIT;
 16           printenv MY_MEM_REQUEST MY_MEM_LIMIT;
 17           sleep 3600;
 18         done;
 19       resources:
 20         requests:
 21           memory: "32Mi"
 22           cpu: "125m"
 23         limits:
 24           memory: "64Mi"
 25           cpu: "250m"
 26       env:
 27         - name: MY_CPU_REQUEST
 28           valueFrom:
 29             resourceFieldRef:
 30               containerName: test-container
 31               resource: requests.cpu
 32         - name: MY_CPU_LIMIT
 33           valueFrom:
 34             resourceFieldRef:
 35               containerName: test-container
 36               resource: limits.cpu
 37         - name: MY_MEM_REQUEST
 38           valueFrom:
 39             resourceFieldRef:
 40               containerName: test-container
 41               resource: requests.memory
 42         - name: MY_MEM_LIMIT
 43           valueFrom:
 44             resourceFieldRef:
 45               containerName: test-container
 46               resource: limits.memory
 47   restartPolicy: Never



提示:Downward API提供如下變量:

requests.cpu:容器的CPU請求值;

limits.cpu:容器的CPU限制值;

requests.memory:容器的內存請求值;

limits.memory:容器的內存限制值。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-container-vars.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-container-vars
  3 1
  4 1
  5 33554432
  6 67108864



舉例3:通過Downward API將Pod的Label、Annotation列表通過Volume掛載為容器內的一個文件。

  1 [root@k8smaster01 study]# vi dapi-test-pod-volume.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-volume
  6   labels:
  7     zone: us-est-coast
  8     cluster: test-cluster1
  9     rack: rack-22
 10   annotations:
 11     build: two
 12     builder: john-doe
 13 spec:
 14   containers:
 15     - name: test-container
 16       image: busybox
 17       imagePullPolicy: Never
 18       command: [ "/bin/sh", "-c" ]
 19       args:
 20       - while true; do
 21           if [[ -e /etc/labels ]]; then
 22             echo -en '\n\n'; cat /etc/labels; fi;
 23           if [[ -e /etc/annotations ]]; then
 24             echo -en '\n\n'; cat /etc/annotations; fi;
 25           sleep 3600;
 26         done;
 27       volumeMounts:
 28         - name: podinfo
 29           mountPath: /etc
 30           readOnly: false
 31   volumes:
 32     - name: podinfo
 33       downwardAPI:
 34         items:
 35           - path: "labels"
 36             fieldRef:
 37               fieldPath: metadata.labels
 38           - path: "annotations"
 39             fieldRef:
 40               fieldPath: metadata.annotations



注意:Volume中的ddownwardAPI的items語法,將會以path的名稱生成文件。如上所示,會在容器內生產/etc/labels和/etc/annotations兩個文件,分別包含metadata.labels和metadata.annotations的全部Label。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-volume.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-volume
  3 



提示:DownwardAPI意義:

在某些集群中,集群中的每個節點需要將自身的標識(ID)及進程綁定的IP地址等信息事先寫入配置文件中,進程啟動時讀取此類信息,然後發布到某個類似註冊服務中心。此時可通過DowanwardAPI,將一個預啟動腳本或Init Container,通過環境變量或文件方式獲取Pod自身的信息,然後寫入主程序配置文件中,最後啟動主程序。本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!