Netty WebSocket 入门
2024-04-25
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
## 前言
头都要学秃了,真的难,建议非必要别学netty
官网:[https://netty.io/](https://netty.io/)
<br>
本文主要是观看以下BiliBili视频中P1 - P11学习笔记
[【Netty项目】这可能是B站唯一完整的Netty实战项目,2小时手把手带你用Netty开发IM在线聊天项目](https://www.bilibili.com/video/BV1Ua4y1r7vP)
<br><br>
## 折腾
#### ===> 基本款(能跑起来)
IDEA新建一个Maven项目
Archetype选择queststart
进入项目界面后需要稍作调整
在`File` - `Project Structure`中,确保每个地方的JDK都是17
在`Settings`中的Maven按自己实际情况优化一下,最好是用本地Maven
`pom.xml`
加入netty-all和fastjson2依赖
```xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zzzmh</groupId>
<artifactId>netty-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>netty-demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.109.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.49</version>
</dependency>
</dependencies>
</project>
```
<br><br>
**后端核心代码**
`WebSocketServer.java`
```java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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;
public class WebSocketServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(1024 * 64))
.addLast(new WebSocketServerProtocolHandler("/"))
.addLast(new WebSocketHandler());
}
});
bootstrap.bind(7001).sync();
}
}
```
`WebSocketHandler.java`
```java
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
System.out.println(frame.text());
}
}
```
之后在IDEA中启动WebSocketServer的main方法即可启动Socket服务器端
<br><br>
客户端随便用html手搓个毛坯房版
`index.html`
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Netty WebSocket Client Demo</title>
</head>
<body>
<input id="text" type="text"/>
<button onclick="send()">Send</button>
<button onclick="closeWebSocket()">Close</button>
<div id="message"></div>
<script>
if (!'WebSocket' in window) {
alert('当前浏览器不支持WebSocket 请更换浏览器或设备!')
}
const websocket = new WebSocket("ws://localhost:7001");
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("连接错误");
};
//连接成功建立的回调方法
websocket.onopen = function (event) {
setMessageInnerHTML("已连接");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML("已收到消息:" + event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("已断开");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += '<br>' + innerHTML;
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
const message = document.getElementById('text').value;
setMessageInnerHTML("已发送消息:" + message)
websocket.send(message);
}
</script>
</body>
</html>
```
<br><br>
启动后效果如图
![](/api/file/getImage?fileId=6629e248da7405001403f625)
![](/api/file/getImage?fileId=6629e247da7405001403f624)
#### ===> 进阶款(能聊天)
过程略
直接上代码
后端一共4个类
`WebSocketServer.java`
```java
package com.zzzmh.websocket.server;
import com.zzzmh.websocket.server.handler.BaseWebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
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 io.netty.util.concurrent.GlobalEventExecutor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class WebSocketServer {
public static final Map<String, Channel> CHANNELS = new ConcurrentHashMap<>();
public static final ChannelGroup GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
public static void main(String[] args) throws InterruptedException {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(1024 * 64))
.addLast(new WebSocketServerProtocolHandler("/"))
.addLast(new BaseWebSocketHandler());
}
});
bootstrap.bind(7001).sync();
}
}
```
`ResultFrame.java`
```java
package com.zzzmh.websocket.server.utils;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.Data;
@Data
public class ResultFrame extends JSONObject {
public static TextWebSocketFrame ok(Integer code, Object data) {
return new TextWebSocketFrame(JSONObject.of(
"code", code,
"msg", "success",
"data", data
).toJSONString());
}
public static TextWebSocketFrame ok(Integer code, JSONObject data) {
return new TextWebSocketFrame(JSONObject.of(
"code", code,
"msg", "success",
"data", data
).toJSONString());
}
public static TextWebSocketFrame ok(Integer code, JSONArray data) {
return new TextWebSocketFrame(JSONObject.of(
"code", code,
"msg", "success",
"data", data
).toJSONString());
}
public static TextWebSocketFrame error() {
return error(500, "网络异常!");
}
public static TextWebSocketFrame error(Integer code, String message) {
return new TextWebSocketFrame(JSONObject.of(
"code", code,
"msg", message,
"data", null
).toJSONString());
}
}
```
`BaseWebSocketHandler.java`
```java
package com.zzzmh.websocket.server.handler;
import com.alibaba.fastjson2.JSONObject;
import com.zzzmh.websocket.server.WebSocketServer;
import com.zzzmh.websocket.server.utils.ResultFrame;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.Iterator;
import java.util.Map;
public class BaseWebSocketHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
WebSocketServer.GROUP.add(ctx.channel());
System.out.println(ctx.channel().id().asShortText() + " 匿名用户连接");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 用户断开连接事件
super.channelInactive(ctx);
WebSocketServer.GROUP.remove(ctx.channel());
Iterator<Map.Entry<String, Channel>> iterator = WebSocketServer.CHANNELS.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Channel> entry = iterator.next();
if(ctx.channel().equals(entry.getValue())){
System.out.println(entry.getKey() + " 断开连接");
iterator.remove();
WebSocketServer.GROUP.writeAndFlush(ResultFrame.ok(201,
WebSocketServer.CHANNELS.keySet()
));
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
TextWebSocketFrame frame = (TextWebSocketFrame) msg;
JSONObject req = JSONObject.parseObject(frame.text());
switch (req.getIntValue("code")) {
// 客户端连接事件
case 10000 -> ConnectionHandler.execute(ctx, req);
case 10001 -> ChatHandler.execute(ctx, req);
// code 不在枚举范围中
default -> ctx.channel().writeAndFlush(ResultFrame.error(501, "无效请求!"));
}
} catch (Exception e) {
ctx.channel().writeAndFlush(ResultFrame.error());
}
}
}
```
`ChatHandler.java`
```java
package com.zzzmh.websocket.server.handler;
import com.alibaba.fastjson2.JSONObject;
import com.zzzmh.websocket.server.WebSocketServer;
import com.zzzmh.websocket.server.utils.ResultFrame;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.internal.StringUtil;
public class ChatHandler {
public static void execute(ChannelHandlerContext ctx, JSONObject req) {
String nickname = req.getString("nickname");
String message = req.getString("message");
if (StringUtil.isNullOrEmpty(nickname)) {
ctx.channel().writeAndFlush(ResultFrame.error(401, "昵称不能为空"));
return;
}
if (StringUtil.isNullOrEmpty(message)) {
ctx.channel().writeAndFlush(ResultFrame.error(401, "消息不能为空"));
return;
}
// 群发消息 这里用code区分方法
WebSocketServer.GROUP.writeAndFlush(ResultFrame.ok(202, JSONObject.of(
"nickname", nickname,
"message", message
)));
}
}
```
`ConnectionHandler.java`
```java
package com.zzzmh.websocket.server.handler;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.zzzmh.websocket.server.WebSocketServer;
import com.zzzmh.websocket.server.utils.ResultFrame;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.internal.StringUtil;
public class ConnectionHandler {
public static void execute(ChannelHandlerContext ctx, JSONObject req) {
// 初次连接 需要保存 user chanel
String nickname = req.getString("nickname");
if (StringUtil.isNullOrEmpty(nickname)) {
ctx.channel().writeAndFlush(ResultFrame.error(401, "昵称不能为空"));
return;
}
WebSocketServer.CHANNELS.put(nickname, ctx.channel());
// 群发消息 这里用code区分方法
WebSocketServer.GROUP.writeAndFlush(ResultFrame.ok(201,
WebSocketServer.CHANNELS.keySet()
));
}
}
```
前端也就100行代码
`index.html`
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Netty WebSocket Client Demo</title>
</head>
<body style="display: flex;flex-direction: column;align-items: center;padding: 16px">
<div style="display:flex;">
<textarea id="console" style="width: 500px;height: 800px" placeholder="控制台"></textarea>
<textarea id="messages" style="width: 500px;height: 800px" placeholder="消息列表"></textarea>
<textarea id="users" style="width: 500px;height: 800px" placeholder="人员列表"></textarea>
</div>
<div style="display: flex;flex-direction: row;justify-content: start;width: 1518px;padding: 16px 0;">
<input id="nickname" type="text" placeholder="昵称"/>
<input id="message" type="text" placeholder="消息"/>
<button onclick="sendMessage()">发送消息</button>
<button onclick="openConn()">加入聊天</button>
<button onclick="closeConn()">中断连接</button>
</div>
<script>
if (!'WebSocket' in window) {
alert('当前浏览器不支持WebSocket 无法继续进行游戏,请更换浏览器或设备!')
}
const websocket = new WebSocket("ws://localhost:7001");
//连接发生错误的回调方法
websocket.onerror = function () {
console("连接错误");
};
//连接成功建立的回调方法
websocket.onopen = function (event) {
console("已连接");
}
//连接关闭的回调方法
websocket.onclose = function () {
console("已断开");
}
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
websocket.close();
}
// 接收到消息的回调方法
websocket.onmessage = function (event) {
console("已收到消息:" + event.data);
const resp = JSON.parse(event.data);
switch (resp.code) {
case 201 :
users(resp.data);
break;
case 202 :
messages(resp.data.nickname, resp.data.message);
break;
}
}
// 关闭连接
function closeConn() {
websocket.close();
}
// 初始化数据
function openConn() {
const nickname = document.querySelector('#nickname').value;
if (!nickname) {
alert('昵称不能为空');
return;
}
websocket.send(JSON.stringify({
'code': 10000,
'nickname': nickname
}));
}
// 发消息
function sendMessage() {
const nickname = document.querySelector('#nickname').value;
const message = document.querySelector('#message').value;
websocket.send(JSON.stringify({
'code': 10001,
'nickname': nickname,
'message': message,
}));
console("已发送消息:" + message)
}
// 控制台
function console(string) {
const area = document.querySelector('#console');
area.value += getCurrentTime() + ' ' + string + '\n';
area.scrollTop = area.scrollHeight;
}
function messages(nickname, string) {
const area = document.querySelector('#messages');
area.value += getCurrentTime() + ' ' + nickname + ': ' + string + '\n';
area.scrollTop = area.scrollHeight;
}
function users(nicknames) {
const area = document.querySelector('#users');
area.value = '';
nicknames.forEach(nickname => {
area.value += nickname + '\n';
})
area.scrollTop = area.scrollHeight;
}
// 时间戳
function getCurrentTime() {
const now = new Date();
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
}
</script>
</body>
</html>
```
大致思路是,前端打开页面自动匿名连接socket,匿名可以收到聊天,但不能发消息。填入nickname就可以加入聊天,右侧textarea显示在线人员名单,加入聊天后再填写message点发送消息就可以正常聊天,所有人都是同步收到。
截图
![](/api/file/getImage?fileId=662b0abfda74050014040059)
![](/api/file/getImage?fileId=662b0abfda7405001404005a)
## END
先折腾到这里,还有客户端的下次在回来更新
累了
![](/api/file/getImage?fileId=64c9c860da74050014005b69)