AI对话交互场景使用WebSocket建立H5客户端和服务端的信息实时双向通信

news2024/12/23 5:23:42

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

一、为什么需要 WebSocket?

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
在这里插入图片描述
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

二、简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
在这里插入图片描述
其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

在这里插入图片描述

服务端的实现

依赖spring-boot-starter-websocket模块实现WebSocket实时对话交互。

CustomTextWebSocketHandler,扩展的TextWebSocketHandler


import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.concurrent.CountDownLatch;

/**
 * 文本处理器
 *
 * @see org.springframework.web.socket.handler.TextWebSocketHandler
 */
@Slf4j
public class CustomTextWebSocketHandler extends TextWebSocketHandler {
    /**
     * 第三方身份,消息身份
     */
    private String thirdPartyId;
    /**
     * 回复消息内容
     */
    private String replyContent;
    private StringBuilder replyContentBuilder;
    /**
     * 完成信号
     */
    private final CountDownLatch doneSignal;

    public CustomTextWebSocketHandler(CountDownLatch doneSignal) {
        this.doneSignal = doneSignal;
    }

    public String getThirdPartyId() {
        return thirdPartyId;
    }

    public String getReplyContent() {
        return replyContent;
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("connection established, session={}", session);
        replyContentBuilder = new StringBuilder(16);
//        super.afterConnectionEstablished(session);
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        super.handleMessage(session, message);
    }

    /**
     * 消息已接收完毕("stop")
     */
    private static final String MESSAGE_DONE = "[DONE]";

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//        super.handleTextMessage(session, message);
        String payload = message.getPayload();
        log.info("payload={}", payload);
        OpenAiReplyResponse replyResponse = Jsons.fromJson(payload, OpenAiReplyResponse.class);
        if (replyResponse != null && replyResponse.isSuccess()) {
            String msg = replyResponse.getMsg();
            if (Strings.isEmpty(msg)) {
                return;
            } else if (msg.startsWith("【超出最大单次回复字数】")) {
                // {"msg":"【超出最大单次回复字数】该提示由GPT官方返回,非我司限制,请缩减回复字数","code":1,
                // "extParam":"{\"chatId\":\"10056:8889007174\",\"requestId\":\"b6af5830a5a64fa8a4ca9451d7cb5f6f\",\"bizId\":\"\"}",
                // "id":"chatcmpl-7LThw6J9KmBUOcwK1SSOvdBP2vK9w"}
                return;
            } else if (msg.startsWith("发送内容包含敏感词")) {
                // {"msg":"发送内容包含敏感词,请修改后重试。不合规汇如下:炸弹","code":1,
                // "extParam":"{\"chatId\":\"10024:8889006970\",\"requestId\":\"828068d945c8415d8f32598ef6ef4ad6\",\"bizId\":\"430\"}",
                // "id":"4d4106c3-f7d4-4393-8cce-a32766d43f8b"}
                matchSensitiveWords = msg;
                // 请求完成
                doneSignal.countDown();
                return;
            } else if (MESSAGE_DONE.equals(msg)) {
                // 消息已接收完毕
                replyContent = replyContentBuilder.toString();
                thirdPartyId = replyResponse.getId();
                // 请求完成
                doneSignal.countDown();
                log.info("replyContent={}", replyContent);
                return;
            }
            replyContentBuilder.append(msg);
        }
    }

    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
        super.handlePongMessage(session, message);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        replyContentBuilder = null;
        log.info("handle transport error, session={}", session, exception);
        doneSignal.countDown();
//        super.handleTransportError(session, exception);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        replyContentBuilder = null;
        log.info("connection closed, session={}, status={}", session, status);
        if (status == CloseStatus.NORMAL) {
            log.error("connection closed fail, session={}, status={}", session, status);
        }
        doneSignal.countDown();
//        super.afterConnectionClosed(session, status);
    }
}

OpenAiHandler


/**
 * OpenAI处理器
 */
public interface OpenAiHandler<Req, Rsp> {
    /**
     * 请求前置处理
     *
     * @param req 入参
     */
    default void beforeRequest(Req req) {
        //
    }

    /**
     * 响应后置处理
     *
     * @param req 入参
     * @param rsp 出参
     */
    default void afterResponse(Req req, Rsp rsp) {
        //
    }
}

OpenAiService


/**
 * OpenAI服务
 * <pre>
 * API reference introduction
 * https://platform.openai.com/docs/api-reference/introduction
 * </pre>
 */
public interface OpenAiService<Req, Rsp> extends OpenAiHandler<Req, Rsp> {
    /**
     * 补全指令
     *
     * @param req 入参
     * @return 出参
     */
    default Rsp completions(Req req) {
        beforeRequest(req);
        Rsp rsp = doCompletions(req);
        afterResponse(req, rsp);
        return rsp;
    }

    /**
     * 操作补全指令
     *
     * @param req 入参
     * @return 出参
     */
    Rsp doCompletions(Req req);
}

OpenAiServiceImpl


import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketClient;

import javax.annotation.Nullable;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * OpenAI服务实现
 */
@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OpenAiProperties.class)
@Service("openAiService")
public class OpenAiServiceImpl implements OpenAiService<CompletionReq, CompletionRsp> {

    private final OpenAiProperties properties;
    /**
     * 套接字客户端
     */
    private final WebSocketClient webSocketClient;
    /**
     * 模型请求记录服务
     */
    private final ModelRequestRecordService modelRequestRecordService;

    private static final String THREAD_NAME_PREFIX = "gpt.openai";

    public OpenAiServiceImpl(
            OpenAiProperties properties,
            ModelRequestRecordService modelRequestRecordService
    ) {
        this.properties = properties;
        this.modelRequestRecordService = modelRequestRecordService;
        webSocketClient = WebSocketUtil.applyWebSocketClient(THREAD_NAME_PREFIX);
        log.info("create OpenAiServiceImpl instance");
    }

    @Override
    public void beforeRequest(CompletionReq req) {
        // 请求身份
        if (Strings.isEmpty(req.getRequestId())) {
            req.setRequestId(UuidUtil.getUuid());
        }
    }

    @Override
    public void afterResponse(CompletionReq req, CompletionRsp rsp) {
        if (rsp == null || Strings.isEmpty(rsp.getReplyContent())) {
            return;
        }
        // 三方敏感词检测
        String matchSensitiveWords = rsp.getMatchSensitiveWords();
        if (Strings.isNotEmpty(matchSensitiveWords)) {
            // 敏感词命中
            rsp.setMatchSensitiveWords(matchSensitiveWords);
            return;
        }
        // 阶段任务耗时统计
        StopWatch stopWatch = new StopWatch(req.getRequestId());
        try {
            // 敏感词检测
            stopWatch.start("checkSensitiveWord");
            String replyContent = rsp.getReplyContent();
//            ApiResult<String> apiResult = checkMsg(replyContent, false);
//            stopWatch.stop();
//            if (!apiResult.isSuccess() && Strings.isNotEmpty(apiResult.getData())) {
//                // 敏感词命中
//                rsp.setMatchSensitiveWords(apiResult.getData());
//                return;
//            }
            // 记录落库
            stopWatch.start("saveModelRequestRecord");
            ModelRequestRecord entity = applyModelRequestRecord(req, rsp);
            modelRequestRecordService.save(entity);
        } finally {
            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }
            log.info("afterResponse execute time, {}", stopWatch);
        }
    }

    private static ModelRequestRecord applyModelRequestRecord(
            CompletionReq req, CompletionRsp rsp) {
        Long orgId = req.getOrgId();
        Long userId = req.getUserId();
        String chatId = applyChatId(orgId, userId);
        return new ModelRequestRecord()
                .setOrgId(orgId)
                .setUserId(userId)
                .setModelType(req.getModelType())
                .setRequestId(req.getRequestId())
                .setBizId(req.getBizId())
                .setChatId(chatId)
                .setThirdPartyId(rsp.getThirdPartyId())
                .setInputMessage(req.getMessage())
                .setReplyContent(rsp.getReplyContent());
    }

    private static String applyChatId(Long orgId, Long userId) {
        return orgId + ":" + userId;
    }

    private static String applySessionId(String appId, String chatId) {
        return appId + '_' + chatId;
    }

    private static final String URI_TEMPLATE = "wss://socket.******.com/websocket/{sessionId}";

    @Nullable
    @Override
    public CompletionRsp doCompletions(CompletionReq req) {
        // 阶段任务耗时统计
        StopWatch stopWatch = new StopWatch(req.getRequestId());
        stopWatch.start("doHandshake");

        // 闭锁,相当于一扇门(同步工具类)
        CountDownLatch doneSignal = new CountDownLatch(1);
        CustomTextWebSocketHandler webSocketHandler = new CustomTextWebSocketHandler(doneSignal);
        String chatId = applyChatId(req.getOrgId(), req.getUserId());
        String sessionId = applySessionId(properties.getAppId(), chatId);
        ListenableFuture<WebSocketSession> listenableFuture = webSocketClient
                .doHandshake(webSocketHandler, URI_TEMPLATE, sessionId);
        stopWatch.stop();
        stopWatch.start("getWebSocketSession");
        long connectionTimeout = properties.getConnectionTimeout().getSeconds();
        try (WebSocketSession webSocketSession = listenableFuture.get(connectionTimeout, TimeUnit.SECONDS)) {
            stopWatch.stop();
            stopWatch.start("sendMessage");
            OpenAiParam param = applyParam(chatId, req);
            webSocketSession.sendMessage(new TextMessage(Jsons.toJson(param)));
            long requestTimeout = properties.getRequestTimeout().getSeconds();
            // wait for all to finish
            boolean await = doneSignal.await(requestTimeout, TimeUnit.SECONDS);
            if (!await) {
                log.error("await doneSignal fail, req={}", req);
            }
            String replyContent = webSocketHandler.getReplyContent();
            String matchSensitiveWords = webSocketHandler.getMatchSensitiveWords();
            if (Strings.isEmpty(replyContent) && Strings.isEmpty(matchSensitiveWords)) {
                // 消息回复异常
                return null;
            }
            String delimiters = properties.getDelimiters();
            replyContent = StrUtil.replaceFirst(replyContent, delimiters, "");
            replyContent = StrUtil.replaceLast(replyContent, delimiters, "");
            String thirdPartyId = webSocketHandler.getThirdPartyId();
            return new CompletionRsp()
                    .setThirdPartyId(thirdPartyId)
                    .setReplyContent(replyContent)
                    .setMatchSensitiveWords(matchSensitiveWords);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            log.error("get WebSocketSession fail, req={}", req, e);
        } catch (IOException e) {
            log.error("sendMessage fail, req={}", req, e);
        } finally {
            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }
            log.info("doCompletions execute time, {}", stopWatch);
        }
        return null;
    }

    private static final int MIN_TOKENS = 11;

    /**
     * 限制单次最大回复单词数(tokens)
     */
    private static int applyMaxTokens(int reqMaxTokens, int maxTokensConfig) {
        if (reqMaxTokens < MIN_TOKENS || maxTokensConfig < reqMaxTokens) {
            return maxTokensConfig;
        }
        return reqMaxTokens;
    }

    private OpenAiParam applyParam(String chatId, CompletionReq req) {
        OpenAiDataExtParam extParam = new OpenAiDataExtParam()
                .setChatId(chatId)
                .setRequestId(req.getRequestId())
                .setBizId(req.getBizId());
        // 提示
        String prompt = req.getPrompt();
        // 分隔符
        String delimiters = properties.getDelimiters();
        String message = prompt + delimiters + req.getMessage() + delimiters;
        int maxTokens = applyMaxTokens(req.getMaxTokens(), properties.getMaxTokens());
        OpenAiData data = new OpenAiData()
                .setMsg(message)
                .setContext(properties.getContext())
                .setLimitTokens(maxTokens)
                .setExtParam(extParam);
        String sign = OpenAiUtil.applySign(message, properties.getSecret());
        return new OpenAiParam()
                .setData(data)
                .setSign(sign);
    }
}

WebSocketUtil


import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;

/**
 * WebSocket辅助方法
 */
public final class WebSocketUtil {
    /**
     * 创建一个新的WebSocket客户端
     */
    public static WebSocketClient applyWebSocketClient(String threadNamePrefix) {
        StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
        int cpuNum = Runtime.getRuntime().availableProcessors();
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(cpuNum);
        taskExecutor.setMaxPoolSize(200);
        taskExecutor.setDaemon(true);
        if (StringUtils.hasText(threadNamePrefix)) {
            taskExecutor.setThreadNamePrefix(threadNamePrefix);
        } else {
            taskExecutor.setThreadNamePrefix("gpt.web.socket");
        }
        taskExecutor.initialize();
        webSocketClient.setTaskExecutor(taskExecutor);
        return webSocketClient;
    }
}

OpenAiUtil


import org.springframework.util.DigestUtils;

import java.nio.charset.StandardCharsets;

/**
 * OpenAi辅助方法
 */
public final class OpenAiUtil {
    /**
     * 对消息内容进行md5加密
     *
     * @param message 消息内容
     * @param secret 加签密钥
     * @return 十六进制加密后的消息内容
     */
    public static String applySign(String message, String secret) {
        String data = message + secret;
        byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
        return DigestUtils.md5DigestAsHex(dataBytes);
    }
}

参考资料

  • WebSocket - 维基百科
  • WebSocket 教程 - 阮一峰
  • 使用WebSocket - 廖雪峰
  • WebSocket Support - Spring Framework
  • Messaging WebSockets - Spring Boot
  • Create WebSocket Endpoints Using @ServerEndpoint - “How-to” Guides - Spring Boot

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/603679.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

设计模式之~工厂系列(简单工厂、工厂方法、抽象工厂)

目录 简单工厂模式 工厂方法模式 简单工厂 VS 工厂方法 抽象工厂模式&#xff1a; 拓展&#xff1a; 利用简单工厂模式优化抽象工厂 利用反射抽象工厂 进行优化 反射配置文件抽象工厂进行优化 简单工厂模式 优点&#xff1a;简单工厂模式的最大优点在于工厂类包含…

Arthas-JVM相关命令使用

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 开头&#xff1a; 我们先说下生产使用频率较高的有哪些&#xff1a;dashboard、heapdump、jvm…

【mqtt】MQTT安装与入门案例

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍MQTT的c版本入门。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习知识&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&…

java高频面试题

集合 前言 时间复杂度 时间复杂度是用来来评估代码的执行耗时的&#xff0c;大O表示法&#xff1a;不具体表示代码的真正执行时间&#xff0c;而是表示代码执行时间随数据规模增长的变化趋势。 当n很大时&#xff0c;低阶、常量、系数并不能影响其增长趋势&#xff0c;因此可以…

压缩感知重构之广义正交匹配追踪法

算法的重构是压缩感知中重要的一步&#xff0c;是压缩感知的关键之处。因为重构算法关系着信号能否精确重建&#xff0c;国内外的研究学者致力于压缩感知的信号重建&#xff0c;并且取得了很大的进展&#xff0c;提出了很多的重构算法&#xff0c;每种算法都各有自己的优缺点&a…

RFID在工业自动化产线工艺中的应用

RFID在工业自动化产线工艺中的应用 随着工业自动化技术的不断发展&#xff0c;RFID&#xff08;Radio Frequency Identification&#xff09;技术在自动化产线数据采集方面得到了广泛应用。RFID技术是一种通过电磁波进行无线通信和识别的技术&#xff0c;它可以对物品进行追踪…

电子器件系列39:反激式变压器

反激式(Flyback)变压器又称单端反激式或Buck-Boost转换器。因其输出端在原边绕组断开电源时获得能量故而得名。反激式变换器以其电路结构简单&#xff0c;成本低廉而深受广大开发工程师的喜爱。 反激式变压器适合小功率电源以及各种电源适配器。但是反激式变换器的设计难点是变…

Oracle中的数据导出(1)

目录 1、基本语法&#xff1a; 2、操作步骤 3、spool作用 SPOOL命令的使用 在 Oracle 中&#xff0c;SPOOL 是一条 SQLPLUS 命令&#xff0c;用于将执行 SQL 脚本的输出结果保存到指定文件中。SPOOL 命令可以帮助用户快速导出查询结果、生成报表等常见任务。 1、基本语法&…

压缩感知重构算法之正交匹配追踪算法(OMP)

算法的重构是压缩感知中重要的一步&#xff0c;是压缩感知的关键之处。因为重构算法关系着信号能否精确重建&#xff0c;国内外的研究学者致力于压缩感知的信号重建&#xff0c;并且取得了很大的进展&#xff0c;提出了很多的重构算法&#xff0c;每种算法都各有自己的优缺点&a…

java-面向对象

java-面向对象 面向对象 首先考虑事物中存在哪些对象&#xff0c;再建立对象与对象的关系 一、面向对象-&#xff08;类和对象&#xff09; 1.1类和对象的理解 客观存在的事物皆为对象 &#xff0c;所以我们也常常说万物皆对象。 类 类的理解 类是对现实生活中一类具有共同属性…

java-字符流和字节流(一)

java-字符流和字节流(一) 一、File类 1.1 File类概述和构造方法 File类介绍 它是文件和目录路径名的抽象表示文件和目录是可以通过File封装成对象的对于File而言&#xff0c;其封装的并不是一个真正存在的文件&#xff0c;仅仅是一个路径名而已。它可以是存在的&#xff0c;也…

【C++ 程序设计】第 1 章:C++ 语言简介

目录 一、C 语言的发展简史 二、C 语言的特点 &#xff08;1&#xff09;基本的输入/输出 &#xff08;2&#xff09;头文件和命名空间 &#xff08;3&#xff09;强制类型转换运算符 &#xff08;4&#xff09;函数参数的默认值 &#xff08;5&#xff09;引用和函数…

超简单好看的HTML5七夕情人节表白网页(表白直接拿去用) HTML+CSS+JS

博主&#xff1a;命运之光 专栏&#xff1a;web开发&#xff08;html css js&#xff09; 目录 ✨简介&#xff1a; ✨前言&#xff1a; ✨视频展示 ✨源代码 ✨代码的使用方法&#xff08;超简单什么都不用下载&#xff09; &#x1f353;1.打开记事本 &#x1f353;2.将…

springboot+vue高校食堂点餐送餐配送系统

食堂送餐系统的开发过程中&#xff0c;采用B / S架构&#xff0c;主要使用java语言进行开发&#xff0c;结合最新流行的springboot框架。使用Mysql数据库和Eclipse/idea开发工具。该四川工商学院食堂送餐系统包括用户、商家、送餐员和管理员。其主要功能包括商家管理、用户管理…

【Azure】微软 Azure 基础解析(八)Azure 存储服务:Blob存储、队列存储、文件存储等特点与应用场景

本系列博文还在更新中&#xff0c;收录在专栏&#xff1a;「Azure探秘&#xff1a;构建云计算世界」 专栏中。 本系列文章列表如下&#xff1a; 【Azure】微软 Azure 基础解析&#xff08;三&#xff09;描述云计算运营中的 CapEx 与 OpEx&#xff0c;如何区分 CapEx 与 OpEx…

chatgpt赋能python:Python创建节点:简单易行的SEO优化技巧

Python创建节点&#xff1a;简单易行的SEO优化技巧 简介 在今天的数字时代&#xff0c;拥有强大在线存在感已经成为了非常重要的一种要素&#xff0c;而搜索引擎优化&#xff08;SEO&#xff09;就是其中重要的一环。优秀的SEO技巧不仅能够帮助网站吸引更多的访客&#xff0c…

chatgpt赋能python:Python编程实现高效的SEO搜索程序

Python编程实现高效的SEO搜索程序 在当今互联网时代&#xff0c;搜索引擎是人类获取信息的主要途径&#xff0c;而优化搜索引擎结果从而使自己的网站得到更多展示机会是一直以来网站优化重要的一环。Python语言以其简洁、高效、易学的特点&#xff0c;成为了编写高效SEO搜索程…

Linux 实操篇--定时任务调度

Linux 实操篇-定时任务调度 crond 任务调度 crontab 进行定时任务的设置 概述 任务调度&#xff1a;是指系统在某个时间执行的特定的命令或程序。 任务调度分类&#xff1a;1.系统工作&#xff1a;有些重要的工作必须周而复始地执行。如病毒扫描等 个别用户工作&#xff…

java-基础语法(二)

java-基础语法(二) 一、流程控制语句 1.1 流程控制语句分类 顺序结构 分支结构(if, switch) 循环 结构(for, while, do…while) 1.2 顺序结构 顺序结构执行流程图&#xff1a; 1.3 分支结构之if语句 if语句格式1 格式&#xff1a;if (关系表达式) {语句体; }执行流程&…

04.JavaWeb-Tomcat服务器+Maven

1.B/S架构 B/S架构即浏览器/服务器模式&#xff0c;他是对C/S架构的一种改进&#xff1b;与C/S架构相比B/S架构可以实现跨平台&#xff0c;客户端零维护&#xff0c;但是个性化能力低&#xff0c;响应速度较慢。 2.Tomcat服务器 Tomcat是一个用于运行Java Web应用程序的服务器&…