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

说明

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

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

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


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: 32001

logging:
  level:
    com.hahashou.netty: info

spring:
  servlet:
    multipart:
      max-file-size: 128MB
      max-request-size: 256MB

userCode: Aa

minio:
  endpoint: http://127.0.0.1:9000
  accessKey: root
  secretKey: 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;

    @Bean
    public 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 {

    @Resource
    private 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")),

        ;

        @Getter
        private Integer key;

        @Getter
        private String describe;

        @Getter
        private String bucketName;

        @Getter
        private 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 {

    @Resource
    private MinioUtils minioUtils;

    @Override
    public 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;
    }

    @Override
    public 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 {

    @Resource
    private 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);

    @Override
    public 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));
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        log.info("有客户端断开连接, channelId : {}", ctx.channel().id().asLongText());
        CHANNEL.remove(ctx.channel().id().asLongText());
    }

    @Override
    public 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));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.info("有客户端发生异常, channelId : {}", ctx.channel().id().asLongText());
    }
}

5. 测试

5.1 上传


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

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


5.4 测试音频


5.5 测试视频


5.6 MioIO界面Buckets

相关推荐
李慕婉学姐6 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆8 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin8 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20058 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉8 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国9 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882489 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈9 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_999 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹9 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理