SpringBoot+Netty开发IM即时通讯系列(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_26975307/article/details/85051833

上篇 “SpringBoot+Netty开发IM即时通讯系列(一)”介绍了Netty与NIO等基础知识点,感兴趣的可以去看下:

https://blog.csdn.net/qq_26975307/article/details/85004424 

本篇使用Netty+WebSocket构建一个最简单的Demo在线IM通信项目(关于接口或类的作用等已在注释中注明)。

前言

实时通信的分类:

(1)Ajax轮训
        通过JS以Ajax异步地让浏览器每隔一段时间(10S)发送请求到后端,去询问服务端是否有新消息、新状态等,如果有则取出并通过前端再渲染。但这很容易造成无限循环,也就是前端Ajax会不停地循环后端的数据                                 (使用场景:浏览器不需要一直刷新,简单的后台管理系统中的数据更新等)


    (2)Long Pull
        与Ajax轮训类似,也是使用异步请求,只不过它的轮训方式不太友好,阻塞式轮训:当客户端发起请求之后,服务端如果未响应,则Long Pull就不会有响应,直到服务端返回response。过程中不停地建立Http请求,等待服务器端进行处理,被动响应,缺点也是非常明显,也很耗费资源,性能低。


    (3)webSokect - 推荐
        Http本身就不支持长连接,Http1.1支持长连接,WebSokect就是使用了Http1.1协议来完成一小部分的握手,简单来讲就是,客户端发起请求到服务端,服务端会去找一个副助理,找到之后服务器端会和客户端一直保持连接,为客户端进行服务,并且可以主动推送一些消息给客户端。

关于WebSocket

WebSokect有哪些协议,又有什么优点?
            1)首先WebSokect相对于Http这种非持久化来讲,是一种持久化的协议,Http的生命周期可以说是通过一个request来进行判定,有一个request请求到后端,后端也会相应的返回一个response给客户端,或者有多个request对应到多个response,两者之间都是一一对应的,有多少个request请求就会有多少个response相应,不会有偏差。此时response其实也是被动的,它不能由服务器端主动发起相应,必须先有request请求。
            2)WebSokect由此诞生,它使得资源不会像以前一样浪费,并且它也是非常的主动,只要链接一旦被建立完毕之后,那么服务端就可以不停的主动推送消息给客户端,客户端不需要主动请求服务端也可以达到一样的效果。 也就是说,只要建立一次Http请求就能达到信息的源源不断的传输。类似于在线Online小游戏,一开始建立连接,就可以一直保持在线了。

WebSocket API(最基础也是最常用的几个)

(1)var socket=new WebSocket("ws://[ip]:[port]");
(2)生命周期:onopen() onmessage() onerror() onclose()
(3)主动方法:Socket.send() Socket.close()

参考API:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

导入相关依赖

<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.pubing</groupId>
  <artifactId>helloNetty</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
	<dependencies>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.25.Final</version>
		</dependency>
	</dependencies>
  
  
</project>

创建服务器启动类 WebSocketServer

package com.phubing.websokect;

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;

/**
 * 用于和客户端进行连接
 * 
 * @author phubing
 *
 */
public class WebSocketServer {
	public static void main(String[] args) throws InterruptedException {
		//定义线程组
		EventLoopGroup mainGroup =  new NioEventLoopGroup();
		EventLoopGroup subGroup =  new NioEventLoopGroup();
		try {
		ServerBootstrap server = new ServerBootstrap();
		server.group(mainGroup, subGroup)
		//channel类型
		.channel(NioServerSocketChannel.class)
		//针对subGroup做的子处理器,childHandler针对WebSokect的初始化器
		.childHandler(new WebSocketinitializer());
		
		//绑定端口并以同步方式进行使用
		ChannelFuture channelFuture = server.bind(10086).sync();
		
		//针对channelFuture,进行相应的监听
		channelFuture.channel().closeFuture().sync();
		
		}finally {
			//针对两个group进行优雅地关闭
			mainGroup.shutdownGracefully();
			subGroup.shutdownGracefully();
		}
	
	}
	
}

创建WebSocket初始化器WebSocketinitializer

package com.phubing.websokect;

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;

public class WebSocketinitializer extends ChannelInitializer<SocketChannel>{

	@Override
	protected void initChannel(SocketChannel socketChannel) throws Exception {
		//从Channel中获取对应的pipeline
		ChannelPipeline channelPipeline = socketChannel.pipeline();
		
		//添加相应的助手类与处理器
		/**
		 * WebSokect基于Http,所以要有相应的Http编解码器,HttpServerCodec()
		 */
		channelPipeline.addLast(new HttpServerCodec());
		
		//在Http中有一些数据流的传输,那么数据流有大有小,如果说有一些相应的大数据流处理的话,需要在此添加
		//ChunkedWriteHandler:为一些大数据流添加支持
		channelPipeline.addLast(new ChunkedWriteHandler());
		
		//UdineHttpMessage进行处理,也就是会用到request以及response
		//HttpObjectAggregator:聚合器,聚合了FullHTTPRequest、FullHTTPResponse。。。,当你不想去管一些HttpMessage的时候,直接把这个handler丢到管道中,让Netty自行处理即可
		channelPipeline.addLast(new HttpObjectAggregator(2048*64));
		
		//================华丽的分割线:以上是用于支持Http协议================
		//================华丽的分割线:以下是用于支持WebSoket==================
		
		// /ws:一开始建立连接的时候会使用到,可自定义
		//WebSocketServerProtocolHandler:给客户端指定访问的路由(/ws),是服务器端处理的协议,当前的处理器处理一些繁重的复杂的东西,运行在一个WebSocket服务端
		//另外也会管理一些握手的动作:handshaking(close,ping,pong) ping + pong = 心跳,对于WebSocket来讲,是以frames进行传输的,不同的数据类型对应的frames也不同
		channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
		
		//添加自动handler,读取客户端消息并进行处理,处理完毕之后将相应信息传输给对应客户端
		channelPipeline.addLast(new ChatHandler());
				
		
		
	}
	
}

添加自定义助手ChatHandler

package com.phubing.websokect;

import java.time.LocalDate;

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;

//TextWebSocketFrame:处理消息的handler,在Netty中用于处理文本的对象,frames是消息的载体
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{

	//用于记录和管理所有客户端的channel,可以把相应的channel保存到一整个组中
	//DefaultChannelGroup:用于对应ChannelGroup,进行初始化
	private static ChannelGroup channelClient =  new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
	
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
		//text()获取从客户端发送过来的字符串
		String content = msg.text();
		System.out.println("客户端传输的数据:"+content);
		
		//针对channel进行发送,客户端对应的是channel
		/**
		 * 方式一
		 */
		for (Channel channel : channelClient) {
			//循环对每一个channel对应输出即可(往缓冲区中写,写完之后再刷到客户端)
			//注:writeAndFlush不可以使用String,因为传输的载体是一个TextWebSocketFrame,需要把消息通过载体再刷到客户端
			channel.writeAndFlush(new TextWebSocketFrame("【服务器于 " + LocalDate.now() + "接收到消息:】 ,消息内容为:" +content));
			
		}		
		/**
		 * 方式二
		channelClient.writeAndFlush(new TextWebSocketFrame("【服务器于 " + LocalDate.now() + "接收到消息:】 ,消息内容为:" +content))
		 */
		
	}

	//当客户端连接服务端(或者是打开连接之后)
	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		//获取客户端所对应的channel,添加到一个管理的容器中即可
		channelClient.add(ctx.channel());
	}

	//客户端断开
	@Override
	public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
		//实际上是多余的,只要handler被移除,client会自动的把对应的channel移除掉
		channelClient.remove(ctx.channel());
		//每一而channel都会有一个长ID与短ID
		//一开始channel就有了,系统会自动分配一串很长的字符串作为唯一的ID,如果使用asLongText()获取的ID是唯一的,asShortText()会把当前ID进行精简,精简过后可能会有重复
		System.out.println("channel的长ID:"+ctx.channel().id().asLongText());
		System.out.println("channel的短ID:"+ctx.channel().id().asShortText());
	}
	
	
}

此时,服务端已完成,接下来再新建一个前端页面,用于发送文本与显示服务端推送的数据

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>Netty+WebSocket案例</title>
	</head>
	<body>
		<div id="">发送消息:</div><br>
		<input type="text" name="messageContent" id="messageContent"/>
		<input type="button" name="" id="" value="发送" onclick="CHAT.chat()"/>
		
		<hr>
		
		<div id="">接收消息:</div><br>
		<div id="receiveNsg" style="background-color: gainsboro;"></div>
		
		
		<script type="text/javascript">
			window.CHAT = {
				socket: null,
				//初始化
				init: function(){
					//首先判断浏览器是否支持WebSocket
					if (window.WebSocket){
						CHAT.socket = new WebSocket("ws://localhost:10086/ws");
						CHAT.socket.onopen = function(){
							console.log("客户端与服务端建立连接成功");
						},
						CHAT.socket.onmessage = function(e){
							console.log("接收到消息:"+e.data);
							var receiveNsg = window.document.getElementById("receiveNsg");
							var html = receiveNsg.innerHTML;
							receiveNsg.innerHTML = html + "<br>" + e.data; 
						},
						CHAT.socket.onerror = function(){
							console.log("发生错误");
						},
						CHAT.socket.onclose = function(){
							console.log("客户端与服务端关闭连接成功");
						}						
					}else{
						alert("8102年都过了,升级下浏览器吧");
					}
				},
				chat: function(){
					var msg = window.document.getElementById("messageContent");
					
					CHAT.socket.send(msg.value);
				}
			}
			
			CHAT.init();
			
		</script>
		
	</body>
</html>

端口号、IP地址、WebSocket的服务端提供名称一定要与前端相对应,否则会出错

例如:服务端给客户端指定访问的路由为:/ws,IP地址为:192.168.45.96,端口号为:10086

那么前端在建立WebSocket连接时填写为:

CHAT.socket = new WebSocket("ws://localhost:10086/ws");   //ws://   为固定写法

最后来看看效果图

如有不当之处请指出,虚心接受建议与批评。

猜你喜欢

转载自blog.csdn.net/qq_26975307/article/details/85051833