关于Netty
Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Maven依赖
<dependencies>n <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->n <dependency>n <groupId>io.netty</groupId>n <artifactId>netty-all</artifactId>n <version>4.1.36.Final</version>n </dependency>nn</dependencies>n
SpringBootApplication
启动器中需要new一个NettyServer,并显式调用启动netty。
@SpringBootApplicationnpublic class SpringCloudStudyDemoApplication {n public static void main(String[] args) {n SpringApplication.run(SpringCloudStudyDemoApplication.class,args);n try {n new NettyServer(12345).start();n System.out.println("https://blog.csdn.net/moshowgame");n System.out.println("http://127.0.0.1:6688/netty-websocket/index");n }catch(Exception e) {n System.out.println("NettyServerError:"+e.getMessage());n }n }n}n
NettyServer
启动的NettyServer,这里进行配置
/**n * NettyServer Netty服务器配置n * @author zhengkai.blog.csdn.netn * @date 2019-06-12n */npublic class NettyServer {n private final int port;n n public NettyServer(int port) {n this.port = port;n }n n public void start() throws Exception {n EventLoopGroup bossGroup = new NioEventLoopGroup();n n EventLoopGroup group = new NioEventLoopGroup();n try {n ServerBootstrap sb = new ServerBootstrap();n sb.option(ChannelOption.SO_BACKLOG, 1024);n sb.group(group, bossGroup) // 绑定线程池n .channel(NioServerSocketChannel.class) // 指定使用的channeln .localAddress(this.port)// 绑定监听端口n .childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作n n @Overriden protected void initChannel(SocketChannel ch) throws Exception {n System.out.println("收到新连接");n //websocket协议本身是基于http协议的,所以这边也要使用http解编码器n ch.pipeline().addLast(new HttpServerCodec());n //以块的方式来写的处理器n ch.pipeline().addLast(new ChunkedWriteHandler());n ch.pipeline().addLast(new HttpObjectAggregator(8192));n ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));n ch.pipeline().addLast(new MyWebSocketHandler());n }n });n ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定n System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());n cf.channel().closeFuture().sync(); // 关闭服务器通道n } finally {n group.shutdownGracefully().sync(); // 释放线程池资源n bossGroup.shutdownGracefully().sync();n }n }n}n
MyChannelHandlerPool
通道组池,管理所有websocket连接
/**n * MyChannelHandlerPooln * 通道组池,管理所有websocket连接n * @author zhengkai.blog.csdn.netn * @date 2019-06-12n */npublic class MyChannelHandlerPool {nn public MyChannelHandlerPool(){}nn public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);nn}n
MyWebSocketHandler
处理ws一下几种情况:
- channelActive与客户端建立连接
- channelInactive与客户端断开连接
- channelRead0客户端发送消息处理
/**n * NettyServer Netty服务器配置n * @author zhengkai.blog.csdn.netn * @date 2019-06-12n */npublic class NettyServer {n private final int port;n n public NettyServer(int port) {n this.port = port;n }n n public void start() throws Exception {n EventLoopGroup bossGroup = new NioEventLoopGroup();n n EventLoopGroup group = new NioEventLoopGroup();n try {n ServerBootstrap sb = new ServerBootstrap();n sb.option(ChannelOption.SO_BACKLOG, 1024);n sb.group(group, bossGroup) // 绑定线程池n .channel(NioServerSocketChannel.class) // 指定使用的channeln .localAddress(this.port)// 绑定监听端口n .childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作n n @Overriden protected void initChannel(SocketChannel ch) throws Exception {n System.out.println("收到新连接");n //websocket协议本身是基于http协议的,所以这边也要使用http解编码器n ch.pipeline().addLast(new HttpServerCodec());n //以块的方式来写的处理器n ch.pipeline().addLast(new ChunkedWriteHandler());n ch.pipeline().addLast(new HttpObjectAggregator(8192));n ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", "WebSocket", true, 65536 * 10));n ch.pipeline().addLast(new MyWebSocketHandler());n }n });n ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定n System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());n cf.channel().closeFuture().sync(); // 关闭服务器通道n } finally {n group.shutdownGracefully().sync(); // 释放线程池资源n bossGroup.shutdownGracefully().sync();n }n }n}n
socket.html
主要是连接ws,发送消息,以及消息反馈
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">n<html xmlns="http://www.w3.org/1999/xhtml">n<head>n <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />n <title>Netty-Websocket</title>n <script type="text/javascript">n // by zhengkai.blog.csdn.netn var socket;n if(!window.WebSocket){n window.WebSocket = window.MozWebSocket;n }n if(window.WebSocket){n socket = new WebSocket("ws://127.0.0.1:12345/ws");n socket.onmessage = function(event){n var ta = document.getElementById('responseText');n ta.value += event.data+"rn";n };n socket.onopen = function(event){n var ta = document.getElementById('responseText');n ta.value = "Netty-WebSocket服务器。。。。。。连接 rn";n };n socket.onclose = function(event){n var ta = document.getElementById('responseText');n ta.value = "Netty-WebSocket服务器。。。。。。关闭 rn";n };n }else{n alert("您的浏览器不支持WebSocket协议!");n }n function send(message){n if(!window.WebSocket){return;}n if(socket.readyState == WebSocket.OPEN){n socket.send(message);n }else{n alert("WebSocket 连接没有建立成功!");n }n n }n n </script>n</head>n<body>n<form onSubmit="return false;">n <label>ID</label><input type="text" name="uid" value="${uid!!}" /> <br />n <label>TEXT</label><input type="text" name="message" value="这里输入消息" /> <br />n <br /> <input type="button" value="发送ws消息"n onClick="send(this.form.uid.value+':'+this.form.message.value)" />n <hr color="black" />n <h3>服务端返回的应答消息</h3>n <textarea id="responseText" style="width: 1024px;height: 300px;"></textarea>n</form>n</body>n</html>n
Controller
写好了html当然还需要一个controller来引导页面。
@RestControllernpublic class IndexController {n n @GetMapping("/index")n public ModelAndView index(){n ModelAndView mav=new ModelAndView("socket");n mav.addObject("uid", RandomUtil.randomNumbers(6));n return mav;n }n n}n
效果演示
改造netty支持url参数
1.首先,调整一下加载handler的顺序,优先MyWebSocketHandler在WebSocketServerProtocolHandler之上。
ch.pipeline().addLast(new MyWebSocketHandler());nch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));n
2.其次,改造MyWebSocketHandler 的channelRead方法,首次连接会是一个FullHttpRequest类型,可以通过FullHttpRequest.uri()获取完整ws的URL地址,之后接受信息的话,会是一个TextWebSocketFrame类型。
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {nn @Overriden public void channelActive(ChannelHandlerContext ctx) throws Exception {n System.out.println("与客户端建立连接,通道开启!");nn //添加到channelGroup通道组n MyChannelHandlerPool.channelGroup.add(ctx.channel());n }nn @Overriden public void channelInactive(ChannelHandlerContext ctx) throws Exception {n System.out.println("与客户端断开连接,通道关闭!");n //添加到channelGroup 通道组n MyChannelHandlerPool.channelGroup.remove(ctx.channel());n }nn @Overriden public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {n //首次连接是FullHttpRequest,处理参数 by zhengkai.blog.csdn.netn if (null != msg && msg instanceof FullHttpRequest) {n FullHttpRequest request = (FullHttpRequest) msg;n String uri = request.uri();nn Map paramMap=getUrlParams(uri);n System.out.println("接收到的参数是:"+JSON.toJSONString(paramMap));n //如果url包含参数,需要处理n if(uri.contains("?")){n String newUri=uri.substring(0,uri.indexOf("?"));n System.out.println(newUri);n request.setUri(newUri);n }nn }else if(msg instanceof TextWebSocketFrame){n //正常的TEXT消息类型n TextWebSocketFrame frame=(TextWebSocketFrame)msg;n System.out.println("客户端收到服务器数据:" +frame.text());n sendAllMessage(frame.text());n }n super.channelRead(ctx, msg);n }nn @Overriden protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {nn }nn private void sendAllMessage(String message){n //收到信息后,群发给所有channeln MyChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));n }nn private static Map getUrlParams(String url){n Map<String,String> map = new HashMap<>();n url = url.replace("?",";");n if (!url.contains(";")){n return map;n }n if (url.split(";").length > 0){n String[] arr = url.split(";")[1].split("&");n for (String s : arr){n String key = s.split("=")[0];n String value = s.split("=")[1];n map.put(key,value);n }n return map;nn }else{n return map;n }n }n}n
3.html中的ws地址也进行改造
socket = new WebSocket("ws://127.0.0.1:12345/ws?uid=666&gid=777");n
4.改造后控制台输出情况
收到新连接n与客户端建立连接,通道开启!n接收到的参数是:{"uid":"666","gid":"777"}n/wsn客户端收到服务器数据:142531:这里输入消息n客户端收到服务器数据:142531:这里输入消息n客户端收到服务器数据:142531:这里输入消息n
failed: WebSocket opening handshake timed out
听说是ssl wss的情况下才会出现,来自 @around-gao 的解决方法:
把MyWebSocketHandler和WebSocketServerProtocolHandler调下顺序就好了。
来源:https://mp.weixin.qq.com/s/1X65lYYh91z0BnYWeST-Vw