websocket
直接使用SpringBoot+Netty來支持WebSocket,并且需要支持wss,其需要注意事項(xiàng)有以下:
-
wss支持 - websocket請求路徑中帶參數(shù)
針對第一個問題:wss支持比較簡單;
- 生成證書
- ChannelPipeline中添加
sslHandler,并且放在First即可。
生成證書
keytool -genkey -alias demo -keypass 123456 -keyalg RSA -keysize 1024 -validity 365 -keystore demo.liukun.com.keystore -storepass 123456 -dname "C=CN,ST=SH,L=SH,O=hfjy,CN=demo.liukun.com" -deststoretype pkcs12
導(dǎo)出證書:
keytool -export -alias demo -file demo.cer -keystore demo.liukun.com.keystore -storepass 123456
ssl/tls支持
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
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.InputStream;
import java.security.KeyStore;
public class NettyServerHandlerInitializer extends ChannelInitializer<Channel> {
// 證書中使用的密碼,因?yàn)閐emo,不做配置,直接寫死
private String password = "123456";
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new HttpObjectAggregator(65535));
ch.pipeline().addLast(new ChunkedWriteHandler());
// 對websocket url中的參數(shù)做解析處理的Handler
ch.pipeline().addLast(new CustomUrlHandler());
// 對websocket做支持,其路徑為/ws
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
// 自定義業(yè)務(wù)邏輯處理的Handler
ch.pipeline().addLast(new MyWebSocketHandler());
// 以下為要支持wss所需處理
KeyStore ks = KeyStore.getInstance("JKS");
InputStream ksInputStream = new FileInputStream("/Users/liukun/ca/demo.liukun.com.keystore");
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);
// 需把SslHandler添加在第一位
ch.pipeline().addFirst("ssl", new SslHandler(sslEngine));
}
}
針對第二個問題:websocket的uri中帶參數(shù)問題。因?yàn)樵趯?shí)際項(xiàng)目中,很多情況下都需要使用到參數(shù),如對請求的URL進(jìn)行認(rèn)證等,如果需要傳遞參數(shù)通常有兩種做法:
- 在Header中傳遞:這種需要對XMLHttpRequest進(jìn)行自定義,比較復(fù)雜,不建議
- 直接在uri中帶參數(shù),如
/ws?token=123,這種比較簡單,建議使用這種,以下處理也是基于這種方式
通過在uri中帶參數(shù)時,如果使用WebSocketServerProtocolHandler時會發(fā)現(xiàn)連接不成功,因?yàn)槠洳蛔R別這種處理方式。其解決方式:
在其處理之前對uri中的參數(shù)解析,并對uri進(jìn)行重寫:即把路徑中?及其后面參數(shù)去除。如業(yè)務(wù)中涉及認(rèn)證及關(guān)聯(lián)等都在此處理
見上面代碼中添加的ch.pipeline().addLast(new CustomUrlHandler())。其中CustomUrlHandler內(nèi)容如下:
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpRequest;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
@ChannelHandler.Sharable
public class CustomUrlHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 只針對FullHttpRequest類型的做處理,其它類型的自動放過
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
int idx = uri.indexOf("?");
if (idx > 0) {
String query = uri.substring(idx + 1);
// uri中參數(shù)的解析使用的是jetty-util包,其性能比自定義及正則性能高。
MultiMap<String> values = new MultiMap<String>();
UrlEncoded.decodeTo(query, values, "UTF-8");
request.setUri(uri.substring(0, idx));
}
}
ctx.fireChannelRead(msg);
}
}
其中MyWebSocketHandler作用為:收到客戶端發(fā)送的消息,然后重新傳給客戶端,注意寫數(shù)據(jù)時格式為TextWebSocketFrame:
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ChannelHandler.Sharable
public class MyWebSocketHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(MyWebSocketHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("與客戶端建立連接,通道開啟");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("與客戶端斷開連接,通道關(guān)閉");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
TextWebSocketFrame message = (TextWebSocketFrame)msg;
logger.info("服務(wù)端收到數(shù)據(jù): " + message.text());
// 此處需注意返回的數(shù)據(jù)的格式為TextWebSocketFrame。否則客戶端收不到消息
ctx.channel().writeAndFlush(new TextWebSocketFrame(message.text()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error(cause.getMessage());
cause.printStackTrace();
}
}
其它代碼參考如下:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
@Component
@Slf4j
public class NettyServer {
private EventLoopGroup boss = new NioEventLoopGroup();
private EventLoopGroup work = new NioEventLoopGroup();
private Integer port = 8000;
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, work)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress("demo.liukun.com", port))
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new NettyServerHandlerInitializer());
ChannelFuture future = bootstrap.bind().sync();
if (future.isSuccess()) {
log.info("啟動Netty Server");
}
}
@PreDestroy
public void destory() throws InterruptedException {
boss.shutdownGracefully().sync();
work.shutdownGracefully().sync();
log.info("關(guān)閉Netty");
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NettyApplication {
public static void main(String[] args) {
SpringApplication.run(NettyApplication.class, args);
}
}
前端html頁面如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<textarea id="msgBoxs"></textarea><br>
待發(fā)送消息:<input type="text" id="msg"><input type="button" id="sendBtn" onclick="send()" value="發(fā)送">
<script type="application/javascript">
var msgBoxs = document.getElementById("msgBoxs")
var msgBox = document.getElementById("msg")
document.cookie="token2=John Doe";
var ws = new WebSocket("wss://demo.liukun.com:8000/ws?token=abc123&type=1")
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSocket!");
}
ws.onmessage = function (evt) {
console.log("Received Message: ", evt.data)
var msgs = msgBoxs.value
msgBoxs.innerText = msgs + "\n" + evt.data
msgBoxs.scrollTop = msgBoxs.scrollHeight;
}
ws.onclose = function (evt) {
console.log("Connect closed.");
}
function send() {
var msg = msgBox.value
ws.send(msg)
msgBox.value = ""
}
</script>
</body>
</html>
測試
直接使用html頁面訪問wss服務(wù)器時,不同的瀏覽器會有不同的表現(xiàn)形式:
- firefox不生效
- chome必須先訪問其https網(wǎng)址,信任后才生效,否則不生效。
- safari只要信任了證書,直接可以使用:直接在html頁面里面即可以訪問wss,有mac的同學(xué)建議直接使用此方式,最為簡單。