1:本文包含的内容
1:java版本:通过netty实现支持ssl,wss方式去链接的服务端
2:java版本:书写支持通过wss方式去链接服务端的客户端
3:js版本:通过wss和ws去链接服务端的客户端
2:使用的jar包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
3:java版本服务端,支持wss链接
- MyChannelHandlerPool
package com.example.fallrainboot.netty;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
/**
* @description: 通道组池,管理所有websocket连接
* @author: fallrain
* @time: 2022/7/7/007
*/
public class MyChannelHandlerPool {
public MyChannelHandlerPool(){}
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
- MyWebSocketHandler
package com.example.fallrainboot.netty;
import com.alibaba.fastjson.JSON;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @description:
* @author: fallrain
* @time: 2022/7/7/007
*/
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final String USER = "user";
private final AttributeKey<String> key = AttributeKey.valueOf(USER);
/**
* 有客户端连接服务器会触发此函数
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启!");
System.out.println(ctx);
//添加到channelGroup通道组
MyChannelHandlerPool.channelGroup.add(ctx.channel());
}
/**
* 有客户端终止连接服务器会触发此函数
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开连接,通道关闭!");
//添加到channelGroup 通道组
MyChannelHandlerPool.channelGroup.remove(ctx.channel());
}
/**
* 有客户端发消息会触发此函数
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest,处理参数 by zhengkai.blog.csdn.net
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
Map<String, String> paramMap=getUrlParams(uri);
System.out.println("接收到的参数是:"+ JSON.toJSONString(paramMap));
online(paramMap.get("uid"), ctx.channel());
//如果url包含参数,需要处理
if(uri.contains("?")){
String newUri=uri.substring(0,uri.indexOf("?"));
System.out.println(newUri);
request.setUri(newUri);
}
}else if(msg instanceof TextWebSocketFrame){
//正常的TEXT消息类型
TextWebSocketFrame frame=(TextWebSocketFrame)msg;
//以下是每次收到消息之后回给client的代码,如果不需要回复则可以注释了。
String uid = ctx.channel().attr(key).get();
System.out.println("客户端收到"+ uid +"服务器数据:" +frame.text());
System.out.println("read0: " + frame.text());
List<Channel> channelList = getChannelByName(uid);
if (channelList.size() <= 0) {
System.out.println("用户" + uid + "不在线!");
}
channelList.forEach(channel -> channel.writeAndFlush(new TextWebSocketFrame(frame.text())));
//批量发送
//sendAllMessage(frame.text());
}
super.channelRead(ctx, msg);
}
private void sendOneMessage(String uid, String message){
//收到信息后,单发给channel
List<Channel> channelList = getChannelByName(uid);
channelList.forEach(channel -> channel.writeAndFlush(new TextWebSocketFrame(message)));
}
/**
* 批量发送
* @param message
*/
private void sendAllMessage(String message){
//收到信息后,群发给所有channel
MyChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));
}
/**
* 根据用户id查找channel
*
* @param name
* @return
*/
public List<Channel> getChannelByName(String name) {
return MyChannelHandlerPool.channelGroup.stream().filter(channel -> channel.attr(key).get().equals(name))
.collect(Collectors.toList());
}
/**
* 上线一个用户
*
* @param channel
* @param userId
*/
private void online(String userId, Channel channel) {
// 保存channel通道的附带信息,以用户的uid为标识
// channel.attr(key).set(userId);
channel.attr(key).setIfAbsent(userId);
MyChannelHandlerPool.channelGroup.add(channel);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
}
private static Map getUrlParams(String url){
Map<String,String> map = new HashMap<>();
url = url.replace("?",";");
if (!url.contains(";")){
return map;
}
if (url.split(";").length > 0){
String[] arr = url.split(";")[1].split("&");
for (String s : arr){
String key = s.split("=")[0];
String value = s.split("=")[1];
map.put(key,value);
}
return map;
}else{
return map;
}
}
}
- NettyServer
package com.example.fallrainboot.netty;
import com.example.fallrainboot.util.SSLContextUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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 io.netty.handler.timeout.IdleStateHandler;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.util.concurrent.TimeUnit;
/**
* @description: NettyServer Netty服务器配置
* @author: fallrain
* @time: 2022/7/7/007
*/
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
//配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024); //服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
//服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// // SSL处理器
// SSLContext sslContext = SSLContextUtil.createSSLContext("JKS", "D:\\WYProject\\fallrainboot\\src\\main\\resources\\tomcat\\8092720_www.fallrain.vip.pfx","fNHszHKD");
SSLContext sslContext = SSLContextUtil.createSSLContext("JKS", "D:\\WYProject\\fallrainboot\\src\\main\\resources\\jks\\8092720_www.fallrain.vip.jks","VBQp6Hxn");
// SSLContext sslContext = SSLContextUtil.createSSLContext("JKS", "D:\\WYProject\\fallrainboot\\demo.liukun.com.keystore","123456");
// SSLContext sslContext = SSLContextUtil.createSSLContext("JKS", "D:\\WYProject\\JgServer\\src\\main\\resources\\wss.jks","netty123");
SSLEngine sslEngine = sslContext.createSSLEngine();
sslEngine.setNeedClientAuth(false);
sslEngine.setUseClientMode(false);
ch.pipeline().addFirst("ssl", new SslHandler(sslEngine));
// System.out.println("收到新连接");
// //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
//参数处理
ch.pipeline().addLast(new MyWebSocketHandler());
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
}
});
// .childOption(ChannelOption.SO_KEEPALIVE, true);//保持长连接,2小时无数据激活心跳机制
// 绑定端口,开始接收进来的连接
// ChannelFuture future = sb.bind(this.port).sync();
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
//关闭channel和块,直到它被关闭
cf.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
}
- SSLContextUtil
package com.example.fallrainboot.util;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
/**
* @description: jks证书处理工具类
* @author: fallrain
* @time: 2022/7/8/008
*/
public class SSLContextUtil {
private static volatile SSLContext sslContext = null;
/**
* type是PKCS12、path是pfx文件路径、password是pfx对应的密码
* @param type
* @param path
* @param password
* @return
* @throws Exception
*/
public static SSLContext createSSLContext(String type ,String path ,String password) throws Exception {
if(null == sslContext){
synchronized (SSLContextUtil.class) {
if(null == sslContext){
// 支持JKS、PKCS12(我们项目中用的是阿里云免费申请的证书,下载tomcat解压后的pfx文件,对应PKCS12)
KeyStore ks = KeyStore.getInstance(type);
// 证书存放地址
InputStream ksInputStream = new FileInputStream(path);
ks.load(ksInputStream, password.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
}
}
}
return sslContext;
}
}
4:java版本客户端,支持wss去链接服务端
- ClientHandler
package com.example.fallrainboot.nettyClient;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
//客户端业务处理类
@Slf4j
public class ClientHandler extends SimpleChannelInboundHandler<Object> {
private ChannelHandlerContext channel;
private WebSocketClientHandshaker handshaker;
private ChannelPromise handshakeFuture;
/**
* 当客户端主动链接服务端的链接后,调用此方法
*
* @param channelHandlerContext ChannelHandlerContext
*/
@Override
public void channelActive(ChannelHandlerContext channelHandlerContext) {
System.out.println("客户端Active .....");
handlerAdded(channelHandlerContext);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("\n\t⌜⎓⎓⎓⎓⎓⎓exception⎓⎓⎓⎓⎓⎓⎓⎓⎓\n" +
cause.getMessage());
ctx.close();
}
public void setHandshaker(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public void handlerAdded(ChannelHandlerContext ctx) {
this.handshakeFuture = ctx.newPromise();
}
public ChannelFuture handshakeFuture() {
return this.handshakeFuture;
}
// protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// System.out.println("channelRead0");
// Channel ch = ctx.channel();
// if (!handshaker.isHandshakeComplete()) {
// try {
// handshaker.finishHandshake(ch, (FullHttpResponse) msg);
// System.out.println("WebSocket Client connected!");
// handshakeFuture.setSuccess();
// } catch (WebSocketHandshakeException e) {
// System.out.println("WebSocket Client failed to connect");
// handshakeFuture.setFailure(e);
//
// }
// return;
// }
// if (msg instanceof FullHttpResponse) {
// FullHttpResponse response = (FullHttpResponse) msg;
// throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.getStatus() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
// }
//
// WebSocketFrame frame = (WebSocketFrame) msg;
// if (frame instanceof TextWebSocketFrame) {
// TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; // resposnse(ctx, frame);
// channel.writeAndFlush(textFrame.text());
// System.out.println("WebSocket Client received message: " + textFrame.text());
// } else if (frame instanceof PongWebSocketFrame) {
// System.out.println("WebSocket Client received pong");
// } else if (frame instanceof CloseWebSocketFrame) {
// System.out.println("WebSocket Client received closing");
// ch.close();
// }
//
// }
// protected void channelRead0(ChannelHandlerContext ctx, Object o) throws Exception {
// System.out.println("channelRead0");
//
// // 握手协议返回,设置结束握手
// if (!this.handshaker.isHandshakeComplete()){
// FullHttpResponse response = (FullHttpResponse)o;
// this.handshaker.finishHandshake(ctx.channel(), response);
// this.handshakeFuture.setSuccess();
// System.out.println("WebSocketClientHandler::channelRead0 HandshakeComplete...");
// return;
// }
// else if (o instanceof TextWebSocketFrame)
// {
// TextWebSocketFrame textFrame = (TextWebSocketFrame)o;
// System.out.println("WebSocketClientHandler::channelRead0 textFrame: " + textFrame.text());
// } else if (o instanceof CloseWebSocketFrame){
// System.out.println("WebSocketClientHandler::channelRead0 CloseWebSocketFrame");
// }
//
// }
protected void channelRead0(ChannelHandlerContext ctx, Object message) throws Exception {
System.out.println("channelRead0");
// 判断是否正确握手
if (!this.handshaker.isHandshakeComplete()){
try {
this.handshaker.finishHandshake(ctx.channel(), (FullHttpResponse) message);
log.debug("websocket Handshake 完成!");
this.handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException e) {
log.debug("websocket连接失败!");
this.handshakeFuture.setFailure(e);
}
return;
}
// 握手失败响应
if (message instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) message;
log.error("握手失败!code:{},msg:{}", response.status(), response.content().toString(CharsetUtil.UTF_8));
}
WebSocketFrame frame = (WebSocketFrame) message;
// 消息处理
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
log.debug("收到消息: " + textFrame.text());
}
if (frame instanceof PongWebSocketFrame) {
log.debug("pong消息");
}
if (frame instanceof CloseWebSocketFrame) {
log.debug("服务器主动关闭连接");
ctx.close();
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
log.debug("超时事件时触发");
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
// 当我们长时间没有给服务器发消息时,发送ping消息,告诉服务器我们还活跃
if (event.state().equals(IdleState.WRITER_IDLE)) {
log.debug("发送心跳");
ctx.writeAndFlush(new PingWebSocketFrame());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
- WebSocketNettyClient
package com.example.fallrainboot.nettyClient;
import com.alibaba.fastjson.JSONObject;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
@Slf4j
public class WebSocketNettyClient {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
final ClientHandler handler =new ClientHandler();
try {
URI websocketURI = new URI("wss://localhost:8109/ws?uid=369AX");
//进行握手
log.debug("握手开始");
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(websocketURI, WebSocketVersion.V13, (String)null, false,new DefaultHttpHeaders());
System.out.println(websocketURI.getScheme());
System.out.println(websocketURI.getHost());
System.out.println(websocketURI.getPort());
SslContext sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//放到第一位 addFirst 支持wss链接服务端
ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc(), websocketURI.getHost(),websocketURI.getPort()));
ChannelPipeline pipeline = ch.pipeline();
// 添加一个http的编解码器
pipeline.addLast(new HttpClientCodec());
// 添加一个用于支持大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 添加一个聚合器,这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
pipeline.addLast(new HttpObjectAggregator(1024 * 64));
pipeline.addLast(handler);
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
}
});
final Channel channel=bootstrap.connect(websocketURI.getHost(),websocketURI.getPort()).sync().channel();
handler.setHandshaker(handshaker);
handshaker.handshake(channel);
//发送消息
System.out.println("发送消息");
JSONObject hhh = new JSONObject();
hhh.put("cmd","test");
for (int i = 0; i < 5; i++) {
Thread.sleep(2000);
channel.writeAndFlush(new TextWebSocketFrame(hhh.toString()));
}
//阻塞等待是否握手成功
handler.handshakeFuture().sync();
System.out.println("握手成功");
//发送消息
System.out.println("发送消息");
JSONObject clientJson = new JSONObject();
clientJson.put("cmd","test");
channel.writeAndFlush(new TextWebSocketFrame(clientJson.toString()));
// 等待连接被关闭
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
5:测试步骤
-
启动顺序:首先启动服务端端代码—>后面启动客户端代码
-
成功截图1
-
成功截图2
-
成功截图3
-
成功截图4
-
成功截图5
项目地址
[项目git地址](https://gitee.com/fallrainliulei/fallrainboot/tree/netty_wss/)