从 0 实现一个文件搜索工具 (Java 项目)

embedded/2024/10/22 16:47:37/

背景

各文件系统下, 都有提供文件查找的功能, 但是一般而言搜索速度很慢
在这里插入图片描述
项目仿照 everything 工具, 实现本地文件的快速搜索

实现功能

  1. 选择指定本地目录, 根据输入的信息, 进行搜索, 显示指定目录下的匹配文件信息
  2. 文件夹包含中文时, 支持汉语拼音搜索 (全拼 / 首字母匹配)

相关技术

Java + Servlet + Pinyin4j
JDBC + SQLite (SQLite 相对于 MySQL 更加轻量, 并且引入 jar 包即可使用, 不必安装配套应用)
JavaFx

数据库设计

在这里插入图片描述

SQLite 创建 SQL 的语句如下

java">create table if not exists file_meta (id INTEGER primary key autoincrement,name varchar(50) not null,path varchar(512) not null,is_directory boolean not null,pinyin varchar(100) not null,pinyin_first varchar(50) not null,size BIGINT not null,last_modified timestamp not null
);

项目的基本框架

在这里插入图片描述

前端页面

在这里插入图片描述

app.fxml 文件

显示界面的图画化结构

java"><?xml version="1.0" encoding="UTF-8"?><?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?><?import javafx.geometry.Insets?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<GridPane fx:controller="gui.GUIController" fx:id="gridPane" vgap="10" alignment="center" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"><children ><Button fx:id="button"  onMouseClicked="#choose" prefWidth="90" text="选择目录" GridPane.rowIndex="0" GridPane.columnIndex="0"></Button><Label fx:id="label" text="当前未选择目录" GridPane.rowIndex="0" GridPane.columnIndex="0"><GridPane.margin><Insets left="100"></Insets></GridPane.margin></Label><TextField fx:id="textField" prefWidth="900" GridPane.rowIndex="1" GridPane.columnIndex="0" ></TextField><TableView fx:id="tableView" prefWidth="900" prefHeight="700" GridPane.rowIndex="2" GridPane.columnIndex="0"><columns><TableColumn prefWidth="220" text="文件名"><cellValueFactory><PropertyValueFactory property="name"></PropertyValueFactory></cellValueFactory></TableColumn><TableColumn prefWidth="400" text="路径"><cellValueFactory><PropertyValueFactory property="path"></PropertyValueFactory></cellValueFactory></TableColumn><TableColumn prefWidth="100" text="大小"><cellValueFactory><PropertyValueFactory property="sizeText"></PropertyValueFactory></cellValueFactory></TableColumn><TableColumn prefWidth="180" text="修改时间"><cellValueFactory><PropertyValueFactory property="lastModifiedText"></PropertyValueFactory></cellValueFactory></TableColumn></columns></TableView></children>
</GridPane>

GUIController 类

与 app.fxml 文件配套使用, 该类离实现了 界面中按键的绑定事件, 以及对搜索框内容进行监听, 当搜索框内容改变时, 重新搜索, 并将结果返回到查询结果显示处 (实现动态搜索功能)

java">public class GUIController implements Initializable {@FXMLprivate Label label;@FXMLprivate GridPane gridPane;@FXMLprivate Button button;@FXMLprivate TextField textField;@FXMLprivate TableView<FileMeta> tableView;private SearchService searchService = null;@Overridepublic void initialize(URL location, ResourceBundle resources) {// 在这里对 输入框 加一个监听器// 需要指定对 text 这个内容属性进行监听// textField.textProperty() 获取输入框里的内容textField.textProperty().addListener(new ChangeListener<String>() {/***  会在用户每次修改 输入框内容 的时候, 被自动调用到* @param observable* @param oldValue 输入框被修改之前的值* @param newValue 输入框被修改之后的值*/@Overridepublic void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {// 此处要干的事情, 是根据新的值, 重新进行查询操作freshTable(newValue);}});}private void freshTable(String query) {// 重新查询数据库, 把查询结果, 设置到表格中if(searchService == null) {System.out.println("searchService 尚未初始化, 不能查询!");return;}// 把之前表里的内容清空掉ObservableList<FileMeta> fileMetas = tableView.getItems();fileMetas.clear();List<FileMeta> results = searchService.search(query);fileMetas.addAll(results);}/*** 使用该方法, 作为鼠标点击事件的回调函数* @param mouseEvent*/public void choose(MouseEvent mouseEvent) {// 创建一个 目录选择器DirectoryChooser directoryChooser = new DirectoryChooser();// 把该对话框显示出来Window window = gridPane.getScene().getWindow();File file = directoryChooser.showDialog(window);if(file == null) {System.out.println("用户选择的路径为空");} else {System.out.println(file.getAbsolutePath());}// 把用户选择的路径,显示到 label 中label.setText(file.getAbsolutePath());// 如果不是首次扫描, 就应该停止上次扫描任务, 执行本次扫描任务if(searchService != null) {searchService.shutdown();}// 对用户选择的路径进行扫描, 初始化searchService = new SearchService();searchService.init(file.getAbsolutePath());}
}

GUIClient

继承 Application 方法, 为界面启动类, 调用 javafx 提供的 launch 方法来启动整个程序

java">public class GUIClient extends Application {/*** 程序启动时, 会立即执行的方法* @param primaryStage* @throws Exception*/@Overridepublic void start(Stage primaryStage) throws Exception {// 加载 fxml 文件, 把 fxml 文件里的内容, 给设置到舞台中Parent parent = FXMLLoader.load(GUIClient.class.getClassLoader().getResource("app.fxml"));primaryStage.setScene(new Scene(parent, 1000, 800));primaryStage.setTitle("文件搜索工具");// 准备工作完成, 显示场景界面primaryStage.show();}public static void main(String[] args) {// 调用 javafx 提供的 launch 方法来启动整个程序launch(args);}
}

后端代码

实体类

FileMeta

本类对应着数据库的 file_meta 表
因为没引入 lombok, 因此只能手写 Setter 和 Getter 方法

java">// 本类的示例就代表 file_meta 表里的每个记录.
public class FileMeta {private int id;private String name;private String path;private boolean isDirectory;// 这里存储的 size 是字节, 但是界面上输出的不应该以字节位单位, k, m, gprivate long size;// 这个存储的是时间戳(机器能看懂)private long lastModified;// 这个是进行格式化转换之后的时间格式(人能看懂的)
//    private long lastModifiedText;// 构造方法public FileMeta(String name, String path, boolean isDirectory, long size, long lastModified) {this.name = name;this.path = path;this.isDirectory = isDirectory;this.size = size;this.lastModified = lastModified;}public FileMeta(File f) {this(f.getName(), f.getParent(), f.isDirectory(), f.length(), f.lastModified());}public String getPinyin() {return PinyinUtil.get(name, true);}public String getPinyinFirst() {return PinyinUtil.get(name, false);}public String getSizeText() {// 常见单位: Byte, KB, MB, GB, TB// 如果 size < 1024, 使用 Byte// 如果 1024 <= size < 1024*1024, 使用 MB// ...double curSize = size;String[] units = {"Byte", "KB", "MB", "GB", "TB"};for(int i=0;i<units.length;i++) {if(curSize < 1024) {return String.format("%.2f " + units[i], new BigDecimal(curSize));}curSize /= 1024;}return String.format("%.2f TB", new BigDecimal(curSize));}public String getLastModifiedText() {DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return dateFormat.format(lastModified);}// -------------------------------public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}public boolean isDirectory() {return isDirectory;}public void setDirectory(boolean directory) {isDirectory = directory;}public long getSize() {return size;}public void setSize(long size) {this.size = size;}public long getLastModified() {return lastModified;}public void setLastModified(long lastModified) {this.lastModified = lastModified;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;FileMeta fileMeta = (FileMeta) o;return isDirectory == fileMeta.isDirectory  && name.equals(fileMeta.name) && path.equals(fileMeta.path);}@Overridepublic int hashCode() {return Objects.hash(name, path, isDirectory);}
}

DBUtil

本类封装 JDBC 的 连接/关闭/管理 操作

java">public class DBUtil {// 使用 单例模式(懒汉模式) 来提供 DataSourceprivate static volatile DataSource dataSource = DBUtil.getDataSource();// 创建数据源: Datasourcepublic static DataSource getDataSource() {if (dataSource == null) { //外层 if 判断是否要加锁 (加锁是要消耗资源的, if判断一下比 synchronized 加一次锁消耗资源要少的多)synchronized (DBUtil.class) {if(dataSource == null ) { //内层 if 判断是否要创建 DataSourcedataSource = new SQLiteDataSource();((SQLiteDataSource)dataSource).setUrl("jdbc:sqlite://D:/AAASpringBootProject/sqlite/fileSearcher.db");}}}return dataSource;}// 建立连接public static Connection getConnection() throws SQLException {return dataSource.getConnection();}// 断开连接public static void close(Connection connection, Statement statement, ResultSet resultSet) {if(resultSet != null) {try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}}if(statement != null) {try {statement.close();} catch (SQLException e) {e.printStackTrace();}}if(connection != null ) {try {connection.close();} catch (SQLException e) {e.printStackTrace();}}}
}

FileDao 类

通过该类来实现数据库的增删改查操作

java">// 通过这个类来封装针对 file_meta 表的操作
public class FileDao {// 1.初始化数据库 (建表)public void initDB() {// 1) 先能够读取到 db.sql 中的 SQL 语句// 2) 根据 SQL 语句调用 jdbc 执行操作Connection connection = null;Statement statement = null;try {connection = DBUtil.getConnection();// 使用 connection.createStatement() 来执行建库建表 sqlstatement = connection.createStatement();String[] sqls = getInitSql();for(String sql : sqls) {System.out.println("[initDB] sql:" + sql);statement.executeUpdate(sql); //该方式用来执行一些基本不变的sql语句}} catch (SQLException | IOException e) {e.printStackTrace();} finally {DBUtil.close(connection, statement, null);}}// 从 db.sql 中读取文件内容// 一个 sql 语句对应一个 String, 多个 sql 语句对应 String[]private String[] getInitSql() throws IOException {// 用这个 StringBuilder 来存储最终结果StringBuilder stringBuilder = new StringBuilder();// 此处需要动态获取到 db.sql 文件的路径, 而不是一个写死的绝对路径(运行在别人的电脑上的)try(InputStream inputStream = FileDao.class.getClassLoader().getResourceAsStream("db.sql")) {// 这里是字节流到字符流的转换(对字符能轻松的进行操作, 对字节的操作要难得多)try(InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf8")) {while(true) {// char 类型读取不到 -1, 也就没有文件读取结束的标记了, 所以这里使用 int 来接收int ch = inputStream.read(); //inputStream.read() 读取到文件结束符会返回 -1if( ch == -1) break;  //文件读取完毕stringBuilder.append((char) ch);}}} catch (IOException e) {e.printStackTrace();}// sql 语句以 ';' 结束, 我们就以 ';' 来拆分字符串, 一句 sql 就是一个 Stringreturn stringBuilder.toString().split(";");}// 2.插入文件/目录数据到数据库中//   此处提供"批量插入"操作public void add(List<FileMeta> fileMetas) {Connection connection = null;PreparedStatement statement = null;try {connection = DBUtil.getConnection();// 关闭自动提交功能// (一次插入多条数据, 如果一个一个插,数据库就要打开-关闭-打开-关闭... 因此一次插入多条数据, 会减小数据库的资源消耗)connection.setAutoCommit(false);String sql = "insert into file_meta values(null, ?, ?, ?, ?, ?, ?, ?)";statement = connection.prepareStatement(sql);for(FileMeta fileMeta : fileMetas) {// 把当前 FileMeta 对象, 替换到 SQL 语句中.statement.setString(1,fileMeta.getName());statement.setString(2,fileMeta.getPath());statement.setBoolean(3, fileMeta.isDirectory());statement.setString(4,fileMeta.getPinyin());statement.setString(5,fileMeta.getPinyinFirst());statement.setLong(6, fileMeta.getSize());statement.setTimestamp(7, new Timestamp(fileMeta.getLastModified()));// 使用 addBatch 把构造好的片段连接起来// addBatch 会把已经构造好的 SQL 保存起来, 同时又会允许重新构造一个新的 SQL 出来statement.addBatch();System.out.println("[insert] 插入文件: " + fileMeta.getPath() + File.separator + fileMeta.getName());}// 执行所有的 sql 片段statement.executeBatch();// 执行完毕后, 通过 commit 告诉数据库, 添加完毕, 执行上述 batch 操作(自动提交已经关闭了)connection.commit();} catch (SQLException e) {try {if(connection != null) {// 如果连接已建立, 并且出现异常, 那就是提交的内容有错误, 此时进行回滚操作connection.rollback();}} catch (SQLException ex) {ex.printStackTrace();}} finally {DBUtil.close(connection, statement, null);}}/***  3.按照特定的关键词进行查询*  此处查询 pattern , 可能是文件名的一部分, 可能是文件名拼音的一部分, 也可能是拼音首字母的一部分 ...* @param pattern 根据 pattern 查询数据库匹配内容* @return*/public List<FileMeta> searchByPattern(String pattern) {List<FileMeta> fileMetas = new ArrayList<>();Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;try {connection = DBUtil.getConnection();String sql = "select name, path, is_directory, size, last_modified from file_meta " +" where name like ? or pinyin like ? or pinyin_first like ? " +" order by path, name";statement = connection.prepareStatement(sql);statement.setString(1,"%" + pattern + "%");statement.setString(2,"%" + pattern + "%");statement.setString(3,"%" + pattern + "%");resultSet = statement.executeQuery();while(resultSet.next()) {String name = resultSet.getString("name");String path = resultSet.getString("path");boolean isDirectory = resultSet.getBoolean("is_directory");long size = resultSet.getLong("size");Timestamp lastModified = resultSet.getTimestamp("last_modified");FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModified.getTime());fileMetas.add(fileMeta);}} catch (SQLException e) {e.printStackTrace();} finally {DBUtil.close(connection, statement, resultSet);}return fileMetas;}/***  根据给定路径查询结果.* @param targetPath 给定路径* @return 该路径下的所有文件信息(一层)*/public List<FileMeta> searchByPath(String targetPath) {List<FileMeta> fileMetas = new ArrayList<>();Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;try {connection = DBUtil.getConnection();String sql = "select name, path, is_directory, size, last_modified from file_meta " +" where path = ?";statement = connection.prepareStatement(sql);statement.setString(1, targetPath);resultSet = statement.executeQuery();while(resultSet.next()) {String name = resultSet.getString("name");String path = resultSet.getString("path");boolean isDirectory = resultSet.getBoolean("is_directory");long size = resultSet.getLong("size");Timestamp lastModified = resultSet.getTimestamp("last_modified");FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModified.getTime());fileMetas.add(fileMeta);}} catch (SQLException e) {e.printStackTrace();} finally {DBUtil.close(connection, statement, resultSet);}return fileMetas;}/***  发现某个文件已从磁盘上删除, 此时需要把对应表里的内容删除掉* @param fileMetas 删除的 文件/目录*/public void delete(List<FileMeta> fileMetas) {Connection connection = null;PreparedStatement statement = null;try {connection = DBUtil.getConnection();connection.setAutoCommit(false);  // 将自动提交关闭, 把下列批量删除操作看作一个事务进行for(FileMeta fileMeta : fileMetas) {String sql = null;if(!fileMeta.isDirectory()) {// 对文件的sql语句构造sql = "delete from file_meta where name = ? and path = ?";statement = connection.prepareStatement(sql);statement.setString(1, fileMeta.getName());statement.setString(2, fileMeta.getPath());} else {// 对目录的sql语句构造sql = "delete from file_meta where (name = ? and path = ?) or (path like ?)";statement = connection.prepareStatement(sql);statement.setString(1, fileMeta.getName());statement.setString(2, fileMeta.getPath());statement.setString(3, fileMeta.getPath() + File.separator + fileMeta.getName() + File.separator + "%");}statement.executeUpdate();System.out.println("[delete] " + fileMeta.getPath() + "\\" + fileMeta.getName());// 此处对于每个 statement 对象都要单独关闭// (每个 statement 都可能是不同的 sql 语句, 以前可以统一关闭是因为 sql 模板相同, 只是填充参数不同, 修改一下参数就可以接着用)statement.close();}// 告知数据库, 事务构造完毕, 进行统一提交connection.commit();} catch (SQLException e) {try {connection.rollback();} catch (SQLException ex) {ex.printStackTrace();}} finally {DBUtil.close(connection, null, null);}}
}

工具类

PinyinUtil

封装 Pinyin4j 的功能
汉字 => 拼音 (全拼 / 首字母)

java">public class PinyinUtil {/***  获取字符串的拼音** @param src  第一个参数表示要获取拼音的字符串* @param fullSpell  第二个参数表示是否是全拼.*                   比如针对"你好啊"该字符串, true 对应全拼: nihaoa, false 对应首字母: nha*                   此处针对多音字不做过多考虑, 采用第一个元素代表的发音(也是最常用的发音)* @return  字符串的拼音*/public static String get(String src, Boolean fullSpell) {// trim() 去除字符串两侧的空白字符. eg: \t  \n  \f  \v  空格 ...if(src == null || src.trim().length() == 0) {// 空字符不做处理return null;}// 针对 Pinyin4j 做一些配置, 让他将拼音 yu 使用 v 表示HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();format.setVCharType(HanyuPinyinVCharType.WITH_V);// 遍历字符串的每个字符, 针对每个字符分别进行转换, 将转换得到的拼音, 拼接到 StringBuilder 里StringBuilder stringBuilder = new StringBuilder();src = src.trim();for(int i=0;i<src.length();i++) {// 针对单个字符进行转换char ch = src.charAt(i);String[] tmp = null;try {tmp = PinyinHelper.toHanyuPinyinStringArray(ch, format);} catch (BadHanyuPinyinOutputFormatCombination e) {e.printStackTrace();}if(tmp == null || tmp.length == 0) {// 拼音转换失败, 返回空数组// 说明当前字符就不是汉字, 可能是字母,数字或符号, eg: a, b, c, 1, 2, 3// 此时保留原始字符就好stringBuilder.append(ch);}else if(fullSpell) {stringBuilder.append(tmp[0]);}else {stringBuilder.append(tmp[0].charAt(0));}}return stringBuilder.toString();}
}

功能处理

扫描文件目录, 将目录下所有文件/目录信息存储到数据库中

当选择搜索路径后, 会递归的扫描指定路径下的所有目录及文件, 并将扫描到的 所有文件/目录信息 存储到数据库中
(查数据库比查文件系统要快, 因此其实每次查找指定文件在文件系统中出现的位置, 都是查询数据库中预存的信息)

java">public class GUIController implements Initializable {@FXMLprivate Label label;@FXMLprivate GridPane gridPane;@FXMLprivate Button button;@FXMLprivate TextField textField;@FXMLprivate TableView<FileMeta> tableView;private SearchService searchService = null;/*** 使用该方法, 作为鼠标点击事件的回调函数* @param mouseEvent*/public void choose(MouseEvent mouseEvent) {// 创建一个 目录选择器DirectoryChooser directoryChooser = new DirectoryChooser();// 把该对话框显示出来Window window = gridPane.getScene().getWindow();// 获取选定的文件File file = directoryChooser.showDialog(window);if(file == null) {System.out.println("用户选择的路径为空");} else {System.out.println(file.getAbsolutePath());}// 把用户选择的路径,显示到 label 中label.setText(file.getAbsolutePath());// 如果不是首次扫描, 就应该停止上次扫描任务, 执行本次扫描任务if(searchService != null) {searchService.shutdown();}// 对用户选择的路径进行扫描, 初始化searchService = new SearchService();searchService.init(file.getAbsolutePath());}
}

在这里插入图片描述

搜索框内容发生改变后, 自动进行数据库搜索, 将匹配内容展示到页面

当搜索框内容改变时, 会被系统绑定的事件监听到, 重新进行数据库搜索, 并将匹配信息作为结果返回到查询结果显示处

java">public class GUIController implements Initializable {@FXMLprivate Label label;@FXMLprivate GridPane gridPane;@FXMLprivate Button button;@FXMLprivate TextField textField;@FXMLprivate TableView<FileMeta> tableView;private SearchService searchService = null;@Overridepublic void initialize(URL location, ResourceBundle resources) {// 在这里对 输入框 加一个监听器// 需要指定对 text 这个内容属性进行监听// textField.textProperty() 获取输入框里的内容textField.textProperty().addListener(new ChangeListener<String>() {/***  会在用户每次修改 输入框内容 的时候, 被自动调用到* @param observable* @param oldValue 输入框被修改之前的值* @param newValue 输入框被修改之后的值*/@Overridepublic void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {// 此处要干的事情, 是根据新的值, 重新进行查询操作freshTable(newValue);}});}private void freshTable(String query) {// 重新查询数据库, 把查询结果, 设置到表格中if(searchService == null) {System.out.println("searchService 尚未初始化, 不能查询!");return;}// 把之前表里的内容清空掉ObservableList<FileMeta> fileMetas = tableView.getItems();fileMetas.clear();List<FileMeta> results = searchService.search(query);fileMetas.addAll(results);}
}

在这里插入图片描述


http://www.ppmy.cn/embedded/42792.html

相关文章

SpringMVC接收请求参数的方式:

接收简单变量的请求参数 直接使用简单变量作为形参进行接收&#xff08;这里简单变量名称需要与接收的参数名称保持一致&#xff0c;否则需要加上RequestParam注解&#xff09;&#xff1a; 细节&#xff1a; 1&#xff1a;SpringMVC会针对常见类型&#xff08;八种基本类型及…

什么是vue

Vue.js&#xff08;通常简称为 Vue&#xff09;是一个用于构建用户界面的渐进式 JavaScript 框架。它由尤雨溪&#xff08;Evan You&#xff09;创建&#xff0c;并于2014年首次发布。Vue 设计的目的是能够灵活地适应开发者的需求&#xff0c;可以通过引入不同的插件和库来构建…

数据可视化

目录 数据可视化: 常见的数据可视化库: Echarts 使用五步曲 Echarts 相关配置 数据可视化: 应对现在数据可视化趋势,越来越多企业需要在很多场景(营销数据,生产数据,用户数据)下使用,可视化图表来展示体现数据,让数据更加直观,数据特点更加突出 主要目的:借助于…

【数据结构与算法篇】二叉树链式结构及实现

【数据结构与算法篇】二叉树链式结构及实现 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 4. 二叉树链式结构的实现 4.1 前置说明 4.2 二叉树的遍历 4.2.1 前序、中序以及…

【XR806开发板试用】SPI驱动数码管显示

准备工作 安装repo 创建repo安装目录。 mkdir ~/bin下载repo wget https://storage.googleapis.com/git-repo-downloads/repo -P ~/bin/改变执行权限 chmod ax ~/bin/repo设置环境变量&#xff0c;在~/.bashrc文件的最后输入 export PATH~/bin:$PATH和export REPO_URLhttps://…

RabbitMQ .NET

setup rabbitmq docker run --namerabbit -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USERadmin -e RABBITMQ_DEFAULT_PASSxxx -d rabbitmq:management ip:15672 login nuget RabbitMQ.Client Send //1.1.实例化连接工厂 var factory new ConnectionFactory() …

d20(184-190)-勇敢开始Java,咖啡拯救人生

目录 网络通信 网络通信三要素&#xff08;IP地址&#xff0c;端口号&#xff0c;协议 IP地址 InetAddress 端口号 协议 传输层的两个通信协议 UDP通信 java.net.Datagramsocket类 客户端 服务端 UDP通信多收多发 客户端 服务端 TCP通信 java.net.Socket类 客…

家政项目day3 区域服务模块开发

目录 1 复习下业务流程2 接口设计2.1 查询区域服务2.1.1 接口梳理2.1.2 接口设计2.1.3 接口定义 1 复习下业务流程 区域服务管理是为运营地区设置要运营的服务项&#xff0c;不同地区所运营的服务项可能不同&#xff0c;比如&#xff1a;本平台在北京运营了老人陪护服务&#…