使用SpringBoot開發群聊應用

来源:https://www.cnblogs.com/viljw/archive/2020/03/02/12397221.html
-Advertisement-
Play Games

通過本文你將學習如何使用Spring Boot和WebSocket API開發一個簡單的群聊天應用。 WebSocket是HTML5開始提供的一種在單個TCP連接上進行全雙工通訊的協議。WebSocket使得客戶端和伺服器之間的數據交換變得更加簡單,允許伺服器主動向客戶端推送數據。在WebSocke ...


通過本文你將學習如何使用Spring Boot和WebSocket API開發一個簡單的群聊天應用。

WebSocket是HTML5開始提供的一種在單個TCP連接上進行全雙工通訊的協議。WebSocket使得客戶端和伺服器之間的數據交換變得更加簡單,允許伺服器主動向客戶端推送數據。在WebSocket API中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,併進行雙向數據傳輸。

很多網站為實現推送技術,所用技術都是Ajax輪詢。輪詢指的是在特定的時間間隔(如每1秒),由瀏覽器對伺服器發出HTTP請求,然後伺服器返回最新的數據給瀏覽器。這種傳統的模式有很明顯的缺點,即瀏覽器需要不斷的向伺服器發出請求,而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,這樣就會浪費很多資源。HTML5定義的WebSocket協議能更好的節省伺服器帶寬等資源,並能夠實時地進行你通訊。

詳情請看HTML5 WebSocket

新建項目

打開IDEA,選擇Spring Initializer

填寫好相關信息:

依賴選擇Spring WebWebSocket

之後選擇Finish即可。創建完畢後,項目目錄結構如下:

WebSocket配置

首先我們配置一下WebSocket端點和消息代理。在com.andy.chat包下創建一個名為config的包,然後在config包新增一個帶有以下內容的類WebSocketConfig

package com.andy.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }
}

@EnableWebSocketMessageBroker註解用於啟用WebSocket服務。 我們實現了WebSocketMessageBrokerConfigurer介面,並覆蓋了用於配置WebSocket連接的一些方法。

在第一個方法中,我們註冊了一個WebSocket端點,客戶端將使用該端點連接到我們的WebSocket伺服器。端點配置中使用withSockJS()方法,用於為不支持WebSocket的瀏覽器啟用備用選項。

你可能註意到方法名中帶有STOMP。STOMP(Simple Text Orientated Messaging Protocol,簡單文本定向消息協議)允許STOMP客戶端與任意STOMP消息代理進行交互。

為什麼需要STOMP?因為WebSocket只是一種通信協議。 它沒有定義如何僅向訂閱了特定主題的用戶發送消息,或者如何向特定用戶發送消息,所以需要STOMP來實現這些功能。舉個例子,在課堂上舉手就好比WebSocket代表的通信協議,因為它定義了在講堂上如何和老師建立通信(舉手)。而具體你想問老師的內容:“李老師,為什麼這個函數不是連續的呢?”就相當於STOMP協議,因為它定義如何向特定用戶發送消息(李老師)。

在第二個方法中,我們配置了一個消息代理,用於將消息從一個客戶端路由到另一個客戶端。第一行定義了以/app開頭為目標的消息應應路由到消息處理方法。第二行定義了以/topic開頭為目標的消息應路由到消息代理。消息代理廣播消息到所有訂閱了特定主題的所有連接的客戶端。

上例中,我們使用了一個簡單的記憶體消息代理。 也可以使用任何其他功能齊全的消息代理,例如RabbitMQActiveMQ

Message模型

Message模型用來表示客戶端與伺服器之間的消息。在com.andy.chat新建一個包model,在model包中新建Message類:

package com.andy.chat.model;

public class Message {
    private MessageType messageType;
    private String content;

    public MessageType getMessageType() {
        return messageType;
    }

    public void setMessageType(MessageType messageType) {
        this.messageType = messageType;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    private String sender;
    
    public enum MessageType {
        JOIN,CHAT, LEAVE
    }
    
    
}

收發消息的控制器

我們將在控制器中定義消息處理方法。在com.andy.chat包下新建一個包controller併在其中創建類ChatController

package com.andy.chat.controller;

import com.andy.chat.model.Message;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {
    @MessageMapping("/sendMessage")
    @SendTo("/topic/public")
    public Message sendMessage(@Payload Message message) {
        return message;
    }

    @MessageMapping("addUser")
    @SendTo("/topic/public")
    public Message addUser(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) {
        headerAccessor.getSessionAttributes().put("username", message.getSender());
        return message;
    }
}

正如在WebSocket配置一節所配置的,從客戶端發送的所有以/app開頭的消息將被路由到這些消息處理方法,這些方法使用@MessageMapping註解。

例如,以/app/sendMessage為目標的消息被路由到sendMessage()方法,以/app/addUser為目標的消息被路由到addUser方法。

添加WebSocket事件監聽器

我們將使用事件監聽器來監聽Socket連接和斷開事件,以便記錄這些事件,併在用戶加入或離開群聊時廣播它們。

package com.andy.chat.controller;

import com.andy.chat.model.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component
public class WebSocketEventListener {
    private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);

    @Autowired
    private SimpMessageSendingOperations messageSendingOperations;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("新連接");
    }

    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if (username != null) {
            logger.info("用戶{}取消連接", username);
            Message message = new Message();
            message.setMessageType(Message.MessageType.LEAVE);
            message.setSender(username);
            messageSendingOperations.convertAndSend("/topic/public", message);
        }
    }
}

SessionDisconnect事件中,從WebSocket會話中提取用戶名,並將用戶離開事件廣播到所有連接的客戶端。

前端開發

在項目的static下創建如下目錄結構:

HTML文件用來顯示用戶群聊信息。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>群聊</title>
    <link rel="stylesheet" href="css/main.css">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
</head>
<body>

<div id="username-page">
    <div class="username-page-container">
        <h1 class="title">請輸入您的用戶名</h1>
        <form id="username-form">
            <div class="form-group">
                <input type="text" class="form-control" id="name" placeholder="用戶名" autocomplete="off">
            </div>
            <div class="form-group">
                <button type="submit" class="username-submit accent">開始聊天</button>
            </div>
        </form>
    </div>
</div>

<div id="chat-page" class="hidden">
    <div class="chat-container">
        <div class="chat-header">
            <h2>我的群聊</h2>
        </div>
        <div class="connecting">
            連接中.....
        </div>
        <ul id="message-area">
        </ul>
        <form  id="message-form">
            <div class="form-group">
                <div class="input-group clearfix">
                    <input type="text" class="form-control" id="message" placeholder="輸入消息" autocomplete="off">
                    <button type="submit" class="primary">發送</button>
                </div>
            </div>
        </form>
    </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>

我們在其中引入了兩個javascript庫——sockjsstomp

SockJS是一個WebSocket客戶端,它嘗試使用WebSocket,併為不支持WebSocket的舊版瀏覽器提供備用選項。 stompjavascript的STOMP客戶端。

現在,我們來編寫用於連接WebSocket端點和收發信息的javascript代碼。將下列代碼添加到main.js文件中:

'use strict';

let usernamePage = document.querySelector("#username-page");
let chatPage = document.querySelector("#chat-page");
let usernameForm = document.querySelector("#username-form");
let messageForm = document.querySelector("#message-form");
let messageInput = document.querySelector("#message");
let messageArea = document.querySelector("#message-area");
let connectingElement = document.querySelector(".connecting");

let username = null;
let stompClient = null;
let colors = [
    '#2196F3', '#32c787', '#00BCD4', '#ff5652',
    '#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];

function connect(event) {
    username = document.querySelector("#name").value.trim();
    if (username) {
        usernamePage.classList.add("hidden");
        chatPage.classList.remove("hidden");
        let socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}

function onConnected() {
    // 訂閱Public主題
    stompClient.subscribe("/topic/public", onMessageReceived);
    stompClient.send("/app/addUser", {}, JSON.stringify({
        sender: username,
        messageType: 'JOIN'
    }));
    connectingElement.classList.add("hidden");
}

function onError(error) {
    connectingElement.textContent = "連接不上WebSocket伺服器,請刷新頁面!";
    connectingElement.style.color = "red";
}

function sendMessage(event) {
    let messageContent = messageInput.value.trim();
    if (messageContent && stompClient) {
        let message = {
            sender: username,
            content: messageContent,
            messageType: "CHAT"
        };
        stompClient.send("/app/sendMessage", {}, JSON.stringify(message));
        messageInput.value = "";
    }
    event.preventDefault();
}

function onMessageReceived(payload) {
    var message = JSON.parse(payload.body);

    var messageElement = document.createElement('li');

    if(message.messageType === 'JOIN') {
        messageElement.classList.add('event-message');
        message.content = message.sender + ' 加入!';
    } else if (message.messageType === 'LEAVE') {
        messageElement.classList.add('event-message');
        message.content = message.sender + ' 離開!';
    } else {
        messageElement.classList.add('chat-message');

        var avatarElement = document.createElement('i');
        var avatarText = document.createTextNode(message.sender[0]);
        avatarElement.appendChild(avatarText);
        avatarElement.style['background-color'] = getAvatarColor(message.sender);

        messageElement.appendChild(avatarElement);

        var usernameElement = document.createElement('span');
        var usernameText = document.createTextNode(message.sender);
        usernameElement.appendChild(usernameText);
        messageElement.appendChild(usernameElement);
    }

    var textElement = document.createElement('p');
    var messageText = document.createTextNode(message.content);
    textElement.appendChild(messageText);

    messageElement.appendChild(textElement);

    messageArea.appendChild(messageElement);
    messageArea.scrollTop = messageArea.scrollHeight;
}


function getAvatarColor(messageSender) {
    var hash = 0;
    for (var i = 0; i < messageSender.length; i++) {
        hash = 31 * hash + messageSender.charCodeAt(i);
    }
    var index = Math.abs(hash % colors.length);
    return colors[index];
}

usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)

connect方法用SockJSstomp客戶端連接到Spring Boot配置的/ws端點。

添加如下代碼到main.css文件中:

* {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}

html,body {
    height: 100%;
    overflow: hidden;
}

body {
    margin: 0;
    padding: 0;
    font-weight: 400;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 1rem;
    line-height: 1.58;
    color: #333;
    background-color: #f4f4f4;
    height: 100%;
}

body:before {
    height: 50%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: #128ff2;
    content: "";
    z-index: 0;
}

.clearfix:after {
    display: block;
    content: "";
    clear: both;
}

.hidden {
    display: none;
}

.form-control {
    width: 100%;
    min-height: 38px;
    font-size: 15px;
    border: 1px solid #c8c8c8;
}

.form-group {
    margin-bottom: 15px;
}

input {
    padding-left: 10px;
    outline: none;
}

h1, h2, h3, h4, h5, h6 {
    margin-top: 20px;
    margin-bottom: 20px;
}

h1 {
    font-size: 1.7em;
}

a {
    color: #128ff2;
}

button {
    box-shadow: none;
    border: 1px solid transparent;
    font-size: 14px;
    outline: none;
    line-height: 100%;
    white-space: nowrap;
    vertical-align: middle;
    padding: 0.6rem 1rem;
    border-radius: 2px;
    transition: all 0.2s ease-in-out;
    cursor: pointer;
    min-height: 38px;
}

button.default {
    background-color: #e8e8e8;
    color: #333;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
}

button.primary {
    background-color: #128ff2;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
    color: #fff;
}

button.accent {
    background-color: #ff4743;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
    color: #fff;
}

#username-page {
    text-align: center;
}

.username-page-container {
    background: #fff;
    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
    border-radius: 2px;
    width: 100%;
    max-width: 500px;
    display: inline-block;
    margin-top: 42px;
    vertical-align: middle;
    position: relative;
    padding: 35px 55px 35px;
    min-height: 250px;
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    margin: 0 auto;
    margin-top: -160px;
}

.username-page-container .username-submit {
    margin-top: 10px;
}


#chat-page {
    position: relative;
    height: 100%;
}

.chat-container {
    max-width: 700px;
    margin-left: auto;
    margin-right: auto;
    background-color: #fff;
    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
    margin-top: 30px;
    height: calc(100% - 60px);
    max-height: 600px;
    position: relative;
}

#chat-page ul {
    list-style-type: none;
    background-color: #FFF;
    margin: 0;
    overflow: auto;
    overflow-y: scroll;
    padding: 0 20px 0px 20px;
    height: calc(100% - 150px);
}

#chat-page #message-form {
    padding: 20px;
}

#chat-page ul li {
    line-height: 1.5rem;
    padding: 10px 20px;
    margin: 0;
    border-bottom: 1px solid #f4f4f4;
}

#chat-page ul li p {
    margin: 0;
}

#chat-page .event-message {
    width: 100%;
    text-align: center;
    clear: both;
}

#chat-page .event-message p {
    color: #777;
    font-size: 14px;
    word-wrap: break-word;
}

#chat-page .chat-message {
    padding-left: 68px;
    position: relative;
}

#chat-page .chat-message i {
    position: absolute;
    width: 42px;
    height: 42px;
    overflow: hidden;
    left: 10px;
    display: inline-block;
    vertical-align: middle;
    font-size: 18px;
    line-height: 42px;
    color: #fff;
    text-align: center;
    border-radius: 50%;
    font-style: normal;
    text-transform: uppercase;
}

#chat-page .chat-message span {
    color: #333;
    font-weight: 600;
}

#chat-page .chat-message p {
    color: #43464b;
}

#message-form .input-group input {
    float: left;
    width: calc(100% - 85px);
}

#message-form .input-group button {
    float: left;
    width: 80px;
    height: 38px;
    margin-left: 5px;
}

.chat-header {
    text-align: center;
    padding: 15px;
    border-bottom: 1px solid #ececec;
}

.chat-header h2 {
    margin: 0;
    font-weight: 500;
}

.connecting {
    padding-top: 5px;
    text-align: center;
    color: #777;
    position: absolute;
    top: 65px;
    width: 100%;
}


@media screen and (max-width: 730px) {

    .chat-container {
        margin-left: 10px;
        margin-right: 10px;
        margin-top: 10px;
    }
}

@media screen and (max-width: 480px) {
    .chat-container {
        height: calc(100% - 30px);
    }

    .username-page-container {
        width: auto;
        margin-left: 15px;
        margin-right: 15px;
        padding: 25px;
    }

    #chat-page ul {
        height: calc(100% - 120px);
    }

    #message-form .input-group button {
        width: 65px;
    }

    #message-form .input-group input {
        width: calc(100% - 70px);
    }

    .chat-header {
        padding: 10px;
    }

    .connecting {
        top: 60px;
    }

    .chat-header h2 {
        font-size: 1.1em;
    }
}

啟動應用

若想啟動該應用,在命令行執行以下命令(或者在IDEA中啟動):

mvn spring-boot:run

應用將會在預設的8080埠啟動,在瀏覽器訪問http://localhost:8080即可。

使用Rabbit MQ

如果要使用RabbitMQ之類的功能全面的消息代理而不是簡單的記憶體消息代理,我們需要先安裝RabbitMQ,具體步驟可以參考下文:

RabbitMQ安裝

然後配置STOMP插件:

rabbitmq-plugins enable rabbitmq_stomp

STOMP Plugin

在pom.xml文件中添加以下依賴項:

<!-- RabbitMQ Starter Dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!-- Following additional dependency is required for Full Featured STOMP Broker Relay -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>

添加好以上依賴後,可以在WebSocketConfig.java文件中啟用RabbitMQ消息代理:

public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.setApplicationDestinationPrefixes("/app");

    // Use this for enabling a Full featured broker like RabbitMQ
    registry.enableStompBrokerRelay("/topic")
            .setRelayHost("localhost")
            .setRelayPort(61613)
            .setClientLogin("guest")
            .setClientPasscode("guest");
}

最後,正常啟動應用即可。

參考

  • https://www.callicoder.com/spring-boot-websocket-chat-example/

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 引子 之前我們學習了線程、進程的概念,瞭解了在操作系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。按道理來說我們已經算是把cpu的利用率提高很多了。但是我們知道無論是創建多進程還是創建多線程來解決問題,都要消耗一定的時間來創建進程、創建線程、以及管理他們之間的切換。 隨著我們對於效率的追 ...
  • 線程概念的引入背景 進程 之前我們已經瞭解了操作系統中進程的概念,程式並不能單獨運行,只有將程式裝載到記憶體中,系統為它分配資源才能運行,而這種執行的程式就稱之為進程。程式和進程的區別就在於:程式是指令的集合,它是進程運行的靜態描述文本;進程是程式的一次執行活動,屬於動態概念。在多道編程中,我們允許多 ...
  • 理論知識 操作系統背景知識 顧名思義,進程即正在執行的一個過程。進程是對正在運行程式的一個抽象。 進程的概念起源於操作系統,是操作系統最核心的概念,也是操作系統提供的最古老也是最重要的抽象概念之一。操作系統的其他所有內容都是圍繞進程的概念展開的。 所以想要真正瞭解進程,必須事先瞭解操作系統,點擊進入 ...
  • 閱讀目錄 手工操作 —— 穿孔卡片 批處理 —— 磁帶存儲和批處理系統 多道程式系統 分時系統 實時系統 通用操作系統 操作系統的進一步發展 操作系統的作用 手工操作 —— 穿孔卡片 1946年第一臺電腦誕生 20世紀50年代中期,電腦工作還在採用手工操作方式。此時還沒有操作系統的概念。 程式員 ...
  • 講有監督學習的線性回歸。 線性回歸是利用數理統計中的回歸分析,來確定兩種或兩種以上變數間相互依賴的定量關係的一種統計分析方法。 只有一個自變數的回歸稱簡單回歸,大於一個變數的情況稱多元回歸。 用途:預測、分析變數與因變數關係的強度。 實例:對房屋尺寸與房價進行線性回歸,預測房價。 分析:數據可視化, ...
  • 1. 什麼事面向對象?主要特征是什麼? 面向對象是程式的一種設計方式,它利於提高程式的重用性,使程式結構更加清晰。主要特征:封裝、繼承、多態。 更多學習內容請訪問: 怎麼從一名碼農成為架構師的必看知識點:目錄大全(不定期更新) 2. SESSION 與 COOKIE的區別是什麼,請從協議,產生的原因 ...
  • 1.代碼 2.定義類 3.註釋 4.定義變數 5.聲明方法 6.常用數據類型 7.運算符 1. 算數運算符 | 操作符 | 名稱 | 描述 | | | | | | + | 加法 | 相加運算符兩側的值 | | – | 減法 | 左操作數減去右操作數 | | | 乘法 | 相乘操作符兩側的值 | | ...
  • 7 Python是如何進行記憶體管理的? http://developer.51cto.com/art/201007/213585.htm Python引用了一個記憶體池(memory pool)機制,即Pymalloc機制(malloc:n.分配記憶體),用於管理對小塊記憶體的申請和釋放 記憶體池(memo ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...