<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.27.Final</version>
</dependency>
- 2、建立服务端
WebSocketServer.java
package com.rw.article.chat.websocket;
import com.rw.article.chat.action.ApiController;
import com.rw.article.chat.websocket.handler.BinaryWebSocketFrameHandler;
import com.rw.article.chat.websocket.handler.TextWebSocketHandler;
import com.rw.article.common.constant.Constants;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebSocketServer {
private Logger log = LoggerFactory.getLogger(this.getClass());
public void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.TRACE))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.TRACE))
.addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(10240))
.addLast(new WebSocketServerCompressionHandler())
.addLast(new TextWebSocketHandler())
.addLast(new BinaryWebSocketFrameHandler())
.addLast(new WebSocketServerProtocolHandler(Constants.DEFAULT_WEB_SOCKET_LINK, null, true, 10485760));
}
});
ChannelFuture channelFuture = bootstrap.bind(8092).sync();
log.info("webSocket server listen on port : [{}]", 8092);
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
- 2、写handle
- TextWebSocketHandler.java 文本消息的处理类
package com.rw.article.chat.websocket.handler;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.util.BeanUtil;
import com.rw.article.chat.entity.vo.Message;
import com.rw.article.chat.queue.DelayOrderQueueManager;
import com.rw.article.chat.queue.DelayOrderWorker;
import com.rw.article.chat.websocket.OnlineContainer;
import com.rw.article.chat.websocket.protocol.IMsgCode;
import com.rw.article.chat.websocket.protocol.ProcessorContainer;
import com.rw.article.common.configuration.GenericConfiguration;
import com.rw.article.common.constant.Constants;
import com.rw.article.common.spring.BeansUtils;
import com.rw.article.common.type.MessageSendType;
import com.rw.article.common.type.MessageType;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import java.net.InetSocketAddress;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class TextWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private Logger log = LoggerFactory.getLogger(this.getClass());
private OnlineContainer onlineContainer;
private BeansUtils beansUtils;
public TextWebSocketHandler() {
onlineContainer = BeansUtils.getBean(OnlineContainer.class);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
String origin = request.headers().get("Origin");
if (null == origin) {
log.info("origin 为空 ");
ctx.close();
} else {
if (null != uri && uri.contains(Constants.DEFAULT_WEB_SOCKET_LINK) && uri.contains("?")) {
String[] uriArray = uri.split("\\?");
if (null != uriArray && uriArray.length > 1) {
String[] paramsArray = uriArray[1].split("=");
if (null != paramsArray && paramsArray.length > 1) {
onlineContainer.putAll(paramsArray[1], ctx);
}
}
request.setUri(Constants.DEFAULT_WEB_SOCKET_LINK);
}
} else {
log.info("不允许 [ {} ] 连接 强制断开", origin);
ctx.close();
}
}
super.channelRead(ctx, msg);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
log.info("接收到客户端的消息:[{}]", msg.text());
InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
String ip = inetSocketAddress.getHostName();
String txtMsg = "[" + ip + "][" + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "] ==> " + msg.text();
ctx.channel().writeAndFlush(new TextWebSocketFrame(txtMsg));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
onlineContainer.removeAll(ctx.channel().id().asLongText());
ctx.close();
log.error("服务器发生了异常: [ {} ]", cause);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
String key = onlineContainer.removeAll(ctx.channel().id().asLongText());
ctx.close();
}
}
- BinaryWebSocketFrameHandler.java 二进制消息的处理类 例如发送图片
package com.rw.article.chat.websocket.handler;
import com.alibaba.fastjson.JSON;
import com.rw.article.chat.service.IFileUploadService;
import com.rw.article.chat.service.impl.AliyunOSSClientServiceImpl;
import com.rw.article.chat.websocket.OnlineContainer;
import com.rw.article.common.configuration.GenericConfiguration;
import com.rw.article.common.constant.Constants;
import com.rw.article.common.spring.BeansUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.BASE64Decoder;
import java.io.*;
import java.sql.Blob;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
public class BinaryWebSocketFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {
private Logger log = LoggerFactory.getLogger(this.getClass());
public BinaryWebSocketFrameHandler() {
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
log.info("服务器接收到二进制消息. [{}]",msg.toString());
ByteBuf content = msg.content();
content.markReaderIndex();
int flag = content.readInt();
log.info("标志位:[{}]", flag);
content.resetReaderIndex();
ByteBuf byteBuf = Unpooled.directBuffer(msg.content().capacity());
byteBuf.writeBytes(msg.content());
byte [] bytes = new byte[msg.content().capacity()];
byteBuf.readBytes(bytes);
ByteBuf byteBuf2 = Unpooled.directBuffer(bytes.length);
byteBuf2.writeBytes(bytes);
log.info("JSON.toJSONString(byteBuf) [ {} ]",JSON.toJSONString(byteBuf));
ctx.writeAndFlush(new BinaryWebSocketFrame(byteBuf));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info(" 客户端加入 [ {} ]", ctx.channel().id().asLongText());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info(" 离线 [ {} ] ", ctx.channel().id().asLongText());
}
}
- OnlineContainer.java 是用来做存储在线用户的,这个对象存在spring中,通过ApplicationContext获取到,没用spring反正建个单例对象都行。
package com.rw.article.chat.websocket;
import io.netty.channel.ChannelHandlerContext;
import lombok.val;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.management.relation.Relation;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class OnlineContainer {
private Logger log = LoggerFactory.getLogger(this.getClass());
private Map<String, ChannelHandlerContext> onlineUserMap = new ConcurrentHashMap<>();
private Map<String, String> userMap = new ConcurrentHashMap<>();
public Map<String, String> getUserMap() {
return userMap;
}
public void setUserMap(Map<String, String> userMap) {
this.userMap = userMap;
}
public Map<String, ChannelHandlerContext> getOnlineUserMap() {
return onlineUserMap;
}
public void setOnlineUserMap(Map<String, ChannelHandlerContext> onlineUserMap) {
this.onlineUserMap = onlineUserMap;
}
public ChannelHandlerContext getChannelHandlerContextByUserId(String userId) {
return onlineUserMap.getOrDefault(userMap.getOrDefault(userId, ""), null);
}
public void putAll(String userId, ChannelHandlerContext ctx) {
userMap.put(userId, ctx.channel().id().asLongText());
onlineUserMap.put(ctx.channel().id().asLongText(), ctx);
log.info("用户 [ {} ] 上线", userId);
}
public String removeAll(String sessionId) {
String key = null;
if (userMap.containsValue(sessionId)) {
for (Map.Entry<String, String> entry : userMap.entrySet()) {
if (null != entry.getValue() && entry.getValue().equals(sessionId)) {
key = entry.getKey();
break;
}
}
if (null != key) {
log.info("用户 [ {} ] 离线 ", key);
userMap.remove(key);
}
onlineUserMap.remove(sessionId);
}
return key;
}
}
- BeansUtils.java 主要用来拿spring容器中bean的实例
package com.rw.article.common.spring;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
@Component
public class BeansUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
BeansUtils.context = applicationContext;
}
public static <T> T getBean(Class<T> bean) {
return context.getBean(bean);
}
public static <T> T getBean(String var1, @Nullable Class<T> var2){
return context.getBean(var1, var2);
}
public static ApplicationContext getContext() {
return context;
}
}
public class Constants {
public static final String REMOVE_CUSTOMER_RELATION = "REMOVE_CUSTOMER_RELATION";
public static final String DEFAULT_SUCCESS_STRING = "success";
public static final String DEFAULT_ZERO = "0";
public static final String DEFAULT_CUSTOMER_PREFIX = "tb_";
public static final String DEFAULT_USER_PREFIX = "user_";
public static final String DEFAULT_WEB_SOCKET_LINK = "/ws";
}
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>web socket 测试</title>
</head>
<body>
<div style="width: 600px;height: 400px;">
<p>服务器输出:</p>
<div style="border: 1px solid #CCC;height: 300px;overflow: scroll" id="server-msg-container">
</div>
<p>
<textarea id="inp-msg" style="height: 50px;width: 500px"></textarea><input type="button" value="发送" id="send"><br/>
选择图片: <input type="file" id="send-pic">
</p>
</div>
<script type="application/javascript">
var ws = new WebSocket("ws://localhost:8092/ws?userId=tb_1");
ws.onopen = function (ev) {
};
ws.onmessage = function (ev) {
console.info("onmessage", ev);
var inpMsg = document.getElementById("server-msg-container");
if (typeof ev.data === "string") {
inpMsg.innerHTML += ev.data + "<br/>";
} else {
var result = ev.data;
var flagReader = new FileReader();
flagReader.readAsArrayBuffer(result.slice(0, 4));
flagReader.onload = function (flag) {
console.log(new DataView(flag.target.result).getInt32(0))
if (new DataView(flag.target.result).getInt32(0) === 20) {
var imageReader = new FileReader();
imageReader.readAsDataURL(result.slice(4));
imageReader.onload = function (img) {
var imgHtml = "<img src='" + img.target.result + "' style='width: 100px;height: 100px;'>";
inpMsg.innerHTML += imgHtml.replace("data:application/octet-stream;", "data:image/png;") + "<br />";
}
} else {
alert("后端返回的是非图片类型数据,无法显示。");
}
}
}
};
ws.onerror = function () {
var inpMsg = document.getElementById("server-msg-container");
inpMsg.innerHTML += "发生异常" + "<br/>";
};
ws.onclose = function () {
var inpMsg = document.getElementById("server-msg-container");
inpMsg.innerHTML += "webSocket 关闭" + "<br/>";
};
document.getElementById("send").addEventListener("click", function () {
var data = {};
var text = document.getElementById("inp-msg").value;
data.text = text;
data.toUserId = "user_1";
data.fromUserId = "tb_1";
ws.send(JSON.stringify(data));
}, false);
document.querySelector('#send-pic').addEventListener('change', function (ev) {
var files = this.files;
if (files && files.length) {
var file = files[0];
var fileType = file.type;
var dataType = 20;
if (!/^image/.test(fileType)) {
dataType = 10;
return;
}
var fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = function (e) {
console.log(this.result);
var data = {};
data.text = this.result;
data.toUserId = "user_1";
data.fromUserId = "tb_1";
ws.send(JSON.stringify(data));
}
}
}, false);
</script>
</body>
</html>
- 发送图片的功能上面两种方式一种是发base64的字符串,一种是Blob对象方式。
- 目前我对netty的还不是很熟悉,如果有错误的地方还望指正。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现