websocket协议是属于服务端和客户端之间建立起长连接的协议,通常在im即时消息等对信息的实时性要求比较高,请求较频繁的操作上使用。本案例的代码将会提交到码云上可以查看,文章后附地址。这里举得案例是wss协议的,属于安全协议的,证书是自签的,如果不会生成自签证书,可以看我往常的一个博客,里面有介绍,这里wss用的证书是jks的,你们如果是ws协议就能满足系统需要,就不需要用这个证书和去掉sslHandler的处理即可。
ws协议通信的时序图如下:
websocket是基于TCP的一个应用协议,与HTTP协议的关联之处在于websocket的握手数据被HTTP服务器当作HTTP包来处理,主要通过Update request HTTP包建立起连接,之后的通信全部使用websocket自己的协议。
请求的network信息如下:
实现步骤如下:
1.引入netty包的pom:
io.netty
netty-all
4.1.79.Final
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 {
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 {
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 {
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代码如下:
测试页面1
测试页面1
您好,欢迎访问login.html
点击按钮和服务端建立连接
整个demo就完成了,就可以前后端建立连接进行业务逻辑编写了;
相关的demo代码在码云地址为:https://gitee.com/xuefei_lin/wss.git