第 4 篇 : Netty客户端互发图片和音/视频

server/2024/9/23 9:27:54/

说明

因为图片和音/视频不能确定其具体大小, 故引入MinIO。客户端之间只发送消息, 通过上传/下载来获取额外信息

1. MinIO搭建(参考前面文章), 并启动

2. 登录MinIO创建3个Bucket: image、voice、video

创建桶
3个桶

3. 客户端改造

3.1 修改 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.hahashou.netty</groupId><artifactId>client</artifactId><version>1.0-SNAPSHOT</version><name>client</name><description>Netty Client Project For Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.100.Final</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.58</version></dependency><dependency><groupId>com.hahashou</groupId><artifactId>minio</artifactId><version>2023.11.16</version></dependency><!-- 此包在引入minio时必须添加--><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.9.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

3.2 修改 application.yml, 限制单文件大小为128M以内, 总大小为256M以内

server:port: 32001logging:level:com.hahashou.netty: infospring:servlet:multipart:max-file-size: 128MBmax-request-size: 256MBuserCode: Aaminio:endpoint: http://127.0.0.1:9000accessKey: rootsecretKey: root123456

3.3 config包下新增 MinIOConfig类

package com.hahashou.netty.client.config;import io.minio.MinioClient;
import io.minio.http.HttpUtils;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @description: MinIO配置* @author: 哼唧兽* @date: 9999/9/21**/
@Configuration
public class MinIOConfig {@Value("${minio.endpoint}")private String endpoint;@Value("${minio.accessKey}")private String accessKey;@Value("${minio.secretKey}")private String secretKey;@Beanpublic MinioClient minioClient() {OkHttpClient okHttpClient = HttpUtils.newDefaultHttpClient(60 * 1000,50 * 1000, 30 * 1000);MinioClient minioClient = MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).httpClient(okHttpClient).build();return minioClient;}
}

3.4 新增utils包, 放入 MinioUtils类

package com.hahashou.netty.client.utils;import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;/*** @description:* @author: 哼唧兽* @date: 9999/9/21**/
@Component
@Slf4j
public class MinioUtils {@Resourceprivate MinioClient minioClient;/*** 文件上传* @param bucketName* @param fileName* @param multipartFile* @return*/public void upload(String bucketName, String multiLevelFolders, String fileName, MultipartFile multipartFile) {try {boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());if (!found) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}String suffix = fileName.substring(fileName.lastIndexOf("."));File file = File.createTempFile(fileName, suffix);multipartFile.transferTo(file);UploadObjectArgs objectArgs = UploadObjectArgs.builder().bucket(bucketName).object(StringUtils.hasText(multiLevelFolders) ? multiLevelFolders + fileName : fileName).filename(file.getAbsolutePath()).contentType(multipartFile.getContentType()).build();minioClient.uploadObject(objectArgs);} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException exception) {log.error("MinIO文件上传异常 : {}", exception.getMessage());}}/*** 预览地址* @param fileName* @param bucketName* @return*/public String preview(String fileName, String bucketName){// 查看文件地址GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs.builder().bucket(bucketName).expiry(30, TimeUnit.MINUTES).object(fileName).method(Method.GET).build();try {return minioClient.getPresignedObjectUrl(build);} catch (Exception exception) {log.error("获取MinIO预览地址异常 : {}", exception.getMessage());}return null;}
}

3.5 修改 Message类

package com.hahashou.netty.client.config;import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import lombok.Data;
import lombok.Getter;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;/*** @description:* @author: 哼唧兽* @date: 9999/9/21**/
@Data
public class Message {/** 发送者用户code */private String userCode;/** 接收者用户code */private String friendUserCode;/** 连接时专用 */private String channelId;/** 消息类型 */private Integer type;public enum TypeEnum {TEXT(0, "文字", "", new ArrayList<>()),IMAGE(1, "图片", "image", Arrays.asList("bmp", "gif", "jpeg", "jpg", "png")),VOICE(2, "语音", "voice", Arrays.asList("mp3", "amr", "flac", "wma", "aac")),VIDEO(3, "视频", "video", Arrays.asList("mp4", "avi", "rmvb", "flv", "3gp", "ts", "mkv")),;@Getterprivate Integer key;@Getterprivate String describe;@Getterprivate String bucketName;@Getterprivate List<String> formatList;TypeEnum(int key, String describe, String bucketName, List<String> formatList) {this.key = key;this.describe = describe;this.bucketName = bucketName;this.formatList = formatList;}public static TypeEnum select(String format) {TypeEnum result = null;for (TypeEnum typeEnum : TypeEnum.values()) {if (typeEnum.getFormatList().contains(format)) {result = typeEnum;break;}}return result;}}/** 文字或文件的全路径名称 */private String text;public static ByteBuf transfer(Message message) {return Unpooled.copiedBuffer(JSON.toJSONString(message), CharsetUtil.UTF_8);}/*** 生成指定长度的随机字符串* @param length* @return*/public static String randomString (int length) {if (length > 64) {length = 64;}List<String> list = new ArrayList<>();for (int i = 0; i < 10; i++) {list.add(i + "");}for (char i = 'A'; i <= 'Z'; i++) {list.add(String.valueOf(i));}for (char i = 'a'; i <= 'z'; i++) {list.add(String.valueOf(i));}list.add("α");list.add("ω");Collections.shuffle(list);String string = list.toString();return string.replace("[", "").replace("]", "").replace(", ", "").substring(0, length);}
}

3.6 新增service包, 放入 ClientService接口

package com.hahashou.netty.client.service;import com.hahashou.netty.client.config.Message;import javax.servlet.http.HttpServletRequest;/*** @description:* @author: 哼唧兽* @date: 9999/9/21**/
public interface ClientService {/*** 上传* @param userCode* @param httpServletRequest* @return*/Message upload(String userCode, HttpServletRequest httpServletRequest);/*** 下载链接* @param fileName* @return*/String link(String fileName);
}

3.7 service包下新建impl包, 放入 ClientServiceImpl类

package com.hahashou.netty.client.service.imol;import com.hahashou.netty.client.config.Message;
import com.hahashou.netty.client.service.ClientService;
import com.hahashou.netty.client.utils.MinioUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.*;/*** @description:* @author: 哼唧兽* @date: 9999/9/21**/
@Service
@Slf4j
public class ClientServiceImpl implements ClientService {@Resourceprivate MinioUtils minioUtils;@Overridepublic Message upload(String userCode, HttpServletRequest httpServletRequest) {Message result = new Message();MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) httpServletRequest;List<MultipartFile> multipartFileList = multipartHttpServletRequest.getFiles("file");if (!CollectionUtils.isEmpty(multipartFileList)) {MultipartFile multipartFile = multipartFileList.get(0);String originalFilename = multipartFile.getOriginalFilename();String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);Message.TypeEnum typeEnum = Message.TypeEnum.select(suffix);if (typeEnum != null) {String fileName = generateFileName(suffix);String multiLevelFolders = userCode + "/"+ LocalDate.now().toString().replace("-", "") + "/";minioUtils.upload(typeEnum.getBucketName(), multiLevelFolders, fileName, multipartFile);result.setType(typeEnum.getKey());result.setText(multiLevelFolders + fileName);}}return result;}public static String generateFileName(String suffix) {LocalTime now = LocalTime.now();return now.toString().replace(":", "").replace(".", "-") + Message.randomString(4) + "." + suffix;}@Overridepublic String link(String fileName) {String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);Message.TypeEnum typeEnum = Message.TypeEnum.select(suffix);if (typeEnum != null) {return minioUtils.preview(fileName, typeEnum.getBucketName());}return null;}
}

3.8 修改 ClientController类

package com.hahashou.netty.client.controller;import com.alibaba.fastjson.JSON;
import com.hahashou.netty.client.config.Message;
import com.hahashou.netty.client.config.NettyClientHandler;
import com.hahashou.netty.client.service.ClientService;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;/*** @description:* @author: 哼唧兽* @date: 9999/9/21**/
@RestController
@RequestMapping("/client")
@Slf4j
public class ClientController {@Resourceprivate ClientService clientService;@PostMapping("/send")public String send(@RequestBody Message dto) {Channel channel = NettyClientHandler.CHANNEL;if (channel == null) {return "服务端已下线";}channel.writeAndFlush(Message.transfer(dto));return "success";}@GetMapping("/upload/{userCode}")public String upload(@PathVariable String userCode, final HttpServletRequest httpServletRequest) {if (StringUtils.isEmpty(userCode)) {return "userCode is null";}Message upload = clientService.upload(userCode, httpServletRequest);return JSON.toJSONString(upload);}@GetMapping("/link")public String link(@RequestParam String fileName) {// 如果Bucket包含多级目录, fileName为Bucket下文件的全路径名if (StringUtils.isEmpty(fileName)) {return "fileName is null";}return clientService.link(fileName);}
}

4. 服务端改造

4.1 复制客户端的 Message类

4.2 修改 NettyServerHandler类

package com.hahashou.netty.server.config;import com.alibaba.fastjson.JSON;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** @description:* @author: 哼唧兽* @date: 9999/9/21**/
@Component
@ChannelHandler.Sharable
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {/** key: 用户code; value: channelId */public static Map<String, String> USER_CHANNEL = new ConcurrentHashMap<>(32);/** key: channelId; value: Channel */public static Map<String, Channel> CHANNEL = new ConcurrentHashMap<>(32);/** 用户离线消息 */public static Map<String, List<Message>> USER_MESSAGE = new ConcurrentHashMap<>(32);@Overridepublic void channelActive(ChannelHandlerContext ctx) {Channel channel = ctx.channel();String channelId = channel.id().asLongText();log.info("有客户端连接, channelId : {}", channelId);CHANNEL.put(channelId, channel);Message message = new Message();message.setChannelId(channelId);channel.writeAndFlush(Message.transfer(message));}@Overridepublic void channelInactive(ChannelHandlerContext ctx) {log.info("有客户端断开连接, channelId : {}", ctx.channel().id().asLongText());CHANNEL.remove(ctx.channel().id().asLongText());}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {if (msg != null) {Message message = JSON.parseObject(msg.toString(), Message.class);String userCode = message.getUserCode(),channelId = message.getChannelId();if (StringUtils.hasText(userCode) && StringUtils.hasText(channelId)) {connect(userCode, channelId);} else if (StringUtils.hasText(message.getText())) {if (StringUtils.hasText(message.getFriendUserCode())) {sendOtherClient(message);} else {sendAdmin(ctx.channel(), message);}}}}/*** 建立连接* @param userCode* @param channelId*/private void connect(String userCode, String channelId) {log.info("客户端 {} 连接", userCode);USER_CHANNEL.put(userCode, channelId);}/*** 发送给其他客户端* @param message*/private void sendOtherClient(Message message) {String friendUserCode = message.getFriendUserCode();String queryChannelId = USER_CHANNEL.get(friendUserCode);if (StringUtils.hasText(queryChannelId)) {Channel channel = CHANNEL.get(queryChannelId);if (channel == null) {offlineMessage(friendUserCode, message);return;}channel.writeAndFlush(Message.transfer(message));} else {offlineMessage(friendUserCode, message);}}/*** 离线消息存储* @param friendUserCode* @param message*/public void offlineMessage(String friendUserCode, Message message) {List<Message> messageList = USER_MESSAGE.get(friendUserCode);if (CollectionUtils.isEmpty(messageList)) {messageList = new ArrayList<>();}messageList.add(message);USER_MESSAGE.put(friendUserCode, messageList);}/*** 发送给服务端* @param channel* @param message*/private void sendAdmin(Channel channel, Message message) {message.setUserCode("ADMIN");message.setText(LocalDateTime.now().toString());channel.writeAndFlush(Message.transfer(message));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {log.info("有客户端发生异常, channelId : {}", ctx.channel().id().asLongText());}
}

5. 测试

5.1 上传

上传图片
<a class=minio文件界面" />

5.2 获取下载链接, 之后直接在浏览器中打开链接

获取下载链接

5.3 Aa向Bb发送消息, Bb收到后可以通过下载链接获取

Aa向Bb发送图片消息
Bb收到图片消息

5.4 测试音频

音频上传
音频位置

5.5 测试视频

视频上传
视频位置

5.6 MioIO界面Buckets

<a class=minio界面buckets" />


http://www.ppmy.cn/server/21627.html

相关文章

使用Python Tkinter创建文件生成工具

我们将使用Python的Tkinter模块创建一个简单的文件生成工具。这个工具可以用来创建Excel、文本、Python脚本和Word文档等不同类型的文件。 1、准备工作 首先&#xff0c;确保你已经安装了Python和所需的第三方库&#xff1a;Tkinter、openpyxl和python-docx。你可以使用pip来…

【QEMU系统分析之启动篇(十一)】

系列文章目录 第十一章 QEMU系统仿真的加速器初始化分析 文章目录 系列文章目录第十一章 QEMU系统仿真的加速器初始化分析 前言一、QEMU是什么&#xff1f;二、QEMU系统仿真的启动分析1.系统仿真的初始化代码2.主循环数据初始化configure_accelerators()phase_advance(PHASE_A…

每天五分钟机器学习:神经网络模型参数的选择

本文重点 在深度学习和人工智能的浪潮中,神经网络作为其中的核心力量,发挥着举足轻重的作用。然而,神经网络的性能并非一蹴而就,而是需要经过精心的参数选择和调优。 神经网络由大量的神经元组成,每个神经元之间通过权重进行连接。这些权重,以及神经元的偏置、激活函数…

好用的远程控制电脑软件有哪些?

远程操作另一台电脑可以在不同的地方&#xff0c;通过网络连接到另一台电脑。 如何远程操作另一台电脑呢&#xff1f; 本文将介绍远程连接电脑的多个软件实现远程操作。 1.域智盾&#xff08;亲测&#xff0c;效果很可&#xff09; 可以对员工进行四种远程控制模式&#xff0…

设计模式学习笔记 - 开源实战五(中):如何利用职责链与代理模式实现Mybatis Plugin

概述 上篇文章对 Mybatis 框架做了简单的背景介绍&#xff0c;并通过对比各种 ORM 框架&#xff0c;学习了代码的易用性、性能、灵活性之间的关系。一般来讲&#xff0c;框架提供的高级功能越多&#xff0c;那性能损耗就越大&#xff1b;框架使用起来越简单&#xff0c;那灵活…

在Docker中运行Jenkins容器:从入门到实践

Jenkins作为一个流行的持续集成和持续交付(CI/CD)工具&#xff0c;其强大的功能和广泛的插件支持使其成为自动化软件开发流程的首选。结合Docker容器化技术&#xff0c;可以轻松地在任何支持Docker的平台上部署和运行Jenkins&#xff0c;实现环境一致性、快速部署和易于管理的C…

测试的分类(3)

目录 按照测试阶段测试 系统测试 冒烟测试和回归测试的区别 验收测试 单元测试, 集成测试, 系统测试, 回归测试之间的关系 是否按手工进行测试 手工测试 自动化测试 自动化测试和手工测试的优缺点 自动化测试优点 自动化测试缺点 手工测试优点 手工测试缺点 按照…

【注解和反射】获取类运行时结构

继上一篇博客【注解和反射】类加载器-CSDN博客 目录 七、获取类运行时结构 测试 getFields()和getDeclaredFields() getMethods()和getDeclaredMethods() 七、获取类运行时结构 获取类运行时结构通常指的是在Java等面向对象编程语言中&#xff0c;使用反射&#xff08;Ref…