- 引入依赖
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.66.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 添加properties配置
netty.port= 10000
netty.ws= ws://192.168.1.204:${netty.port}${server.servlet.context-path}/ws
- 关闭SpringSecurity 安全检验
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll();
http.cors().and().csrf().disable();
}
}
@Configuration
public class SpringWebMvcConfig implements WebMvcConfigurer {
@Resource(name = "thymeleafViewResolver")
private ThymeleafViewResolver thymeleafViewResolver;
@Value("${server.servlet.context-path}")
String contextPath;
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
if (thymeleafViewResolver != null) {
Map<String, Object> vars = new HashMap<>(1);
vars.put("contextPath", contextPath);
thymeleafViewResolver.setStaticVariables(vars);
}
WebMvcConfigurer.super.configureViewResolvers(registry);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS")
.maxAge(3600);
}
}
- 定义NettyInitRunner 项目启动后运行
@Component
public class NettyInitRunner implements CommandLineRunner {
@Value("${netty.port}")
Integer nettyPort;
@Value("${server.servlet.context-path}")
String contextPath;
@Override
public void run(String... args){
try {
System.out.println("nettyServer starting ..." + nettyPort);
new NettyServerConfig(nettyPort, contextPath).start();
} catch (Exception e) {
System.out.println("NettyServerError:" + e.getMessage());
}
}
}
- netty配置
import fangrong.com.cn.im.handler.MessageHandler;
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.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class NettyServerConfig {
private final int port;
private final String contextPath;
public NettyServerConfig(int port, String contextPath) {
this.port = port;
this.contextPath = contextPath;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(group, bossGroup)
.channel(NioServerSocketChannel.class)
.localAddress(this.port)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
log.info("收到新的客户端连接");
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new MessageHandler());
ch.pipeline().addLast(new WebSocketServerProtocolHandler(contextPath + "/ws", null, true, 65536 * 10));
}
});
ChannelFuture cf = sb.bind().sync();
System.out.println(NettyServerConfig.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
bossGroup.shutdownGracefully().sync();
}
}
}
- 自定义MessageHandler
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import fangrong.com.cn.entity.MessageEntity;
import fangrong.com.cn.im.dto.SocketMessageDTO;
import fangrong.com.cn.service.IMessageService;
import fangrong.com.cn.utils.SpringUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class MessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
public static ConcurrentHashMap<Integer, ChannelId> userMap = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Integer, ChannelGroup> groupMap = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("与客户端建立连接,通道开启!");
channelGroup.add(ctx.channel());
ctx.channel().id();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
for (Map.Entry<Integer, ChannelId> next : userMap.entrySet()) {
if (next.getValue().equals(ctx.channel().id())) {
logger.info(next.getKey() + " 与服务端断开连接,通道关闭!");
userMap.remove(next.getKey());
}
}
channelGroup.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
Integer userId = getUrlParams(uri);
userMap.put(getUrlParams(uri), ctx.channel().id());
logger.info("登录的用户id是:{}", userId);
List<Integer> groupIds = new ArrayList<>();
ChannelGroup cGroup = null;
for (Integer groupId : groupIds) {
cGroup = groupMap.get(groupId);
if (cGroup == null) {
cGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
groupMap.put(groupId, cGroup);
}
cGroup.add(ctx.channel());
}
if (uri.contains("?")) {
String newUri = uri.substring(0, uri.indexOf("?"));
request.setUri(newUri);
}
} else if (msg instanceof TextWebSocketFrame) {
IMessageService messageService = (IMessageService) SpringUtil.getBean("messageServiceImpl");
TextWebSocketFrame frame = (TextWebSocketFrame) msg;
SocketMessageDTO socketMessageDTO = JSON.parseObject(frame.text(), SocketMessageDTO.class);
String messageType = socketMessageDTO.getMessageType();
switch (messageType) {
case "group":
logger.info("客户端收到服务器群聊数据:{}", frame.text());
groupMap.get(socketMessageDTO.getChatId()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(socketMessageDTO)));
MessageEntity groupEntity = new MessageEntity(socketMessageDTO.getMessageType(), socketMessageDTO.getUserId().toString(), socketMessageDTO.getChatId().toString(), socketMessageDTO.getMessage(), 1, new Date());
messageService.add(groupEntity);
break;
case "chat":
MessageEntity chatEntity = new MessageEntity(socketMessageDTO.getMessageType(), socketMessageDTO.getUserId().toString(), socketMessageDTO.getChatId().toString(), socketMessageDTO.getMessage(), 1, new Date());
ChannelId channelId = userMap.get(socketMessageDTO.getChatId());
if (channelId != null) {
Channel ct = channelGroup.find(channelId);
if (ct != null) {
ct.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(socketMessageDTO)));
logger.info("对方在线 收到私聊数据:{}", frame.text());
} else {
chatEntity.setRead(0);
logger.info(socketMessageDTO.getChatId() + " 登录信息丢失,请重新登录");
}
} else {
chatEntity.setRead(0);
logger.info("对方不在线 收到私聊数据:{}", frame.text());
}
messageService.add(chatEntity);
break;
case "all":
channelGroup.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(socketMessageDTO)));
channelGroup.forEach(channel -> {
for (Map.Entry<Integer, ChannelId> next : userMap.entrySet()) {
if (next.getValue().equals(channel.id())) {
MessageEntity onlineEntity = new MessageEntity(socketMessageDTO.getMessageType(), socketMessageDTO.getUserId().toString(), next.getKey().toString(), socketMessageDTO.getMessage(), 1, new Date());
messageService.add(onlineEntity);
}
}
});
break;
default:
logger.info("未知类型:{}", frame.text());
break;
}
}
super.channelRead(ctx, msg);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.channel().close();
for (Map.Entry<Integer, ChannelId> next : userMap.entrySet()) {
if (next.getValue().equals(ctx.channel().id())) {
logger.info(next.getKey() + " 发生异常,通道关闭!");
userMap.remove(next.getKey());
}
}
channelGroup.remove(ctx.channel());
logger.info(" netty 异常...... ");
}
private static Integer getUrlParams(String url) {
if (!url.contains("=")) {
return null;
}
String userId = url.substring(url.indexOf("=") + 1);
return Integer.parseInt(userId);
}
}
@Data
public class SocketMessageDTO {
private String messageType;
private Integer userId;
private Integer chatId;
private String message;
}
- 页面接口跳转
@Controller
public class TestIMController {
@Value("${netty.ws}")
private String ws;
@RequestMapping("/login")
public String login() {
return "/login";
}
@PostMapping("/login.do")
public String login(@RequestParam Integer userId, HttpSession session, Model model) {
model.addAttribute("ws", ws);
session.setAttribute("userId", userId);
model.addAttribute("groupList", new ArrayList<>());
return "/index";
}
}
- 登录和聊天首页 页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action= "${contextPath} + '/login.do'" method="post">
登录(默认的4个用户id:[1,2,3,4])
用户Id:<input type="number" name="userId"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style type="text/css">
.flexBox {display: flex;width: 100%;}
.flexBox div {width: 50%;background-color: pink;}
#messageBox ul {border: solid 1px #ccc;width: 600px;height: 400px}
</style>
<body>
<div class="flexBox">
<div style="text-align: left;" th:text="'当前登录的用户:'+${session.userId}"></div>
</div>
<div class="flexBox" id="messageBox">
<ul th:id="${groupId}" th:each="groupId,iterObj : ${groupList}">
<li th:text="房间号+${groupId}"></li>
</ul>
<ul id="chat">
<li style="color: #0c31ec">聊天记录</li>
</ul>
</div>
<div style="width:100%;border: solid 1px #ccc;">
<form style="width: 40%;border: solid 1px red;margin: 0px auto">
<h3>给好友发送数据</h3>
<div>
测试数据: 好友编号为 1-4<br/><br/>
请输入好友编号 <input type="number" id="chatId" value="1"><br/><br/>
<textarea id="message" style="width: 96%">呼叫呼叫</textarea>
</div>
<div>
消息类型<input name="messageType" type="radio" value="chat" checked>私聊
<a href="#" id="send">发送</a>
</div>
</form>
</div>
</body>
<script th:inline="javascript">
var userId = [[${session.userId}]];
var ws = [[${ws}]]
</script>
<script type="text/javascript">
var websocket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
websocket = new WebSocket(ws + "?userId=" + userId);
websocket.onmessage = function (event) {
var json = JSON.parse(event.data);
console.log(json)
chat.onmessage(json);
};
websocket.onopen = function (event) {
console.log("Netty-WebSocket服务器。。。。。。连接");
};
websocket.onclose = function (event) {
console.log("Netty-WebSocket服务器。。。。。。关闭");
};
} else {
alert("您的浏览器不支持WebSocket协议!");
}
window.onbeforeunload = function () {
if (websocket != null) {
websocket.close();
}
};
</script>
<script>
var chat = {
sendMessage: function () {
var message = document.getElementById("message").value;
if (message == "") {
alert('不能发送空消息');
return;
}
if (!window.WebSocket) {
return;
}
var chatId = document.getElementById("chatId").value;
var radio=document.getElementsByName("messageType");
var messageType=null;
for(var i=0;i<radio.length;i++){
if(radio[i].checked==true) {
messageType=radio[i].value;
break;
}
}
if (messageType == "chat") {
if (chatId == userId) {
alert("不能给自己发私聊信息,请换个好友吧");
return;
}
var li = document.createElement("li");
li.innerHTML = "自己: " + message
var ul = document.getElementById("chat");
ul.appendChild(li);
}
if (websocket.readyState == WebSocket.OPEN) {
var data = {};
data.chatId = chatId;
data.message = message;
data.userId = userId;
data.messageType = messageType;
websocket.send(JSON.stringify(data));
} else {
alert("和服务器连接异常!" + websocket.readyState);
}
},
onmessage: function (jsonData) {
var id;
if (jsonData.messageType == "chat") {
id = "chat";
} else {
id = jsonData.chatId;
}
console.log(id);
var li = document.createElement("li");
li.innerHTML = "好友 " + jsonData.userId + " 发来消息: " + jsonData.message;
var ul = document.getElementById(id);
ul.appendChild(li);
}
}
document.onkeydown = keyDownSearch;
function keyDownSearch(e) {
var theEvent = e || window.event;
var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
if (code == 13) {
chat.sendMessage();
return false;
}
return true;
}
document.getElementById("send").onclick = function () {
chat.sendMessage();
}
</script>
</html>