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產品,價格不怕你比較

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

對js中局部變量、全局變量和閉包的理解

對js中局部變量、全局變量和閉包的理解

局部變量

對於局部變量,js給出的定義是這樣的:在 JavaScript函數內部聲明的變量(使用 var)是局部變量,所以只能在函數內部訪問它。(該變量的作用域是局部的)。可以在不同的函數中使用名稱相同的局部變量,因為只有聲明過該變量的函數才能識別出該變量。只要函數運行完畢,本地變量就會被刪除

我們先來逐步理解:

  • 只能在函數內部訪問

    function test() {
        var a = 0;
        return a;
    }
    
    console.log(a);
    //結果:a is not defined

    上面的代碼聲明了一個test()函數,在函數內部聲明了一個局部變量a,當我們嘗試在函數外訪問局部變量a時,出來的結果是a is not defined

    我們再來看下面這個例子:

    function test() {
        var a = 0;
        return a;
    }
    
    console.log(test());
    //結果:0

    以上兩個例子很好的闡述了局部變量只能在函數內部訪問,當調用函數時,函數域自動執行其中的代碼,局部變量自然也被調用。

  • 只要函數運行完畢,本地變量就會被刪除

    function b() {
        var y = 0;
        z = ++y;
        console.log("這是局部變量y:",z)
        return z;
    }
    
    console.log(b(),b(),b());
    //結果:這是局部變量y: 1
    //這是局部變量y: 1
    //這是局部變量y: 1
    //1 1 1

    從上面代碼我們可以看出,我們執行了3次函數調用,得到的結果都是1,可能有人會說,這很簡單啊,每次出來的結果都是1,那是因為每次執行函數,函數內都會將局部變量y初始化為0。沒錯,的確是這樣,但是如果不初始化變量,則得到的返回值是NaN,所以初始化是必要的。所以,無論用什麼辦法,在函數內部用一個局部變量去做累加,是不可能實現的。但是,我們可以通過全局變量和閉包來實現累加。

全局變量

在js中,這樣定義全局變量, 在函數外聲明的變量是全局變量,網頁上的所有腳本和函數都能訪問它。 全局變量會在頁面關閉后被刪除

  • 我們再來看一個例子

    var a = 0;
    
    function b() {
        ++a;
        console.log("這是全局變量a",a);
        return a;
    }
    console.log("這是未改變的全局變量a:",a,"這是函數b():",b(),b(),b(),"這是改變后的全局變量a:",a);
    //結果:這是全局變量a 1
    //這是全局變量a 2
    //這是全局變量a 3
    //這是未改變的全局變量a: 0 這是函數b(): 1 2 3 這是改變后的全局變量a: 3

    上面代碼定義了一個全局變量a,和一個b()函數,通過函數內部對a執行自加加,實現了累加目的,通過三次調用函數,得到的結果a為3。

閉包

什麼是閉包呢?閉包的定義是這樣的,閉包是一種保護私有變量的機制,在函數執行時形成私有的作用域,保護裏面的私有變量不受外界干擾。直觀的說就是形成一個不銷毀的棧環境。

我對閉包的理解是這樣的,閉包就是一個內嵌函數引用頂層函數的變量,而頂層函數是一個立即執行函數(自調用函數),因為它會自動調用,所以局部變量不會被刪除,但是這會增加內存消耗。

  • 來看一個例子

    function a() {
        var b = 0;
        return function() {
            return ++b;
        }
    }
    
    var closure = a();
    console.log("這是閉包:",closure(),closure(),closure());
    //結果:這是閉包: 1 2 3

    我們看到,由於閉包的特殊機制,使得局部變量在函數執行完之後不會被銷毀,由此得到的最後結果為3 ,而不是1。

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

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

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

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

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產品,價格不怕你比較

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

Redis 4.0鮮為人知的功能將加速您的應用程序

來源:Redislabs

作者:Kyle Davis

翻譯:Kevin (公眾號:中間件小哥)

Redis 4.0給Redis生態帶來了一個驚人的功能:Modules(模塊)。Modules是Redis的一大轉變,它是Redis內部自定義數據類型和全速計算的開放環境。但是,儘管對該版本的大多數關注都集中在Modules上,但新版本還引入了一個非常重要的命令,它就是遊戲規則的改變者:UNLINK。

您可以使用redis-cli連接redis-server執行info命令,去查看當前redis版本中是否可以使用UNLINK命令。info響應將告訴您有關服務器的所有信息。在第一部分(#Server)中,返回結果有一行值為redis_version。如果該值大於4.0,則可以使用UNLINK命令。並非所有Redis提供商都保持最新版本,因此最好在更改代碼之前檢查redis版本。

讓我們回顧一下Redis的關鍵架構功能之一:“單線程”。Redis在大多數情況下是一個單線程應用程序。它一次只做一件事,這樣可以把這些事做的更快。多線程有點複雜,並且引入了鎖和其他可能降低應用程序速度的問題。儘管Redis(最高4.0版)通過多線程方式執行了少量操作,但它通常在啟動另一個命令之前先要完成一個命令。

相比於快速讀寫,您可能會覺得使用DEL命令去刪除一個鍵值不需要考慮太多,但是在很多情況下,刪除數據同樣很重要。與Redis中的大多數命令一樣,DEL命令在單個線程中運行,如果您獲取一個幾千字節的鍵值,花費不到一毫秒的時間,這是您所感知不到的。然而,當您獲取的鍵值大小是兆字節、100兆字節或者500兆字節會發生什麼呢?哈希、排序、列表等數據結構會隨着時間的推移而添加更多的數據進去,這樣會生成一個數GB大小的數據集。然後用DEL命令去刪除大Key時會發生什麼呢?由於Redis是單線程操作的,處理這種請求時整個服務都處於等待中,需要等待該命令執行完成才能執行其它操作。同時,我們考慮更複雜的一種場景,這些鍵中保存的數據可能已經包含數以千萬個微小請求,因此應用程序或操作員可能無法真正了解刪除這些數據需要花費多長時間。

理智會告訴我們不要在擁有100萬元素的排序集上運行如下這樣的命令:

> ZRANGE some-zset 0 -1

但是,在上面的some-zset集合中執行DEL命令將花費和上面一樣的時間-中間沒有傳輸開銷,但是它會一直去分配內存,而且您會一直卡死在CPU繁忙中。在使用UNLINK之前,您可能會結合SCAN命令採用非原子性的方法進行一些少量刪除,去避免這種持續分配內存的噩夢。上面無論使用哪種方式,都是讓人無法接受的。

您可能已經猜到了,就是使用UNLINK命令來替換DEL!從語法上講,UNLINK與DEL相同,但UNLINK提供了更為理想的解決方案。首先,它將鍵值從整個鍵值空間中刪除。然後,在另一個線程中,它開始回收內存。從多線程的角度來看,這是一種安全的操作,因為它(在主線程中)從鍵空間中刪除了該項,從而使Redis其它命令無法訪問。

如果你有一個快速增長的鍵值-不管鍵值的大小如何,UNLINK都是O(1)操作(每個鍵;在主線程中)。使用DEL刪除一個大值可能需要幾百毫秒或更長時間,而UNLINK將在不到一毫秒的時間內完成(包括網絡往返)。當然,您的服務器仍將需要花一些時間在另一個線程中重新分配該值的內存(其中的工作是O(N),其中N是已刪除值的分配數),但是主線程的性能不會被另一個線程中正在進行的操作嚴重影響到。

因此,您是否應該用UNLINK命令替換代碼中的所有DEL命令?當然,在少數情況下,DEL正是您所需要的。這裏我可以想到兩點:

1、   在MULTI / EXEC或pipeline中,在添加和刪除大值時DEL命令是一種理想選擇。在這種情況下,UNLINK不會立即釋放空間,並且在處理繁忙的情況下(如果內存已滿),您可能會遇到麻煩。

2、   在更緊急的情況下,在無快速響應驅逐數據下您可以寫入數據。

在沒有極端內存限制的理想環境中,很難想到不使用UNLINK的情況。UNLINK將提供更一致的行為,總體上具有更好的性能,並且代碼更改非常小(如果可以在客戶端中重命名命令,則無需更改)。如果UNLINK適合您的應用程序,請就此將您的DEL更改為UNLINK,然後查看它的性能提高。

 

更多優質中間件技術資訊/原創/翻譯文章/資料/乾貨,請關注“中間件小哥”公眾號!

 

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

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

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

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

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產品,價格不怕你比較

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

高併發編程學習(1)——併發基礎

為更良好的閱讀體驗,請訪問原文:

一、前言

當我們使用計算機時,可以同時做許多事情,例如一邊打遊戲一邊聽音樂。這是因為操作系統支持併發任務,從而使得這些工作得以同時進行。

  • 那麼提出一個問題:如果我們要實現一個程序能一邊聽音樂一邊玩遊戲怎麼實現呢?
public class Tester {

    public static void main(String[] args) {
        System.out.println("開始....");
        playGame();
        playMusic();
        System.out.println("結束....");
    }

    private static void playGame() {
        for (int i = 0; i < 50; i++) {
            System.out.println("玩遊戲" + i);
        }
    }

    private static void playMusic() {
        for (int i = 0; i < 50; i++) {
            System.out.println("播放音樂" + i);
        }
    }
}

我們使用了循環來模擬過程,因為播放音樂和打遊戲都是連續的,但是結果卻不盡人意,因為函數體總是要執行完之後才能返回。那麼到底怎麼解決這個問題?

并行與併發

并行性和併發性是既相似又有區別的兩個概念。

并行性是指兩個或多個事件在同一時刻發生。而併發性是指兩個或多個事件在同一時間間隔內發生。

在多道程序環境下,併發性是指在一段時間內宏觀上有多個程序在同時運行,但在單處理機環境下(一個處理器),每一時刻卻僅能有一道程序執行,故微觀上這些程序只能是分時地交替執行。例如,在 1 秒鐘時間內,0 – 15 ms 程序 A 運行;15 – 30 ms 程序 B 運行;30 – 45 ms 程序 C 運行;45 – 60 ms 程序 D 運行,因此可以說,在 1 秒鐘時間間隔內,宏觀上有四道程序在同時運行,但微觀上,程序 A、B、C、D 是分時地交替執行的。

如果在計算機系統中有多個處理機,這些可以併發執行的程序就可以被分配到多個處理機上,實現併發執行,即利用每個處理機愛處理一個可併發執行的程序。這樣,多個程序便可以同時執行。以此就能提高系統中的資源利用率,增加系統的吞吐量。

進程和線程

進程是指一個內存中運行的應用程序。一個應用程序可以同時啟動多個進程,那麼上面的問題就有了解決的思路:我們啟動兩個進程,一個用來打遊戲,一個用來播放音樂。這當然是一種解決方案,但是想象一下,如果一個應用程序需要執行的任務非常多,例如 LOL 遊戲吧,光是需要播放的音樂就有非常多,人物本身的語音,技能的音效,遊戲的背景音樂,塔攻擊的聲音等等等,還不用說遊戲本身,就光播放音樂就需要創建許多許多的進程,而進程本身是一種非常消耗資源的東西,這樣的設計顯然是不合理的。更何況大多數的操作系統都不需要一個進程訪問其他進程的內存空間,也就是說,進程之間的通信很不方便,此時我們就得引入“線程”這門技術,來解決這個問題。

線程是指進程中的一個執行任務(控制單元),一個進程可以同時併發運行多個線程。我們可以打開任務管理器,觀察到幾乎所有的進程都擁有着許多的「線程」(在 WINDOWS 中線程是默認隱藏的,需要在「查看」裏面點擊「選擇列」,有一個線程數的勾選項,找到並勾選就可以了)。

進程和線程的區別

進程:有獨立的內存空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。

線程:堆空間是共享的,棧空間是獨立的,線程消耗的資源也比進程小,相互之間可以影響的,又稱為輕型進程或進程元。

因為一個進程中的多個線程是併發運行的,那麼從微觀角度上考慮也是有先後順序的,那麼哪個線程執行完全取決於 CPU 調度器(JVM 來調度),程序員是控制不了的。我們可以把多線程併發性看作是多個線程在瞬間搶 CPU 資源,誰搶到資源誰就運行,這也造就了多線程的隨機性。下面我們將看到更生動的例子。

Java 程序的進程(Java 的一個程序運行在系統中)里至少包含主線程和垃圾回收線程(後台線程),你可以簡單的這樣認為,但實際上有四個線程(了解就好):

  • [1] main——main 線程,用戶程序入口
  • [2] Reference Handler——清除 Reference 的線程
  • [3] Finalizer——調用對象 finalize 方法的線程
  • [4] Signal Dispatcher——分發處理髮送給 JVM 信號的線程

多線程和單線程的區別和聯繫?

  1. 單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。

  2. 多線程會存在線程上下文切換,會導致程序執行速度變慢,即採用一個擁有兩個線程的進程執行所需要的時間比一個線程的進程執行兩次所需要的時間要多一些。

結論:即採用多線程不會提高程序的執行速度,反而會降低速度,但是對於用戶來說,可以減少用戶的響應時間。

多線程的優勢

儘管面臨很多挑戰,多線程有一些優點仍然使得它一直被使用,而這些優點我們應該了解。

優勢一:資源利用率更好

想象一下,一個應用程序需要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要 5 秒,處理一個文件需要 2 秒。處理兩個文件則需要:

1| 5秒讀取文件A
2| 2秒處理文件A
3| 5秒讀取文件B
4| 2秒處理文件B
5| ---------------------
6| 總共需要14秒

從磁盤中讀取文件的時候,大部分的 CPU 時間用於等待磁盤去讀取數據。在這段時間里,CPU 非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用 CPU 資源。看下面的順序:

1| 5秒讀取文件A
2| 5秒讀取文件B + 2秒處理文件A
3| 2秒處理文件B
4| ---------------------
5| 總共需要12秒

CPU 等待第一個文件被讀取完。然後開始讀取第二個文件。當第二文件在被讀取的時候,CPU 會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU 大部分時間是空閑的。

總的說來,CPU 能夠在等待 IO 的時候做一些其他的事情。這個不一定就是磁盤 IO。它也可以是網絡的 IO,或者用戶輸入。通常情況下,網絡和磁盤的 IO 比 CPU 和內存的 IO 慢的多。

優勢二:程序設計在某些情況下更簡單

在單線程應用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態。相反,你可以啟動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用 CPU 去處理已經讀取完的文件。其結果就是,磁盤總是在繁忙地讀取不同的文件到內存中。這會帶來磁盤和 CPU 利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現。

優勢三:程序響應更快

有時我們會編寫一些較為複雜的代碼(這裏的複雜不是說複雜的算法,而是複雜的業務邏輯),例如,一筆訂單的創建,它包括插入訂單數據、生成訂單趕快找、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這麼多業務操作,如何能夠讓其更快地完成呢?

在上面的場景中,可以使用多線程技術,即將數據一致性不強的操作派發給其他線程處理(也可以使用消息隊列),如生成訂單快照、發送郵件等。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間,提升了用戶體驗。

其他優勢

多線程還有一些優勢也顯而易見:

  • 進程之前不能共享內存,而線程之間共享內存(堆內存)則很簡單。
  • 系統創建進程時需要為該進程重新分配系統資源,創建線程則代價小很多,因此實現多任務併發時,多線程效率更高.
  • Java 語言本身內置多線程功能的支持,而不是單純地作為底層系統的調度方式,從而簡化了多線程編程.

上下文切換

即使是單核處理器也支持多線程執行代碼,CPU 通過給每個線程分配 CPU 時間片來實現這個機制。時間片是 CPU 分配給各個線程的時間,因為時間片非常短,所以 CPU 通過不停地切換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。

CPU 通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務的時候,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,於是打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書獨到了多少頁的多少行,等查完單詞之後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。

二、創建線程的兩種方式

繼承 Thread 類

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread extends Thread {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式1:繼承 Thread 類
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                PlayMusicThread musicThread = new PlayMusicThread();
                musicThread.start();
            }
        }
    }
}

運行結果發現打遊戲和播放音樂交替出現,說明已經成功了。

實現 Runnable 接口

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread implements Runnable {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式2:實現 Runnable 方法
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                Thread musicThread = new Thread(new PlayMusicThread());
                musicThread.start();
            }
        }
    }
}

也能完成效果。

以上就是傳統的兩種創建線程的方式,事實上還有第三種,我們後邊再講。

多線程一定快嗎?

先來一段代碼,通過并行和串行來分別執行累加操作,分析:下面的代碼併發執行一定比串行執行快嗎?

import org.springframework.util.StopWatch;

// 比較并行和串行執行累加操作的速度
public class Tester {

    // 執行次數
    private static final long COUNT = 100000000;
    private static final StopWatch TIMER = new StopWatch();

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
        // 打印比較測試結果
        System.out.println(TIMER.prettyPrint());
    }

    private static void serial() {
        TIMER.start("串行執行" + COUNT + "條數據");

        int a = 0;
        for (long i = 0; i < COUNT; i++) {
            a += 5;
        }
        // 串行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }

        TIMER.stop();
    }

    private static void concurrency() throws InterruptedException {
        TIMER.start("并行執行" + COUNT + "條數據");

        // 通過匿名內部類來創建線程
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a += 5;
            }
        });
        thread.start();

        // 并行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }
        // 等待線程結束
        thread.join();
        TIMER.stop();
    }
}

大家可以自己測試一下,每一台機器 CPU 不同測試結果可能也會不同,之前在 WINDOWS 本兒上測試的時候,多線程的優勢從 1 千萬數據的時候才開始體現出來,但是現在換了 MAC,1 億條數據時間也差不多,到 10 億的時候明顯串行就比并行快了… 總之,為什麼併發執行的速度會比串行慢呢?就是因為線程有創建和上下文切換的開銷。

繼承 Thread 類還是實現 Runnable 接口?

想象一個這樣的例子:給出一共 50 個蘋果,讓三個同學一起來吃,並且給蘋果編上號碼,讓他們吃的時候順便要說出蘋果的編號:

運行結果可以看到,使用繼承方式實現,每一個線程都吃了 50 個蘋果。這樣的結果顯而易見:是因為顯式地創建了三個不同的 Person 對象,而每個對象在堆空間中有獨立的區域來保存定義好的 50 個蘋果。

而使用實現方式則滿足要求,這是因為三個線程共享了同一個 Apple 對象,而對象中的 num 數量是一定的。

所以可以簡單總結出繼承方式和實現方式的區別:

繼承方式:

  1. Java 中類是單繼承的,如果繼承了 Thread 了,該類就不能再有其他的直接父類了;
  2. 從操作上分析,繼承方式更簡單,獲取線程名字也簡單..(操作上,更簡單)
  3. 從多線程共享同一個資源上分析,繼承方式不能做到…

實現方式:

  1. Java 中類可以實現多個接口,此時該類還可以繼承其他類,並且還可以實現其他接口(設計上,更優雅)..
  2. 從操作上分析,實現方式稍微複雜點,獲取線程名字也比較複雜,需要使用 Thread.currentThread() 來獲取當前線程的引用..
  3. 從多線程共享同一個資源上分析,實現方式可以做到..

在這裏,三個同學完成搶蘋果的例子,使用實現方式才是更合理的方式。

對於這兩種方式哪種好並沒有一個確定的答案,它們都能滿足要求。就我個人意見,我更傾向於實現 Runnable 接口這種方法。因為線程池可以有效的管理實現了 Runnable 接口的線程,如果線程池滿了,新的線程就會排隊等候執行,直到線程池空閑出來為止。而如果線程是通過實現 Thread 子類實現的,這將會複雜一些。

有時我們要同時融合實現 Runnable 接口和 Thread 子類兩種方式。例如,實現了 Thread 子類的實例可以執行多個實現了 Runnable 接口的線程。一個典型的應用就是線程池。

常見錯誤:調用 run() 方法而非 start() 方法

創建並運行一個線程所犯的常見錯誤是調用線程的 run() 方法而非 start() 方法,如下所示:

1| Thread newThread = new Thread(MyRunnable());
2| newThread.run();  //should be start();

起初你並不會感覺到有什麼不妥,因為 run() 方法的確如你所願的被調用了。但是,事實上,run() 方法並非是由剛創建的新線程所執行的,而是被創建新線程的當前線程所執行了。也就是被執行上面兩行代碼的線程所執行的。想要讓創建的新線程執行 run() 方法,必須調用新線程的 start() 方法。

三、線程的安全問題

吃蘋果遊戲的不安全問題

我們來考慮一下上面吃蘋果的例子,會有什麼問題?

儘管,Java 並不保證線程的順序執行,具有隨機性,但吃蘋果比賽的案例運行多次也並沒有發現什麼太大的問題。這並不是因為程序沒有問題,而只是問題出現的不夠明顯,為了讓問題更加明顯,我們使用 Thread.sleep() 方法(經常用來模擬網絡延遲)來讓線程休息 10 ms,讓其他線程去搶資源。(注意:在程序中並不是使用 Thread.sleep(10)之後,程序才出現問題,而是使用之後,問題更明顯.)

為什麼會出現這樣的錯誤呢?

先來分析第一種錯誤:為什麼會吃重複的蘋果呢?就拿 B 和 C 都吃了編號為 47 的蘋果為例吧:

  • A 線程拿到了編號為 48 的蘋果,打印輸出然後讓 num 減 1,睡眠 10 ms,此時 num 為 47。
  • 這時 B 和 C 同時都拿到了編號為 47 的蘋果,打印輸出,在其中一個線程作出了減一操作的時候,A 線程從睡眠中醒過來,拿到了編號為 46 的蘋果,然後輸出。在這期間並沒有任何操作不允許 B 和 C 線程不能拿到同一個編號的蘋果,之前沒有明顯的錯誤僅僅可能只是因為運行速度太快了。

再來分析第二種錯誤:照理來說只應該存在 1-50 編號的蘋果,可是 0 和-1 是怎麼出現的呢?

  • 當 num = 1 的時候,A,B,C 三個線程同時進入了 try 語句進行睡眠。
  • C 線程先醒過來,輸出了編號為 1 的蘋果,然後讓 num 減一,當 C 線程醒過來的時候發現 num 為 0 了。
  • A 線程醒過來一看,0 都沒有了,只有 -1 了。

歸根結底是因為沒有任何操作來限制線程來獲取相同的資源並對他們進行操作,這就造成了線程安全性問題。

如果我們把打印和減一的操作分成兩個步驟,會更加明顯:

ABC 三個線程同時打印了 50 的蘋果,然後同時做出減一操作。

像這樣的原子操作,是不允許分步驟進行的,必須保證同步進行,不然可能會引發不可設想的後果。

要解決上述多線程併發訪問一個資源的安全性問題,就需要引入線程同步的概念。

線程同步

多個執行線程共享一個資源的情景,是最常見的併發編程情景之一。為了解決訪問共享資源錯誤或數據不一致的問題,人們引入了臨界區的概念:用以訪問共享資源的代碼塊,這個代碼塊在同一時間內只允許一個線程執行。

為了幫助編程人員實現這個臨界區,Java(以及大多數編程語言)提供了同步機制,當一個線程試圖訪問一個臨界區時,它將使用一種同步機制來查看是不是已經有其他線程進入臨界區。如果沒有其他線程進入臨界區,他就可以進入臨界區。如果已經有線程進入了臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。如果在等待進入臨界區的線程不止一個,JVM 會選擇其中的一個,其餘的將繼續等待。

synchronized 關鍵字

如果一個對象已用 synchronized 關鍵字聲明,那麼只有一個執行線程被允許訪問它。使用 synchronized 的好處顯而易見:保證了多線程併發訪問時的同步操作,避免線程的安全性問題。但是壞處是:使用 synchronized 的方法/代碼塊的性能比不用要低一些。所以好的做法是:盡量減小 synchronized 的作用域。

我們還是先來解決吃蘋果的問題,考慮一下 synchronized 關鍵字應該加在哪裡呢?

發現如果還再把 synchronized 關鍵字加在 if 裏面的話,0 和 -1 又會出來了。這其實是因為當 ABC 同是進入到 if 語句中,等待臨界區釋放的時,拿到 1 編號的線程已經又把 num 減一操作了,而此時最後一個等待臨界區的進程拿到的就會是 -1 了。

同步鎖 Lock

Lock 機制提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作,同步代碼塊/ 同步方法具有的功能 Lock 都有,除此之外更強大,更體現面向對象。在併發包的類族中,Lock 是 JUC 包的頂層接口,它的實現邏輯並未用到 synchronized,而是利用了 volatile 的可見性。

使用 Lock 最典型的代碼如下:

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();
        try {
            // ..... method body
        } finally {
            lock.unlock();
        }
    }
}

線程安全問題

線程安全問題只在多線程環境下才會出現,單線程串行執行不存在此類問題。保證高併發場景下的線程安全,可以從以下四個維度考量:

維度一:數據單線程可見

單線程總是安全的。通過限制數據僅在單線程內可見,可以避免數據被其他線程篡改。最典型的就是線程局部變量,它存儲在獨立虛擬機棧幀的局部變量表中,與其他線程毫無瓜葛。TreadLocal 就是採用這種方式來實現線程安全的。

維度二:只讀對象

只讀對象總是安全的。它的特性是允許複製、拒絕寫入。最典型的只讀對象有 String、Integer 等。一個對象想要拒絕任何寫入,必須要滿足以下條件:

  • 使用 final 關鍵字修飾類,避免被繼承;
  • 使用 private final 關鍵字避免屬性被中途修改;
  • 沒有任何更新方法;
  • 返回值不能為可變對象。

維度三:線程安全類

某些線程安全類的內部有非常明確的線程安全機制。比如 StringBuffer 就是一個線程安全類,它採用 synchronized 關鍵字來修飾相關方法。

維度四:同步與鎖機制

如果想要對某個對象進行併發更新操作,但又不屬於上述三類,需要開發工程師在代碼中實現安全的同步機制。雖然這個機制支持的併發場景很有價值,但非常複雜且容易出現問題。

處理線程安全的核心理念

要麼只讀,要麼加鎖。

合理利用好 JDK 提供的併發包,往往能化腐朽為神奇。Java 併發包(java.util.concurrent,JUC)中大多數類註釋都寫有:@author Doug Lea。如果說 Java 是一本史書,那麼 Doug Lea 絕對是開疆拓土的偉大人物。Doug Lea 在當大學老師時,專攻併發編程和併發數據結構設計,主導設計了 JUC 併發包,提高了 Java 併發編程的易用性,大大推進了 Java 的商用進程。

參考資料

  • 《Java 零基礎入門教程》 –
  • 《Java 併發編程的藝術》
  • 《Java 7 併發編程實戰手冊》
  • 《碼出高效 Java 開發手冊》 – 楊冠寶(孤盡) 高海慧(鳴莎)著

按照慣例黏一個尾巴:

歡迎轉載,轉載請註明出處!
獨立域名博客:wmyskxz.com
簡書 ID:
github:
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加 qq 群:3382693

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

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

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

【前端知識體系-JS相關】10分鐘搞定JavaScript正則表達式高頻考點

1.正則表達式基礎

1.1 創建正則表達式

1.1.1 使用一個正則表達式字面量

const regex = /^[a-zA-Z]+[0-9]*\W?_$/gi;

1.1.2 調用RegExp對象的構造函數

const regex = new RegExp(pattern, [, flags])

1.1.3 特殊字符

 - ^ 匹配輸入的開始
 - $ 匹配輸入的結束
 - \* 0次或多次  {0,}
 - \+ 1次或多次  {1,}
 - ?
   - 0次或者1次 {0,1}。
   - 用於先行斷言
   - 如果緊跟在任何量詞 *、 +、? 或 {} 的後面,將會使量詞變為非貪婪
     - 對 "123abc" 用 /\d+/ 將會返回 "123",
     - 用 /\d+?/,那麼就只會匹配到 "1"。
 - . 匹配除換行符之外的任何單個字符
 - (x)  匹配 'x' 並且記住匹配項
 - (?:x)  匹配 'x' 但是不記住匹配項
 - x(?=y)  配'x'僅僅當'x'後面跟着'y'.這種叫做正向肯定查找。
 - x(?!y)  匹配'x'僅僅當'x'後面不跟着'y',這個叫做正向否定查找。
 - x|y  匹配‘x’或者‘y’。
 - {n}  重複n次
 - {n, m}  匹配至少n次,最多m次
 - [xyz]   代表 x 或 y 或 z
 - [^xyz]  不是 x 或 y 或 z
 - \d  数字
 - \D  非数字
 - \s  空白字符,包括空格、製表符、換頁符和換行符。
 - \S  非空白字符
 - \w  單詞字符(字母、数字或者下劃線)  [A-Za-z0-9_]
 - \W  非單字字符。[^A-Za-z0-9_]
 - \3  表示第三個分組
 - \b   詞的邊界
   - /\bm/匹配“moon”中得‘m’;
 - \B   非單詞邊界

1.2 使用正則表達式的方法

  • exec 一個在字符串中執行查找匹配的RegExp方法,它返回一個數組(未匹配到則返回null)。
  • test 一個在字符串中測試是否匹配的RegExp方法,它返回true或false。
  • match 一個在字符串中執行查找匹配的String方法,它返回一個數組或者在未匹配到時返回null。
  • search 一個在字符串中測試匹配的String方法,它返回匹配到的位置索引,或者在失敗時返回-1。
  • replace 一個在字符串中執行查找匹配的String方法,並且使用替換字符串替換掉匹配到的子字符串。
  • split 一個使用正則表達式或者一個固定字符串分隔一個字符串,並將分隔后的子字符串存儲到數組中的String方法。

1.2.1 正則對象的三個方法

        //①test()判斷字符串中是否出現某個字符串,返回布爾值
        var re = /abc/;
        var str = '00abc66';
        console.log(re.test(str));  // true
        //②exec()查找並返回字符串中指定的某個字符串,只匹配一次
        var re = /abc/;
        var str = 'a0bc88abc00abc';
        console.log(re.exec(str));  // ["abc", index: 6, input: "a0bc88abc00abc", groups: undefined]
        //③compile()方法用於改變正則匹配的內容
        var re = /ab/;
        var str = "aabcdef";
        console.log(re.test(str));  //true
        re.compile(/bd/);
        console.log(re.test(str));  //false
        re.compile('66');
        console.log(re.test(str));  //false

1.2.2 字符串中與正則相關的方法

        //①search()方法,返回符合條件的字符串首次出現的位置(下標)
        var re = /abc/;
        var str = '00abc66';
        console.log(str.search(re));        // 2
        //②match()方法,返回查找的結果,如果查詢不到返回NULL
        console.log(str.match(re));         // ["abc", index: 2, input: "00abc66", groups: undefined]
        //③replace()方法,將匹配到的內容替換成指定內容
        console.log(str.replace(re, "*"));
        //④split()方法,將字符串分割成字符串數組
        console.log(str.split(re));

1.3 正則表達式子表達式相關

1.3.1 子表達式

在正則表達式中,通過一對圓括號括起來的內容,我們就稱之為“子表達式”。如:var re = /\d(\d)\d/;

1.3.2 捕獲

在正則表達式中,子表達式匹配到相應的內容時,系統會自動捕獲這個行為,然後將子表達式匹配到的內容放入系統的緩存區中。我們把這個過程就稱之為“捕獲”。

1.3.3 反向引用

在正則表達式中,我們可以使用\n(n>0,正整數,代表系統中的緩衝區編號)來獲取緩衝區中的內容,我們把這個過程就稱之為“反向引用”。

    var str = "d1122jj7667h6868s9999";
    //查找AABB型的数字
    console.log(str.match(/(\d)\1(\d)\2/)); //1122
    //查找ABBA型的数字
    console.log(str.match(/(\d)(\d)\2\1/)); //7667
    //查找ABAB型的数字
    console.log(str.match(/(\d)(\d)\1\2/)); //6868
    //查找四個連續相同的数字
    console.log(str.match(/(\d)\1\1\1/));   //9999

1.4 限定符

[!NOTE]
限定符可以指定正則表達式的一個給定字符必須要出現多少次才能滿足匹配。

    *:匹配前面的子表達式零次或多次,0到多
    +:匹配前面的子表達式一次或多次,1到多
    ?:匹配前面的子表達式零次或一次,0或1
    {n}:匹配確定的 n 次 
    {n,}:至少匹配 n 次 
    {n,m}:最少匹配 n 次且最多匹配 m 次

[!WARNING]
注意:針對於{n,m},正則在匹配到一個符合多種次數的字符串時,優先匹配次數多的,即能匹配到m次就不會匹配n次,這就是貪婪模式(默認)。

如果在其後加?即{n,m}?則會更改為非貪婪模式(惰性模式),則此時正則優先匹配n次。

    var str = "aa1a22a333a6a8a";
    console.log(str.match(/a\d*/));      //a
    console.log(str.match(/a\d+/));      //a1
    console.log(str.match(/a\d?/));      //a
    console.log(str.match(/a\d{3}/));    //a333
    console.log(str.match(/a\d{2,}/));   //a22
    console.log(str.match(/a\d{1,3}/));  //a1
    console.log(str.match(/a\d{1,3}?/)); //a1

    //貪婪模式加深理解,案例如下:
    //貪婪模式下最開始的'a2就符合條件',但是它會返回'a22'
    //注意:它是在遇到一個同時符合多個次數條件的字符串時,取符合次數多字符串
    var str = "a22aa1a333a6a8a";
    console.log(str.match(/a\d{1,3}/));   //a22
    console.log(str.match(/a\d{1,3}/g));  //a22 a1 a333 a6 a8
    console.log(str.match(/a\d{1,3}?/));  //a2
    console.log(str.match(/a\d{1,3}?/g)); //a2 a1 a3 a6 a8

1.5 定位符

[!NOTE]
定位符可以將一個正則表達式固定在一行的開始或結束。也可以創建只在單詞內或只在單詞的開始或結尾處出現的正則表達式。

^ (脫字符):匹配輸入字符串的開始位置
$:匹配輸入字符串的結束位置
\b:匹配一個單詞邊界
\B:匹配非單詞邊界

1.6 正則表達式的匹配模式(修飾符)

[!NOTE]
表示正則匹配的附加規則,放在正則模式的最尾部。修飾符可以單個使用,也可以多個一起使用。

  • ①g全局匹配,找到所有匹配,而不是在第一個匹配后停止
  • ②i匹配全部大小寫
  • ③m多行,將開始和結束字符(^和$)視為在多行上工作(也就是,分別匹配每一行的開始和結束(由\n或\r分割),而不只是只匹配整個輸入字符串的最開始和最末尾處。
  • ④s與m相反,單行匹配
    var re = /^[a-z]/gim;   //可組合使用

1.7 轉義字符

[!NOTE]
因為在正則表達式中 . +  等屬於表達式的一部分,但有時也需要匹配這些特殊字符,所以,需要使用反斜杠對特殊字符進行轉義。

  需要轉義的字符:
  點號.
  小括號()
  中括號[]
  左斜杠/
  右斜杠\
  選擇匹配符|
  
  * 
  ?
  {}
  + 
  $
  ^

2. 正則練習題

2.1 匹配結尾的数字

/\d+$/g

2.2 統計空格個數

字符串內如有空格,但是空格的數量可能不一致,通過正則將空格的個數統一變為一個。

let reg = /\s+/g
str.replace(reg, " ");

2.3 判斷字符串是不是由数字組成

str.test(/^\d+$/);

2.4 電話號碼正則

  • 區號必填為3-4位的数字
  • 區號之後用“-”與電話號碼連接電話號碼為7-8位的数字
  • 分機號碼為3-4位的数字,非必填,但若填寫則以“-”與電話號碼相連接
/^\d{3,4}-\d{7,8}(-\d{3,4})?$/

2.5 手機號碼正則表達式

正則驗證手機號,忽略前面的0,支持130-139,150-159。忽略前面0之後判斷它是11位的。

/^0*1(3|5)\d{9}$/

2.6 使用正則表達式實現刪除字符串中的空格

funtion trim(str) {
  let reg = /^\s+|\s+$/g
  return str.replace(reg, '');
}

2.7 限制文本框只能輸入数字和兩位小數點等等

/^\d*\.\d{0,2}$/

2.8 只能輸入小寫的英文字母和小數點,和冒號,正反斜杠(:./)

/^[a-z\.:\/\\]*$/

2.9 替換小數點前內容為指定內容

例如:infomarket.php?id=197 替換為 test.php?id=197

var reg = /^[^\.]+/;
var target = '---------';
str = str.replace(reg, target)

2.10 只匹配中文的正則表達式

/[\u4E00-\u9FA5\uf900-\ufa2d]/ig

2.11 返回字符串的中文字符個數

先去掉非中文字符,再返回length屬性。

function cLength(str){
  var reg = /[^\u4E00-\u9FA5\uf900-\ufa2d]/g;
  //匹配非中文的正則表達式
  var temp = str.replace(reg,'');
  return temp.length;
}

2.12 正則表達式取得匹配IP地址前三段

只要匹配掉最後一段並且替換為空字符串就行了

function getPreThrstr(str) {
  let reg = /\.\d{1,3}$/;
  return str.replace(reg,'');
}

2.13 匹配ul標籤之間的內容

/<ul>[\s\S]+?</ul>/i

2.14 用正則表達式獲得文件名

c:\images\tupian\006.jpg

可能是直接在盤符根目錄下,也可能在好幾層目錄下,要求替換到只剩文件名。
首先匹配非左右斜線字符0或多個,然後是左右斜線一個或者多個。

function getFileName(str){
  var reg = /[^\\\/]*[\\\/]+/g;
  // xxx\ 或是 xxx/
  str = str.replace(reg,'');
  return str;
}

2.15 絕對路徑變相對路徑

“http://23.123.22.12/image/somepic.gif”轉換為:”/image/somepic.gif”

var reg = /http:\/\/[^\/]+/;
str = str.replace(reg,"");

2.16 用戶名正則

用於用戶名註冊,,用戶名只 能用 中文、英文、数字、下劃線、4-16個字符。

/^[\u4E00-\u9FA5\uf900-\ufa2d\w]{4,16}$/

2.17 匹配英文地址

規則如下:
包含 “點”, “字母”,”空格”,”逗號”,”数字”,但開頭和結尾不能是除字母外任何字符。

/^[a-zA-Z][\.a-zA-Z,0-9]*[a-zA-Z]$/

2.18 正則匹配價格

開頭数字若干位,可能有一個小數點,小數點後面可以有兩位数字。

/^\d+(\.\d{2})?$/

2.19 身份證號碼的匹配

身份證號碼可以是15位或者是18位,其中最後一位可以是X。其它全是数字

/^(\d{14}|\d{17})(X|x)$/

2.20 單詞首字母大寫

每單詞首字大寫,其他小寫。如blue idea轉換為Blue Idea,BLUE IDEA也轉換為Blue Idea

function firstCharUpper(str) {
  str = str.toLowerCase();
  let reg = /\b(\w)/g;
  return str.replace(reg, m => m.toUpperCase());
}

2.21 正則驗證日期格式

yyyy-mm-dd格式, 4位数字,橫線,1或者2位数字,再橫線,最後又是1或者2位数字。

/^\d{4}-\d{1,2}-\d{1,2}$/

2.22 去掉文件的後綴名

www.abc.com/dc/fda.asp 變為 www.abc.com/dc/fda

function removeExp(str) {
  return str.replace(/\.\w$/,'')
}

2.23 驗證郵箱的正則表達式

開始必須是一個或者多個單詞字符或者是-,加上@,然後又是一個或者多個單詞字符或者是-。然後是點“.”和單詞字符和-的組合,可以有一個或者多個組合。

/^[\w-]+@\w+\.\w+$/

2.24 正則判斷標籤是否閉合

標籤可能有兩種方式閉合,自閉和或者對稱閉合的方式。

/<([a-z]+)(\s*\w*?\s*=\s*".+?")*(\s*?>[\s\S]*?(<\/\1>)+|\s*\/>)/i

2.25 正則判斷是否為数字與字母的混合

不能小於12位,且必須為字母和数字的混合

/^(([a-z]+[0-9]+)|([0-9]+[a-z]+))[a-z0-9]*$/i

2.26 將阿拉伯数字替換為中文大寫形式

function replaceReg(reg,str){
  let arr=["零","壹","貳","叄","肆","伍","陸","柒","捌","玖"];
  let reg = /\d/g;
  return str.replace(reg,function(m){return arr[m];})
}

2.27 去掉標籤的所有屬性

<td style="width: 23px; height: 26px;" align="left">***</td>
變成沒有任何屬性的
<td>***</td>

思路:非捕獲匹配屬性,捕獲匹配標籤,使用捕獲結果替換掉字符串。正則如下:

/(<td)\s(?:\s*\w*?\s*=\s*".+?")*?\s*?(>)/

2.28 駝峰表示

String.prototype.camelCase = function () {
        // .*?是非貪婪的匹配,點可以匹配任意字符,星號是前邊的字符有0-n個均匹配,問號是則是0-1;
        // (^\w{1}): 用於匹配第一個首字母
        // (.*):用於匹配任意個的前面的字符,.表示的就是任意字符

        // - param 1: 匹配到的字符串
        // - param 2: 匹配的的子字符串
        // - param 3: 匹配的子字符串
        // - param的位置
        // - param 5: 原始字符串 4: 匹配到的字符串在字符串中

        return this.replace(/(^\w{1})(.*)/g, function (match, g1, g2) {
            return g1.toUpperCase() + g2.toLowerCase();
        });
    }

2.29 模板字符串

// str = 'name: @(name), age:@(age)'
       // data = {name : 'xiugang', age : 18}
       /**
        * 實現一個簡單的數據綁定
        * @param str
        * @param data
        * @return {*}
        */
       String.prototype.formateString = function (data) {
           return this.replace(/@\((\w+)\)/g, function (match, key) {
               // 注意這裏找到的值必須返回出去(如果是undefined,就是沒有數據)
               // 注意:判斷一個值的類型是不是undefined,可以通過typeof判斷
               console.log(typeof data[key] === 'undefined');
               return data[key] === 'undefined' ? '' : data[key];
           });

       }

2.30 去掉兩邊的空格

/**
        * 去掉兩邊的空格
        * @param str
        * @return {*}
        */
       String.prototype.trim = function () {
           return this.replace(/(^\s*)|(\s*$)/g, '');
       }

2.31 獲取url參數: 使用replace保存到一個數組裡面,然後從數組裡面取出數據

'http://www.189dg.com/ajax/sms_query.ashx?undefined&undefined&undefined-06-27&undefined-06-27'
 url.replace(/(\w+)=(\w+)/g, function(a, b, c){
   console.log(a, b, c)
 })
action=smsdetail action smsdetail
sid=22 sid 22
stime=2014 stime 2014
etime=2014 etime 2014


// 封裝為一個函數
var url = "http://127.0.0.1/e/action/ShowInfo.php?classid=9&id=2";
function parse_url(_url){
 var pattern = /(\w+)=(\w+)/ig;
 var parames = {};
 url.replace(pattern, function(a, b, c){
   parames[b] = c;
 });
 return parames;
}
var parames = parse_url(url);
alert(parames['classid'] + ", " + parames['id']);

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

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

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

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

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

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

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

SpringSecurity動態加載用戶角色權限實現登錄及鑒權

很多人覺得Spring Security實現登錄驗證很難,我最開始學習的時候也這樣覺得。因為我好久都沒看懂我該怎麼樣將自己寫的用於接收用戶名密碼的Controller與Spring Security結合使用,這是一個先入為主的誤區。後來我搞懂了:根本不用你自己去寫Controller。你只需要告訴Spring Security用戶信息、角色信息、權限信息、登錄頁是什麼?登陸成功頁是什麼?或者其他有關登錄的一切信息。具體的登錄驗證邏輯它來幫你實現。

一、動態數據登錄驗證的基礎知識

在本號之前的文章中,已經介紹了Spring Security的formLogin登錄認證模式,RBAC的權限控制管理模型,並且針對Spring Security的登錄認證邏輯源碼進行了解析等等。我們所有的用戶、角色、權限信息都是在配置文件裏面寫死的,然而在實際的業務系統中,這些信息通常是存放在RBAC權限模型的數據庫表中的。下面我們來回顧一下其中的核心概念:

  • RBAC的權限模型可以從用戶獲取為用戶分配的一個或多個角色,從用戶的角色又可以獲取該角色的多種權限。通過關聯查詢可以獲取某個用戶的角色信息和權限信息。
  • 在源碼解析的文章中,我們知道如果我們不希望用戶、角色、權限信息寫死在配置裏面。我們應該實現UserDetails與UserDetailsService接口,從而從數據庫或者其他的存儲上動態的加載這些信息。

以上是對一些核心的基礎知識的總結,如果您對這些知識還不是很清晰,建議您先往下讀本文。如果看完本文仍然理解困難,建議您翻看本號之前的文章。

二、UserDetails與UserDetailsService接口

  • UserDetailsService接口有一個方法叫做loadUserByUsername,我們實現動態加載用戶、角色、權限信息就是通過實現該方法。函數見名知義:通過用戶名加載用戶。該方法的返回值就是UserDetails。
  • UserDetails就是用戶信息,即:用戶名、密碼、該用戶所具有的權限。

下面我們來看一下UserDetails接口都有哪些方法。

public interface UserDetails extends Serializable {
    //獲取用戶的權限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    //獲取密碼
    String getPassword();

    //獲取用戶名
    String getUsername();

    //賬號是否沒過期
    boolean isAccountNonExpired();

    //賬號是否沒被鎖定
    boolean isAccountNonLocked();

    //密碼是否沒過期
    boolean isCredentialsNonExpired();

    //賬戶是否可用
    boolean isEnabled();
}

現在,我們明白了,只要我們把這些信息提供給Spring Security,Spring Security就知道怎麼做登錄驗證了,根本不需要我們自己寫Controller實現登錄驗證邏輯。

三、實現UserDetails 接口

public class SysUser implements UserDetails{
    
    String password();  //密碼
    String username();  //用戶名
    boolean accountNonExpired;   //是否沒過期
    boolean accountNonLocked;   //是否沒被鎖定
    boolean credentialsNonExpired;  //是否沒過期
    boolean enabled;  //賬號是否可用
    Collection<? extends GrantedAuthority> authorities;  //用戶的權限集合

    //省略構造方法
    //省略set方法
    //省略get方法(即接口UserDetails的方法)
}

我們就是寫了一個適應於UserDetails的java POJO類,所謂的 UserDetails接口實現就是一些get方法。get方法由Spring Security調用,我們通過set方法或構造函數為 Spring Security提供UserDetails數據。

四、實現UserDetailsService接口

@Component
public class MyUserDetailsService implements UserDetailsService{

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            
       //這裏從數據庫sys_user表裡面查詢實體類對象。loadUser方法可使用Mybatis或JDBC或JPA自行實現。
       SysUser sysUser =  loadUser(username);   

        // 判斷用戶是否存在 
       if(user == null)  {  throw  new  UsernameNotFoundException("用戶名不存在");  }

       //從數據庫該用戶所有的角色信息,所有的權限標誌
       //遍歷所有的ROLE角色及所有的Authority權限(菜單、按鈕)。
       //用逗號分隔他們的唯一標誌,具體過程自行實現。
       sysUser.setAuthorities(
               AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_AMIN , system:user:delete"));
        
        //sysUser.setAccountNonLocked(true或false);
        return sysUser;
    }
}
  • 通常數據庫表sys_user字段要和SysUser屬性一一對應,比如username、password、enabled。但是比如accountNonLocked字段用於登錄多次錯誤鎖定,但我們一般不會在表裡存是否鎖定,而是存一個鎖定時間字段。通過鎖定時間是否大於當前時間判斷賬號是否鎖定,所以實現過程中可以靈活做判斷並用好set方法,不必拘泥於一一對應的形式。
  • 角色是一種特殊的權限,在Spring Security我們可以使用hasRole(角色標識)表達式判斷用戶是否具有某個角色,決定他是否可以做某個操作;通過hasAuthority(權限標識)表達式判斷是否具有某個操作權限。

五、最後說明

至此,我們將系統裏面的所有的用戶、角色、權限信息都通過UserDetailsService和UserDetails告知了Spring Security。但是多數朋友可能仍然不知道該怎樣實現登錄的功能,其實剩下的事情很簡單了:

  • 寫一個登錄界面,寫一個登錄表單,表單使用post方法提交到默認的/login路徑
  • 表單的用戶名、密碼字段名稱默認是username、password。
  • 寫一個登錄成功之後的跳轉頁面,比如index.html

然後把這些信息通過配置方式告知Spring Security ,以上的配置信息名稱都可以靈活修改。如果您不知道如何配置請參考本號之前的文章《formLogin登錄認證模式》。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

特斯拉 Cybertruck 的 6 個設計原則與 5 個未公開的疑問

特斯拉上週發表了最新車種 Cybertruck,破格的外型與性能讓全球傻眼,本篇文章將告訴你 Cybertruck 設計成這樣的 6 個理由,以及 5 個它尚未說清楚的疑點。

老派科幻電影的線條、全車冷鑄不鏽鋼、跑得比保時捷還快、拉得比福特還給力,特斯拉 Cybertruck 一出場就奪走全世界目光,如同特斯拉執行長馬斯克(Elon Musk)說的,他要改變皮卡車一成不變的外觀。改變歸改變,改成這樣確實是有點誇張了,但 Cybertruck 在設計時,是遵循著以下 6 個原則,才會長成這樣。

  1. 原則一:強悍外表
  2. 原則二:將低風阻,提高效率
  3. 原則三:內在舒適大空間
  4. 原則四:適應不同路面駕駛
  5. 原則五:大載貨量與拉力
  6. 原則六:大電池容量

這些原則(或說是目標),部分來自於馬斯克個人的期待,一部分來自於市場調查的回饋,相信收到這個提案要求的專案,當下應該很想離職。但從發表會的成果來看,他們確實做到了。

為了看起來更強悍,Cybertruck 全車採用 301 不鏽鋼包覆。

皮卡車外表強悍,幾乎是一種固定形象,方方正正的大塊頭是百年傳統。但這樣的外表牴觸了原則二的低風阻要求,同時別忘了,低風阻的跑車,可不會配備原則三的車內大空間。

最終,結合這三項要求的最大公約數,原來就存在於 80 年代的科幻電影跟電玩中,當然這一切若沒有電動馬達是不可能達成的。燃油車的引擎通常配置於車頭,這是為了讓後方留出寬敞的乘坐空間,犧牲了車頭的流線設計,也在傳輸過程中浪費了部分動力。因此追求速度的跑車,通常將車頭壓低,引擎後置,這樣子減低了風阻,也讓動力傳導更直接,但是乘坐空間就非常狹窄,更別提燃油皮卡車後面的大型貨斗了。

Cybertruck 和目前越來越多的電動車一樣,將馬達、電池組整合在底盤,離輪胎更近,也讓上部空間更有彈性,Cybertruck 進一步運用了大眾對皮卡車車身較高的印象,讓它的底盤空間能塞進更多電池,還有其他黑科技,來滿足這些設計原則,像是適應性氣壓懸吊、龐大的電池容量以及可容納六人乘坐的車廂和一個可開閉的貨斗。

從試乘影片中,可以看到後座乘客(身高約 180 公分)在入座後,頭頂上仍然有兩個拳頭左右的空間,算是相當舒適,然而腿部空間似乎就比較侷促,後座寬度看起來和一般房車差不多,塞進兩位大漢剛剛好,整體來說,算是勉強達成了原則三的大空間要求。(官方數據,能容納 6 個成人)

但是,在完成這樣的設計之後,我們會發現,這種外觀線條看起來並不是那麼強悍,因為斜切的車頭讓人聯想到的是跑車,不是皮卡車或悍馬車這種硬漢風格。解決的辦法,就如同我們現在看到的,捨棄一般車輛的鈑金與烤漆,改用與姊妹公司 SpaceX 的太空船同樣材料的 301 不鏽鋼板,而且厚度達到 3 公厘,足以抵擋 9mm 口徑手槍的射擊。代價是,每一扇車門重達 27 公斤,萬一被車門夾到,後果可是不得了。

Cybertruck 內部空間比想像中寬闊,前方只有一塊 17 吋觸控螢幕作為主控台。

5 個等待揭開的疑問

發表會後隔天,馬斯克宣布訂單數量已經突破 14 萬,換句話說,這個發表會已經幫特斯拉賺進了 1,400 萬美元,然而這位愛做夢、愛說大話的老闆,心中也明白最後成交的數量可能不到一半,因為這台科幻裝甲車,仍然有幾個問題需要釐清。

問題一:後照鏡

許多人都發現了,發表會上的 Cybertruck 並沒有裝備車側後照鏡,官網上的影片與照片也都沒有看到,推測是使用攝影機來取代了傳統後照鏡,然而並非所有國家都開放電子後照鏡上路,包含美國跟台灣都還沒開放。在 Cybertruck 試乘的過程中,也沒看到螢幕上有播放左右後側的畫面,只有裝設數位正後方後照鏡,究竟 Cybertruck 怎麼解決車側後照鏡的問題,尚有待解答。

問題二:外觀塗裝

全不鏽鋼車身肯定會吸引到一部分的客群,但是正式販售時會不會提供不同款式的彩繪或是塗裝,並未說明,畢竟許多車主仍然希望自己的愛車能保有一些特殊性,至少花了 7 萬美元購買頂級款的車主,會希望自己的車看起來跟 4 萬美元的有些不同。

而提到外觀就不能不提那一整片乾乾淨淨的車頭,一般車輛會有車頭 Logo、進氣壩等,即使特斯拉其他車款,也都留有進氣孔,或許 Cybertruck 可以不用 Logo 來辨識,但是令人好奇的是它怎麼解決進氣的問題,又或者 Cybertruck 有了不同的方式來處理散熱和車內空氣流通問題

問題三:上下車

一般的皮卡車,車身都較高,對於家有老小的家庭來說,上下車會比較辛苦。從 Cybertruck 的照片中來看,底盤高度約在成年女子的膝蓋,算是有一點點難度。

在發表會上,Cybertruck 展示了它的適應性氣壓懸吊系統,在貨斗載重時,能夠自動調整車身高度;特斯拉官方也表示,當行駛於不同路面時,Cybertruck 會自動調整懸吊,在高速公路時會降低車身,在路面崎嶇時,會提高車身。這讓人不免想知道,當車主或乘客要上下車的時候,車身是否也會自動降低,方便進出呢?

問題四:貨斗操縱方式

市面上的皮卡車,後方貨斗大都是開放式的,也有車主會加裝棚罩,除了保護行李外,最重要是不讓路人亂丟垃圾。Cybertruck 的貨斗是機械式的,在發表會中,可以看到機車騎士走到車尾按了一些隱藏的開關,開啟貨斗,接著再用手拉開尾門,放下斜板。

特斯拉是否會將貨斗開閉設計得更容易使用呢?

對於一般卡車司機來說,這些動作稀鬆平常,但對於標榜高科技的 Cybertruck 來說,似乎不太「性感」,因此我們不免好奇,正式上市後,特斯拉是否會將操作貨斗開閉的功能,放到他們的手機 App 裡,讓車主可以更輕鬆地開關?

問題五:交車時間

最後的問題,也是特斯拉在過去幾年最為人詬病的問題,真的能夠如期交車嗎?特斯拉官網上寫得非常保守,「生產日期接近 2021 年底」,最高規格的三馬達版本,更是標明「預計在 2022 年底開始生產」。這等於是在說,我沒有說什麼時候交車,我只說了什麼時候開始製造,從生產到交車要多久,沒有人知道。

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

延伸閱讀:

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

【其他文章推薦】

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

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

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

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

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

快速搭建 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產品,價格不怕你比較

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