[Netty实践] 简单WebSocket服务实现

目录

一、介绍

二、依赖导入

三、基础类准备

四、Handler实现

五、WebSocketChannelInitializer实现

六、WebSocketServer实现

七、前端实现

八、测试

九、参考链接


一、介绍

关于WebSocket此处不进行过多介绍,本章主要着重通过Netty实现WebSocket通信服务端,并且实现一个简单的通过网页进行聊天的功能。

讲到WebSocket,这里简单介绍一下为什么要使用WebSocket。以往我们通过网页与服务器进行交互时,都是通过发起一个http/https请求,该请求是无状态的,发送请求后,等待获取服务器返回的结果之后,这次请求就结束了,客户端与服务端就断开了。如果此时服务端想向客户端推送消息的话,由于连接已经断开,服务端无法进行消息推送,此时可以通过使用WebSocket进行客户端与服务端建立长连接,当服务端想向客户端推送消息时,就可以向客户端进行消息推送了。

接下来就进行代码实现。

二、依赖导入

XML 复制代码
<dependencies>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.101.Final</version>
        </dependency>

        <!--添加tomcat依赖模块.-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.41</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

    </dependencies>

三、基础类准备

1、ChannelManager, 用于管理Channel等相关信息

java 复制代码
public class ChannelManager {

    public final static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    public final static Map<Channel, WebSocketServerHandshaker> handShakerMap = new ConcurrentHashMap<>();

}

2、WebSocketRequestMessage,用于将客户端传递过来的json字符串转换该类的对象

java 复制代码
@Data
@ToString
public class WebSocketRequestMessage {

    public String user;

    public String type;

    public String message;


}

3、WebSocketResponseMessage,用于返回给客户端的类型数据

java 复制代码
public class WebSocketResponseMessage {

    public String user;

    public String type;

    public String message;

    public String date;

    public WebSocketResponseMessage(WebSocketRequestMessage webSocketRequestMessage) {
        this.user = webSocketRequestMessage.getUser();
        this.type = webSocketRequestMessage.getType();
        this.message = webSocketRequestMessage.getMessage();
        this.date = new Date().toString();
    }

}

四、Handler实现

建立WebSocket通信时,第一次发送的就是http请求,进行协议升级为WebSocket,需要实现针对该连接请求进行处理的handler,协议升级成功之后,后续发送的消息是数据帧,消息将由WebSocketFrame相关的Handler进行处理,为了方便理解,此处简单画个图:

1、DefaultHandler, 抽象类,重写一些基础的方法(处理触发的事件、建立连接、断开连接、遗异常处理的),供其他Handler继承,其他Handler则无需实现上述功能

java 复制代码
public abstract class DefaultHandler<T> extends SimpleChannelInboundHandler<T> {

   
    // 配合心跳使用,由客户端进行发送心跳数据包
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;

            // 触发读空闲, 关闭客户端
            if(idleStateEvent.state().equals(IdleState.READER_IDLE)) {
                System.out.println("触发读空闲,关闭channel");
                // 将消息传递CloseWebSocketFrame进行处理
                CloseWebSocketFrame closeWebSocketFrame = new CloseWebSocketFrame();
                ctx.fireChannelRead(closeWebSocketFrame);
            }

        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端建立连接: " + ctx.channel());
        ChannelManager.channelGroup.add(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端断开连接: " + ctx.channel());
        ChannelManager.channelGroup.remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        System.out.println("发生异常: " + ctx.channel());
    }
    
}

2、FullHttpRequestHander

java 复制代码
public class FullHttpRequestHandler extends DefaultHandler<FullHttpRequest> {

    private WebSocketServerHandshaker handShaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws Exception {
        System.out.println("客户端消息:" + httpRequest);

        if(httpRequest.decoderResult().isFailure() || !"websocket".equals(httpRequest.headers().get(HttpHeaderValues.UPGRADE))) {
            DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);

            ctx.writeAndFlush(httpResponse);

            return;
        }

        WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory("ws:/" + ctx.channel() + "/websocket", null, false);
        handShaker = webSocketServerHandshakerFactory.newHandshaker(httpRequest);
        if (null == handShaker) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            // 在此处为channel添加WebSocketFrameEncoder和WebSocketFrameDecoder
            handShaker.handshake(ctx.channel(), httpRequest);
        }

        ChannelManager.handShakerMap.put(ctx.channel(), handShaker);

    }

}

3、TextWebSocketFrameHandler,当WebSocket通信建立成功之后,在此阶段发送的文本消息在该handler中进行处理,如果发送的是二进制流,那么请自己自行实现一个处理BinaryWebSocketFrame类型数据的Handler

java 复制代码
public class TextWebSocketFrameHandler extends DefaultHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        WebSocketRequestMessage webSocketRequestMessage = JSON.parseObject(msg.text(), WebSocketRequestMessage.class);

        if("ping".equals(webSocketRequestMessage.getType())) {

            System.out.println("ping message");

            return;
        }

        System.out.println(ctx.channel() + ", 客户端消息:" + msg);

        WebSocketResponseMessage webSocketResponseMessage = new WebSocketResponseMessage(webSocketRequestMessage);

        TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(JSON.toJSONString(webSocketResponseMessage));

        ChannelManager.channelGroup.writeAndFlush(textWebSocketFrame);
    }
}

4、CloseWebSocketFrameHandler,客户端进行关闭时,将通过该Handler进行处理

java 复制代码
public class CloseWebSocketFrameHandler extends DefaultHandler<CloseWebSocketFrame> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, CloseWebSocketFrame msg) throws Exception {
        System.out.println("客户端要断开");
        WebSocketServerHandshaker handShaker = ChannelManager.handShakerMap.get(ctx.channel());
        handShaker.close(ctx.channel(), msg.retain());
    }

}

五、WebSocketChannelInitializer实现

java 复制代码
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {


    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        // 将建立连接时发送的第一次http请求数据装换为转换为 HttpRequest
        pipeline.addLast(new HttpServerCodec());
        // 将拆分的http消息(请求内容、请求体)聚合成一个消息
        pipeline.addLast(new HttpObjectAggregator(65536));
        // 块写出
        pipeline.addLast(new ChunkedWriteHandler());

        // 配置读空闲Handler, 3秒该Channel没有产生读将会触发读空闲事件
        pipeline.addLast(new IdleStateHandler(3, 0, 0));

        pipeline.addLast(new FullHttpRequestHandler());
        pipeline.addLast(new TextWebSocketFrameHandler());
        pipeline.addLast(new CloseWebSocketFrameHandler());
    }

}

六、WebSocketServer实现

java 复制代码
public class WebSocketServer {

    public void bind(Integer port) {

        EventLoopGroup parent = new NioEventLoopGroup();
        EventLoopGroup child = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(parent, child)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new WebSocketChannelInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();

            System.out.println("web socket server 启动成功...");

            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            parent.shutdownGracefully();
            child.shutdownGracefully();
        }


    }

}

七、前端实现

接下来写一个简单的前端web页,无须引入框架,只需要使用原生HTML5和JavaScript即可。

html 复制代码
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	
	<body>
		
		<button id = "login-btn" onclick="login()">登录</button>
		<button id = "logout-btn" onclick="logout()">退出</button>
		<br/>
		<p id="connection-status">连接状态:关闭</p>
		<p id="username-tag">user:</p>
		
		<input id="send-msg-box" type="text"/>
		<button onclick="send()">发送</button>
		
		<p>==================================群发消息=====================================</p>
		
		<textarea  id="msg-box"></textarea >
		
	</body>
	
	<script>
		var socket;
		var user;
		
		function login() {
			if (window.WebSocket){
				
				socket = new WebSocket("ws://localhost:8888/websocket");
			
				socket.onerror = function(event) {
					document.getElementById("connection-status").innerHTML = "连接状态:异常";
					document.getElementById("channel-tag").innerHTML = "channel:";
				}
				
				socket.onopen = function(event) {
					document.getElementById("connection-status").innerHTML = "连接状态:正常";
					var max = 999
					var min = 100
					user = 'admin' + (Math.floor(Math.random() * (max - min + 1)) + min)
					document.getElementById("username-tag").innerHTML = "user: " + user

;
					
					// 每隔 2 秒钟发送一次 Ping 帧
					setInterval(function() {
					  if (socket.readyState === socket.OPEN) {
								
							var pingMessage = {
								type: 'ping',
								message: ''
							}
							
							socket.send(JSON.stringify(pingMessage))
					  }
					}, 2000);
				}
			
				socket.onclose = function(event){
				    document.getElementById("connection-status").innerHTML = "连接状态:关闭";
					document.getElementById("username-tag").innerHTML = "user:"
				}
				
				socket.onmessage = function(event){
					var message = document.getElementById("msg-box").value;
				
					var response = JSON.parse(event.data)
				
					message = message + '\n' + response.date + " " + response.user + " " + response.message;
					
					document.getElementById("msg-box").value = message;
					
				}
				
			} else {
				alter("不支持websocket")
			}
		}
		
		function send() {
			
			if(socket == null) {
				alert('连接未建立')
				return;
			}
			
			if (socket.readyState == WebSocket.OPEN) {
					var value = document.getElementById("send-msg-box").value;
			        
					var textMessage = {
						type: 'text',
						user: user,
						message: value
					}
					
					//alert(message);
					//console.log(message)
			        socket.send(JSON.stringify(textMessage));
			} else {
				alert('连接未建立')
			}
		}
		
		
		function logout() {
			
			if(socket == null) {
				alert('连接未建立')
				return;
			}
			
			if (socket.readyState == WebSocket.OPEN) {
				socket.close();
			} else {
				alert('连接未建立')
			}
		}
	
	</script>
	
</html>

八、测试

1、启动服务端

java 复制代码
new WebSocketServer().bind(8888);

控制台打印如下信息

java 复制代码
web socket server 启动成功...

2、打开两个web页分别进行登录点击

点击登录之后,可以看到连接状态正常,以及随机分配了一个用户名

3、发送消息

在任一客户端进行消息发送,都能在所有客户端聊天框中看到如图信息:

4、关闭客户端,可手动点击关闭按钮,或则通过关闭网页以及浏览器,即可断开连接

九、参考链接

JS实时通信三把斧系列之一: websocket - 知乎

相关推荐
是小崔啊6 分钟前
开源轮子 - EasyExcel02(深入实践)
java·开源·excel
myNameGL38 分钟前
linux安装idea
java·ide·intellij-idea
青春男大40 分钟前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.1 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
我要学编程(ಥ_ಥ)1 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
music0ant1 小时前
Idea 添加tomcat 并发布到tomcat
java·tomcat·intellij-idea
计算机徐师兄2 小时前
Java基于SSM框架的无中介租房系统小程序【附源码、文档】
java·微信小程序·小程序·无中介租房系统小程序·java无中介租房系统小程序·无中介租房微信小程序
源码哥_博纳软云2 小时前
JAVA智慧养老养老护理帮忙代办陪诊陪护小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
忒可君3 小时前
C# winform 报错:类型“System.Int32”的对象无法转换为类型“System.Int16”。
java·开发语言
斌斌_____3 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端