遷移桌面程序到MS Store(12)——WPF使用UWP InkToolbar和InkCanvas

我們在提到了對Win10 API的調用,但仍存在無法在WPF中使用UWP控件的問題,雖然都是XAML控件,但卻是兩套命名空間下的同名類型,無法混用。
人總會被現實打敗,強大如某軟也得向生活低頭,UWP一直沒有起色,某軟的老大又一心去搞Azure。Windows平台的重振,似乎想走回頭路,從1903版本開始,支持在.NET Framwork的WPF和WinForm工程中,直接使用部分的UWP控件了。首當其沖的,就是有點騷包的InkToolbar和InkCanvas。

接下來我們就來試試如何在WPF工程中,使用UWP的InkToolbar和InkCanvas。
首先創建一個空的WPF工程,完成后,在Nuget的搜索界面填入 Microsoft.Toolkit.Wpf.UI.Controls ,選中第一個進行安裝。

完成安裝后,打開MainWindow.xaml,添加對命名空間的引用xmlns:Controls=”clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls”。接着就可以在<Grid>節點中添加UWP版本的InkToolbar和InkCanvas控件了。

<Window x:Class="WPFInkSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFInkSample"
        xmlns:Controls="clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Controls:InkToolbar TargetInkCanvas="{x:Reference myInkCanvas}" Grid.Row="0" />
        <Controls:InkCanvas x:Name="myInkCanvas" Grid.Row="1" />
    </Grid>
</Window>

同時我們還需要在MainWindow.xaml.cs中設置InputDeviceTypes。

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.myInkCanvas.InkPresenter.InputDeviceTypes = CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Pen;
        }
    }

然後按下F5運行,某軟的騷操作來了……因為僅在1903以後的版本才支持這種騷操作(10.0.18226是稍早的preview版),所以需要做額外的處理才可以。

我們這裡有兩種選擇,一是通過來打包這個WPF程序,然後在Packaging工程的屬性里,將Target version和Minimum version同時設置為Windows 10, version 1903 (10.0.18362) 。這是MSDN上推薦的標準做法,這樣做的好處在於,打包好的程序可以直接上傳MS Store。
如果我們想保持exe的可執行文件形式,還有另一種做法,在Project文件上右鍵點擊Add->New Item,添加一個manifest文件。
在這個文件中,找到<!–Windows 10–>,然後做如下編輯:

  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- A list of the Windows versions that this application has been tested on
           and is designed to work with. Uncomment the appropriate elements
           and Windows will automatically select the most compatible environment. -->
  
      <!-- Windows Vista -->
      <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
  
      <!-- Windows 7 -->
      <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
  
      <!-- Windows 8 -->
      <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
  
      <!-- Windows 8.1 -->
      <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
  
      <!-- Windows 10 -->
      <maxversiontested Id="10.0.18362.0"/>
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
  
    </application>
  </compatibility>

保存后,再通過F5運行,即發現一切正常,不在出現之前的運行時錯誤了。
本篇我們介紹了如何在WPF工程中使用UWP InkToolbar和InkCavas。因為這個功能僅在1903后的版本支持,所以下一篇我們會介紹如何簡單地判斷Win10 API 版本,在運行時判斷是否執行對應版本的代碼。
Github:

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

jdbc-mysql測試例子和源碼詳解

目錄

簡介

什麼是JDBC

JDBC是一套連接和操作數據庫的標準、規範。通過提供DriverManagerConnectionStatementResultSet等接口將開發人員與數據庫提供商隔離,開發人員只需要面對JDBC接口,無需關心怎麼跟數據庫交互。

幾個重要的類

類名 作用
DriverManager 驅動管理器,用於註冊驅動,是獲取 Connection對象的入口
Driver 數據庫驅動,用於獲取Connection對象
Connection 數據庫連接,用於獲取Statement對象、管理事務
Statement sql執行器,用於執行sql
ResultSet 結果集,用於封裝和操作查詢結果
prepareCall 用於調用存儲過程

使用中的注意事項

  1. 記得釋放資源。另外,ResultSetStatement的關閉都不會導致Connection的關閉。

  2. maven要引入oracle的驅動包,要把jar包安裝在本地倉庫或私服才行。

  3. 使用PreparedStatement而不是Statement。可以避免SQL注入,並且利用預編譯的特點可以提高效率。

使用例子

需求

使用JDBC對mysql數據庫的用戶表進行增刪改查。

工程環境

JDK:1.8

maven:3.6.1

IDE:sts4

mysql driver:8.0.15

mysql:5.7

主要步驟

一個完整的JDBC保存操作主要包括以下步驟:

  1. 註冊驅動(JDK6後會自動註冊,可忽略該步驟);

  2. 通過DriverManager獲得Connection對象;

  3. 開啟事務;

  4. 通過Connection獲得PreparedStatement對象;

  5. 設置PreparedStatement的參數;

  6. 執行保存操作;

  7. 保存成功提交事務,保存失敗回滾事務;

  8. 釋放資源,包括ConnectionPreparedStatement

創建表

CREATE TABLE `demo_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶id',
  `name` varchar(16) COLLATE utf8_unicode_ci NOT NULL COMMENT '用戶名',
  `age` int(3) unsigned DEFAULT NULL COMMENT '用戶年齡',
  `gmt_create` datetime DEFAULT NULL COMMENT '記錄創建時間',
  `gmt_modified` datetime DEFAULT NULL COMMENT '記錄最近修改時間',
  `deleted` bit(1) DEFAULT b'0' COMMENT '是否刪除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`),
  KEY `index_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

創建項目

項目類型Maven Project,打包方式jar

引入依賴

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- mysql驅動的jar包 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>
<!-- oracle驅動的jar包 -->
<!-- <dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.2.0</version>
</dependency> -->

注意:由於oracle商業版權問題,maven並不提供Oracle JDBC driver,需要將驅動包手動添加到本地倉庫或私服。

編寫jdbc.prperties

下面的url拼接了好幾個參數,主要為了避免亂碼和時區報錯的異常。

路徑:resources目錄下

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
#這裏指定了字符編碼和解碼格式,時區,是否加密傳輸
username=root
password=root
#注意,xml配置是&採用&amp;替代

如果是oracle數據庫,配置如下:

driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@//localhost:1521/xe
username=system
password=root

獲得Connection對象

    private static Connection createConnection() throws Exception {
        // 導入配置文件
        Properties pro = new Properties();
        InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "jdbc.properties" );
        Connection conn = null;
        pro.load( in );
        // 獲取配置文件的信息
        String driver = pro.getProperty( "driver" );
        String url = pro.getProperty( "url" );
        String username = pro.getProperty( "username" );
        String password = pro.getProperty( "password" );
        // 註冊驅動,JDK6后不需要再手動註冊,DirverManager的靜態代碼塊會幫我們註冊
        // Class.forName(driver);
        // 獲得連接
        conn = DriverManager.getConnection( url, username, password );
        return conn;
    }

使用Connection對象完成保存操作

這裏簡單地模擬實際業務層調用持久層,並開啟事務。另外,獲取連接、開啟事務、提交回滾、釋放資源都通過自定義的工具類 JDBCUtil 來實現,具體見源碼。

    @Test
    public void save() {
        UserDao userDao = new UserDaoImpl();
        // 創建用戶
        User user = new User( "zzf002", 18, new Date(), new Date() );
        try {
            // 開啟事務
            JDBCUtil.startTrasaction();
            // 保存用戶
            userDao.insert( user );
            // 提交事務
            JDBCUtil.commit();
        } catch( Exception e ) {
            // 回滾事務
            JDBCUtil.rollback();
            e.printStackTrace();
        } finally {
            // 釋放資源
            JDBCUtil.release();
        }
    }

接下來看看具體的保存操作,即DAO層方法。

    public void insert( User user ) throws Exception {
        String sql = "insert into demo_user (name,age,gmt_create,gmt_modified) values(?,?,?,?)";
        Connection connection = JDBCUtil.getConnection();
        //獲取PreparedStatement對象
        PreparedStatement prepareStatement = connection.prepareStatement( sql );
        //設置參數
        prepareStatement.setString( 1, user.getName() );
        prepareStatement.setInt( 2, user.getAge() );
        prepareStatement.setDate( 3, new java.sql.Date( user.getGmt_create().getTime() ) );
        prepareStatement.setDate( 4, new java.sql.Date( user.getGmt_modified().getTime() ) );
        //執行保存
        prepareStatement.executeUpdate();
        //釋放資源
        JDBCUtil.release( prepareStatement, null );
    }

源碼分析

驅動註冊

DriverManager.registerDriver

DriverManager主要用於管理數據庫驅動,併為我們提供了獲取連接對象的接口。其中,它有一個重要的成員屬性registeredDrivers,是一個CopyOnWriteArrayList集合(通過ReentrantLock實現線程安全),存放的是元素是DriverInfo對象。

    //存放數據庫驅動包裝類的集合(線程安全)
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        //調用重載方法,傳入的DriverAction對象為null
        registerDriver(driver, null);
    }
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
        if(driver != null) {
            //當列表中沒有這個DriverInfo對象時,加入列表。
            //注意,這裏判斷對象是否已經存在,最終比較的是driver地址是否相等。
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

為什麼集合存放的是Driver的包裝類DriverInfo對象,而不是Driver對象呢?

  1. 通過DriverInfo的源碼可知,當我們調用equals方法比較兩個DriverInfo對象是否相等時,實際上比較的是Driver對象的地址,也就是說,我可以在DriverManager中註冊多個MYSQL驅動。而如果直接存放的是Driver對象,就不能達到這種效果(因為沒有遇到需要註冊多個同類驅動的場景,所以我暫時理解不了這樣做的好處)。

  2. DriverInfo中還包含了另一個成員屬性DriverAction,當我們註銷驅動時,必須調用它的deregister方法后才能將驅動從註冊列表中移除,該方法決定註銷驅動時應該如何處理活動連接等(其實一般在構造DriverInfo進行註冊時,傳入的DriverAction對象為空,根本不會去使用到這個對象,除非一開始註冊就傳入非空DriverAction對象)。

綜上,集合中元素不是Driver對象而DriverInfo對象,主要考慮的是擴展某些功能,雖然這些功能幾乎不會用到。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

class DriverInfo {

    final Driver driver;
    DriverAction da;
    DriverInfo(Driver driver, DriverAction action) {
        this.driver = driver;
        da = action;
    }

    @Override
    public boolean equals(Object other) {
        //這裏對比的是地址
        return (other instanceof DriverInfo)
                && this.driver == ((DriverInfo) other).driver;
    }

}

為什麼Class.forName(com.mysql.cj.jdbc.Driver) 可以註冊驅動?

當加載com.mysql.cj.jdbc.Driver這個類時,靜態代碼塊中會執行註冊驅動的方法。

    static {
        try {
            //靜態代碼塊中註冊當前驅動
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

為什麼JDK6后不需要Class.forName也能註冊驅動?

因為從JDK6開始,DriverManager增加了以下靜態代碼塊,當類被加載時會執行static代碼塊的loadInitialDrivers方法。

而這個方法會通過查詢系統參數(jdbc.drivers)SPI機制兩種方式去加載數據庫驅動。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    static {
        loadInitialDrivers();
    }
    //這個方法通過兩個渠道加載所有數據庫驅動:
    //1. 查詢系統參數jdbc.drivers獲得數據驅動類名
    //2. SPI機制
    private static void loadInitialDrivers() {
        //通過系統參數jdbc.drivers讀取數據庫驅動的全路徑名。該參數可以通過啟動參數來設置,其實引入SPI機制后這一步好像沒什麼意義了。
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        //使用SPI機制加載驅動
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //讀取META-INF/services/java.sql.Driver文件的類全路徑名。
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                //加載並初始化類
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        if (drivers == null || drivers.equals("")) {
            return;
        }
        //加載jdbc.drivers參數配置的實現類
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            try {
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

補充:SPI機制本質上提供了一種服務發現機制,通過配置文件的方式,實現服務的自動裝載,有利於解耦和面向接口編程。具體實現過程為:在項目的META-INF/services文件夾下放入以接口全路徑名命名的文件,並在文件中加入實現類的全限定名,接着就可以通過ServiceLoder動態地加載實現類。

打開mysql的驅動包就可以看到一個java.sql.Driver文件,裏面就是mysql驅動的全路徑名。

獲得連接對象

DriverManager.getConnection

獲取連接對象的入口是DriverManager.getConnection,調用時需要傳入url、username和password。

獲取連接對象需要調用java.sql.Driver實現類(即數據庫驅動)的方法,而具體調用哪個實現類呢?

正如前面講到的,註冊的數據庫驅動被存放在registeredDrivers中,所以只有從這個集合中獲取就可以了。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    public static Connection getConnection(String url, String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }
        //傳入url、包含username和password的信息類、當前調用類
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        //遍歷所有註冊的數據庫驅動
        for(DriverInfo aDriver : registeredDrivers) {
            //先檢查這當前類加載器是否有權限加載這個驅動,如果是才進入
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                //這一步是關鍵,會去調用Driver的connect方法
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    return con;
                }
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }
    }

com.mysql.cj.jdbc.Driver.connection

由於使用的是mysql的數據驅動,這裏實際調用的是com.mysql.cj.jdbc.Driver的方法。

從以下代碼可以看出,mysql支持支持多節點部署的策略,本文僅對單機版進行擴展。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    //mysql支持多節點部署的策略,根據架構不同,url格式也有所區別。
    private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
    private static final String URL_PREFIX = "jdbc:mysql://";
    private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
    public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
    public java.sql.Connection connect(String url, Properties info) throws SQLException {
        //根據url的類型來返回不同的連接對象,這裏僅考慮單機版
        ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
        switch (conStr.getType()) {
            case SINGLE_CONNECTION:
                //調用ConnectionImpl.getInstance獲取連接對象
                return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

            case LOADBALANCE_CONNECTION:
                return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

            case FAILOVER_CONNECTION:
                return FailoverConnectionProxy.createProxyInstance(conStr);

            case REPLICATION_CONNECTION:
                return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

            default:
                return null;
        }
    }

ConnectionImpl.getInstance

這個類有個比較重要的字段session,可以把它看成一個會話,和我們平時瀏覽器訪問服務器的會話差不多,後續我們進行數據庫操作就是基於這個會話來實現的。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    private NativeSession session = null;
    public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
        //調用構造
        return new ConnectionImpl(hostInfo);
    }
    public ConnectionImpl(HostInfo hostInfo) throws SQLException {
        //先根據hostInfo初始化成員屬性,包括數據庫主機名、端口、用戶名、密碼、數據庫及其他參數設置等等,這裏省略不放入。
        //最主要看下這句代碼 
        createNewIO(false);
    }
    public void createNewIO(boolean isForReconnect) {
        if (!this.autoReconnect.getValue()) {
            //這裏只看不重試的方法
            connectOneTryOnly(isForReconnect);
            return;
        }

        connectWithRetries(isForReconnect);
    }
    private void connectOneTryOnly(boolean isForReconnect) throws SQLException {

        JdbcConnection c = getProxy();
        //調用NativeSession對象的connect方法建立和數據庫的連接
        this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
        return;
    }

NativeSession.connect

接下來的代碼主要是建立會話的過程,首先時建立物理連接,然後根據協議建立會話。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
            throws IOException {
        //首先獲得TCP/IP連接
        SocketConnection socketConnection = new NativeSocketConnection();
        socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout);

        // 對TCP/IP連接進行協議包裝
        if (this.protocol == null) {
            this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
        } else {
            this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
        }

        // 通過用戶名和密碼連接指定數據庫,並創建會話
        this.protocol.connect(user, password, database);
    }

針對數據庫的連接,暫時點到為止,另外還有涉及數據庫操作的源碼分析,後續再完善補充。

本文為原創文章,轉載請附上原文出處鏈接:https://github.com/ZhangZiSheng001/jdbc-demo

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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

生產者-消費者模型在Hudi中的應用

介紹

生產者-消費者模型用於解耦生產者與消費者,平衡兩者之間的能力不平衡,該模型廣泛應用於各個系統中,Hudi也使用了該模型控制對記錄的處理,即記錄會被生產者生產至隊列中,然後由消費者從隊列中消費,更具體一點,對於更新操作,生產者會將文件中老的記錄放入隊列中等待消費者消費,消費后交由HoodieMergeHandle處理;對於插入操作,生產者會將新記錄放入隊列中等待消費者消費,消費后交由HandleCreateHandle處理。

入口

前面的文章中提到過無論是HoodieCopyOnWriteTable#handleUpdate處理更新時直接生成了一個SparkBoundedInMemoryExecutor對象,還是HoodieCopyOnWriteTable#handleInsert處理插入時生成了一個CopyOnWriteLazyInsertIterable對象,再迭代時調用該對象的CopyOnWriteLazyInsertIterable#computeNext方法生成SparkBoundedInMemoryExecutor對象。最後兩者均會調用SparkBoundedInMemoryExecutor#execute開始記錄的處理,該方法核心代碼如下

  public E execute() {
    try {
      ExecutorCompletionService<Boolean> producerService = startProducers();
      Future<E> future = startConsumer();
      // Wait for consumer to be done
      return future.get();
    } catch (Exception e) {
      throw new HoodieException(e);
    }
  }

該方法會啟動所有生產者和單個消費者進行處理。

Hudi定義了BoundedInMemoryQueueProducer接口表示生產者,其子類實現如下

  • FunctionBasedQueueProducer,基於Function來生產記錄,在合併日誌log文件和數據parquet文件時使用,以便提供RealTimeView
  • IteratorBasedQueueProducer,基於迭代器來生產記錄,在插入更新時使用。

定義了BoundedInMemoryQueueConsumer類表示消費者,其主要子類實現如下

  • CopyOnWriteLazyInsertIterable$CopyOnWriteInsertHandler,主要處理CopyOnWrite表類型時的插入。
    • MergeOnReadLazyInsertIterable$MergeOnReadInsertHandler,主要處理MergeOnRead

表類型時的插入,其為CopyOnWriteInsertHandler的子類。

  • CopyOnWriteLazyInsertIterable$UpdateHandler,主要處理CopyOnWrite表類型時的更新。

整個生產消費相關的類繼承結構非常清晰。

對於生產者的啟動,startProducers方法核心代碼如下

  public ExecutorCompletionService<Boolean> startProducers() {
    // Latch to control when and which producer thread will close the queue
    final CountDownLatch latch = new CountDownLatch(producers.size());
    final ExecutorCompletionService<Boolean> completionService =
        new ExecutorCompletionService<Boolean>(executorService);
    producers.stream().map(producer -> {
      return completionService.submit(() -> {
        try {
          preExecute();
          producer.produce(queue);
        } catch (Exception e) {
          logger.error("error producing records", e);
          queue.markAsFailed(e);
          throw e;
        } finally {
          synchronized (latch) {
            latch.countDown();
            if (latch.getCount() == 0) {
              // Mark production as done so that consumer will be able to exit
              queue.close();
            }
          }
        }
        return true;
      });
    }).collect(Collectors.toList());
    return completionService;
  }

該方法使用CountDownLatch來協調生產者線程與消費者線程的退出動作,然後調用produce方法開始生產,對於插入更新時的IteratorBasedQueueProducer而言,其核心代碼如下

  public void produce(BoundedInMemoryQueue<I, ?> queue) throws Exception {
    ...
    while (inputIterator.hasNext()) {
      queue.insertRecord(inputIterator.next());
    }
    ...
  }

可以看到只要迭代器還有記錄(可能為插入時的新記錄或者更新時的舊記錄),就會往隊列中不斷寫入。

對於消費者的啟動,startConsumer方法的核心代碼如下

  private Future<E> startConsumer() {
    return consumer.map(consumer -> {
      return executorService.submit(() -> {
        ...
        preExecute();
        try {
          E result = consumer.consume(queue);
          return result;
        } catch (Exception e) {
          queue.markAsFailed(e);
          throw e;
        }
      });
    }).orElse(CompletableFuture.completedFuture(null));
  }

消費時會先進行執行前的準備,然後開始消費,其中consume方法的核心代碼如下

  public O consume(BoundedInMemoryQueue<?, I> queue) throws Exception {
    Iterator<I> iterator = queue.iterator();

    while (iterator.hasNext()) {
      consumeOneRecord(iterator.next());
    }

    // Notifies done
    finish();

    return getResult();
  }

可以看到只要隊列中還有記錄,就可以獲取該記錄,然後調用不同BoundedInMemoryQueueConsumer子類的consumeOneRecord進行更新插入處理。

值得一提的是Hudi對隊列進行了流控,生產者不能無限制地將記錄寫入隊列中,隊列緩存的大小由用戶配置,隊列能放入記錄的條數由採樣的記錄大小和隊列緩存大小控制。

在生產時,會調用BoundedInMemoryQueue#insertRecord將記錄寫入隊列,其核心代碼如下

  public void insertRecord(I t) throws Exception {
    ...
    rateLimiter.acquire();
    // We are retrieving insert value in the record queueing thread to offload computation
    // around schema validation
    // and record creation to it.
    final O payload = transformFunction.apply(t);
    adjustBufferSizeIfNeeded(payload);
    queue.put(Option.of(payload));
  }

首先獲取一個許可(Semaphore),未成功獲取會被阻塞直至成功獲取,然後獲取記錄的負載以便調整隊列,然後放入內部隊列(LinkedBlockingQueue)中,其中adjustBufferSizeIfNeeded方法的核心代碼如下

  private void adjustBufferSizeIfNeeded(final O payload) throws InterruptedException {
    if (this.samplingRecordCounter.incrementAndGet() % RECORD_SAMPLING_RATE != 0) {
      return;
    }

    final long recordSizeInBytes = payloadSizeEstimator.sizeEstimate(payload);
    final long newAvgRecordSizeInBytes =
        Math.max(1, (avgRecordSizeInBytes * numSamples + recordSizeInBytes) / (numSamples + 1));
    final int newRateLimit =
        (int) Math.min(RECORD_CACHING_LIMIT, Math.max(1, this.memoryLimit / newAvgRecordSizeInBytes));

    // If there is any change in number of records to cache then we will either release (if it increased) or acquire
    // (if it decreased) to adjust rate limiting to newly computed value.
    if (newRateLimit > currentRateLimit) {
      rateLimiter.release(newRateLimit - currentRateLimit);
    } else if (newRateLimit < currentRateLimit) {
      rateLimiter.acquire(currentRateLimit - newRateLimit);
    }
    currentRateLimit = newRateLimit;
    avgRecordSizeInBytes = newAvgRecordSizeInBytes;
    numSamples++;
  }

首先看是否已經達到採樣頻率,然後計算新的記錄平均大小和限流速率,如果新的限流速率大於當前速率,則可釋放一些許可(供阻塞的生產者獲取後繼續生產),否則需要獲取(回收)一些許可(許可變少後生產速率自然就降低了)。該操作可根據採樣的記錄大小動態調節速率,不至於在記錄負載太大和記錄負載太小時,放入同等個數,從而起到動態調節作用。

在消費時,會調用BoundedInMemoryQueue#readNextRecord讀取記錄,其核心代碼如下

  private Option<O> readNextRecord() {
    ...
    rateLimiter.release();
    Option<O> newRecord = Option.empty();
    while (expectMoreRecords()) {
      try {
        throwExceptionIfFailed();
        newRecord = queue.poll(RECORD_POLL_INTERVAL_SEC, TimeUnit.SECONDS);
        if (newRecord != null) {
          break;
        }
      } catch (InterruptedException e) {
        throw new HoodieException(e);
      }
    }
    ...

    if (newRecord != null && newRecord.isPresent()) {
      return newRecord;
    } else {
      // We are done reading all the records from internal iterator.
      this.isReadDone.set(true);
      return Option.empty();
    }
  }

可以看到首先會釋放一個許可,然後判斷是否還可以讀取記錄(還在生產或者停止生產但隊列不為空都可讀取),然後從內部隊列獲取記錄或返回。

上述便是生產者-消費者在Hudi中應用的分析。

總結

Hudi採用了生產者-消費者模型來控制記錄的處理,與傳統多生產者-多消費者模型不同的是,Hudi現在只支持多生產者-單消費者模型,單消費者意味着Hudi暫時不支持文件的併發寫入。而對於生產消費的隊列的實現,Hudi並未僅僅只是基於LinkedBlockingQueue,而是採用了更精細化的速率控制,保證速率會隨着記錄負載大小的變化和配置的隊列緩存大小而動態變化,這也降低了系統發生OOM的概率。

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

[FPGA]淺談LCD1602字符型液晶显示器(Verilog)

概述

本文圍繞LCD1602字符型液晶显示器展開,並在FPGA開發板上用VerilogHDL語言實現模塊驅動.

首先來一張效果展示

那麼怎麼在這塊綠油油的平面上显示出點陣構成的字符呢?本文將為你提供一些思路.

注:本文僅討論寫入操作,實現在LCD1602上显示指定字符串,不講解讀取相關操作.

LCD1602

LCD1602是什麼?

LCD1602是一種字符型液晶显示模塊,不同於七段數碼管,它可以通過點陣的形式显示出各種圖案或字符,可拓展性較強.

其名稱中”LCD”即為 Liquid Crystal Display (液晶显示器),”1602″代表显示屏上可同時显示32個字符(16×2).

LCD1602的管腳

LCD1602共有16根管腳(部分型號只有14根,沒有背光管腳),管腳功能表如下

符號 管腳說明 符號 管腳說明
VSS 電源地 D2 數據
VDD 電源正極 D3 數據
VL 偏壓 D4 數據
RS 數據/命令選擇 D5 數據
R/W 讀/寫選擇 D6 數據
E 使能 D7 數據
D0 數據 BLA 背光正極
D1 數據 BLK 背光負極

其中需要我們關心的只有RS,E,和D0-D7.

RS_數據/命令選擇

RS端用來控制輸入給D0-D7的序列代表命令還是數據.

如果代表輸入命令,則輸入給D0-D7的序列相當於對模塊進行設置(下文會有輸入序列對應的指令表及其功能);如果代表輸入數據,則輸入給D0-D7的序列相當於寫入需要显示的字符串(輸入的是每個字符所對應的地址碼).

若RS為低電平,代表輸入命令;若RS為高電平,代表輸入數據.

E_使能

E端是用來執行命令的使能引腳,當它從高電平變成低電平時(下降沿),液晶模塊執行命令.

D0-D7

八位雙向并行數據線,在本文中僅作輸入端(寫入).

LCD1602有個DDRAM

DDRAM( Display Data Random Access Memory )即為显示數據隨機存取存儲器,相當於”顯存”,用來存放待显示的字符代碼.

DDRAM一共有80個字節,它和1602的显示屏上32個字符位的對應地址如下圖

第一行的16個字符位的地址對應0x00-0x0F,第二行則對應0x40-0x4F(“0x”代表16進制數).

LCD1602還有個CGROM

CGROM( Character Generator Read-Only Memory )即為字符產生只讀存儲器,用來存放192個常用字符的字模.

值得一提的是,表中的左半部分字符和他們的ASCII碼是對應的,所以在寫代碼時可以直接寫成”A”而不必要寫成”0x41″.

另外還有一個CGRAM用來存放用戶自定義的字符,可存放8個5×8字符或4個5×10字符,不過這不在本文討論範圍內.

指令集

前文已經提到,當RS為低電平時,代表輸入命令,那麼這些命令都有哪些呢?

將能實現某種功能的序列稱為一條命令,每條命令有幾個固定的位和幾個可變的位,可變的位可以改變功能/模式,將這些命令總稱為指令集.全體指令集如下錶

指令 RS R/W D7 D6 D5 D4 D3 D2 D1 D0
清屏 0 0 0 0 0 0 0 0 0 1
光標複位 0 0 0 0 0 0 0 0 0 x
進入模式設置 0 0 0 0 0 0 0 1 I/D S
显示開關設置 0 0 0 0 0 0 1 D C B
移位控制 0 0 0 0 0 1 S/C R/L x x
工作方式設置 0 0 0 0 1 DL N F x x
字符發生器地址設置 0 0 0 1 a a a a a a
數據存儲器地址設置 0 0 1 b b b b b b b
讀忙標誌或地址 0 1 BF c c c c c c c
寫入數據至CDRAM或DDRAM 1 0 d d d d d d d d
從CGRAM或DDRAM中讀取數據 1 1 e e e e e e e e

注:其中a代表字符發生存儲器地址,b代表显示數據存儲器地址,c代表計數器地址,d代表要寫入的數據內容,e代表讀取的數據內容.

我們關心的是其中的清屏,進入模式設置,显示開關設置,工作方式設置,數據存儲器地址設置.

清屏

清除屏幕显示內容,光標返回屏幕左上角.

執行這個指令時需要一定時間.

進入模式設置

I/D = 1:寫入新數據后光標右移,I/D = 0:寫入新數據后光標左移

S = 1:显示移動,S = 0:显示不移動.

显示開關設置

D = 1:显示功能開,D = 0,显示功能關(但是DDRAM中的數據依然保留).

C = 1:有光標,C = 0,沒有光標.

B = 1:光標閃爍,B = 0.光標不閃爍.

工作方式設置

DL = 1:8位數據接口(D7-D0),DL = 0:4位數據接口(D7-D4).

N = 0:一行显示,N = 1;兩行显示.

F = 0: 5×8點陣字符,F = 1: 5×10點陣字符.

數據存儲器地址設置

在對DDRAM進行讀寫之前,首先要設置DDRAM地址,然後才能進行讀寫.

地址設置見.

Verilog驅動

了解了1602的原理和功能后,就可以着手編寫驅動模塊了.想要讓LCD1602显示指定的字符,需要有一個驅動程序將模塊和用戶連接起來,實現輸入什麼就輸出什麼的功能,並能夠簡單的進行設置.

接下來開始寫驅動(造輪子).分為若干個次級模塊逐個分析.

模塊定義

模塊共有5個端口(其中8個數據端合為一個8位寬端口),分別為CLK時鐘輸入端,_RST低電平有效的複位端,LCD_E使能端,LCD_RS數據/命令選擇端,LCD_DATA數據端.

module LCD1602
(input CLK
,input _RST
,output LCD_E 
,output reg LCD_RS
,output reg[7:0]LCD_DATA
);

上電穩定

這是一個簡單的初始化模塊,數據手冊要求要先通電20ms才可以進行下一步操作,為了使之上電穩定.

parameter TIME_20MS=1_000_000;//需要20ms以達上電穩定(初始化)
reg[19:0]cnt_20ms;
always@(posedge CLK or negedge _RST)
    if(!_RST)
        cnt_20ms<=1'b0;
    else if(cnt_20ms==TIME_20MS-1'b1)
        cnt_20ms<=cnt_20ms;
    else
        cnt_20ms<=cnt_20ms+1'b1 ;

wire delay_done=(cnt_20ms==TIME_20MS-1'b1)?1'b1:1'b0;//上電延時完畢

工作周期分頻

LCD1602的工作周期為500Hz,所以要進行分頻(板載晶振為50MHz).

parameter TIME_500HZ=100_000;//工作周期
reg[19:0]cnt_500hz;
always@(posedge CLK or negedge _RST)
    if(!_RST)
        cnt_500hz<=1'b0;
    else if(delay_done)
        if(cnt_500hz==TIME_500HZ-1'b1)
            cnt_500hz<=1'b0;
        else
            cnt_500hz<=cnt_500hz+1'b1;
    else
        cnt_500hz<=1'b0;

assign LCD_E=(cnt_500hz>(TIME_500HZ-1'b1)/2)?1'b0:1'b1;//使能端,每個工作周期一次下降沿,執行一次命令
wire write_flag=(cnt_500hz==TIME_500HZ-1'b1)?1'b1:1'b0;//每到一個工作周期,write_flag置高一周期

狀態機

模塊工作採用狀態機驅動.

//狀態機有40種狀態,此處用了格雷碼,一次只有一位變化(在二進制下)
parameter IDLE=8'h00;
parameter SET_FUNCTION=8'h01;
parameter DISP_OFF=8'h03;
parameter DISP_CLEAR=8'h02;
parameter ENTRY_MODE=8'h06;
parameter DISP_ON=8'h07;
parameter ROW1_ADDR=8'h05;
parameter ROW1_0=8'h04;
parameter ROW1_1=8'h0C;
parameter ROW1_2=8'h0D;
parameter ROW1_3=8'h0F;
parameter ROW1_4=8'h0E;
parameter ROW1_5=8'h0A;
parameter ROW1_6=8'h0B;
parameter ROW1_7=8'h09;
parameter ROW1_8=8'h08;
parameter ROW1_9=8'h18;
parameter ROW1_A=8'h19;
parameter ROW1_B=8'h1B;
parameter ROW1_C=8'h1A;
parameter ROW1_D=8'h1E;
parameter ROW1_E=8'h1F;
parameter ROW1_F=8'h1D;
parameter ROW2_ADDR=8'h1C;
parameter ROW2_0=8'h14;
parameter ROW2_1=8'h15;
parameter ROW2_2=8'h17;
parameter ROW2_3=8'h16;
parameter ROW2_4=8'h12;
parameter ROW2_5=8'h13;
parameter ROW2_6=8'h11;
parameter ROW2_7=8'h10;
parameter ROW2_8=8'h30;
parameter ROW2_9=8'h31;
parameter ROW2_A=8'h33;
parameter ROW2_B=8'h32;
parameter ROW2_C=8'h36;
parameter ROW2_D=8'h37;
parameter ROW2_E=8'h35;
parameter ROW2_F=8'h34;

reg[5:0]c_state;//Current state,當前狀態
reg[5:0]n_state;//Next state,下一狀態

always@(posedge CLK or negedge _RST)
    if(!_RST)
        c_state<=IDLE;
    else if(write_flag)//每一個工作周期改變一次狀態
        c_state<=n_state;
    else
        c_state<=c_state;

always@(*)
    case (c_state)
        IDLE:n_state=SET_FUNCTION;
        SET_FUNCTION:n_state=DISP_OFF;
        DISP_OFF:n_state=DISP_CLEAR;
        DISP_CLEAR:n_state=ENTRY_MODE;
        ENTRY_MODE:n_state=DISP_ON;
        DISP_ON:n_state=ROW1_ADDR;
        ROW1_ADDR:n_state=ROW1_0;
        ROW1_0:n_state=ROW1_1;
        ROW1_1:n_state=ROW1_2;
        ROW1_2:n_state=ROW1_3;
        ROW1_3:n_state=ROW1_4;
        ROW1_4:n_state=ROW1_5;
        ROW1_5:n_state=ROW1_6;
        ROW1_6:n_state=ROW1_7;
        ROW1_7:n_state=ROW1_8;
        ROW1_8:n_state=ROW1_9;
        ROW1_9:n_state=ROW1_A;
        ROW1_A:n_state=ROW1_B;
        ROW1_B:n_state=ROW1_C;
        ROW1_C:n_state=ROW1_D;
        ROW1_D:n_state=ROW1_E;
        ROW1_E:n_state=ROW1_F;
        ROW1_F:n_state=ROW2_ADDR;
        ROW2_ADDR:n_state=ROW2_0;
        ROW2_0:n_state=ROW2_1;
        ROW2_1:n_state=ROW2_2;
        ROW2_2:n_state=ROW2_3;
        ROW2_3:n_state=ROW2_4;
        ROW2_4:n_state=ROW2_5;
        ROW2_5:n_state=ROW2_6;
        ROW2_6:n_state=ROW2_7;
        ROW2_7:n_state=ROW2_8;
        ROW2_8:n_state=ROW2_9;
        ROW2_9:n_state=ROW2_A;
        ROW2_A:n_state=ROW2_B;
        ROW2_B:n_state=ROW2_C;
        ROW2_C:n_state=ROW2_D;
        ROW2_D:n_state=ROW2_E;
        ROW2_E:n_state=ROW2_F;
        ROW2_F:n_state=ROW1_ADDR;//循環到1-1進行掃描显示
        default:;
    endcase

RS端控制

控制輸入為數據或命令

always@(posedge CLK or negedge _RST)
    if(!_RST)
        LCD_RS<=1'b0;//為0時輸入指令,為1時輸入數據
    else if(write_flag)
        //當狀態為七個指令任意一個,將RS置為指令輸入狀態
        if((n_state==SET_FUNCTION)||(n_state==DISP_OFF)||(n_state==DISP_CLEAR)||(n_state==ENTRY_MODE)||(n_state==DISP_ON)||(n_state==ROW1_ADDR)||(n_state==ROW2_ADDR))
            LCD_RS<=1'b0; 
        else
            LCD_RS<=1'b1;
    else
        LCD_RS<=LCD_RS;

显示控制

always@(posedge CLK or negedge _RST)
    if(!_RST)
        LCD_DATA<=1'b0;
    else if(write_flag)
        case(n_state)
            IDLE:LCD_DATA<=8'hxx;
            SET_FUNCTION:LCD_DATA<=8'h38;//8'b0011_1000,工作方式設置:DL=1(DB4,8位數據接口),N=1(DB3,兩行显示),L=0(DB2,5x8點陣显示).
            DISP_OFF:LCD_DATA<=8'h08;//8'b0000_1000,显示開關設置:D=0(DB2,显示關),C=0(DB1,光標不显示),D=0(DB0,光標不閃爍)
            DISP_CLEAR:LCD_DATA<=8'h01;//8'b0000_0001,清屏
            ENTRY_MODE:LCD_DATA<=8'h06;//8'b0000_0110,進入模式設置:I/D=1(DB1,寫入新數據光標右移),S=0(DB0,显示不移動)
            DISP_ON:LCD_DATA<=8'h0c;//8'b0000_1100,显示開關設置:D=1(DB2,显示開),C=0(DB1,光標不显示),D=0(DB0,光標不閃爍)
            ROW1_ADDR:LCD_DATA<=8'h80;//8'b1000_0000,設置DDRAM地址:00H->1-1,第一行第一位
            //將輸入的row_1以每8-bit拆分,分配給對應的显示位
            ROW1_0:LCD_DATA<=row_1[127:120];
            ROW1_1:LCD_DATA<=row_1[119:112];
            ROW1_2:LCD_DATA<=row_1[111:104];
            ROW1_3:LCD_DATA<=row_1[103: 96];
            ROW1_4:LCD_DATA<=row_1[ 95: 88];
            ROW1_5:LCD_DATA<=row_1[ 87: 80];
            ROW1_6:LCD_DATA<=row_1[ 79: 72];
            ROW1_7:LCD_DATA<=row_1[ 71: 64];
            ROW1_8:LCD_DATA<=row_1[ 63: 56];
            ROW1_9:LCD_DATA<=row_1[ 55: 48];
            ROW1_A:LCD_DATA<=row_1[ 47: 40];
            ROW1_B:LCD_DATA<=row_1[ 39: 32];
            ROW1_C:LCD_DATA<=row_1[ 31: 24];
            ROW1_D:LCD_DATA<=row_1[ 23: 16];
            ROW1_E:LCD_DATA<=row_1[ 15:  8];
            ROW1_F:LCD_DATA<=row_1[  7:  0];
            ROW2_ADDR:LCD_DATA<=8'hc0;//8'b1100_0000,設置DDRAM地址:40H->2-1,第二行第一位
            ROW2_0:LCD_DATA<=row_2[127:120];
            ROW2_1:LCD_DATA<=row_2[119:112];
            ROW2_2:LCD_DATA<=row_2[111:104];
            ROW2_3:LCD_DATA<=row_2[103: 96];
            ROW2_4:LCD_DATA<=row_2[ 95: 88];
            ROW2_5:LCD_DATA<=row_2[ 87: 80];
            ROW2_6:LCD_DATA<=row_2[ 79: 72];
            ROW2_7:LCD_DATA<=row_2[ 71: 64];
            ROW2_8:LCD_DATA<=row_2[ 63: 56];
            ROW2_9:LCD_DATA<=row_2[ 55: 48];
            ROW2_A:LCD_DATA<=row_2[ 47: 40];
            ROW2_B:LCD_DATA<=row_2[ 39: 32];
            ROW2_C:LCD_DATA<=row_2[ 31: 24];
            ROW2_D:LCD_DATA<=row_2[ 23: 16];
            ROW2_E:LCD_DATA<=row_2[ 15:  8];
            ROW2_F:LCD_DATA<=row_2[  7:  0];
        endcase
    else
        LCD_DATA<=LCD_DATA;

自定義字符輸入

輸入要显示的字符.

wire[127:0]row_1;
wire[127:0]row_2;
assign row_1 ="   Welcome to   ";//第一行显示的內容(16個字符)
assign row_2 ="    My Blog!    ";//第二行显示的內容(16個字符)

效果展示

將以上代碼有機整合后,燒錄至開發板上,按下複位鍵即可看到显示屏上显示出了指定字樣.

你可以修改字符串來讓屏幕显示出不同的內容,甚至可以調整模式讓显示屏滾動显示大於16字符的字符串.

總結

LCD1602是一個很基礎的模塊,把這個掌握后對以後的學習幫助很大,所以很有必要學習.

這個模塊不止可以通過Verilog驅動,也可以用其他語言或其他開發板來實現,例如STM32,51單片機或者SV,VHDL語言,都可以寫一套讓他工作的驅動.

另外,如果有現成的輪子,為什麼還要自己造一個出來呢?在碰到類似情況時可以藉助互聯網參考一下別人對此問題有怎麼樣的解決方案,加以借鑒並內化於心,才能達到最高效率的學習.

參考資料

[1] aslmer. “verilog寫的LCD1602 显示”[ED/OL]. https://www.cnblogs.com/aslmer/p/5819422.html ,2016(8).

[2] aslmer. “LCD1602指令集解讀”[ED/OL]. https://www.cnblogs.com/aslmer/p/5801363.html ,2016(8).

[3] 阿忠ZHONG. “單片機显示原理(LCD1602)”[ED/OL]. https://www.cnblogs.com/hui088/p/4732034.html 2015(8).

[4] 百度百科. “詞條-LCD1602″[ED/OL]. https://baike.baidu.com/item/LCD1602/6014393 ,2019(9).

[5] HITACHI©Ltd. “HD44780U (LCD-II)(Dot Matrix Liquid Crystal Display Controller/Driver)”[M]. Japan HITACHI,1998.

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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

Alibaba Nacos 學習(五):K8S Nacos搭建,使用nfs

 

準備環境

Centos7  192.168.50.21 k8s-master 2G
Centos7  192.168.50.22 k8s-node01 2G
Centos7  192.168.50.23 k8s-node02 2G

K8S集群搭建參考 

 

master安裝好Git ,yum install git

master,node01,node02  安裝 nfs-utils

yum install nfs-utils

master,node01,node02添加nfs exports配置,為了解決後續的nfs報錯異常

/data/mysql-slave *(insecure,fsid=0,rw,async,no_root_squash)
/data/mysql-master *(insecure,fsid=0,rw,async,no_root_squash)
/data/nfs-share *(rw,fsid=0,sync,no_root_squash)
mysql-slave 數據庫從庫 
mysql-master 數據庫主庫
nfs-share nocas文件掛在目錄

後面的yml中會提到
master,node01,node02創建目錄
mkdir /data/mysql-slave
mkdir /data/mysql-master
mkdir /data/nfs-share 

 master 克隆代碼

   git clone https://github.com/nacos-group/nacos-k8s.git

克隆完成進入以下目錄

 cd /opt/nacos-k8s/deploy/

 

1.nfs安裝

kubectl create -f nfs/rbac.yaml 
kubectl create -f nfs/class.yaml 

修改nfs/deployment.yaml IP配置

 

 

 

kubectl create -f nfs/deployment.yaml

查看安裝狀態

kubectl get pod -l app=nfs-client-provisioner

 

 

 

2.mysql部署

cd /opt/nacos-k8s/deploy/mysql/

修改數據配置文件ip

vi mysql-master-nfs.yaml

 

 

 部署主庫

kubectl create -f mysql-master-nfs.yaml 

修改存庫ip

vi mysql-slave-nfs.yaml
kubectl create -f mysql-slave-nfs.yaml 

主從部署非常慢 耐心等待,如果報nfs相關的錯,重啟nfs即可

service nfs restart

 

 

3. 部署nacos

cd /opt/nacos-k8s/deploy/nacos/

 

 

 

 

 

kubectl create -f nacos-pvc-nfs.yaml 

 查看訪問端口

kubectl get svc|grep nacos

 

 

 

 

 查看K8S集群狀態

 

 Failed to pull image “nacos/nacos-server:latest”: rpc error: code = Unknown desc = context canceled

進去對應節點機器 ,拉取鏡像后,重新應用即可

kubectl apply -f

 4. 部署問題

部署過程中大部分都是NFS問題

可以參考

mount.nfs: No route to host
Warning FailedMount 100s (x5 over 10m) kubelet, node2 Unable to mount volumes for pod “nfs-client-provisioner-594f778474-whhb5_default(56aef93a-9d31-11e9-a4c4-00163e069f44)”: timeout expired waiting for volumes to attach or mount for pod “default”/”nfs-client-provisioner-594f778474-whhb5”. list of unmounted volumes=[nfs-client-root]. list of unattached volumes=[nfs-client-root nfs-client-provisioner-token-8dcrx]

修改deployment.yaml中server的IP地址為某個node節點的內網IP地址,圖1已標註

 

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

http協議詳細介紹

1.1 HTTP協議簡介
我們日常生活中經常會使用瀏覽器訪問Web站點,但是大家有思考過在這個過程中到底發生了什麼嗎?為什麼我們在瀏覽器地址欄上面輸入要訪問的URL后就可以訪問到Web頁面呢?

1.1.1 瀏覽器背後的故事
當我們在瀏覽器地址欄上輸入要訪問的URL后,瀏覽器會分析出URL上面的域名,然後通過DNS服務器查詢出域名映射的IP地址,瀏覽器根據查詢到的IP地址與Web服務器進行通信,而通信的協議就是HTTP協議。

我們可以把這個過程類比成一個電話對話的過程。當我們要打電話給某個人,首先要知道對方的電話號碼,然後進行撥號。打通電話后我們會進行對話,當然要對話肯定需要共同的語言,如果一個人說國語,而另一個人說英語,那肯定不能進行溝通的。在本例中,電話號碼相當於上面的IP地址,而共同語言相當於HTTP協議。

我們通過一個簡單的圖來闡述這個過程:

瀏覽器與Web服務器使用HTTP協議進行通信,那麼什麼是HTTP協議呢?接下來我們會詳細介紹HTTP協議的相關知識。

1.1.2 TCP/IP協議
HTTP協議是構建在TCP/IP協議之上的,是TCP/IP協議的一個子集,所以要理解HTTP協議,有必要先了解下TCP/IP協議相關的知識。

由於TCP/IP協議族包含眾多的協議,在這裏我們無法一一討論。接下來,我們僅介紹理解HTTP協議需要掌握的TCP/IP協議族的一些相關知識點。如果想深入理解TCP/IP協議,可以參考經典書籍《TCP/IP詳解》。

TCP/IP協議族分層

TCP/IP協議族是由一個四層協議組成的系統,這四層分別為:應用層、傳輸層、網絡層和數據鏈路層。如圖所示

分層的好處是把各個相對獨立的功能解耦,層與層之間通過規定好的接口來通信。如果以後需要修改或者重寫某一個層的實現,只要接口保持不變也不會影響到其他層的功能。接下來,我們將會介紹各個層的主要作用。

1) 應用層

應用層一般是我們編寫的應用程序,其決定了向用戶提供的應用服務。應用層可以通過系統調用與傳輸層進行通信。

處於應用層的協議非常多,比如:FTP(File Transfer Protocol,文件傳輸協議)、DNS(Domain Name System,域名系統)和我們本章討論的HTTP(HyperText Transfer Protocol,超文本傳輸協議)等。

2) 傳輸層

傳輸層通過系統調用嚮應用層提供處於網絡連接中的兩台計算機之間的數據傳輸功能。

在傳輸層有兩個性質不同的協議:TCP(Transmission Control Protocol,傳輸控制協議)和UDP(User Data Protocol,用戶數據報協議)。

3) 網絡層

網絡層用來處理在網絡上流動的數據包,數據包是網絡傳輸的最小數據單位。該層規定了通過怎樣的路徑(傳輸路線)到達對方計算機,並把數據包傳輸給對方。

4) 鏈路層

鏈路層用來處理連接網絡的硬件部分,包括控制操作系統、硬件設備驅動、NIC(Network Interface Card,網絡適配器)以及光纖等物理可見部分。硬件上的範疇均在鏈路層的作用範圍之內。

數據包封裝

上層協議數據是如何轉變為下層協議數據的呢?這是通過封裝(encapsulate)來實現的。應用程序數據在發送到物理網絡之前,會沿着協議棧從上往下傳遞。每層協議都將在上層協議數據的基礎上加上自己的頭部信息(鏈路層還會加上尾部信息),以為實現該層功能提供必要的信息。如圖所示:

發送端發送數據時,數據會從上層傳輸到下層,且每經過一層都會被打上該層的頭部信息。而接收端接收數據時,數據會從下層傳輸到上層,傳輸前會把下層的頭部信息刪除。過程如圖所示:

數據傳輸過程

由於下層協議的頭部信息對上層協議是沒有實際的用途,所以在下層協議傳輸數據給上層協議的時候會把該層的頭部信息去掉,這個封裝過程對於上層協議來說是完全透明的。這樣做的好處是,應用層只需要關心應用服務的實現,而不用管底層的實現。

TCP三次握手

從上面的介紹可知,傳輸層協議主要有兩個:TCP協議和UDP協議。TCP協議相對於UDP協議的特點是:TCP協議提供面向連接、字節流和可靠的傳輸。

使用TCP協議進行通信的雙方必須先建立連接,然後才能開始傳輸數據。TCP連接是全雙工的,也就是說雙方的數據讀寫可以通過一個連接進行。為了確保連接雙方可靠性,在雙方建立連接時,TCP協議採用了三次握手(Three-way handshaking)策略。
過程如圖所示:

TCP協議三次握手的描述如下:

第一次握手:客戶端發送帶有SYN標誌的連接請求報文段,然後進入SYN_SEND狀態,等待服務端的確認。

第二次握手:服務端接收到客戶端的SYN報文段后,需要發送ACK信息對這個SYN報文段進行確認。同時,還要發送自己的SYN請求信息。服務端會將上述的信息放到一個報文段(SYN+ACK報文段)中,一併發送給客戶端,此時服務端將會進入SYN_RECV狀態。

第三次握手:客戶端接收到服務端的SYN+ACK報文段后,會想服務端發送ACK確認報文段,這個報文段發送完畢后,客戶端和服務端都進入ESTABLISHED狀態,完成TCP三次握手。

當三次握手完成后,TCP協議會為連接雙方維持連接狀態。為了保證數據傳輸成功,接收端在接收到數據包后必須發送ACK報文作為確認。如果在指定的時間內(這個時間稱為重新發送超時時間),發送端沒有接收到接收端的ACK報文,那麼就會重發超時的數據。

1.1.3 DNS服務
前面介紹了與HTTP協議有着密切關係的TCP/IP協議,接下來介紹的DNS服務也是與HTTP協議有着密不可分的關係。

通常我們訪問一個網站,使用的是主機名或者域名來進行訪問的。因為相對於IP地址(一組純数字),域名更容易讓人記住。但TCP/IP協議使用的是IP地址進行訪問的,所以必須有個機制或服務把域名轉換成IP地址。DNS服務就是用來解決這個問題的,它提供域名到IP地址之間的解析服務。

如下圖所示,展示了DNS服務把域名解析成IP地址的過程:

DNS服務是通過DNS協議進行通信的,而DNS協議跟HTTP協議一樣也是應用層協議。由於我們的重點是HTTP協議,所以這裏不打算對DNS協議進行詳細的分析,我們只需要知道可以通過DNS服務把域名解析成IP地址即可。

1.1.4 HTTP與TCP/IP、DNS的關係
到現在,我們介紹了與HTTP協議有密切關係的TCP/IP協議和DNS服務,接下來我們通過下圖來整理一下HTTP協議與它們之間的關係:

HTTP與TCP/IP、DNS的關係
從上圖中可以知道,當客戶端訪問Web站點時,首先會通過DNS服務查詢到域名的IP地址。然後瀏覽器生成HTTP請求,並通過TCP/IP協議發送給Web服務器。Web服務器接收到請求後會根據請求生成響應內容,並通過TCP/IP協議返回給客戶端。

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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

PHP 的 self 關鍵字用法

之前有人詢問 self 關鍵字的用法,答案是比較明顯的:靜態成員函數內不能用 this 調用非成員函數,但可以用 self 調用靜態成員函數/變量/常量;其他成員函數可以用 self 調用靜態成員函數以及非靜態成員函數。隨着討論的深入,發現 self 並沒有那麼簡單。鑒於此,本文先對幾個關鍵字做對比和區分,再總結 self 的用法。

parentstatic 以及 this 的區別

要想將徹底搞懂 self ,要與 parentstatic 以及 this 區分開。以下分別做對比。

parent

selfparent 的區分比較容易: parent 引用父類/基類被隱蓋的方法(或變量), self則引用自身方法(或變量)。例如構造函數中調用父類構造函數:

class Base {
    public function __construct() {
        echo "Base contructor!", PHP_EOL;
    }
}

class Child {
    public function __construct() {
        parent::__construct();
        echo "Child contructor!", PHP_EOL;
    }
}

new Child;
// 輸出:
// Base contructor!
// Child contructor!

 

static

static 常規用途是修飾函數或變量使其成為類函數和類變量,也可以修飾函數內變量延長其生命周期至整個應用程序的生命周期。但是其與 self 關聯上是PHP 5.3以來引入的新用途:靜態延遲綁定。

有了 static 的靜態延遲綁定功能,可以在運行時動態確定歸屬的類。例如:

class Base {
    public function __construct() {
        echo "Base constructor!", PHP_EOL;
    }

    public static function getSelf() {
        return new self();
    }

    public static function getInstance() {
        return new static();
    }

    public function selfFoo() {
        return self::foo();
    }

    public function staticFoo() {
        return static::foo();
    }

    public function thisFoo() {
        return $this->foo();
    }

    public function foo() {
        echo  "Base Foo!", PHP_EOL;
    }
}

class Child extends Base {
    public function __construct() {
        echo "Child constructor!", PHP_EOL;
    }

    public function foo() {
        echo "Child Foo!", PHP_EOL;
    }
}

$base = Child::getSelf();
$child = Child::getInstance();

$child->selfFoo();
$child->staticFoo();
$child->thisFoo();

 

程序輸出結果如下:

Base constructor!
Child constructor!
Base Foo!
Child Foo!
Child Foo!

 

在函數引用上, selfstatic 的區別是:對於靜態成員函數, self 指向代碼當前類, static 指向調用類;對於非靜態成員函數, self 抑制多態,指向當前類的成員函數, static 等同於 this ,動態指向調用類的函數。

parentselfstatic 三個關鍵字聯合在一起看挺有意思,分別指向父類、當前類、子類,有點“過去、現在、未來”的味道。

this

selfthis 是被討論最多,也是最容易引起誤用的組合。兩者的主要區別如下:

  1. this 不能用在靜態成員函數中, self 可以;
  2. 對靜態成員函數/變量的訪問, 建議 用 self ,不要用 $this::$this-> 的形式;
  3. 對非靜態成員變量的訪問,不能用 self ,只能用 this ;
  4. this 要在對象已經實例化的情況下使用, self 沒有此限制;
  5. 在非靜態成員函數內使用, self 抑制多態行為,引用當前類的函數;而 this 引用調用類的重寫(override)函數(如果有的話)。

self 的用途

看完與上述三個關鍵字的區別, self 的用途是不是呼之即出?一句話總結,那就是: self總是指向“當前類(及類實例)”。詳細說則是:

  1. 替代類名,引用當前類的靜態成員變量和靜態函數;
  2. 抑制多態行為,引用當前類的函數而非子類中覆蓋的實現;

槽點

  1. 這幾個關鍵字中,只有 this 要加 $ 符號且必須加,強迫症表示很難受;
  2. 靜態成員函數中不能通過 $this-> 調用非靜態成員函數,但是可以通過 self:: 調用,且在調用函數中未使用 $this-> 的情況下還能順暢運行。此行為貌似在不同PHP版本中表現不同,在當前的7.3中ok;
  3. 在靜態函數和非靜態函數中輸出 self ,猜猜結果是什麼?都是 string(4) "self" ,迷之輸出;
  4. return $this instanceof static::class; 會有語法錯誤,但是以下兩種寫法就正常:
    $class = static::class;
    return $this instanceof $class;
    // 或者這樣:
    return $this instanceof static;

 

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

drf組件之jwt認證

drf組件之jwt認證模塊

一、認證規則

全稱:json web token
解釋:加密字符串的原始數據是json,後台產生,通過web傳輸給前台存儲
格式:三段式 – 頭.載荷.簽名 – 頭和載荷用的是base64可逆加密,簽名用md5不可逆加密
內容:
頭(基礎信息,也可以為空):加密方式、公司信息、項目組信息、…
載荷(核心信息):用戶信息、過期時間、…
簽名(安全保障):頭加密結果+載荷加密結果+服務器秘鑰 的md5加密結果

認證規則:
後台一定要保障 服務器秘鑰 的安全性(它是jwt的唯一安全保障)
後台簽發token(login接口 ) -> 前台存儲 -> 發送需要認證的請求帶着token -> 後台校驗得到合法的用戶 -> 權限管理

為什麼要有jwt認證:
1)服務器壓力小, 後台不需要存儲token,只需要存儲簽發與校驗token的算法,效率遠遠大於後台存儲和取出token完成校驗
2) jwt算法認證,更適合服務器集群部署

二、認證模塊

安裝:pip install djangorestframework-jwt
模塊包:rest_framework_jwt

採用drf-jwt框架,後期任務只需要書寫登錄
為什麼要重寫登錄:drf-jwt只完成了賬號密碼登錄,我們還需要手機登錄,郵箱登錄
為什麼不需要重寫認證類:因為認證規則已經完成且固定不變,變得只有認證字符串的前綴,前綴可以在配置文件中配置

三、JWT使用

jwt配置;

在settings.py文件中配置,如果不配置,默認走jwt默認的

jwt插件的三個接口:

在urls.py中配置

在postman中測試一下籤發token

注意:上面三個接口都是發送POST請求

四、利用JWT實現多方式登錄

注:APIResponse 為自定義Response對象

# views.py
from rest_framework.views import APIView
from . import models,serializers
from utils.response import APIResponse

class LoginAPIView(APIView):
    # 登錄接口應該禁用所有的認證和、權限,因為不管是誰都應該能進來
    authentication_classes = []
    permission_classes = []
    def post(self, request, *args, **kwargs):
        # 將數據傳到序列化組件進行校驗
        user_ser = serializers.LoginSerializer(data=request.data)
        user_ser.is_valid(raise_exception=True)

        return APIResponse(msg='login success', data={
            'username': user_ser.user.username,
            'token': user_ser.token
        })

注意:

通過user對象生成payload載荷
payload = jwt_payload_handler(user)

通過payload簽發token
token = jwt_encode_handler(payload)

# serializer.py
from rest_framework.serializers import ModelSerializer, CharField, ValidationError, SerializerMethodField
from . import models
from django.contrib.auth import authenticate
import re
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class LoginSerializer(ModelSerializer):
    username = CharField(write_only=True)
    password = CharField(write_only=True)
    class Meta:
        model = models.User
        fields = ('username', 'password')

    def validate(self, attrs):
        # user_obj = authenticate(**attrs)
        # if not user_obj:
        #     raise ValidationError('用戶名或密碼錯誤')

        # 賬號密碼登錄 ==》 多方式登錄
        user = self._many_method_login(**attrs)

        # 通過user對象生成payload載荷
        payload = jwt_payload_handler(user)
        # 通過payload簽發token
        token = jwt_encode_handler(payload)

        # 將user和token存放在序列化對象中,方便返回到前端去
        self.user = user
        self.token = token

        return attrs

    # 多方式登錄 (用戶名、郵箱、手機號三種方式登錄)
    def _many_method_login(self, **attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 利用正則匹配判斷用戶輸入的信息
        # 1.判斷郵箱登錄
        if re.match(r'.*@.*', username):
            user = models.User.objects.filter(email=username).first()  # type: models.User
        # 2.判斷手機號登錄
        elif re.match(r'^1[3-9][0-9]{9}$',username):
            user = models.User.objects.filter(mobile=username).first()
        # 3.用戶名登錄
        else:
            user = models.User.objects.filter(username=username).first()

        if not user:
            raise ValidationError({'username': '賬號有誤'})

        if not user.check_password(password):
            raise ValidationError({'password': '密碼錯誤'})

        return user

使用postman測試代碼:

五、前後台分離模式下信息交互規則

"""
1)任何人都能直接訪問的接口
    請求不是是get、還是post等,不需要做任何校驗

2)必須登錄后才能訪問的接口
    任何請求方式都可能做該方式的限制,請求必須在請求頭中攜帶認證信息 - authorization
    
3)前台的認證信息獲取只能通過登錄接口
    前台提供賬號密碼等信息,去後台換認證信息token
    
4)前台如何完成登錄註銷
    前台登錄成功一般在cookie中保存認證信息token,分離註銷就是前台主動清除保存的token信息
"""

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

特斯拉發表電動卡車 Cybertruck,一台從科幻電影走出來的鋼鐵車

今天的 Elon Musk 看起來不像鋼鐵人,更像蝙蝠俠,因為他們的新車 Cybertruck,不但外型酷似蝙蝠車,同時還能防彈防撞,並且擁有超越保時捷的加速度,跟勝過市面上卡車的拖吊能力,更驚人的是,售價只要 39,900 美元起。

眾所期待的特斯拉新車 Cybertruck 今日正式發表,和之前流出的影像不同,Cybertruck 酷似隱形戰機 F-117 的設計,讓人聯想到蝙蝠車,甚至懷疑這是不是一台防雷達偵測的戰車?

Tesla Cybertruck 全車採用冷鑄鋼板,能夠抵擋 9mm 口徑手槍的射擊,現場展示用重鎚敲擊也毫髮無傷;車窗玻璃同樣採用防彈設計,然而有趣的是,現場展示時,被大鐵球砸出了一片雪花。「至少,它沒被打穿,你坐在裡面很安全。」Elon Musk 笑著說。

Cybertruck 為了因應負重,搭載了適應性氣壓懸吊系統,針對高速公路,或是越野泥巴路,能夠自動調整懸吊高度,同時也順便使用這個氣壓系統,做了一個高壓出力裝置,使用者可以自行加裝不同氣壓工具,像是高壓水槍或是電鑽等。

當重裝電動機車開上後廂時,懸吊系統會自動調整車尾高度,讓車身保持平衡。

車尾與其他皮卡車開放式貨斗不同,Cybertruck 採用封閉式貨斗,並有升降式尾門,現場展示時,將這台電動機車 ATV 直接騎上貨斗後,還能直接充電,顯然是在致敬蝙蝠車跟蝙蝠機車。

Cybertruck 如同其他皮卡車,車尾裝有釣鉤,能夠充當拖車使用,而歸功於它的強力馬達,拖車能力屌打了皮卡車霸主 Ford F-150,在現場展示的影片中,特斯拉讓 Cybertruck 跟 F-150 互相拖住對方,進行拔河測試,結果 F-150 整台被 Cybertruck 拖走。

F-150 慘遭 Cybertruck 拖走。

馬斯克強調,一般皮卡車需要另外裝載發電機才能使用電動工具,Cybertruck 直接提供了電源,因此省下不少空間,同時還提供強大的拖力。

此外,做為一台卡車,Cybertruck 莫名其妙地擁有超越保時捷的加速度,根據現場公布數據,最頂級版的 0-100 公里加速時間不到 3 秒。現場展示了 Cybertruck 與 Porsche 911 賽跑的影片,起步雖然小輸一點,但隨後就超越了 911。

現場展示競速影片,大約 1 秒後,Cybertruck 就超過了 911。

Tesla Cybertruck 共有 3 種版本,依照馬達數量來分別,最低價 39,900 美元起,最高 69,900 美元。Cybertruck 從今天起在美國開放預購,實際交車時間預計要等到 2021 年底。頂級的三馬達款,更預計要等到 2022 年底才會開始生產。

如同馬斯克開場所說,卡車在過去幾十年來都長得差不多,特斯拉要打造一台完全不一樣的卡車,同時還要保持零排放,跟超高性能,從今天的現場展示來看,特斯拉再次完成一個不可能的任務。在興奮之餘也別忘了,這一切都是現場展示,實際上如何,就有待實際交車後驗證了!

(合作媒體:。圖片來源:)

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

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

快速搭建 SpringCloud 微服務開發環境的腳手架

本文適合有 SpringBoot 和 SpringCloud 基礎知識的人群,跟着本文可使用和快速搭建 SpringCloud 項目。

本文作者:HelloGitHub-秦人

HelloGitHub 推出的系列,今天給大家帶來一款基於 SpringCloud2.1 的微服務開發腳手開源項目——SpringCloud

項目源碼地址:

一、微服務的簡介

微服務是可以獨立部署、水平擴展、獨立訪問的服務單元。Java 中常見最小的微服務單元就是基於 SpringBoot 框架的一個獨立項目。一個微服務只做一件事(單一職責),多個微服務組合才能稱之為一個完整的項目或產品。那麼多個微服務的就需要來管理,而 SpringCloud 就是統籌這些微服務的大管家。它是一系列有序框架的集合,簡單易懂、易部署易維護的分佈式系統開發工具包。

今天介紹的開源項目就是基於 SpringCloud2.1 的腳手架,讓項目開發快速進入業務開發,而不需過多時間花費在架構搭建上,下面就讓我們一起來看看這個項目的使用吧。

二、項目結構

這裏以一個網關(gateway-admin)微服務來說明。

項目目錄結構如下圖:

目錄說明:

  1. db:項目初始化數據庫腳本。
  2. docker:Docker 配置文件目錄,將微服務打包為 docker 鏡像(image)。
  3. config:項目配置信息目錄,包括數據庫配置,消息轉化配置等。
  4. dao:數據庫操作目錄,主要對底層數據進行增刪查改。
  5. entity:項目實體類目錄。
  6. events:事件處理目錄。
  7. exception:異常處理目錄,通過面向切面處理全局異常。
  8. rest:微服務控制器目錄,也就是對外提供的接口。
  9. service:微服務業務層目錄。
  10. GatewayAdminApplication:微服務 SpringBoot 入口類。
  11. resources:項目配置文件目錄。
  12. test:項目單元測試目錄。
  13. pom.xml:maven 項目對象模型文件。

三、實戰操作

3.1 前提

  • 確保本地安裝 Git、Java8、Maven。
  • 懂一些 SpringMVC 的知識,因為 SpringBoot 是基於 SpringMVC 演化而來的。
  • 懂一些應用容器引擎 Docker、Docker-compose 的知識。

3.2 微服務架構說明

一個完整的項目,微服務架構一般包括下面這些服務:

  • 註冊中心(常用的框架 Nacos、Eureka)
  • 統一網關(常用的框架 Gateway、Zuul)
  • 認證中心(常用技術實現方案 Jwt、OAuth)
  • 分佈式事務(常用的框架 Txlcn、Seata)
  • 文件服務
  • 業務服務

3.3 運行項目

下面介紹了三種運行的方式:

第一種:一鍵運行

Linux 和 Mac 系統下可在項目根目錄下執行 ./install.sh 快速搭建開發環境。

第二種:本地環境運行

不推薦此方法,但還是簡單介紹下。

  1. 基礎環境安裝:mysql、redis,rabbitmq

  2. 環境運行:
    git clone https://github.com/zhoutaoo/SpringCloud.git #克隆項目

  3. 安裝認證公共包到本地 maven 倉庫,執行如下命令:
    cd common mvn clean install #安裝認證公共包到本地 maven 倉庫

  4. 安裝註冊中心 Nacos
    • 下載
    • 執行如下命令:

      unzip nacos-server-0.9.0.zip  OR tar -xvf nacos-server-0.9.0.tar.gz
      cd nacos/bin
      bash startup.sh -m standalone # Linux 啟動命令
      cmd startup.cmd # Windows 啟動命令
  5. 運行網關服務、認證服務、業務服務等

這裏以網關服務為例:執行 GatewayAdminApplication.java

注意:認證服務(auth)、網關服務(gateway)、組織管理服務(sysadmin)需要執行數據庫初始化腳本。

可通過 swager 接口: 測試是否搭建成功,如果能正常訪問表示服務啟動成功。

說明:

  • application.yml 文件主要配置 rabbitmq,redis, mysql 的連接信息。

    spring:
      rabbitmq:
        host: ${RABBIT_MQ_HOST:localhost}
        port: ${RABBIT_MQ_PORT:5672}
        username: ${RABBIT_MQ_USERNAME:guest}
        password: ${RABBIT_MQ_PASSWORD:guest}
      redis:
        host: ${REDIS_HOST:localhost}
        port: ${REDIS_PORT:6379}
        #password: ${REDIS_PASSWORD:}
        lettuce:
          pool:
            max-active: 300
    
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:${DATASOURCE_DBTYPE:mysql}://${DATASOURCE_HOST:localhost}:${DATASOURCE_PORT:3306}/sc_gateway?characterEncoding=UTF-8&useUnicode=true&useSSL=false
        username: ${DATASOURCE_USERNAME:root}
        password: ${DATASOURCE_PASSWORD:root123}
  • bootstrap.yml 文件主要配置服務基本信息(端口,服務名稱),註冊中心地址等。

    server:
      port: ${SERVER_PORT:8445}
    spring:
      application:
        name: gateway-admin
      cloud:
        nacos:
          discovery:
            server-addr: ${REGISTER_HOST:localhost}:${REGISTER_PORT:8848}
          config:
            server-addr: ${REGISTER_HOST:localhost}:${REGISTER_PORT:8848}
            file-extension: yml
        sentinel:
          transport:
            dashboard: ${SENTINEL_DASHBOARD_HOST:localhost}:${SENTINEL_DASHBOARD_PORT:8021}

第三種:Docker 環境運行

  1. 基礎環境安裝
    • 通過 docker 命令安裝

      # 安裝redis
      docker run -p 6379:6379 --name redis -d docker.io/redis:latest --requirepass "123456" 
      # 安裝mysql
      docker run --name mysql5.7 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root123 -d docker.io/mysql:5.7
      # 安裝rabbitmq 
      docker run -d -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --name rabbitmq docker.io/rabbitmq:latest
    • 也可以通過 docker-compose 命令安裝

      cd docker-compose
      docker-compose up -d  #docker-compose 安裝mysql,redis,rabbitmq 服務
  2. 下載項目到本地
    git clone https://github.com/zhoutaoo/SpringCloud.git #克隆項目

  3. 安裝認證公共包到本地 maven 倉庫執行如下命令:
    cd common && mvn install #安裝認證公共包到本地maven倉庫

  4. docker-compose 運行 Nacos
    cd docker-compose docker-compose -f docker-compose.yml -f docker-compose.nacos.yml up -d nacos #啟動註冊中心

  5. 構建消息中心鏡像
    cd ./center/bus mvn package && mvn docker:build cd docker-compose #啟動消息中心 docker-compose -f docker-compose.yml -f docker-compose.center.yml up -d bus-server

需要構建鏡像的其他服務有:(注:操作和消息中心鏡像構建方式類似)

  • 網關管理服務 (gateway-admin、gateway-web)

  • 組織服務(sysadmin/organization)

  • 認證服務 (auth/authentication-server)

  • 授權服務(auth authorization-server)

  • 管理台服務(monitor/admin)

3.4 運行效果

Nacos 服務中心

所有服務都正常啟動,在 nacos 管理中心可查看,實例數表示運行此服務的個數,值為 1 可以理解為服務正常啟動。

查看後台服務

命令行執行:docker ps -a 查看 docker 所有進程信息

通過訪問微服務對外暴露的接口(swagger)檢測服務是否可用。

swager 接口地址:

測試如下圖:

四、最後

微服務(SpringBoot、SpringCloud、Docker)現在吵得特別火,它並不是一門新的技術,而是在老技術的基礎上衍生出來的,增加了一些新的特性。

教程至此,你應該能夠通過 SpringCloud 這項目快速搭建微服務了。那麼就可以開始你的微服務學習之旅了,是時候更新一下自己的技能樹了,讓我們一起來學習微服務吧!

五、參考資料

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟着我們的文章,你會發現編程的樂趣、使用和發現參与開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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