编辑
点/击/蓝/字 关/注/我/们
一、背景
笔者之前的项目中需要用webSocket协议进行长连接上游供应商,笔者为此做了一些小小的技术预研,特将此记录下来。
tips:因笔者主要是连接上游供应商的服务端,因此本文主要介绍webSocket客户端的知识,暂不编写服务端知识,请见谅。
具体详细代码地址:
https://github.com/ikuncoder/webSocketConnector
二、javax
在java的扩展包javax.websocket中已经定义了一套WebSocket的接口规范,应用起来比较简单快捷。
public class MainApplication {
public static void main(String[] args) {
connect();
}
private static void connect() {
String url = "";//your websocket url,for example,wss://xxxxx.com/websocket/**
// 实例化WebSocketService
WebSocketService webSocketService = new WebSocketService();
// 打开WebSocket连接
try {
webSocketService.openWebSocketConnection(url);
//do something after connect
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先创建一个WebSocketService对象,传入需要连接的url,需要是wss或者是ws开头的。
@ClientEndpoint
@Slf4j
public class WebSocketService {
private Session userSession = null;
private Long startTime = System.currentTimeMillis();
public void openWebSocketConnection(String url) {
try {
URI uri = URI.create(url);
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(this, uri);
} catch (Exception e) {
throw new RuntimeException("Could not open WebSocket connection", e);
}
}
@OnOpen
public void onOpen(Session userSession) {
System.out.println(Thread.currentThread().getName() + " Connection opened.");
this.userSession = userSession;
}
@OnClose
public void onClose(Session userSession, CloseReason reason) {
System.out.println(Thread.currentThread().getName() + " Connection closed because: " + reason);
this.userSession = null;
}
@OnMessage
public void onMessage(String message) {
System.out.println(Thread.currentThread().getName() + " 相隔" + (System.currentTimeMillis() - startTime) + "ms,Received: " + message);
//do something with the message
}
}
在openWebSocketConnection方法中创建一个WebSocketContainer 进行连接,这些方法都是javax.webSocket已经封装好了的,可以直接使用。
连接成功会回调onOpen方法,断开会回调onClose方法,接收的信息回调用onMessage方法。
javax这种方式比较简单,笔者主要是做一下技术调研,因为笔者更偏向用netty,实际项目中也是用netty做客户端进行长连接。
三、netty
netty的介绍就不多说了,相信应该都听过大名鼎鼎的netty,下面直接show code
public class WebSocketNettyClient {
public static void main(String[] args) throws Exception {
connect();
}
private static void connect() throws URISyntaxException {
EventLoopGroup group = new NioEventLoopGroup(1);
final ClientHandler handler = new ClientHandler();
String url = "";//your websocket url,for example,wss://xxxxx.com/websocket/**
URI uri = new URI(url);
String scheme = uri.getScheme() == null ? "ws" : uri.getScheme();
final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
try {
SslContext sslContext = SslContextBuilder.forClient()// 配置支持的协议版本
.build();
URI websocketURI = uri;
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(websocketURI, WebSocketVersion.V13, (String) null, true, new DefaultHttpHeaders());
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 {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(sslContext.newHandler(ch.alloc(), host, 443));
// 添加一个http的编解码器
pipeline.addLast(new HttpClientCodec());
// 添加一个用于支持大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 添加一个聚合器,这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
pipeline.addLast(new HttpObjectAggregator(1024 * 64));
pipeline.addLast(handler);
}
});
HttpHeaders httpHeaders = new DefaultHttpHeaders();
//进行握手
final Channel channel = bootstrap.connect(websocketURI.getHost(), 443).sync().channel();
handler.setHandshaker(handshaker);
handshaker.handshake(channel);
//阻塞等待是否握手成功
handler.handshakeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
private static class ClientHandler extends SimpleChannelInboundHandler<Object> {
private WebSocketClientHandshaker handshaker;
ChannelPromise handshakeFuture;
private Long startTime;
/**
* 当客户端主动链接服务端的链接后,调用此方法
*
* @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();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive");
super.channelInactive(ctx);
}
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 o) throws Exception {
// 握手协议返回,设置结束握手
if (!this.handshaker.isHandshakeComplete()) {
FullHttpResponse response = (FullHttpResponse) o;
this.handshaker.finishHandshake(ctx.channel(), response);
this.handshakeFuture.setSuccess();
System.out.println("---------握手成功------------");
//do something after handshake success;
return;
} else if (o instanceof TextWebSocketFrame) {
//do something after receive text message;
} else if (o instanceof CloseWebSocketFrame) {
System.out.println("连接关闭");
}
}
}
}
用netty进行webSocket长连接,如果是wss开头的,需要声明SslContext进行ssl校验,需要进行握手连接验证,这一步是比较关键我认为也是难点,需要自己稍微处理一下握手相关的东西,如果是url是ws开头的,就不需要进行握手验证。还需要注意一下连接的端口,如果是wss,默认端口是443,如果是ws,默认端口是80。
好啦,本次分享就到这里了,希望大家有所收获~
重点!重点!听说笔者开通的微信公众号,感兴趣的话可以关注一下:有理唔理
有兴趣的话,感兴趣的话可以下载源码:
https://github.com/ikuncoder/webSocketConnector
编辑
点/击/蓝/字 关/注/我/们