pom文件引入
<!-- websocket -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
Application文件修改
端口信息自行修改
public static void main(String[] args) throws IOException {
SpringApplication.run(Application.class, args);
try {
System.out.println("http://127.0.0.1:8083/ws/index");
new NettyServer(12345).start();
}catch(Exception e) {
System.out.println("NettyServerError:"+e.getMessage());
}
}
NettyServer服务启动类
包含基本配置信息
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
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) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", "WebSocket", true, 65536 * 10));
ch.pipeline().addLast(new MyWebSocketHandler());//自定义助手类
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
}
MyChannelHandlerPool
通道组池,管理所有websocket连接
channelIdMap用来存储用户id与通道id关系,用来实现点对点通讯,本来准备使用redis管理,遇到@Resource无论如何都是null的问题,暂时使用Map管理。
public class MyChannelHandlerPool {
public MyChannelHandlerPool(){}
public static Map<String, ChannelId> channelIdMap = new HashMap<>();
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
MyWebSocketHandler
WebSocket处理器,处理websocket连接相关
该类继承了SimpleChannelInboundHandler<TextWebSocketFrame>并重写部分生命周期以实现部分需求
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启!");
//添加到channelGroup通道组
MyChannelHandlerPool.channelGroup.add(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开连接,通道关闭!");
//添加到channelGroup 通道组
MyChannelHandlerPool.channelGroup.remove(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("客户端收到服务器数据:" + msg.text());
String msgText = msg.text();
JSONObject jsonObject = JSONObject.parseObject(msgText);
Map<String, Object> map = jsonObject;
String longId = ctx.channel().id().asLongText();
if (map.containsKey("login")) {
login(map, ctx.channel().id());
} else {
sendToSomeone(map);
}
}
//ping、pong
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//用于触发用户事件,包含触发读空闲、写空闲、读写空闲
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.ALL_IDLE) {
Channel channel = ctx.channel();
//关闭无用channel,以防资源浪费
channel.close();
}
}
}
private void sendAllMessage(String message){
//收到信息后,群发给所有channel
MyChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));
}
private void sendToSomeone(Map<String, Object> map) {
// ChannelId channelId = (ChannelId) redisOperator.getObject(6, map.get("toId").toString());
ChannelId fromId = MyChannelHandlerPool.channelIdMap.get(map.get("fromId"));
MyChannelHandlerPool.channelGroup.find(fromId).writeAndFlush(new TextWebSocketFrame(map.toString()));
ChannelId toId = MyChannelHandlerPool.channelIdMap.get(map.get("toId"));
MyChannelHandlerPool.channelGroup.find(toId).writeAndFlush(new TextWebSocketFrame(map.toString()));
}
private void login(Map<String, Object> map, ChannelId channelId) {
String uid = map.get("login").toString();
// redisOperator.set(6, uid, channelId);
MyChannelHandlerPool.channelIdMap.put(uid, channelId);
}
关于SimpleChannelInboundHandler其他的生命周期
测试
方便测试引入额外html依赖pom文件
<!--添加static和templates的依赖/html-->
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
修改配置文件
spring:
thymeleaf:
prefix: classpath:/templates/
一个简单的Controller
@Controller
@RequestMapping("/ws")
@Slf4j
public class IndexController {
@Resource
private RedisOperator redisOperator;
@GetMapping("/index")
public ModelAndView index(){
ModelAndView mav=new ModelAndView("socket");
mav.addObject("uid", RandomUtil.getRandomLetter(6, RandomUtil.CodeType.ALL_NUMBER));
return mav;
}
}
/resource/templates/socket.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Netty-Websocket</title>
<script type="text/javascript">
// by zhengkai.blog.csdn.net
var socket;
if(!window.WebSocket){
window.WebSocket = window.MozWebSocket;
}
if(window.WebSocket){
socket = new WebSocket("ws://127.0.0.1:12345/ws");
socket.onmessage = function(event){
var ta = document.getElementById('responseText');
ta.value += event.data+"\r\n";
};
socket.onopen = function(event){
var ta = document.getElementById('responseText');
ta.value = "Netty-WebSocket服务器。。。。。。连接 \r\n";
var uid = document.getElementById("uid").value;
login(uid);
};
socket.onclose = function(event){
var ta = document.getElementById('responseText');
ta.value = "Netty-WebSocket服务器。。。。。。关闭 \r\n";
};
}else{
alert("您的浏览器不支持WebSocket协议!");
}
function send(fromId, msg, toId){
if(!window.WebSocket){return;}
if(socket.readyState == WebSocket.OPEN){
var jsonObject = {};
jsonObject["fromId"] = fromId;
jsonObject["msg"] = msg;
jsonObject["toId"] = toId;
socket.send(JSON.stringify(jsonObject));
}else{
alert("WebSocket 连接没有建立成功!");
}
}
function login(fromId){
if(!window.WebSocket){return;}
if(socket.readyState == WebSocket.OPEN){
var jsonObject = JSON.parse("{}");
jsonObject["login"] = fromId;
socket.send(JSON.stringify(jsonObject));
}else{
alert("WebSocket 连接没有建立成功!");
}
}
</script>
</head>
<body>
<form onSubmit="return false;">
<label>ID</label><input type="text" id="uid" name="uid" th:value="${uid}" /><br />
<label>TEXT</label><input type="text" name="message" value="这里输入消息" /> <br />
<label>TEXT</label><input type="text" name="toUser" value="这里输入对方id" /> <br />
<br /> <input type="button" value="发送ws消息"
onClick="send(this.form.uid.value, this.form.message.value, this.form.toUser.value)" />
<hr color="black" />
<h3>服务端返回的应答消息</h3>
<textarea id="responseText" style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
</html>