用Java Socket手撸了一个HTTP服务器

作为一个 Java 后端,提供 HTTP 服务可以说是基本技能之一了,但是你真的了解 HTTP 协议么?你知道知道如何手撸一个 HTTP 服务器么?Tomcat 的底层是怎么支持 HTTP 服务的呢?大名鼎鼎的 Servlet 又是什么东西呢,该怎么使用呢?

在初学 Java 时,Socket 编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为 0, 但并不意味着不用学。本篇将主要介绍如何使用 Socket 来实现一个简单的 HTTP 服务器,提供常见的 get/post 请求支持,并在此过程中了解下 HTTP 协议。

I. HTTP 服务器从 0 到 1

既然我们的目标是借助 Socket 来搭建 HTTP 服务器,那么我们首先需要确认两点,一是如何使用 Socket;另一个则是 HTTP 协议如何解析数据;下面分别进行说明。

1. Socket 编程基础

我们这里主要是利用 ServerSocket 来绑定端口,提供 TCP 服务,基本使用姿势也比较简单,一般套路如下

  • 创建 ServerSocket 对象,绑定监听端口
  • 通过 accept() 方法监听客户端请求
  • 连接建立后,通过输入流读取客户端发送的请求信息
  • 通过输出流向客户端发送响应信息
  • 关闭相关资源
    对应的伪代码如下:
ServerSocket serverSocket = new ServerSocket(port, ip)
serverSocket.accept();
// 接收请求数据
socket.getInputStream();// 返回数据给请求方
out = socket.getOutputStream()
out.print(xxx)
out.flush();;// 关闭连接
socket.close()

具体的代码,我们前面的章节详细地讲过了,第一次来的小伙伴可以戳链接去学习一下。

2. HTTP 协议

ServerSocket 走的是 TCP 协议,HTTP 协议本身是在 TCP 协议之上的一层。

TCP 是一种面向连接的、可靠的、基于字节流的传输层协议。TCP 在两个网络节点之间提供了一条可靠的通信信道,确保数据在传输过程中不会丢失、重复或乱序。TCP 使用握手过程建立连接,通过确认和重传机制确保数据可靠传输,并使用流量控制和拥塞控制算法来优化网络性能。

HTTP 是一个用于在 Web 浏览器和 Web 服务器之间传输超文本、图像、视频和其他媒体资源的应用层协议。HTTP 使用请求-响应模型,即客户端(通常是 Web 浏览器)发送请求给服务器,服务器处理请求并返回响应。HTTP 协议定义了一组方法(如 GET、POST、PUT、DELETE 等),用于指定请求的类型和目的。此外,HTTP 协议还定义了一组状态代码(如 200、404、500 等),用于表示响应的结果。

HTTP 协议依赖于 TCP 协议来传输数据。当 Web 浏览器向 Web 服务器发送 HTTP 请求时,它首先使用 TCP 协议与服务器建立连接。一旦连接建立,HTTP 请求消息会被封装在 TCP 数据包中,然后通过 TCP 信道发送给服务器。服务器收到 TCP 数据包后,解包提取 HTTP 请求消息,处理请求并生成 HTTP 响应消息。最后,HTTP 响应消息被封装在 TCP 数据包中,并通过相同的 TCP 信道发送回客户端。客户端收到 TCP 数据包后,解包提取 HTTP 响应消息并显示给用户。
在这里插入图片描述
这幅图展示了客户端(Web 浏览器)与服务器(Web 服务器)之间的 HTTP 请求和响应,它们通过可靠的、面向连接的 TCP 连接进行数据传输。

好,再说回 HTTP 服务器这件事,最需要关注的无非两点:

请求消息

HTTP 请求消息由请求行(Request Line)、请求头(Request Headers)、空行(Empty Line)、请求体(Request Body,可选)几个部分组成。

①、请求行又包含三个部分,HTTP 方法(例如 GET, POST, PUT, DELETE 等)、请求的目标 URL(通常是相对 URL,但也可以是绝对 URL)、HTTP 版本(例如 HTTP/1.1 或 HTTP/2),这些部分用空格分隔,例如:

GET /index.html HTTP/1.1

②、请求头是一系列以键值对表示的元数据,用于描述请求的附加信息。每个请求头占一行,键和值之间用冒号(:)分隔。请求头包含诸如 Host、User-Agent、Content-Type、Content-Length、Accept 等信息。例如:

Host: www.tobebetterjavaer.com
User-Agent: Mozilla/5.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

③、请求头和请求体之间有一个空行,表示请求头的结束。
④、对于某些 HTTP 方法(例如 POST、PUT 等),还可以在请求消息中包含请求体。请求体用于传输要发送给服务器的数据。请求体的格式和内容取决于 Content-Type 请求头的值。

例如,当提交 HTML 表单时,请求体可能如下所示:

username=恒二哥&password=123456

将这些部分放在一起,就构成了一个完整的 HTTP 请求消息:

POST /login HTTP/1.1
Host: Host: www.tobebetterjavaer.com
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
username=恒二哥&password=123456

在这里插入图片描述

响应消息

一个典型的 HTTP 响应消息由三部分组成:状态行(Status Line)、响应头(Response Headers)、响应体(Response Body)。
在这里插入图片描述

上面两张图,可以让你对 HTTP 请求和响应有个直观映象,接下来开始抓重点。
不管是请求消息还是响应消息,都可以划分为三部分,这就为我们后面的处理简化了很多工作。

  • 第一行:状态行
  • 第二行到第一个空行:header(请求头/相应头)
  • 剩下所有:正文

3. HTTP 服务器设计

接下来进入正题,基于 Socket 创建一个 HTTP 服务器,使用 Socket 基本没啥太大的问题,我们需要额外关注以下两点:

  • 对请求数据进行解析
  • 封装返回结果

a. 请求数据解析

注意📢,这些代码放在 HttpMessageParser 类中,随后会给出完整的代码。

@Data
public static class Request {/*** 请求方法 GET/POST/PUT/DELETE/OPTION...*/private String method;/*** 请求的uri*/private String uri;/*** HTTP版本*/private String version;/*** 请求头*/private Map<String, String> headers;/*** 请求参数相关*/private String message;
}

根据前面的 HTTP 协议介绍,解析过程如下,我们先看请求行的解析过程。

请求行,包含三个基本要素:请求方法 + URI + HTTP 版本,用空格进行分割,所以解析代码如下

/*** 根据标准的HTTP协议,解析请求行** @param reader* @param request*/
private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {String[] strs = StringUtils.split(reader.readLine(), " ");assert strs.length == 3;request.setMethod(strs[0]);request.setUri(strs[1]);request.setVersion(strs[2]);
}

**请求头的解析,**从第二行,到第一个空白行之间的所有数据,都是请求头;请求头的格式也比较清晰,形如 key:value, 具体实现如下:

最后就是正文的解析了,这一块需要注意一点,正文可能为空,也可能有数据;有数据时,我们要如何把所有的数据都取出来呢?

先看具体实现如下:

/*** 根据标注HTTP协议,解析正文** @param reader    输入流读取器,用于读取请求中的数据* @param request   Request 对象,表示 HTTP 请求* @throws IOException 当发生 I/O 错误时抛出*/
private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {// 从请求头中获取 Content-Length,如果没有,则默认为 0int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));// 如果 Content-Length 为 0,表示没有请求正文,直接返回。// 例如 GET 和 OPTIONS 请求通常不包含请求正文if (contentLen == 0) {return;}// 根据 Content-Length 创建一个字符数组来存储请求正文char[] message = new char[contentLen];// 使用 BufferedReader 读取请求正文reader.read(message);// 将字符数组转换为字符串,并将其设置为 Request 对象的 messagerequest.setMessage(new String(message));
}

注意上面我的使用姿势,首先是根据请求头中的Content-Type的值,来获得正文的数据大小,因此我们获取的方式是创建一个这么大的char[] 数组来读取流中所有数据,如果我们的数组比实际的小,则读不完;如果大,则数组中会有一些空的数据;

最后将上面的几个解析封装一下,完成 request 解析:

/*** HTTP 请求可以分为三部分:* 1. 请求行:包括请求方法、URI 和 HTTP 协议版本* 2. 请求头:从第二行开始,直到一个空行为止* 3. 消息正文:紧跟在空行后的所有内容,长度由请求头中的 Content-Length 决定** 本方法将 InputStream 中的 HTTP 请求数据解析为一个 Request 对象** @param reqStream  包含 HTTP 请求数据的输入流* @return           一个表示 HTTP 请求的 Request 对象* @throws IOException 当发生 I/O 错误时抛出*/
public static Request parse2request(InputStream reqStream) throws IOException {// 使用 BufferedReader 和 InputStreamReader 读取输入流中的数据BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));// 创建一个新的 Request 对象Request httpRequest = new Request();// 解析请求行并设置到 Request 对象中decodeRequestLine(httpReader, httpRequest);// 解析请求头并设置到 Request 对象中decodeRequestHeader(httpReader, httpRequest);// 解析消息正文并设置到 Request 对象中decodeRequestMessage(httpReader, httpRequest);// 返回解析后的 Request 对象return httpRequest;
}

接下来,是请求结果的封装,给一个简单的进行演示:

/*** Response 类表示一个 HTTP 响应,包括版本、状态码、状态信息、响应头和响应正文。*/
@Data
public static class Response {private String version;private int code;private String status;private Map<String, String> headers;private String message;
}/*** 根据给定的 Request 对象和响应字符串构建一个 HTTP 响应。** @param request   用于构建响应的 Request 对象* @param response  响应字符串* @return          一个表示 HTTP 响应的字符串*/
public static String buildResponse(Request request, String response) {// 创建一个新的 Response 对象,并设置版本、状态码和状态信息Response httpResponse = new Response();httpResponse.setCode(200);httpResponse.setStatus("ok");httpResponse.setVersion(request.getVersion());// 设置响应头Map<String, String> headers = new HashMap<>();headers.put("Content-Type", "application/json");headers.put("Content-Length", String.valueOf(response.getBytes().length));httpResponse.setHeaders(headers);// 设置响应正文httpResponse.setMessage(response);// 构建响应字符串StringBuilder builder = new StringBuilder();buildResponseLine(httpResponse, builder);buildResponseHeaders(httpResponse, builder);buildResponseMessage(httpResponse, builder);return builder.toString();
}/*** 构建响应行,包括版本、状态码和状态信息。** @param response      用于构建响应行的 Response 对象* @param stringBuilder 用于拼接响应字符串的 StringBuilder 对象*/
private static void buildResponseLine(Response response, StringBuilder stringBuilder) {stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ").append(response.getStatus()).append("\n");
}/*** 构建响应头。** @param response      用于构建响应头的 Response 对象* @param stringBuilder 用于拼接响应字符串的 StringBuilder 对象*/
private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");}stringBuilder.append("\n");
}/*** 构建响应正文。** @param response      用于构建响应正文的 Response 对象* @param stringBuilder 用于拼接响应字符串的 StringBuilder 对象*/
private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {stringBuilder.append(response.getMessage());
}

b. 请求任务 HttpTask

每个请求,单独分配一个任务来干这个事情,就是为了支持并发,对于 ServerSocket 而言,接收到了一个请求,那就创建一个 HttpTask 任务来实现 HTTP 通信。

那么这个 httptask 干啥呢?

  • 从请求中捞数据
  • 响应请求
  • 封装结果并返回
/*** HttpTask 类实现了 Runnable 接口,用于处理一个 HTTP 请求。* 当在一个线程中执行时,该任务将处理一个 Socket 连接上的 HTTP 请求,* 并发送响应消息。*/
public class HttpTask implements Runnable {// 用于处理 HTTP 请求的 Socketprivate Socket socket;/*** 构造一个新的 HttpTask,用于处理指定的 Socket 连接。** @param socket  用于处理 HTTP 请求的 Socket*/public HttpTask(Socket socket) {this.socket = socket;}/*** 实现 Runnable 接口的 run 方法,用于处理 HTTP 请求并发送响应消息。*/@Overridepublic void run() {// 检查 socket 是否为 null,如果为 null 则抛出异常if (socket == null) {throw new IllegalArgumentException("socket can't be null.");}try {// 获取 Socket 的输出流,并创建一个 PrintWriter 对象OutputStream outputStream = socket.getOutputStream();PrintWriter out = new PrintWriter(outputStream);// 从 Socket 的输入流中解析 HTTP 请求HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream());try {// 根据请求结果进行响应,省略返回String result = null;// 根据请求和结果构建 HTTP 响应String httpRes = HttpMessageParser.buildResponse(httpRequest, result);// 将 HTTP 响应发送到客户端out.print(httpRes);} catch (Exception e) {// 如果发生异常,构建一个包含异常信息的 HTTP 响应String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString());out.print(httpRes);}// 刷新输出流,确保响应消息被发送out.flush();} catch (IOException e) {e.printStackTrace();} finally {// 关闭 Socket 连接try {socket.close();} catch (IOException e) {e.printStackTrace();}}}
}

c. HTTP 服务搭建

前面的基本上把该干的事情都干了,剩下的就简单了,创建ServerSocket,绑定端口接收请求,我们在线程池中跑这个 HTTP 服务

public class BasicHttpServer {// 创建一个单线程执行器,用于启动 HTTP 服务器private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor();// 创建一个线程池,用于处理来自客户端的 HTTP 请求private static ExecutorService taskExecutor;// 设置服务器监听的端口号private static int PORT = 8999;// 启动 HTTP 服务器的方法static void startHttpServer() {// 获取处理器可用核心数,用于设置线程池大小int nThreads = Runtime.getRuntime().availableProcessors();// 初始化线程池,设置线程池大小,队列大小和丢弃策略taskExecutor =new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100),new ThreadPoolExecutor.DiscardPolicy());// 循环尝试启动服务器,如果启动失败,则等待10秒后重试while (true) {try {ServerSocket serverSocket = new ServerSocket(PORT);bootstrapExecutor.submit(new ServerThread(serverSocket));break;} catch (Exception e) {try {// 重试,等待 10 秒TimeUnit.SECONDS.sleep(10);} catch (InterruptedException ie) {Thread.currentThread().interrupt();}}}// 关闭启动执行器bootstrapExecutor.shutdown();}// HTTP 服务器主要任务类private static class ServerThread implements Runnable {// 保存传递给构造函数的 ServerSocket 实例private ServerSocket serverSocket;// 构造函数public ServerThread(ServerSocket s) throws IOException {this.serverSocket = s;}// 任务主体方法@Overridepublic void run() {while (true) {try {// 等待客户端连接Socket socket = this.serverSocket.accept();// 创建一个 HttpTask 实例,将 Socket 实例作为参数传递HttpTask eventTask = new HttpTask(socket);// 将 HttpTask 提交给 taskExecutor 执行taskExecutor.submit(eventTask);} catch (Exception e) {e.printStackTrace();try {// 如果发生异常,等待 1 秒后继续尝试TimeUnit.SECONDS.sleep(1);} catch (InterruptedException ie) {Thread.currentThread().interrupt();}}}}}
}

这段代码是一个简单的 HTTP 服务器实现。以下是关于这个 HTTP 服务器的主要组件和功能的详细解释:

1、bootstrapExecutor:一个单线程的 ExecutorService,用于执行 HTTP 服务器的启动任务。

2、taskExecutor:一个线程池,用于处理来自客户端的 HTTP 请求。线程池的大小等于处理器可用核心数,队列大小为100,使用 DiscardPolicy 丢弃策略。

3、PORT:服务器侦听的端口号,默认为 8999。

4、startHttpServer() 方法:

  • a.创建一个线程池 taskExecutor 用于处理 HTTP 请求。
  • b.在一个循环中,尝试创建一个 ServerSocket 实例并绑定到指定端口。如果失败,则等待 10 秒后重试。
  • c.当成功创建 ServerSocket 实例后,将其作为参数提交给 bootstrapExecutor 执行 ServerThread 任务。
  • d.关闭 bootstrapExecutor。
    5、ServerThread 类实现了 Runnable 接口,它是 HTTP 服务器的主要任务:
  • a.serverSocket 成员变量:保存传递给构造函数的 ServerSocket 实例。
  • b.run() 方法:
  • 在一个无限循环中,调用 serverSocket.accept() 方法等待客户端的连接。
  • 当接受到一个新的客户端连接时,创建一个 HttpTask 实例,将 Socket 实例作为参数传递。
  • 将 HttpTask 提交给 taskExecutor 执行。

这个 HTTP 服务器的主要逻辑是:使用一个线程来监听客户端连接,当有新的客户端连接时,创建一个 HttpTask 来处理客户端的 HTTP 请求,并将这个任务提交给线程池 taskExecutor 执行。这样可以实现多个客户端请求的并发处理。

到这里,一个基于 Socket 实现的 HTTP 服务器基本上就搭建完了,接下来就可以进行测试了

4. 测试

做这个服务器,主要是基于项目 quick-fix 产生的,这个项目主要是为了解决应用内部服务访问与数据订正,我们在这个项目的基础上进行测试。

一个完整的 post 请求如下
在这里插入图片描述
接下来我们看下打印出返回头的情况

在这里插入图片描述

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

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

相关文章

可观测性是什么?新手入门指南!

如果您之前对可观测性重要性&#xff0c;益处&#xff0c;以及组成不甚了解&#xff0c;本文是一个合适的指南手册。 什么是可观测性&#xff1f; 可观测性被定义为根据系统产生的输出数据&#xff08;如日志&#xff0c;指标和链路追踪&#xff09;来衡量当前系统运行状态的…

代码理解 pseudo_labeled = outputs.max(1)[1]

import torchls torch.Tensor([[0.2,0.4],[0.3,0.2]]) print(ls.max(1))

STL之deque容器代码详解

1 基础概念 功能&#xff1a; 双端数组&#xff0c;可以对头端进行插入删除操作。 deque与vector区别&#xff1a; vector对于头部的插入删除效率低&#xff0c;数据量越大&#xff0c;效率越低。 deque相对而言&#xff0c;对头部的插入删除速度回比vector快。 vector访问…

Unity Shader实现UI流光效果

效果&#xff1a; shader Shader "UI/Unlit/Flowlight" {Properties{[PerRendererData] _MainTex("Sprite Texture", 2D) "white" {}_Color("Tint", Color) (1, 1, 1, 1)[MaterialToggle] PixelSnap("Pixel snap", float…

简洁的在线观影开源项目

公众号&#xff1a;【可乐前端】&#xff0c;每天3分钟学习一个优秀的开源项目&#xff0c;分享web面试与实战知识。 每天3分钟开源 hi&#xff0c;这里是每天3分钟开源&#xff0c;很高兴又跟大家见面了&#xff0c;今天介绍的开源项目简介如下&#xff1a; 仓库名&#xff1…

海外媒体发稿:7种媒体套餐推广策略解析-华煤舍

有效的媒体宣传策略对于产品或服务的推广至关重要。本文将介绍7种媒体套餐推广策略&#xff0c;帮助您惊艳市场&#xff0c;并取得成功。以下是每种策略的拆解描述&#xff1a; 1. 广告投放 广告投放是最常见的宣传手段之一。通过在各种媒体平台上购买广告&#xff0c;如电视、…

Web渗透测试流程

什么是渗透测试 渗透测试 (penetration test),是通过模拟恶意黑客的攻击方法&#xff0c;来评估计算机网络系统安全的一种评估方法。这个过程包括对系统的任何弱点、技术缺陷或漏洞的主动分析&#xff0c;这个分析是从一个攻击者可能存在的位置来进行的&#xff0c;并且从这个…

linux 查看打开使用了哪些端口

你可以使用 netstat 命令来查看Linux系统中正在使用的端口。例如&#xff0c;要查看所有正在使用的TCP和UDP端口&#xff0c;你可以运行&#xff1a; sudo netstat -tulpn如果你只想查看所有正在使用的TCP端口&#xff0c;你可以运行&#xff1a; sudo netstat -tpln 如果你只…

全网最最最详细centos7如何安装docker教程

在CentOS 7上安装Docker主要包括以下步骤&#xff1a; 1. 卸载旧版本的Docker 首先&#xff0c;需要确保系统上没有安装旧版本的Docker。可以通过以下命令来卸载它们&#xff1a; sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-late…

【Linux】编译器-gcc/g++使用

个人主页 &#xff1a; zxctscl 文章封面来自&#xff1a;艺术家–贤海林 如有转载请先通知 文章目录 1. 前言2. 初见gcc和g3. 程序的翻译过程3.1 预处理3.1.1 宏替换 去注释 头文件展开3.1.2 条件编译 3.2 编译3.3 汇编3.4 链接 4. 链接4.1 动态链接4.2 静态链接 1. 前言 在之…

吴恩达机器学习-可选实验:特征缩放和学习率(多变量)

文章目录 目标工具概念问题陈述数据集多变量梯度下降学习率a9.9e-7a9e-7a1e-7 特征缩放实现 恭喜致谢 目标 在本实验中&#xff0c;你将: 利用在上一个实验中开发的多变量例程对具有多个特征的数据集运行梯度下降探讨学习率alpha对梯度下降的影响通过使用z-score归一化的特征…

域控操作十:安装包exe转msi软件下发

需要的文件 Advanced Installer 软件用来将exe转换成msi因为域控只能下发msi格式 一个exe安装包这里拿微信举例 一个没有密码的共享文件夹 1.exe转MSI 2&#xff0c;开始下发 服务器和用户刷新策略 #完成

某品零食交易平台设计与实现|基于springboot+ Mysql+Java的某品交易平台设计与实现(源码+数据库+文档+PPT)

目录 基于springboot MysqlJava的某品交易平台设计与实现 摘 要 系统详细设计 数据库设计 论文参考 源码获取 文末获取源码联系 基于springboot MysqlJava的某品交易平台设计与实现 摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的…

html--彩虹爱心

文章目录 js内容cssreset.min.cssstyle.css html内容 js内容 const colors ["#e03776","#8f3e98","#4687bf","#3bab6f","#f9c25e","#f47274"]; const SVG_NS http://www.w3.org/2000/svg; const SVG_XLINK &q…

Spring Boot 多环境配置

Spring Boot 多环境配置 在现代的软件开发中&#xff0c;通常需要将应用程序部署到不同的环境中&#xff0c;如开发环境、生产环境和测试环境等。每个环境可能需要不同的配置参数&#xff0c;例如数据库连接信息、日志级别等。在 Spring Boot 中&#xff0c;我们可以通过简单的…

智慧库室管控系统-智慧枪弹管控系统

项目背景&#xff1a; 针对部队装备管理现状&#xff0c;部队在进行硬件系统建设的同时&#xff0c;需通过装备管理系统软件的建设&#xff0c;综合集成监控、报警、出入库、信息发布、库管理、监测、防护等系统&#xff0c;对装备进行立体监控、实时预警、快速处理&#xff0…

springboot255基于spring boot的疫情信息管理系统

疫情信息管理系统的设计与实现 摘要 近年来&#xff0c;信息化管理行业的不断兴起&#xff0c;使得人们的日常生活越来越离不开计算机和互联网技术。首先&#xff0c;根据收集到的用户需求分析&#xff0c;对设计系统有一个初步的认识与了解&#xff0c;确定疫情信息管理系统…

【每日刷题】栈与队列-LC394、LC347、LC215

题外话&#xff1a;感觉脑子没长到栈这块…最近刷栈的题都好难啊…哭哭…坚持坚持&#xff01;多刷几遍就好了&#xff01;&#xff01; 1. LC394.字符串解码 题目链接 先说数据结构。 维护两个栈&#xff1a;一个栈存之前的字符串&#xff0c;另一个栈存之后的字符串的重复…

基于命名实体链接的事件抽取与知识图谱在电商领域的应用

开源项目推荐 多模态AI能力引擎平台: 免费的自然语言处理、情感分析、实体识别、图像识别与分类、OCR识别、语音识别接口&#xff0c;功能强大&#xff0c;欢迎体验。 多模态AI能力引擎平台: 免费的自然语言处理、情感分析、实体识别、图像识别与分类、OCR识别、语音识别接口…

mysql | 查询数据的过程|优化-->索引 |存储引擎

查询的过程 首先确认mysql 服务器是否启动 systemctl mysqld status 登录连接 mysql -h i p − u ip -u ip−uuser -p (-h 指定服务器ip -u 指定用户名 -p 指定密码) mysql 数据包 经过抓包分析&#xff08;mysql包其实就是基于tcp协议 3306端口) 传输采用mysql 协议&#xff0…