前言
曾几何时,不知道大家有没有在项目里遇到过需要服务端给客户端推送消息的需求,是否曾经苦恼过、纠结过,我们知道要想实现这样的需求肯定离不开websocket长连接方式,那么到底是该选原生的websocket还是更加高级的netty框架呢?在此我极力推荐netty,因为一款好的框架一般都是在原生的基础上进行包装成更好、更方便、更实用的东西,很多我们需要自己考虑的问题都基本可以不用去考虑,不过此文不会去讲netty有多么的高深莫测,因为这些概念性的东西随处可见,而是通过实战来达到推送消息的目的。
实战
一、逻辑架构图
从图中可以看出本次实战的基本流程是客户端A请求服务端核心模块,核心模块生产一条消息到消息队列,然后服务端消息模块消费消息,消费完之后就将消息推送给客户端B,流程很简单,没有太多技巧,唯一的巧妙之处就在消息模块这边的处理上,本文的重点也主要讲解消息模块这一块,主要包括netty server、netty client、channel的存储等等。
二、代码
1、添加依赖
< groupId > io.netty groupId>
< artifactId > netty - all artifactId>
< version > 4.1 .6.Final version>
< / dependency>
2、NettyServer类
@Service
public class NettyServer {
public void run( int port )
{
new Thread()
{
public void run()
{
runServer( port );
}
}.start();
}
private void runServer( int port )
{
Print.info( "===============Message服务端启动===============" );
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group( bossGroup, workerGroup ); b.channel( NioServerSocketChannel.class );
b.childHandler( new ChannelInitializer()
{
protected void initChannel( SocketChannel ch ) throws Exception {
ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast( "codec-http", new HttpServerCodec() );
pipeline.addLast( "aggregator", new HttpObjectAggregator( 65536 ) );
pipeline.addLast( "handler", new MyWebSocketServerHandler() );
}
} ); Channel ch = b.bind( port ).sync().channel(); Print.info( "Message服务器启动成功:" + ch.toString() );
ch.closeFuture().sync();
} catch ( Exception e ) {
Print.error( "Message服务运行异常:" + e.getMessage() );
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); Print.info( "Message服务已关闭" );
}
}
}
3、MyWebSocketServerHandler类
public class MyWebSocketServerHandler extends SimpleChannelInboundHandler
4、ChannelSupervise类
public class ChannelSupervise {
private static ChannelGroup GlobalGroup = new DefaultChannelGroup( GlobalEventExecutor.INSTANCE );
private static ConcurrentMap ChannelMap = new ConcurrentHashMap();
public static void addChannel( String apiToken, Channel channel )
{
GlobalGroup.add( channel );
if ( null != apiToken )
{
ChannelMap.put( apiToken, channel.id() );
}
}
public static void updateChannel( String apiToken, Channel channel )
{
Channel chan = GlobalGroup.find( channel.id() ); if ( null == chan )
{
addChannel( apiToken, channel );
}else {
ChannelMap.put( apiToken, channel.id() );
}
}
public static void removeChannel( Channel channel )
{
GlobalGroup.remove( channel );
Collection values = ChannelMap.values(); values.remove( channel.id() );
}
public static Channel findChannel( String apiToken )
{
ChannelId chanId = ChannelMap.get( apiToken );
if ( null == chanId )
{
return(null);
}
return(GlobalGroup.find( ChannelMap.get( apiToken ) ) );
}
public static void sendToAll( TextWebSocketFrame tws )
{
GlobalGroup.writeAndFlush( tws );
}
public static void sendToSimple( String apiToken, TextWebSocketFrame tws )
{
GlobalGroup.find( ChannelMap.get( apiToken ) ).writeAndFlush( tws );
}
}
5、NettyClient类
@Servicepublic class NettyClient { private Channel channel; public void run(String strUri){ new Thread(){ public void run(){ runClient(strUri); } }.start(); private void runClient(String strUri) { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); URI uri = new URI(strUri); String protocol = uri.getScheme(); if (!"ws".equals(protocol)) { throw new IllegalArgumentException("Unsupported protocol: " + protocol); } HttpHeaders customHeaders = new DefaultHttpHeaders(); customHeaders.add("MyHeader", "MyValue"); // Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00\. // If you change it to V00, ping is not supported and remember to change // HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline. final MyWebSocketClientHandler handler = new MyWebSocketClientHandler( WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, false, customHeaders)); b.group(group); b.channel(NioSocketChannel.class); b.handler(new ChannelInitializer() { @Overpublic void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("http-codec", new HttpClientCodec()); pipeline.addLast("aggregator", new HttpObjectAggregator(8192)); pipeline.addLast("ws-handler", handler); } }); Print.info("===============Message客户端启动==============="); channel = b.connect(uri.getHost(), uri.getPort()).sync().channel(); handler.handshakeFuture().sync(); channel.closeFuture().sync(); } catch (Exception e){ Print.error(e.getMessage()); } finally { group.shutdownGracefully(); } }@Servicepublic class NettyClient {
private Channel channel;
public void run( String strUri )
{
new Thread()
{
public void run()
{
runClient( strUri );
}
}.start(); private void runClient( String strUri )
{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
URI uri = new URI( strUri );
String protocol = uri.getScheme(); if ( !"ws".equals( protocol ) )
{
throw new IllegalArgumentException( "Unsupported protocol: " + protocol );
}
HttpHeaders customHeaders = new DefaultHttpHeaders();
customHeaders.add( "MyHeader", "MyValue" );
/*
* Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
* If you change it to V00, ping is not supported and remember to change
* HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
*/
final MyWebSocketClientHandler handler =
new MyWebSocketClientHandler(
WebSocketClientHandshakerFactory.newHandshaker( uri, WebSocketVersion.V13, null, false, customHeaders ) ); b.group( group ); b.channel( NioSocketChannel.class ); b.handler( new ChannelInitializer()
{
@Overpublic void initChannel( SocketChannel ch ) throws Exception {
ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast( "http-codec", new HttpClientCodec() ); pipeline.addLast( "aggregator", new HttpObjectAggregator( 8192 ) ); pipeline.addLast( "ws-handler", handler );
}
} ); Print.info( "===============Message客户端启动===============" ); channel = b.connect( uri.getHost(), uri.getPort() ).sync().channel(); handler.handshakeFuture().sync(); channel.closeFuture().sync();
} catch ( Exception e ) {
Print.error( e.getMessage() );
} finally {
group.shutdownGracefully();
}
}
6、MyWebSocketClientHandler类
public class MyWebSocketClientHandler extends SimpleChannelInboundHandler
7、启动类
@SpringBootApplication
@Servicepublic
class MessageApplication {
@Autowired
private NettyServer server; @Autowired
private NettyClient client; public static void main( String[] args )
{
SpringApplication.run( MessageApplication.class, args );
}
@PostConstruct
public void initMessage()
{
server.run( 9502 );
try {
Thread.sleep( 1000 );
} catch ( InterruptedException e ) {
e.printStackTrace();
} client.run( "ws://localhost:" + 9502 );
}
8、客户端B测试页面
< html >
< head >
< meta charset = "UTF-8" >
< title > WebSocket Chat title>
< / head >
< body >
< script type = "text/javascript" >
var socket;
if ( !window.WebSocket )
{
window.WebSocket = window.MozWebSocket;
}
if ( window.WebSocket )
{
socket = new WebSocket( "ws://localhost:9502" );
socket.onmessage = function( event )
{
var ta = document.getElementById( 'responseText' );
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function( event )
{
var ta = document.getElementById( 'responseText' );
ta.value = "连接开启!";
};
socket.onclose = function( event )
{
var ta = document.getElementById( 'responseText' );
ta.value = ta.value + "连接被关闭";
};
} else {
alert( "你的浏览器不支持 WebSocket!" );
}
function send( message )
{
if ( !window.WebSocket )
{
return;
}
if ( socket.readyState == WebSocket.OPEN )
{
socket.send( message );
} else {
alert( "连接没有开启." );
}
}
< / script >
< form onsubmit = "return false;" >
< h3 > WebSocket : < / h3 >
< textarea id = "responseText" style = "width: 500px; height: 300px;" > < / textarea >
< br >
< input type = "text" name = "message" style = "width: 300px" value = "1" >
< input type = "button" value = "发送消息" onclick = "send(this.form.message.value)" >
< input type = "button" onclick = "javascript:document.getElementById('responseText').value=''" value = "清空聊天记录" >
< / form >
< br >
< / body >
< / html>
三、测试
1、先运行启动类,此时会先启动netty服务器,然后启动一个netty客户端,然后过30s模拟客户端A进行消息发送
2、打开测试页面,在底下的输入框输入:{"event_type":"front", "api_token":"11111"},表示客户端B连接上netty服务器
测试结果如下:
消息模块:
客户端B:
四、结束语
本文只是抛砖引玉,主要启发有类似需求的朋友知道怎么去存储channel,进而怎么给指定客户推送消息,如果想要进行大型项目的高并发、可靠稳定地使用,还需进一步地改进。