Java netty 实现websocket搭建

websocket协议是属于服务端和客户端之间建立起长连接的协议,通常在im即时消息等对信息的实时性要求比较高,请求较频繁的操作上使用。本案例的代码将会提交到码云上可以查看,文章后附地址。这里举得案例是wss协议的,属于安全协议的,证书是自签的,如果不会生成自签证书,可以看我往常的一个博客,里面有介绍,这里wss用的证书是jks的,你们如果是ws协议就能满足系统需要,就不需要用这个证书和去掉sslHandler的处理即可。

ws协议通信的时序图如下:

 websocket是基于TCP的一个应用协议,与HTTP协议的关联之处在于websocket的握手数据被HTTP服务器当作HTTP包来处理,主要通过Update request HTTP包建立起连接,之后的通信全部使用websocket自己的协议。

 请求的network信息如下:

 

实现步骤如下:

1.引入netty包的pom:

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

2.服务端的代码:

 启动服务端服务的时候,新建服务端对象 new ServerBootstrap,建立通道

WebSocketServer类如下:
package com.example.demo.ws;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.ImmediateEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;

@Component("wss")
public class WebSocketServer {
	private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);
	private String address;
	private final ChannelGroup group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
	private final EventLoopGroup workerGroup = new NioEventLoopGroup();
	private ChannelFuture future;
	private Channel channel;

	@PostConstruct
	public void init() {
		LOG.info("ws starting...");

	    int port = 8090;
	    String host = "192.168.2.61";

	    if (!StringUtils.hasLength(host)) {
	    	LOG.error("can't fetch host, start fail...");
	    	System.exit(0);
	    }

		address = host + ":" + port;
		LOG.info("registry.address {}", address);
		ServerBootstrap boot = new ServerBootstrap();
		boot.group(workerGroup).channel(NioServerSocketChannel.class)
				.childHandler(createInitializer(group));
		InetSocketAddress inetAddr = new InetSocketAddress(port);
		future = boot.bind(inetAddr).syncUninterruptibly();
		channel = future.channel();
		LOG.info("wss start succeed, port {} listening...", port);
	}

	@PreDestroy
	public void destroy() {
		LOG.info("wss {} shutdown...", address);

		if (channel != null) {
			channel.close();
		}

		group.close();
		workerGroup.shutdownGracefully();
		LOG.info("wss {} shutdown succeed");
	}

	protected ChannelHandler createInitializer(ChannelGroup group) {
		return new WebSocketServerInitializer(group);
	}

}
WebSocketServer在建立服务端对象的时候增加了childHandler的处理方法,这里就可以初始化相关的handler处理类
即ws初始化的handler处理类为 WebSocketServerInitializer,代码如下:

package com.example.demo.ws;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
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.ssl.SslHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {

	private final ChannelGroup group;

	public WebSocketServerInitializer(ChannelGroup group) {
		this.group = group;
	}

	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
		ChannelPipeline pipeline = ch.pipeline();
		//todo 这里到时候要加个开关设置,可以把wss的配置做到nginx是更合理的做法
		//需把SslHandler添加在第一位
		pipeline.addFirst("ssl", new SslHandler(getSslEngine()));
		pipeline.addLast(new HttpServerCodec());
		pipeline.addLast(new ChunkedWriteHandler());
		pipeline.addLast(new HttpObjectAggregator(64*1024));
		pipeline.addLast(new HttpRequestHandler("/vcf"));
		pipeline.addLast(new WebSocketServerProtocolHandler("/vcf"));
		pipeline.addLast(new InstantMessagingHandler(group));


	}

	private SSLEngine getSslEngine() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
		String password = "12345678";
		// 以下为要支持wss所需处理
		KeyStore ks = KeyStore.getInstance("JKS");
		String keyFile = WebSocketServerInitializer.class.getResource("/jxzy-cert.jks").getPath();
		InputStream ksInputStream = new FileInputStream(keyFile);
		ks.load(ksInputStream, password.toCharArray());
		KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
		kmf.init(ks, password.toCharArray());
		SSLContext sslContext = SSLContext.getInstance("TLS");
		sslContext.init(kmf.getKeyManagers(), null, null);
		SSLEngine sslEngine = sslContext.createSSLEngine();
		sslEngine.setUseClientMode(false);
		sslEngine.setNeedClientAuth(false);
		return sslEngine;
	}

}
以下两个handler就是在初始化handle中的,分别是HttpRequestHandler和InstantMessagingHandler;
HttpRequestHandler代码如下:
package com.example.demo.ws;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpRequestHandler extends
		SimpleChannelInboundHandler<FullHttpRequest> {

	private static final Logger LOG = LoggerFactory.getLogger(HttpRequestHandler.class);

	private final String wsUri;

	public HttpRequestHandler(String wsUri) {
		this.wsUri = wsUri;
	}

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg)
			throws Exception {
		String uri = msg.uri();
		LOG.info("http req.uri:{}", uri);
		if (wsUri.equalsIgnoreCase(uri)) {
			ctx.fireChannelRead(msg.retain());
		} else {
			LOG.warn("invalid http request!");
			ctx.close();
		}
	}


	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
			throws Exception {
		ctx.close();
		cause.printStackTrace(System.err);
	}
}
InstantMessagingHandler代码如下:
package com.example.demo.ws;

import com.fasterxml.jackson.databind.JsonNode;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class InstantMessagingHandler extends
		SimpleChannelInboundHandler<TextWebSocketFrame> {

	private final Logger LOG = LoggerFactory.getLogger(InstantMessagingHandler.class);

	private final ChannelGroup group;


	public InstantMessagingHandler(ChannelGroup group) {
		this.group = group;
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		LOG.error("exceptionCaught chId: {},clientIp:{},cause:{}", ctx.channel().id(),""+ctx.channel().remoteAddress(), cause.getMessage(), cause);
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LOG.info("channelActive chId: {},client Ip:{}", ctx.channel().id(),""+ctx.channel().remoteAddress());
	}

	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		LOG.info("channelRegistered chId: {},client Ip:{}", ctx.channel().id(),""+ctx.channel().remoteAddress());
	}

	@Override
	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
		LOG.info("channelUnregistered chId: {},client Ip:{}", ctx.channel().id(),""+ctx.channel().remoteAddress());
		super.channelUnregistered(ctx);
	}

    @Override
    protected void channelRead0(ChannelHandlerContext ctx,
                                TextWebSocketFrame msg) throws Exception {
        String text = msg.text();
        JsonNode node = JSONUtil.json2node(text);
        LOG.info("recv:{}", node);
		Channel ch = ctx.channel();
		ch.writeAndFlush(new TextWebSocketFrame(JSONUtil.obj2json(node)));
	}



	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
			throws Exception {

		if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
			ctx.pipeline().remove(HttpRequestHandler.class);
			group.add(ctx.channel());
		} else {
			super.userEventTriggered(ctx, evt);
		}
	}

	// when channel disconnect exception, send finish request to agent.
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		LOG.info("channelInActive...");
		//获取disconnect Channel
		Channel ch = ctx.channel();

		try {
			group.remove(ch);
		} catch (Exception e) {
			LOG.error("exception: {}", e.getMessage(), e);
		}
		ctx.fireChannelInactive();
	}


}
至此,服务端的代码就完成了,剩下的就是前端js和后端服务进行连接了,
前端的demo代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试页面1</title>
</head>
<body>
<h3>测试页面1</h3>
<p>您好,欢迎访问login.html</p>
<button id="qwe" value="qew" onclick="btnClick()" />点击按钮和服务端建立连接
<script type="text/javascript">
    function btnClick() {
        //设置websocket通信连接,连接到monitor,给主会场发送在线通知,主会场收到通知后帮忙发送采音指令
        var socket = null;

        if (socket) {
            socket.close();
        }
        socket = new WebSocket('wss://192.168.2.61:8090/vcf');

        // Connection opened
        var regMsg = {"test":"1"};
        socket.addEventListener('open', function (event) {
            console.log('open from server', event.data);
            socket.send(JSON.stringify(regMsg));
        });

        //listen for message
        socket.addEventListener('message', function (event) {
            console.log('message from server', event.data);
        });

        // Listen for close
        socket.addEventListener('close', function (event) {
            console.log('close from server', event.data);
        });

        // Listen for error
        socket.addEventListener('error', function (event) {
            console.log('error from server', event.data);
        });
    }
</script>
</body>
</html>

整个demo就完成了,就可以前后端建立连接进行业务逻辑编写了;

相关的demo代码在码云地址为:https://gitee.com/xuefei_lin/wss.git

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值