看这篇博文之前,你需要对netty的使用有基本的了解,比如服务端建立,handler中的事件等等有所了解。但并不需要太过深入。
最近一段时间都在学习netty。强烈推荐《Z00317 NETTY权威指南(第2版)》这本书。
废话不多说,结合自己的实践,在此给大家做一下使用springboot+netty开发聊天室的详细介绍。我力求话语简单直白,不给大家增加疑惑。
老套路,服务端+客户端。(详细的代码请参照我的GitHub)
一、服务端
1、服务启动类代码
服务启动类比较重要的东西是ChannelInitializer的内部类中的内容,其实整个服务端的东西核心是我下面将要说的处理事件的handler类,而这个服务启动类就看一下就行,不要深究。当netty掌握到一定程度时再详细的去学习启动过程。
@Component //加入容器
public class WebSocketServer {
@Value("${socket.server.port}") //在.xml文件中配置属性注入
private int port;
@Value("${socket.server.address}")
private String address;
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() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast("http-codec",new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast("aggregator",new HttpObjectAggregator(8192));
ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
ch.pipeline().addLast(new MyChannelHandler());
}
});
ChannelFuture cf = sb.bind(address,port).sync(); // 服务器异步创建绑定
System.out.println(WebSocketServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
}
2、handler
重点来了,handler是一个服务端的核心,来看看handler中是怎么实现用户聊天的
比较重要的是这样几个事件方法:
1、channelActive--对应连接建立事件
2、channelRead0--对应服务端读事件
两个用户之间可以聊天归功于 private static volatile Vector
@ChannelHandler.Sharable //此处要加Sharable注解,标注此handler可以被多个连接使用
public class MyChannelHandler extends SimpleChannelInboundHandler
整个handler代码看下来其实大体的东西很简单,核心就是channelRead0这个方法。我们知道根据websocket协议,建立连接时需要使用http协议进行一次握手,握手成功之后根据tcp协议传输数据。知道了这些再看这个方法就显而易见了。
首先第一个if语句中的“handleHttpRequest(ctx,(FullHttpRequest) msg)”方法,是自定义的方法,作用就是进行http握手。
else if语句中的“handleWebSocketFrame(ctx,(WebSocketFrame) msg)”方法,也是自定义的方法,作用就是传输聊天内容。
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//传统的HTTP介入
if(msg instanceof FullHttpRequest){
handleHttpRequest(ctx,(FullHttpRequest) msg);
}
//WebSocket 接入
else if(msg instanceof WebSocketFrame){
handleWebSocketFrame(ctx,(WebSocketFrame) msg);
}
}
着重讲一下handleWebSocketFrame(ctx,(WebSocketFrame) msg)方法的实现。
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
//判断是否是关闭链路的指令
if(frame instanceof CloseWebSocketFrame){
handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain());
return;
}
//判断是否是ping消息
if(frame instanceof PongWebSocketFrame){
PingWebSocketFrame ping = new PingWebSocketFrame(frame.content().retain());
ctx.channel().writeAndFlush(ping);
return ;
}
//判断是否是pong消息
if(frame instanceof PingWebSocketFrame){
PongWebSocketFrame pong = new PongWebSocketFrame(frame.content().retain());
ctx.channel().writeAndFlush(pong);
return ;
}
//仅支持文本消息,不支持二进制消息
if(!(frame instanceof TextWebSocketFrame)){
throw new UnsupportedOperationException("不支持二进制");
}
//返回应答消息
//可以对消息进行处理
//获取接收的消息
String request=((TextWebSocketFrame) frame).text();
if (contexts.size() <= 1) {
ctx.channel().write(new TextWebSocketFrame("对方不在线"));
//return;
}else{
int currentIndex = contexts.indexOf(ctx);
int anotherIndex = Math.abs(currentIndex - 1);
//给另一个客户端转发信息
contexts.get(anotherIndex).writeAndFlush(new TextWebSocketFrame(request));
}
}
看完代码我们知道了,原来两个用户之间实现消息的发送与接受其实就是获取到对方的ChannelHandlerContext对象,即Vector中所存储的,是不是豁然开朗。
3、通过springboot启动
既然是通过springboot整合的,那需要在springboot启动的时候就开启服务端。很简单,代码如下:
@SpringBootApplication
public class SocketServerApplication implements CommandLineRunner {
@Resource
WebSocketServer webSocketServer;
public static void main(String[] args) throws Exception {
SpringApplication.run(SocketServerApplication.class, args);
}
//启动websocket服务端
@Override
public void run(String... strings) throws Exception {
webSocketServer.start();
}
}
二、客户端
客户端的代码就没有那么复杂了,就是一个简单的web项目。
1、访问到聊天页面的controller
@Controller
public class Client_Entrance {
@GetMapping(value = "chat")
public String test(){
return "index";
}
}
2、静态资源
静态资源没什么可说的,通过JavaScript向服务端发送websocket请求的路劲要写对:
socket = new WebSocket("ws://ip:端口/websocket");
三、部署到阿里云
1、首先需要将客户端和服务端打成jar包,idea中打jar包有个小坑,正确的步骤请参照 使用idea打jar包正确教程。
2、坑!
重要的事情说三遍:服务端部署到阿里云的时候,绑定的地址不是公网ip,是内网ip!是内网ip!是内网ip!
但是客户端JavaScript中访问的地址是:公网ip!
四、效果展示
经过上述步骤,一个简单的聊天室就就好了