Netty实现WebSocket通信
基于springboot2.x版本实现
menu
🌿maven依赖
🌿自定义handler
🌿自定义子处理器
🌿Server
🌿WebSocket启动配置类
🌿前端测试页
maven依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
自定义handler
package com.credi.websocket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.time.LocalDateTime;
/**
* 自定义的handler , 本handler用于处理消息
* TextWebSocketFrame:
* @Author: He Xingchi
* Created by ALIENWARE on 2019/12/21.
*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用以记录和管理所有客户端的channel
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 获取客户端传输过来的消息
String content = msg.text();
System.out.println("接收到来自客户端的数据: " + content );
// 遍历用以记录和管理所有客户端的channel中的通道channel
for (Channel channel : clients){ //将该客户端channel通道收到的消息刷出给每一个channel通道
/**
* writeAndFlush : 将消息刷出给该channel , 虽然接受的参数类型为Object , 但是参数不能是String , 而是TextWebSocketFrame
*/
channel.writeAndFlush(new TextWebSocketFrame(LocalDateTime.now() + " [服务器接收到消息:] " + " 接收到消息, 消息为: " + content));
}
// 用以记录和管理所有客户端的channel其实自带一个writeAndFlush函数 : 作用是向维护的所有通道刷出消息 , 作用是一样的
//clients.writeAndFlush(new TextWebSocketFrame(LocalDateTime.now() + " [服务器接收到消息:] " + " 接收到消息, 消息为: " + content));
}
/**
* 当客户端链接服务端之后(打开链接)
* 获取客户端的channel , 并且放到ChannelGroup中进行管理
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 处理客户端所对应的的channal的通道
Channel channel = ctx.channel();//当客户端和服务端链接之后 , 就有了一个双向的通道channel
clients.add(channel); // 将通道channel交给用以记录和管理所有客户端的channel进行维护和管理
}
// 用户离开或关闭客户端触发的方法
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//clients.remove(ctx.channel()); //当客户端关闭 , 移除链接客户端和服务端的双向通道的chuannal
// 但以上这句话是多余的 , 因为客户端关闭之后 , 用以记录和管理所有客户端的channel会自动把通道移除
System.out.println(ctx.channel().id()); //打印通道的id(每一个通道channel都有一份唯一的id , 长id(asLongText)和短id(asShortText))
System.out.println("客户端断开 , channle对应的长id为: " + ctx.channel().id().asLongText());
System.out.println("客户端断开 , channle对应的短id为: " + ctx.channel().id().asShortText()); // 短id可能出现重复
}
}
自定义子处理器
package com.credi.websocket;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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 org.springframework.context.annotation.Configuration;
/**
* 自定义子处理器
* @Author: He Xingchi
* Created by ALIENWARE on 2019/12/21.
*/
@Configuration
//@EnableWebSocket
public class WSServerInitialzer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// websocket 基于http协议 , 所以要有http编解码器
pipeline.addLast(new HttpServerCodec());
// 对写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 聚合器 , 对httpMessage进行聚合 , 聚合成FullHttpRequest或FullHttpResponse
// 几乎在netty中的编程 , 都会使用此handler
pipeline.addLast(new HttpObjectAggregator(1024*64));
// ============================= 以上是用于支持http协议 ==========================================>>>>>>>>>>
/**
* websocket 服务器处理的协议 , 用于指定连接客服端访问的路由 ip:端口/ws
* 本handler会帮你处理一些繁重的复杂的事
* 会帮你处理websocket的握手动作: handshaking(close, ping , pong) ping + pong = 心跳
* 对于websocket来讲 , 都是以frames进行传输的 , 不同的数据类型对应的frames也不同
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); //
// 自定义的handler
pipeline.addLast(new ChatHandler());
}
}
Server
package com.credi.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.springframework.stereotype.Component;
/**
* @Author: He Xingchi
* Created by ALIENWARE on 2019/12/21.
*/
@Component
public class WSServer {
private static class SingletionWSServer {
static final WSServer instance = new WSServer();
}
public static WSServer getInstance(){
return SingletionWSServer.instance;
}
// 线程组
EventLoopGroup mainGroup;
EventLoopGroup subGroup;
ServerBootstrap server;
ChannelFuture future;
public WSServer(){
// 线程组初始化
mainGroup = new NioEventLoopGroup();
subGroup = new NioEventLoopGroup();
// server初始化
server = new ServerBootstrap();
server.group(mainGroup,subGroup) //维护的channel
.channel(NioServerSocketChannel.class) //channel的类型 : nio
.childHandler(new WSServerInitialzer()); // 自定义子处理器
}
// 启动服务器
public void start(){
this.future = server.bind(8088); //绑定端口
System.err.println("netty websocket server 启动完毕...");
}
// public static void main(String[] args) throws Exception{
线程组
// EventLoopGroup mainGroup = new NioEventLoopGroup();
// EventLoopGroup subGroup = new NioEventLoopGroup();
// try {
server
// ServerBootstrap server = new ServerBootstrap();
// server.group(mainGroup,subGroup) //维护的channel
// .channel(NioServerSocketChannel.class) //channel的类型 : nio
// .childHandler(new WSServerInitialzer()); // 子处理器
//
// ChannelFuture future = server.bind(8088).sync();//绑定端口 , 设置同步 . 返回一个异步的future
future监听channel
// future.channel().closeFuture().sync();
// } finally {
关闭线程
// mainGroup.shutdownGracefully();
// subGroup.shutdownGracefully();
// }
// }
}
WebSocket启动配置类
package com.credi;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import com.credi.websocket.WSServer;
/**
* netty websocket 启动配置类
* @Author: He Xingchi
* Created by ALIENWARE on 2019/12/21.
*/
@Component
public class NettyBoot implements ApplicationListener<ContextRefreshedEvent>{
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent() == null){
try {
WSServer instance = WSServer.getInstance();
instance.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
前端测试页
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div>发送消息:</div>
<input type="text" id="msgContent" />
<input type="button" value="点我发送" onclick="CHAT.chat()" />
<div>接受消息:</div>
<div id="receiveMsg" style="background-color: gainsboro;"></div>
<!-- js -->
<script type="application/javascript">
//定义一个CHAT对象
window.CHAT = {
//socket对象
socket: null,
//初始化对象
init: function(){
//浏览器版本兼容问题
if(window.WebSocket){
//创建WebSocket对象123123
CHAT.socket = new WebSocket("ws://127.0.0.1:8088/ws") // ws://服务端的ip:绑定的端口/接口路由
// 客户端和服务端进行连接时
CHAT.socket.onopen = function(){
console.log("连接建立成功...")
},
// 客户端和服务端关闭时
CHAT.socket.onclose = function(){
console.log("连接关闭...");
},
// 客户端和服务端链接异常时
CHAT.socket.onerror = function(){
console.log("连接异常...");
},
// 服务端向客户端刷出消息时
CHAT.socket.onmessage = function(e){
console.log("服务器刷出的消息: " + e.data);
var receiveMsg = document.getElementById("receiveMsg");
var html = receiveMsg.innerHTML;
receiveMsg.innerHTML = html + "<br/>" + e.data;
}
}else{
alert("浏览器不支持WebSocket");
}
},
chat: function(){
var msgContent = document.getElementById("msgContent");
// 发送消息方法
CHAT.socket.send(msgContent.value);
// 关闭连接方法
//CHAT.socket.close();
}
};
// 初始化websocket链接
CHAT.init();
</script>
</body>
</html>
附
完整POM.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>
<!--<packaging>war</packaging>-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.hp</groupId>
<artifactId>Talk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Talk</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- java生成条形码 -->
<dependency>
<groupId>net.sf.barcode4j</groupId>
<artifactId>barcode4j-light</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- thymeleaf的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- swagger -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--commons-lang3:数组集合字符串非空校验-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!-- Lombok包 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
<!-- 使用 JSONObject net.sf.json.JSON -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<!-- 添加GSON依赖包 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- mybatis-generator-core 反向生成java代码-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<!-- 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
<!-- 高性能分布式文件服务器 -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.2</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--自动化生成代码-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
</plugin>
<!--base64找不到问题解决-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>${project.build.sourceEncoding}</encoding>
<!-- added by xiluhua 20160627
com.sun.org.apache.xml.internal.security.utils.Base64;编译异常
将jre/lib/rt.jar添加到maven的compiler里面 编译正常..。
-->
<compilerArguments>
<verbose />
<bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<!-- 前端图标展示BUG , 使用该注解会让所有xml映射文件失效 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
<nonFilteredFileExtension>xls</nonFilteredFileExtension>
<nonFilteredFileExtension>zip</nonFilteredFileExtension>
<nonFilteredFileExtension>eot</nonFilteredFileExtension>
<nonFilteredFileExtension>svg</nonFilteredFileExtension>
<nonFilteredFileExtension>ttf</nonFilteredFileExtension>
<nonFilteredFileExtension>woff</nonFilteredFileExtension>
<nonFilteredFileExtension>woff2</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
启动类
package com.credi;
import javafx.application.Application;
import javafx.stage.Stage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Author: He Xingchi
* Created by Administrator on 2019/12/21.
*/
@SpringBootApplication
public class Starter extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
SpringApplication.run(Starter.class);
}
}
application.yml
# 开发环境
server:
#端口号
port: 8080
# 配置DataSource
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
# 开发
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
initialSize: 5
maxActive: 100
minIdle: 3
maxWait: 50000
main:
allow-bean-definition-overriding: true
thymeleaf:
prefix: classpath:/templates/
suffix: .html
cache: false
tomcat:
uri-encoding: UTF-8
# 配置MyBatis
mybatis:
type-aliases-package: com.credi.pojo
mapper-locations: classpath:mapper/*.xml
# 配置PageHelper
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql