主頁 > 軟體設計 > 開源專案SMSS發開指南(四)——SSL/TLS加密通信詳解(上)

開源專案SMSS發開指南(四)——SSL/TLS加密通信詳解(上)

2020-09-12 18:56:57 軟體設計

本文將詳細介紹如何在Java端、C++端和NodeJs端實作基于SSL/TLS的加密通信,重點分析Java端利用SocketChannel和SSLEngine從握手到資料發送/接收的完整程序,本文也涵蓋了在Ubuntu系統上利用OpenSSL和Libevent如何創建一個支持SSL的服務端,文章中介紹的知識點并未全部在SMSS專案中實作,因此筆者會列出所有相關原始碼以方便讀者查閱,提醒:由于知識點較多,分享涵蓋了多種語言,預計的學習時間可能會大于3小時,為了保證讀者能有良好的學習體驗,繼續前請先安排好時間,如果遇到困難,您也可以根據自己的實際情況有選擇的學習,也歡迎與我交流,

一 相關前置知識

libevent網路庫:libevent是一個用c語言撰寫的高性能支持事件回應的網路庫,編譯libevent前需要確保目標機器上已經完成對openssl的編譯,否則生成的動態庫中可能會缺少呼叫openssl的介面,這里選擇的openssl版本為1.1.1d,如果你選擇1.0以前的版本可能與后面的代碼示例有所不同,

electron桌面應用:electron是一套依賴google的V8引擎直接使用HTML/JS/CSS創建桌面應用的跨平臺解決方案,如果你需要開發輕量化的桌面端應用,electron基本是不二選擇,從個人的實踐來看,無論是開發生態還是開發效率都強于Qt,使用electron可以呼叫nodejs相關介面完成與系統的互動,

Java-nio開發包:基本是現在作為Java中高級開發的必備技能,

javax.net.ssl開發包:屬于Java對SSL/TLS支持的比較底層的開發包,目前在應用中更多會選擇Netty等集成式框架,如果你的專案中需要一些定制化功能可以選擇它作為支持,建議在專案中慎重使用,由于一些特殊原因,Java只提供了SSLSocket物件,底層只支持阻塞式訪問,文章最后會提供一個我個人實作的SSLSocketChannel物件,方便讀者在基礎上進行二次封裝,

SSL/TLS通信:安全通信的目的是在原有的tcp/ip層和應用層之間增加了一個稱之為SSL/TLS的加/解密層來實作的,在網路協議層中的位置大致如下:

在OSI七層網路協議的定義中,它處于表示層,程式開發的方式一般是在完成tcp/ip建立連接后,開始ssl/tls握手,發布ssl的服務端需要具備一個私鑰檔案(.key)以及與私鑰配套的證書檔案(.crt),證書包含了公鑰和對公鑰的簽名,還有一些用來證明源安全的資訊,證書需要到專門的機構申請并且有年費要求,鑒于各位讀者僅用于自學,后面生成的證書我們會做自簽名,ssl/tls握手的目的是在客戶端和服務端之間協商一個安全的對稱秘鑰,用來為本次會話的訊息加解密,由于這對秘鑰僅通信的服務端和客戶端持有,會話結束即消失,

二 libevent和openssl

生成x.509證書

首選在安裝好openssl的機器上創建私鑰檔案:server.key

> openssl genrsa -out server.key 2048

得到私鑰檔案后我們需要一個證書請求檔案:server.csr,將來你可以拿這個證書請求向正規的證書管理機構申請證書

> openssl req -new -key server.key -out server.csr

最后我們生成自簽名的x.509證書(有效期365天):server.crt

> openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

x.509證書是密碼學里公鑰證書的格式標準,被應用在包括ssl/tls等多項場景中,

OpenSSL加密通信介面分析

與ssl/tls通信相關的介面基本可以分為兩大類,SSL_CTX通信背景關系和SSL直接通信介面,下面逐一分析:

  1. SSL_CTX_new:新版本摒棄了一些老的介面,目前建議基本統一使用此方法來創建通信背景關系
  2. SSL_CTX_free:釋放SSL_CTX*
  3. SSL_CTX_use_certificate_file:設定證書檔案
  4. SSL_CTX_use_PrivateKey_file:設定私鑰檔案,與上面的證書檔案必須配套否則檢測不通過
  5. SSL_CTX_check_private_key:檢查私鑰和證書檔案
  6. SSL_new:方法一創建完成的背景關系在通過此方法創建配套的SSL*
  7. SSL_set_fd:與上面創建的SSL和socket_fd系結
  8. SSL_accept:服務端握手方法
  9. SSL_connect:客戶端握手方法
  10. SSL_write:訊息發送,內部會對明文訊息加密并呼叫socket發送
  11. SSL_read:訊息接收,內部會從socket接收到密文資料再解碼成文明回傳
  12. SSL_shutdown:通知對方關閉本次加密會話
  13. SSL_free:釋放SSL*

C++撰寫socket利用openssl介面開發測驗代碼

在熟悉以上基本概念之后,根據測驗先行和敏捷開發的原則,我們接下來就要直接使用c++開發一個socket測驗程式,并利用openssl介面進行加密通信,以下代碼的開發和運行系統為ubuntu 16.04 LTS,openssl版本為1.1.1d 10 Sep 2019,開發工具為Visual Studio Code 1.41.1,

服務端原始碼 server.cpp

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <string>
#include "openssl/ssl.h"
#include "openssl/err.h"

using namespace std;

// 前置申明
struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file);

int main(int argc, char *argv[])
{
    ssl_ctx_st *ssl_ctx = InitSSLServer("../server.crt", "../server.key"); // 引入之前生成好的私鑰檔案和證書檔案
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(10020); // 指定通信埠
    int res = ::bind(sock, (sockaddr *)&sin, sizeof(sin));
    if (res == -1)
    {
        return -1;
    }
    listen(sock, 1); // 開始監聽
    // 只接受一次客戶端的連接
    int client_fd = accept(sock, 0, 0);
    cout << "Client accept success!" << endl;
    ssl_st *ssl = SSL_new(ssl_ctx);
    SSL_set_fd(ssl, client_fd);
    res = SSL_accept(ssl); // 執行SSL層握手
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return -1;
    }
    // 握手完成,接受訊息并發送一次應答
    char buf[1024] = {0};
    int len = SSL_read(ssl, buf, sizeof(buf));
    cout << buf << endl;
    string s = "Hi Client, I'm CppSSLSocket Server.";
    SSL_write(ssl, s.c_str(), s.size());
    // 釋放資源
    SSL_free(ssl);
    SSL_CTX_free(ssl_ctx);
    return 0;
}

struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file)
{
    // 創建通信背景關系
    ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method());
    if (!ssl_ctx)
    {
        cout << "ssl_ctx new failed" << endl;
        return nullptr;
    }
    int res = SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM);
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return nullptr;
    }
    res = SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM);
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return nullptr;
    }
    res = SSL_CTX_check_private_key(ssl_ctx);
    if (res != 1)
    {
        return nullptr;
    }
    return ssl_ctx;
}

客戶端原始碼 client.cpp

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include "openssl/ssl.h"
#include "openssl/err.h"

using namespace std;

struct ssl_ctx_st *InitSSLClient();

int main(int argc, char *argv[])
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");
    sin.sin_port = htons(10020);
    // 首先執行socket連接
    int res = connect(sock, (sockaddr *)&sin, sizeof(sin));
    if (res != 0)
    {
        return -1;
    }
    cout << "Client connect success." << endl;

    ssl_ctx_st *ssl_ctx = InitSSLClient();
    ssl_st *ssl = SSL_new(ssl_ctx);
    SSL_set_fd(ssl, sock);
    // 進行SSL層握手
    res = SSL_connect(ssl);
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return -1;
    }
    string send_msg = "Hello Server, I'm CppSSLSocket Client.";
    SSL_write(ssl, send_msg.c_str(), send_msg.size());
    char recv_msg[1024] = {0};
    int recv_len = SSL_read(ssl, recv_msg, sizeof(recv_msg));
    recv_msg[recv_len] = '\0';
    cout << recv_msg << endl;
    SSL_shutdown(ssl);
    SSL_free(ssl);
    SSL_CTX_free(ssl_ctx);
    return 0;
}

struct ssl_ctx_st *InitSSLClient()
{
    // 創建一個ssl客戶端的背景關系
    ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_client_method());
    return ssl_ctx;
}

編譯使用Makefile,客戶端的修改TARGET即可

TARGET=server.x
SRC=$(wildcard *.cpp)
OBJS=$(patsubst %.cpp,%.o,$(SRC))
LIBS=-lssl -lcrypto
$(TARGET):$(SRC)
    g++ -std=c++11 $^ -o $@ $(LIBS)
clean:
    rm -fr $(TARGET) $(OBJS)

如果在服務端和客戶端都可以正常發送和接收顯示訊息,即表示通信正常,

C++撰寫openssl與libevent安全通信服務端

當前專案使用的libevent版本為2.1,在編譯的時候需要在目標機器上預先編譯好openssl,否則編譯時檢測不到,無法生成對應介面,有關libevent的基礎可以參考smss開源系列的前期文章,這里不再贅述,考慮到同構系統的開發案例網上的資料相對豐富,同時筆者目前的作業大多為異構系統開發為主,因此這里選擇使用C++作為服務端,Java和NodeJs為客戶端的方式,如果讀者有需要也可以給我留言,我會補充Java作為服務端C++作為客戶端的相關案例,

目前使用libevent和openssl作為通信框架,在追求性能優先的物聯網專案中應用廣泛,開發難度也相對較低,libevent也提供了專門呼叫openssl的介面,它可以幫助我們管理SSL物件,不過SSL_CTX的維護還需要我們自己實作,與直接使用libevent創建服務端相比最大的區別在于我們需要自己創建socket并同時交給event_base和SSL_CTX來使用,

服務端原始碼 libevent_server.cpp

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <string>
#include "openssl/ssl.h"
#include "event2/event.h"
#include "event2/listener.h"
#include "event2/bufferevent.h"
#include "event2/bufferevent_ssl.h"

using namespace std;

// 設定x.509證書檔案和私鑰檔案
ssl_ctx_st *InitServer(const char *crt_file, const char *key_file);

// 創建通信ssl
ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket);

// 服務端連接監聽回呼函式
void EvconnlistenerCB(struct evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx);

// 訊息讀、寫和事件回呼
void ReadCB(struct bufferevent *bev, void *ctx);
void WriteCB(struct bufferevent *bev, void *ctx);
void EventCB(struct bufferevent *bev, short what, void *ctx);

static bool isSsl = false;

int main(int argc, char *argv[])
{

    if (argc == 2)
    {
        if (strcmp(argv[1], "SSL") == 0)
        {
            isSsl = true;
        }
    }
    // 創建event_base
    event_base *base = event_base_new();
    if (!base)
    {
        cout << "event_base_new fail" << endl;
        return -1;
    }
    // 創建SSL_CTX通信背景關系
    ssl_ctx_st *ssl_ctx = InitServer("../server.crt", "../server.key");
    // 創建socket
    sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10020);
    evconnlistener *listener = evconnlistener_new_bind(
        base,
        EvconnlistenerCB,
        ssl_ctx,
        LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
        10,
        (sockaddr *)&addr,
        sizeof(addr));
    
    // 阻塞當前執行緒執行事件回圈
    event_base_dispatch(base);
    // 釋放資源
    SSL_CTX_free(ssl_ctx);
    event_base_free(base);
    return 0;
}

void EvconnlistenerCB(evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx)
{
    cout << "Server EvconnlistenerCB..." << endl;
    // 獲取當前的事件回圈背景關系
    event_base *base = evconnlistener_get_base(listener);
    bufferevent *bev = nullptr;
    // 判斷當前是否啟用ssl通信模式
    if (isSsl)
    {
        ssl_ctx_st *ssl_ctx = (ssl_ctx_st *)ctx;
        ssl_st *ssl = NewSSL(ssl_ctx, socket);
        // 創建bufferevent,當bufferevent關閉的時候,會同時釋放ssl資源
        bev = bufferevent_openssl_socket_new(base, socket, ssl, BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE);
        bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, ssl);
    }
    else
    {
        bev = bufferevent_socket_new(base, socket, BEV_OPT_CLOSE_ON_FREE);
        bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, base);
    }
    // 注冊事件型別
    bufferevent_enable(bev, EV_READ | EV_WRITE);
}

/**
 * ssl背景關系初始化
 * 考慮測驗簡潔的需要,這里沒有做多余判斷
 */
ssl_ctx_st *InitServer(const char *crt_file, const char *key_file)
{
    ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method());
    SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM);
    SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM);
    SSL_CTX_check_private_key(ssl_ctx);
    return ssl_ctx;
}

/**
 * 創建ssl介面并且和socket系結
 */
ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket)
{
    ssl_st *ssl = SSL_new(ssl_ctx);
    SSL_set_fd(ssl, socket);
    return ssl;
}

void ReadCB(bufferevent *bev, void *ctx)
{
    char buf[1024] = {0};
    int len = bufferevent_read(bev, buf, sizeof(buf) - 1);
    buf[len] = '\0';
    cout << buf << endl;
    string msg = "hello client, I'm server.\n";
    bufferevent_write(bev, msg.c_str(), msg.size());
    bufferevent_write(bev, buf, len);
}

void WriteCB(bufferevent *bev, void *ctx)
{
}

void EventCB(bufferevent *bev, short what, void *ctx)
{
    cout << "EventCB: " << what << endl;
    if (what & BEV_EVENT_CONNECTED)
    {
        cout << "Event:BEV_EVENT_CONNECTED" << endl;
    }
    if (what & BEV_EVENT_ERROR && what & BEV_EVENT_READING)
    {
        cout << "Event:BEV_EVENT_READING" << endl;
        bufferevent_free(bev);
    }
    if (what & BEV_EVENT_ERROR && what & BEV_EVENT_WRITING)
    {
        cout << "Event:BEV_EVENT_WRITING" << endl;
        bufferevent_free(bev);
    }
}

編譯用的Makefile檔案

TARGET=server.x
SRC=https://www.cnblogs.com/learnhow/p/$(wildcard *.cpp)
OBJS=$(patsubst %.cpp,%.o,$(SRC))
LIBS=-lssl -lcrypto -levent -levent_openssl
$(TARGET):$(SRC)
    g++ -std=c++11 $^ -o $@ $(LIBS)
clean:
    rm -fr $(TARGET) $(OBJS)

特別需要注意bufferevent_openssl_socket_new方法包含了對bufferevent和SSL的管理,因此當連接關閉的時候不再需要SSL_free,可執行檔案server.x接收SSL作為引數,作為是否啟用安全通信的標識,

讀者可以使用上一節生成的client.x與本節的程式通信,方便測驗結果,

三 *基于Node.js的(加密)通信測驗

*注:如果您不熟悉electron可以跳過本節,不妨礙后面的學習

由于electron不是本文的重點,因此如何創建和開發electron專案做過過多介紹,本例使用electron-vue作為模板,使用vue-cli直接創建,我們將分別使用Node.js的net包和tls包創建通信客戶端,

net.Socket連接示例:

this.socket = net.connect(10020, "127.0.0.1", () => {
  console.log("socket 服務器連接成功...");
  this.socket.write("Hello Server, I'm Nodejs.", () => {
    console.log("發送完成~");
  });
});

this.socket.on("data", data =https://www.cnblogs.com/learnhow/p/> {
  console.log(data.toString());
});

tls.connect連接示例:

this.socket = tls.connect(
  { host: "127.0.0.1", port: 10020, rejectUnauthorized: false },
  () => {
    console.log("ssl 服務器連接成功...");
    this.socket.write("Hello Server, I'm Nodejs.", () => {
      console.log("發送完成~");
    });
  }
);

this.socket.on("data", data =https://www.cnblogs.com/learnhow/p/> {
  console.log(data.toString());
});

由于之前我們通過openssl生成的x.509證書為自簽名證書,因此在使用tls.connect的時候需要指定rejectUnauthorized屬性,

讀者可以利用這套代碼和上一節創建的server.x分別進行普通通信和安全通信,以判斷功能是否正常,

四 創建基于SSLEngine的NIO通信

如果說之前的知識你都能夠掌握,那么從這里開始才是本文的重點,也是難點所在,網上對于SSLEngine的介紹資料相對較少,且大多都沒有經過完整測驗,確實造成學習曲線過于陡峭,加之筆者認為Java對于SSLEngine的設計的確不太合理,因此強烈不建議讀者在實際專案中使用,事實上,SSL/TLS協議的握手程序非常復雜,涉及到加密和秘鑰交換等多個步驟,無論是基于C語言的openssl還是基于Node.js的tls.connect都將握手的程序封裝到內部,現在筆者將通過介紹SSLEngine讓你對這一程序有所了解,

ByteBuffer分析

io面向流(stream)開發,而nio面向緩沖(buffer)開發,很多人對此也不陌生,但是在作業中我發現能夠深入理解這句話的人比較少,什么叫面向流(stream)?為什么有區別于面向緩沖(buffer)?傳統io在向檔案或資料庫請求資料的時候,由于需要請求作業系統資源,因此存在需要等待回應的程序,它不同于單純的代碼執行只需要使用cpu資源,io操作還需要涉及總線資源,磁盤資源等,在這個程序中,由于無法確定資料什么時候會回傳,只能做阻塞等待,nio的做法相當于告知作業系統:我已經在用戶態申請好了一塊記憶體空間(buffer),當內核接收到資料以后請直接寫到我的空間中,因此,使用nio編程的特點之一就是對資料的處理往往需要通過回呼函式(callback),作為最常用的緩沖物件——ByteBuffer,你有多熟悉?

ByteBuffer最重要的三個屬性:

  • capacity 表示該緩沖區的最大容量,任何操作最大容量的讀寫操作都屬于非法
  • limit 如果當前是寫入態,limit等于capacity,如果當前是讀取態,limit表示當前一共有多少有效資料,注意,寫入態和讀取態是我創造的名詞,buffer本身并不存在這兩個狀態
  • position 當前資料區的讀/寫位置指標

當你開始往buffer中寫入資料的時候,pos會不斷增加,limit等于cap,寫入完成后,如果你想要讀取資料,第一步必須進行翻轉(flip),翻轉以后的資料區pos為0,而limit則等于之前寫入的pos,如果在讀取資料的時候,無法一次性處理完,我們可以使用compact()方法將已經讀取的資料清除,

為了加深印象,請大家思考一個問題:如果我向一個ByteBuffer中寫入了資料,假設當前緩沖區的狀態為 java.nio.HeapByteBuffer[pos=1305 lim=16921 cap=16921],我又讀取了94個位元組,當前緩沖區狀態為 java.nio.HeapByteBuffer[pos=94 lim=1305 cap=16921],此時呼叫compact(),緩沖區的狀態是什么情況?

根據jdk官方檔案上的解釋,compact()方法會將緩沖區中的資料按位復制,pos復制到0,pos + 1復制到1,以此類推,最后是將limit-1復制到limit-pos,事實上方法內部還幫我們做了一次翻轉操作,當前的緩沖區狀態為 java.nio.HeapByteBuffer[pos=1211 lim=16921 cap=16921],

非阻塞SocketChannel

目前幾乎所有支持非阻塞的通信框架都基于React模式開發,通過在IO管道上注冊多個事件回呼以達到異步處理的效果,又因為回呼的使用原來越多,因此Java 8也提出了函式式介面的概念,同時引入蘭姆達運算式以讓用戶能夠設計出更適合閱讀和維護的代碼,

NIO在socket上的運用Java提供了SocketChannel和Selector物件,

非阻塞客戶端 NioSocket.java

package socket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Set;

public class NioSocket {
    /**
     * 連接方法
     * 
     * @param host 服務器主機地址
     * @param port 服務器埠
     */
    public static void connection(String host, int port) throws IOException {
        Selector sel = Selector.open(); // 創建事件選擇器
        InetSocketAddress addr = new InetSocketAddress(host, port);
        SocketChannel socket = SocketChannel.open(); // 創建非阻塞socket物件
        socket.configureBlocking(false).register(sel,
                SelectionKey.OP_CONNECT | SelectionKey.OP_READ); // 配置非阻塞模式和向Selector注冊連接事件與資料可讀事件
        socket.connect(addr);
        while (true) {
            // 等待間隔
            if (sel.select(10) > 0) {
                Set<SelectionKey> keys = sel.selectedKeys();
                for(SelectionKey key : keys) {
                    keys.remove(key); // 移除事件并處理
                    if(key.isConnectable()) {
                        socket.finishConnect();
                        String reqMsg = "Hello Server, I'm JavaClient.";
                        ByteBuffer reqBuf = ByteBuffer.wrap(reqMsg.getBytes());
                        socket.write(reqBuf);
                    } else if(key.isReadable()) {
                        ByteBuffer respBuf = ByteBuffer.allocate(1024);
                        int length = socket.read(respBuf);
                        if(length > 0) {
                            String respMsg = new String(respBuf.array(), 0, length);
                            System.out.println(respMsg);
                        }
                    }
                }
            }
        }
    }
    
    public static void main(String[] args) {
        try {
            NioSocket.connection("127.0.0.1", 10020);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

當有注冊的事件產生的時候,我們能夠通過selectedKey()方法獲取完整的事件佇列,如果事件沒有被處理,會在下一次事件回圈中重新觸發,因此處理完成的事件需要從佇列中洗掉,

阻塞式加密通信 SSLSocket

接下來我們將難度升級,看一下利用SSLSocket如何開發加密通信的客戶端,Java為我們提供了javax.net.ssl包,里面都是與SSL/TLS加密通信相關的組件,由于服務端使用的是自簽名證書,因此我們需要重寫TrustManager的實作

package tls;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;

public class X509SelfSignTrustManager implements X509TrustManager {

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        for (int i = 0; i < chain.length; i++) {
            System.out.println(chain[i]);
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }

}

作為客戶端checkClientTrusted()和getAcceptedIssuers()方法都不會被呼叫,checkServerTrusted()方法用來檢查服務端的證書,我們只將證書內容列印出來,

package tls;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

public class Ssl {
    public SSLSocket connection(String host, int port) throws Exception {
        SSLContext context = SSLContext.getInstance("SSL");
        context.init(null, new TrustManager[] {new X509SelfSignTrustManager()}
         , new java.security.SecureRandom());
        SSLSocketFactory factory = context.getSocketFactory();
        return (SSLSocket) factory.createSocket(host, port);
    }

    public static void main(String[] args) {
        Ssl ssl = new Ssl();
        SSLSocket sslSocket = null;
        try {
            sslSocket = ssl.connection("127.0.0.1", 10020);
            OutputStream output = sslSocket.getOutputStream();
            String msg = "Hello Server, I'm BioSSLClient.";
            output.write(msg.getBytes());
            output.flush();
            InputStream input = sslSocket.getInputStream();
            byte[] buf = new byte[1024];
            int len = input.read(buf);
            String ss = new String(buf, 0, len);
            System.out.println(ss);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                sslSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

首先是需要創建基于SSL協議的背景關系物件SSLContext

使用我們自己實作的證書管理器進行初始化

創建SSLSocketFactory,并通過它實體化SSLSocket

通信程序基本就是操作io流,這里不做贅述

SSLEngine——抽象化的握手和加/解密介面

先看一下規范的SSL/TLS握手步驟:

基本的通信大致可以分為4個程序:

  1. 選擇協議版本和會話ID
  2. 服務端發送證書和秘鑰交換資料
  3. 客戶端處理證書和生成秘鑰交換資料并發送給服務端
  4. 會話秘鑰協商成功,握手完成

因為SSLEngine僅僅是針對SSL層進行了抽象,因此底層通訊介面需要自己創建,因為打算使用nio,我將創建一個SocketChannel,

SSLEngine也通過SSLContext實體化,SSLContext還能夠實體化一個SSLSession物件,使用SSLSession幫助我們創建兩種快取:應用資料快取和網路資料快取,顧名思義,應用資料快取用來存盤明文資料,網路資料快取代表將要發送或接收到的密文資料,它們通過SSLEngine的wrap()和unwrap()方法相互轉換,使用SSLEngine的難點是執行握手操作,關鍵點在于如何理解內部的兩個列舉型別:

SSLEngineResult.HandshakeStatus:

  • NEED_WRAP 當前有資料需要被加密并發送
  • NEED_UNWRAP 當前有資料應該被讀取并解密
  • NEED_TASK 需要執行運算任務
  • FINISHED 握手完成
  • NOT_HANDSHAKING 當前不處于握手狀態中

特別注意,FINISHED狀態只會在握手完成后的最后一步操作中出現,之后再獲取狀態都會顯示為NOT_HANDSHAKING(SSLEngine為什么會這樣設計我也沒看懂),我曾經以為NOT_HANDSHAKING狀態表示握手已斷開,一度很不理解,

SSLEngineResult.Status:在執行wrap()或unwrap()操作后

  • OK 執行成功
  • BUFFER_OVERFLOW 寫入快取區不足,通常表示unwrap()的第二個引數設定的buffer剩余空間不足
  • BUFFER_UNDERFLOW 輸出緩沖區不足,通常表示wrap()的第一個引數設定的buffer中沒有資料
  • CLOSED SSLEngine已經被關閉,無法執行任何方法

利用SSLEngine進行握手的時候,我們會多次使用wrap()和unwrap()方法,此時如果打開斷點你會發現明明沒有提供明文資料,經過wrap()后密文快取中卻有資料,或者接收到密文資料后經過unwrap()方法,卻沒得到任何明文資料,原因是,握手階段的任何資料都在SSLEngine內部處理(這個設計很奇怪,不明白Java的設計者們如此設計的初衷是什么),

package tls;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;

public class NioSsl {
    private SocketChannel sc;
    private SSLEngine sslEngine;
    private Selector selector;
    private HandshakeStatus hsStatus;
    private Status status;
    private ByteBuffer localNetData;
    private ByteBuffer localAppData;
    private ByteBuffer remoteNetData;
    private ByteBuffer remoteAppData;

    public void connection(String host, int port) throws Exception {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom());
        sslEngine = sslContext.createSSLEngine();
        sslEngine.setUseClientMode(true);
        SSLSession session = sslEngine.getSession();
        localAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
        localNetData = ByteBuffer.allocate(session.getPacketBufferSize());
        remoteAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
        remoteNetData = ByteBuffer.allocate(session.getPacketBufferSize());
        remoteNetData.clear();
        SocketChannel channel = SocketChannel.open();
        selector = Selector.open();
        channel.configureBlocking(false).register(selector,
                SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        InetSocketAddress addr = new InetSocketAddress(host, port);
        channel.connect(addr);
        sslEngine.beginHandshake();
        hsStatus = sslEngine.getHandshakeStatus();
        while (true) {
            if (selector.select(10) > 0) {
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    it.remove();
                    handleSocketEvent(selectionKey);
                }
            }
        }
    }

    private void handleSocketEvent(SelectionKey key) throws IOException, InterruptedException {
        if (key.isConnectable()) {
            System.out.println("isConnectable...");
            sc = (SocketChannel) key.channel();
            sc.finishConnect();
            doHandshake();
            localAppData.clear();
            localAppData.put("Hello Server, I'm NioSslClient.".getBytes());
            localAppData.flip();
            localNetData.clear();
            SSLEngineResult result = sslEngine.wrap(localAppData, localNetData);
            hsStatus = result.getHandshakeStatus();
            status = result.getStatus();
            if (status == Status.OK) {
                localNetData.flip();
                while (localNetData.hasRemaining()) {
                    sc.write(localNetData);
                }
            }
        } else if (key.isReadable()) {
            System.out.println("isReadable...");
            sc = (SocketChannel) key.channel();
            remoteNetData.clear();
            remoteAppData.clear();
            int len = sc.read(remoteNetData);
            System.out.println("接受服務端加密資料長度:" + len);
            remoteNetData.flip();
            SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData);
            hsStatus = result.getHandshakeStatus();
            status = result.getStatus();
            remoteAppData.flip();
            byte[] buf = new byte[remoteAppData.limit()];
            remoteAppData.get(buf);
            System.out.println(new String(buf));
        }
    }

    private void doHandshake() throws IOException, InterruptedException {
        SSLEngineResult result;
        int count = 0;
        while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
            TimeUnit.MILLISECONDS.sleep(100);
            switch (hsStatus) {
            case NEED_TASK:
                System.out.println("當前握手狀態:NEED_TASK");
                Runnable runnable;
                while ((runnable = sslEngine.getDelegatedTask()) != null) {
                    runnable.run();
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            case NEED_UNWRAP:
                System.out.println("當前握手狀態:NEED_UNWRAP");
                count = sc.read(remoteNetData);
                System.out.println("獲取位元組數:" + count);
                remoteNetData.flip();
                remoteAppData.clear();

                do {
                    result = sslEngine.unwrap(remoteNetData, remoteAppData);
                } while (result.getStatus() == SSLEngineResult.Status.OK
                        && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);


                hsStatus = result.getHandshakeStatus();
                status = result.getStatus();

                remoteNetData.compact();
                if (hsStatus == SSLEngineResult.HandshakeStatus.FINISHED) {
                    System.out.println("===========" + hsStatus + "===========");
                    
                }
                break;
            case NEED_WRAP:
                System.out.println("當前握手狀態:NEED_WRAP");
                localNetData.clear();
                result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData);
                hsStatus = result.getHandshakeStatus();
                status = result.getStatus();
                if (status != Status.OK) {
                    throw new RuntimeException("status: " + status);
                }
                localNetData.flip();
                while (localNetData.hasRemaining()) {
                    int len = sc.write(localNetData);
                    System.out.println("發送位元組數:" + len);
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            default:
                break;
            }
        }
        hsStatus = sslEngine.getHandshakeStatus();
        System.out.println("===========" + hsStatus + "===========");
    }

    public static void main(String[] args) {
        NioSsl nioSsl = new NioSsl();
        try {
            nioSsl.connection("127.0.0.1", 10020);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

原始碼中我已經設定了睡眠時間和必要的訊息輸出,讀者可以復制到IDE中結合C++端的服務協同測驗,如果通信成功,你應該可以在客戶端看到x.509證書列印和13次狀態改變,去除NEED_TASK狀態,再對比SSL/TLS協議的握手規范學習,

SSLSocketChannel 原始碼

如果你能夠順利看到這里,那么恭喜你,在這篇知識分享的文章中,你應該多少有些識訓,為了準備這些東西,我用了幾乎整個2020年的春節假期(幸好假期延長了,否則時間還不夠),最后是我自己封裝的SSLSocketChannel,使用了函式式介面以及蘭姆達運算式,

package tls;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;

public class SSLSocketChannel {
    private volatile boolean isQuit = false;
    private SocketChannel socket = null;
    private Selector selector = null;
    private ExecutorService pool = null;
    private LinkedList<Function<byte[], byte[]>> readBufQueue = new LinkedList<>();
    private LinkedList<Supplier<byte[]>> writeBufQueue = new LinkedList<>();
    private Lock writeLock = new ReentrantLock();
    private Lock readLock = new ReentrantLock();
    private SSLEngine sslEngine;
    private HandshakeStatus hsStatus;

    private ByteBuffer localAppData, remoteAppData;
    private ByteBuffer localNetData, remoteNetData;

    public SSLSocketChannel() throws IOException {
        this.selector = Selector.open(); // 打開事件選擇器
        this.pool = Executors.newSingleThreadExecutor();
    }

    /**
     * 創建一個非堵塞的Socket并注冊連接事件和讀取事件
     * 
     * @param host
     * @param port
     * @throws IOException
     */
    public void connect(String host, int port) throws IOException {
        InetSocketAddress addr = new InetSocketAddress(host, port);
        socket = SocketChannel.open();
        socket.configureBlocking(false).register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
        socket.connect(addr);
    }

    /**
     * 網路事件回圈執行緒
     * 
     * @return 執行緒結束
     */
    public Future<Void> dispatch() {
        Future<Void> fut = this.pool.submit(() -> {
            while (!isQuit) {
                if (selector.select(10) > 0) {
                    Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        if (key.isConnectable()) {
                            socket.finishConnect();
                            this.sslHandshake();
                        } else if (key.isReadable()) {
                            remoteNetData.clear();
                            int length = socket.read(remoteNetData);
                            if (length > 0) {
                                remoteNetData.flip();
                                remoteAppData.clear();
                                SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData);
                                if (handleResult(result)) {
                                    remoteAppData.flip();
                                    byte[] b = new byte[remoteAppData.limit()];
                                    remoteAppData.get(b);
                                    try {
                                        readLock.lock();
                                        for (Function<byte[], byte[]> fn : readBufQueue) {
                                            byte[] r = fn.apply(b);
                                            if (r != null) {
                                                ByteBuffer buf = ByteBuffer.wrap(r);
                                                socket.write(buf);
                                            }
                                        }
                                    } finally {
                                        readLock.unlock();
                                    }
                                }
                            }
                        }
                        iter.remove();
                    }
                }
                if (socket.isConnected() && writeBufQueue.size() > 0
                        && (hsStatus == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING
                                || hsStatus == SSLEngineResult.HandshakeStatus.FINISHED)) {
                    try {
                        writeLock.lock();
                        Supplier<byte[]> sup = null;
                        while ((sup = writeBufQueue.poll()) != null) {
                            localAppData.clear();
                            localAppData.put(sup.get());
                            localAppData.flip();
                            localNetData.clear();
                            SSLEngineResult result = sslEngine.wrap(localAppData, localNetData);
                            if (handleResult(result)) {
                                localNetData.flip();
                                while (localNetData.hasRemaining()) {
                                    socket.write(localNetData);
                                }
                            }
                        }
                    } finally {
                        writeLock.unlock();
                    }
                }
            }
            return null;
        });
        this.pool.shutdown();
        return fut;
    }

    /**
     * 添加資料進入發送佇列
     * 
     * @param 函式式介面
     * @see Supplier
     */
    public void write(Supplier<byte[]> s) {
        try {
            writeLock.lock();
            writeBufQueue.push(s);
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 添加接收器進入接收佇列
     * 
     * @param 函式式介面
     * @see Function
     */
    public void read(Function<byte[], byte[]> f) {
        try {
            readLock.lock();
            readBufQueue.push(f);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * SSL/TLS 握手
     * 
     * @throws InterruptedException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     * @throws IOException
     */
    public void sslHandshake()
            throws InterruptedException, NoSuchAlgorithmException, KeyManagementException, IOException {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom());
        sslEngine = sslContext.createSSLEngine();
        sslEngine.setUseClientMode(true);
        SSLSession sslSession = sslEngine.getSession();
        localAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 本地應用資料快取
        localNetData = https://www.cnblogs.com/learnhow/p/ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 本地加密資料快取
        remoteAppData = https://www.cnblogs.com/learnhow/p/ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 遠端應用資料快取
        remoteNetData = https://www.cnblogs.com/learnhow/p/ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 遠端加密資料快取
        sslEngine.beginHandshake();
        hsStatus = sslEngine.getHandshakeStatus();
        SSLEngineResult result;
        // 回圈判斷指導握手完成
        while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
            switch (hsStatus) {
            case NEED_WRAP:
                localNetData.clear();
                result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData); // 第一個引數設定空包,SSLEngine會將握手資料寫入網路包
                hsStatus = result.getHandshakeStatus();
                if (handleResult(result)) {
                    localNetData.flip();
                    // 確保資料全部發送完成
                    while (localNetData.hasRemaining()) {
                        socket.write(localNetData);
                    }
                }
                break;
            case NEED_UNWRAP:
                int len = socket.read(remoteNetData); // 讀取網路資料
                if (len == -1) {
                    break;
                }
                remoteNetData.flip();
                remoteAppData.clear();
                do {
                    result = sslEngine.unwrap(remoteNetData, remoteAppData); // 與握手相關的資料SSLEngine會自行處理,不會輸出至第二個引數
                    hsStatus = result.getHandshakeStatus();
                } while (handleResult(result) && hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
                // 一次性沒有完成處理的資料通過壓縮的方式處理,等待下一次資料寫入
                remoteNetData.compact();
                break;
            case NEED_TASK:
                // SSLEngine后臺任務
                Runnable runnable;
                while ((runnable = sslEngine.getDelegatedTask()) != null) {
                    runnable.run();
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            default:
                break;
            }
        }
        // 握手完成將所有快取清空
        localAppData.clear();
        localNetData.clear();
        remoteAppData.clear();
        remoteNetData.clear();
    }

    private boolean handleResult(SSLEngineResult result) {
        switch (result.getStatus()) {
        case OK:
            return true;
        case BUFFER_OVERFLOW:
            return false;
        case BUFFER_UNDERFLOW:
            return false;
        case CLOSED:
            return false;
        default:
            return false;
        }
    }

    public static void main(String[] args) {
        try {
            SSLSocketChannel sslSocketChannel = new SSLSocketChannel();
            sslSocketChannel.connect("127.0.0.1", 10020);
            sslSocketChannel.dispatch();
            sslSocketChannel.read((b) -> {
                String s = new String(b);
                System.out.println(s);
                return null;
            });
            sslSocketChannel.write(() -> {
                return "hello ssl".getBytes();
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

相關文章:《開源專案SMSS開發指南》

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/16258.html

標籤:架構設計

上一篇:性能優化-資源的壓縮與合并

下一篇:Springboot vue 前后分離 跨域 Activiti6 作業流 集成代碼生成器 shiro權限

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more