手把手搭建WebSocket多人在线聊天室(SpringBoot+WebSocket)
前言
本文中搭建了一个简易的多人聊天室,使用了WebSocket的基础特性。
源代码来自老外的一篇好文:
https://www.callicoder.com/spring-boot-websocket-chat-example/
本文内容摘要:
- 初步理解WebSocket的前后端交互逻辑
- 手把手使用 SpringBoot + WebSocket 搭建一个多人聊天室Demo
- 代码源码及其解释
- 前端展示页面
此外,在下一篇文章中,我们将做到:
- 对该WebSocket聊天室进行分布式改造,同时部署多台机器来作为集群,支撑高并发。
- 保存用户session,并且在集群上实现session同步,比如实时展示当前在线的用户!
正文
WebSocket多人在线聊天室
本文工程源代码:
https://github.com/qqxx6661/springboot-websocket-demo
新建工程
我们新建一个SpringBoot2的项目工程,在默认依赖中,添加websocket依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket 配置
我们先来设置websocket的配置,新建config文件夹,在里面新建类WebSocketConfig
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@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服务器。
withSockJS()是用来为不支持websocket的浏览器启用后备选项,使用了SockJS。
方法名中的STOMP是来自Spring框架STOMP实现。 STOMP代表简单文本导向的消息传递协议。它是一种消息传递协议,用于定义数据交换的格式和规则。为啥我们需要这个东西?因为WebSocket只是一种通信协议。它没有定义诸如以下内容:如何仅向订阅特定主题的用户发送消息,或者如何向特定用户发送消息。我们需要STOMP来实现这些功能。
在configureMessageBroker方法中,我们配置一个消息代理,用于将消息从一个客户端路由到另一个客户端。
第一行定义了以“/app”开头的消息应该路由到消息处理方法(之后会定义这个方法)。
第二行定义了以“/topic”开头的消息应该路由到消息代理。消息代理向订阅特定主题的所有连接客户端广播消息。
在上面的示例中,我们使用的是内存中的消息代理。
之后也可以使用RabbitMQ或ActiveMQ等其他消息代理。
创建 ChatMessage 实体
ChatMessage用来在客户端和服务端中交互
我们新建model文件夹,创建实体类ChatMessage。
public class ChatMessage {
    private MessageType type;
    private String content;
    private String sender;
    public enum MessageType {
        CHAT,
        JOIN,
        LEAVE
    }
    public MessageType getType() {
        return type;
    }
    public void setType(MessageType type) {
        this.type = type;
    }
    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;
    }
}
实体中,有三个字段:
- type:消息类型
- content:消息内容
- sender:发送者
类型有三种:
- CHAT: 消息
- JOIN:加入
- LEAVE:离开
创建Controller来接收和发送消息
创建controller文件夹,在controller文件夹添加类ChatController
import com.example.websocketdemo.model.ChatMessage;
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("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }
    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage,
                               SimpMessageHeaderAccessor headerAccessor) {
        // Add username in web socket session
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }
}
代码解释:
我们在websocket配置中,从目的地以/app开头的客户端发送的所有消息都将路由到这些使用@MessageMapping注释的消息处理方法。
例如,具有目标/app/chat.sendMessage的消息将路由到sendMessage()方法,并且具有目标/app/chat.addUser的消息将路由到addUser()方法
添加WebSocket事件监听
完成了上述代码后,我们还需要对socket的连接和断连事件进行监听,这样我们才能广播用户进来和出去等操作。
创建listener文件夹,新建WebSocketEventListener类
import com.example.websocketdemo.model.ChatMessage;
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 messagingTemplate;
    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("Received a new web socket connection");
    }
    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if(username != null) {
            logger.info("User Disconnected : " + username);
            ChatMessage chatMessage = new ChatMessage();
            chatMessage.setType(ChatMessage.MessageType.LEAVE);
            chatMessage.setSender(username);
            messagingTemplate.convertAndSend("/topic/public", chatMessage);
        }
    }
}
代码解释:
我们已经在ChatController中定义的addUser()方法中广播了用户加入事件。因此,我们不需要在SessionConnected事件中执行任何操作。
在SessionDisconnect事件中,编写代码用来从websocket会话中提取用户名,并向所有连接的客户端广播用户离开事件。
创建前端聊天室页面
我们在src/main/resources文件下创建前端文件,结构类似这样:
static
  └── css
       └── main.css
  └── js
       └── main.js
  └── index.html
1. HTML文件 index.html
HTML文件包含用于显示聊天消息的用户界面。 它包括sockjs和stomp 两个js库。
SockJS是一个WebSocket客户端,它尝试使用本机WebSockets,并为不支持WebSocket的旧浏览器提供支持。 STOMP JS是javascript的stomp客户端。
笔者在文件里使用了国内的CDN源
<!DOCTYPE html>
<html>
  <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
      <title>Spring Boot WebSocket Chat Application</title>
      <link rel="stylesheet" href="/css/main.css" />
  </head>
  <body>
    <noscript>
      <h2>Sorry! Your browser doesn't support Javascript</h2>
    </noscript>
    <div id="username-page">
        <div class="username-page-container">
            <h1 class="title">Type your username</h1>
            <form id="usernameForm" name="usernameForm">
                <div class="form-group">
                    <input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control" />
                </div>
                <div class="form-group">
                    <button type="submit" class="accent username-submit">Start Chatting</button>
                </div>
            </form>
        </div>
    </div>
    <div id="chat-page" class="hidden">
        <div class="chat-container">
            <div class="chat-header">
                <h2>Spring WebSocket Chat Demo</h2>
            </div>
            <div class="connecting">
                Connecting...
            </div>
            <ul id="messageArea">
            </ul>
            <form id="messageForm" name="messageForm">
                <div class="form-group">
                    <div class="input-group clearfix">
                        <input type="text" id="message" placeholder="Type a message..." autocomplete="off" class="form-control"/>
                        <button type="submit" class="primary">Send</button>
                    </div>
                </div>
            </form>
        </div>
    </div>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="/js/main.js"></script>
  </body>
</html>
2. JavaScript main.js
添加连接到websocket端点以及发送和接收消息所需的javascript。
'use strict';
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var 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');
        var socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}
function onConnected() {
    // Subscribe to the Public Topic
    stompClient.subscribe('/topic/public', onMessageReceived);
    // Tell your username to the server
    stompClient.send("/app/chat.addUser",
        {},
        JSON.stringify({sender: username, type: 'JOIN'})
    )
    connectingElement.classList.add('hidden');
}
function onError(error) {
    connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
    connectingElement.style.color = 'red';
}
function sendMessage(event) {
    var messageContent = messageInput.value.trim();
    if(messageContent && stompClient) {
        var chatMessage = {
            sender: username,
            content: messageInput.value,
            type: 'CHAT'
        };
        stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
        messageInput.value = '';
    }
    event.preventDefault();
}
function onMessageReceived(payload) {
    var message = JSON.parse(payload.body);
    var messageElement = document.createElement('li');
    if(message.type === 'JOIN') {
        messageElement.classList.add('event-message');
        message.content = message.sender + ' joined!';
    } else if (message.type === 'LEAVE') {
        messageElement.classList.add('event-message');
        message.content = message.sender + ' left!';
    } 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()函数使用SockJS和stomp客户端连接到我们在Spring Boot中配置的/ws端点。
成功连接后,客户端订阅/topic/public,并通过向/app/chat.addUser目的地发送消息将该用户的名称告知服务器。
stompClient.subscribe()函数采用一种回调方法,只要消息到达订阅主题,就会调用该方法。
其它的代码用于在屏幕上显示和格式化消息。
3. CSS 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 #messageForm {
    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;
}
#messageForm .input-group input {
    float: left;
    width: calc(100% - 85px);
}
#messageForm .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);
    }
    #messageForm .input-group button {
        width: 65px;
    }
    #messageForm .input-group input {
        width: calc(100% - 70px);
    }
    .chat-header {
        padding: 10px;
    }
    .connecting {
        top: 60px;
    }
    .chat-header h2 {
        font-size: 1.1em;
    }
}
整个项目结构如下:
启动
启动SpringBoot项目
效果入下:
补充:使用RabbitMQ代替内存作为消息代理
添加依赖:
<!-- 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类中configureMessageBroker方法改为使用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");
}
如此一来,便可以通过RabbitMq进行消息的订阅。
总结
我们在本文中搭建了一个简易的多人聊天室,使用了WebSocket的特性。
本文工程源代码:
https://github.com/qqxx6661/springboot-websocket-demo
在下一篇文章中,我们将做到:
- WebSocket进行分布式改造,同时部署多台机器来作为集群,支撑高并发。
- 保存用户session,并且在集群上实现session同步,比如实时展示当前在线的用户!
下一篇文章过两天奉上,也可以关注我的公众号:Rude3Knife,就不会忘记了看,哈哈哈哈哈。
关注我
我目前是一名后端开发工程师。主要关注后端开发,数据安全,爬虫,边缘计算等方向。
微信:yangzd1102
Github:@qqxx6661
个人博客:
- CSDN:@Rude3Knife
- 知乎:@Zhendong
- 简书:@蛮三刀把刀
- 掘金:@蛮三刀把刀
原创博客主要内容
- Java知识点复习全手册
- Leetcode算法题解析
- 剑指offer算法题解析
- SpringCloud菜鸟入门实战系列
- SpringBoot菜鸟入门实战系列
- 爬虫相关技术文章
- 后端开发相关技术文章
个人公众号:后端技术漫谈
如果文章对你有帮助,不妨收藏起来并转发给您的朋友们~
手把手搭建WebSocket多人在线聊天室(SpringBoot+WebSocket)的更多相关文章
- 基于JQuery+JSP的无数据库无刷新多人在线聊天室
		JQuery是一款非常强大的javascript插件,本文就针对Ajax前台和JSP后台来实现一个无刷新的多人在线聊天室,该实现的数据全部存储在服务端内存里,没有用到数据库,本文会提供所有源程序,需要 ... 
- Node.js下基于Express + Socket.io 搭建一个基本的在线聊天室
		一.聊天室简单介绍 采用nodeJS设计,基于express框架,使用WebSocket编程之 socket.io机制.聊天室增加了 注册登录模块 ,并将用户个人信息和聊天记录存入数据库. 数据库采用 ... 
- javaweb学习路之三--websocket多人在线聊天
		在之前的项目基础上,加入了一个聊天室的功能,为了界面好看 引入了AmazeUI和umeditor最终效果图如下: 源码在 https://github.com/Zering/MyWeb 目前练习都在这 ... 
- 用NodeJS打造多人在线聊天室(NodeJS & SocketIO & Express & EJS & MongoDB & Gulp)
		项目背景 这个项目主要是为了玩玩NodeJS,项目的方向大概是做出类似QQ的在线聊天系统.想要在线体验可以点击在线演示. 项目使用PM2进行部署和管理,功能在不断的迭代开发中.如果你觉得这个项目比较有 ... 
- php基于websocket实现的在线聊天室
		听说websocket技术可以实现 1. 
- webSocket前端+nodejs后端简单案例多人在线聊天
		一:下面是一个简单的案例,回车发送消息,多人在线聊天 1.前端代码 <!DOCTYPE html> <html> <head> <meta charset=& ... 
- 在线聊天室的实现(1)--websocket协议和javascript版的api
		前言: 大家刚学socket编程的时候, 往往以聊天室作为学习DEMO, 实现简单且上手容易. 该Demo被不同语言实现和演绎, 网上相关资料亦不胜枚举. 以至于很多技术书籍在讲解网络相关的编程时, ... 
- 使用WebSocket实现简单的在线聊天室
		前言:我自已在网上找好了好多 WebSocket 制作 在线聊天室的案列,发现大佬们写得太高深了 我这种新手看不懂,所以就自已尝试写了一个在线简易聊天室 (我只用了js 可以用jq ) 话不多说,直接 ... 
- JavaWeb网页聊天室(WebSocket即时通讯)
		原文:http://baike.xsoftlab.net/view/656.html Git地址 http://git.oschina.net/loopcc/WebSocketChat 概要: Web ... 
- 基于Server-Sent Event的简单在线聊天室
		Web即时通信 所谓Web即时通信,就是说我们可以通过一种机制在网页上立即通知用户一件事情的发生,是不需要用户刷新网页的.Web即时通信的用途有很多,比如实时聊天,即时推送等.如当我们在登陆浏览知乎时 ... 
随机推荐
- 鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
			一.mediaquery 1.概述 媒体查询(mediaquery)它允许根据设备的不同特性(如屏幕大小.屏幕方向.分辨率.颜色深度等)来动态地调整网页的样式和布局. 通过媒体查询,可以为不同的设备定 ... 
- verilog之monitor
			verilog之monitor 1.函数作用 monitor用于追踪变量的变化情况,这在实际使用中还是非常实用的.电路中的某个信号的变化可以通过monitor检测,不需要使用波形图去仔细查找.也便于准 ... 
- KingbaseES V8R3 集群运维系列 -- failover切换后集群自动恢复
			 案例说明: KingbaseES V8R3集群默认在触发failover切换后,为保证数据安全,原主库需要通过人工介入后,恢复为新的备库加入到集群.在无人值守的现场环境,需要在触发failover ... 
- LiftPool:双向池化操作,细节拉满,再也不怕丢特征了 | ICLR 2021
			论文参考信号处理中提升方案提出双向池化操作LiftPool,不仅下采样时能保留尽可能多的细节,上采样时也能恢复更多的细节.从实验结果来看,LiftPool对图像分类能的准确率和鲁棒性都有不错的提升,而 ... 
- 关于JDK21控制台字符集编码问题
			关于JDK21控制台字符集编码问题 前言: 某日尝试JDK21,idea控制台字符集编码一直乱码,后将idea所有能配置UTF-8的配置都配了一遍,无果,后搜索JDK21字符集编码相关后解决 1.配置 ... 
- #树形dp,直径#51nod 1812 树的双直径
			题目 给定一棵树,边权是整数 \(c_i\) ,找出两条不相交的链(没有公共点), 使得链长的乘积最大(链长定义为这条链上所有边的权值之和,如果这条链只有1个点则链长视为0). \(n\leq 4*1 ... 
- SEO — 搜索引擎优化
			一.多词排名标题设置 SEO(Search Engine Optimization)是指搜索引擎优化,即利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名 优化目的是:让网站在搜索引擎上的曝光率达到 ... 
- nginx重新整理——————分析log数据[六]
			前言 简单介绍一下goaccess. 正文 安装: yum install epel-release yum install GeoIP GeoIP-devel GeoIP-data yum inst ... 
- docker 应用篇————具名挂载和匿名挂载[十三]
			前言 简单整理一下具名挂载和匿名挂载. 正文 来看一下匿名挂载. 这里-v指定了容器内部的路径,但是没有指定容器外部的路径,那么挂载到了什么地方. 用inspect 查看一下. 挂载到这个位置了. 然 ... 
- 【笔记】go语言--切片的操作
			go语言--切片的操作 接上篇切片的概念开始 //Slice添加元素 arr := [...]int{0,1,2,3,4,5,6,7} s1 := arr[2:6] s2 := s1[3:5]//[5 ... 
