Repository navigation
Expand file tree
/
Copy pathNettyWebSocketClient.java
More file actions
199 lines (179 loc) · 7.68 KB
/
Copy pathNettyWebSocketClient.java
File metadata and controls
199 lines (179 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package com.google.firebase.database.connection;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Strings;
import com.google.firebase.internal.FirebaseScheduledExecutor;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
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.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import java.io.EOFException;
import java.net.URI;
import java.security.KeyStore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import javax.net.ssl.TrustManagerFactory;
/**
* A {@link WebsocketConnection.WSClient} implementation based on the Netty framework. Uses
* a single-threaded NIO event loop to read and write bytes from a WebSocket connection. Netty
* handles all the low-level IO, SSL and WebSocket handshake, and other protocol-specific details.
*
* <p>This implementation does not initiate connection close on its own. In case of errors or loss
* of connectivity, it notifies the higher layer ({@link WebsocketConnection}), which then decides
* whether to initiate a connection tear down.
*/
class NettyWebSocketClient implements WebsocketConnection.WSClient {
private static final int DEFAULT_WSS_PORT = 443;
private final URI uri;
private final WebsocketConnection.WSClientEventHandler eventHandler;
private final ChannelHandler channelHandler;
private final ExecutorService executorService;
private final EventLoopGroup group;
private Channel channel;
NettyWebSocketClient(
URI uri, String userAgent, ThreadFactory threadFactory,
WebsocketConnection.WSClientEventHandler eventHandler) {
this.uri = checkNotNull(uri, "uri must not be null");
this.eventHandler = checkNotNull(eventHandler, "event handler must not be null");
this.channelHandler = new WebSocketClientHandler(uri, userAgent, eventHandler);
this.executorService = new FirebaseScheduledExecutor(threadFactory,
"firebase-websocket-worker");
this.group = new NioEventLoopGroup(1, this.executorService);
}
@Override
public void connect() {
checkState(channel == null, "channel already initialized");
try {
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustFactory.init((KeyStore) null);
final SslContext sslContext = SslContextBuilder.forClient()
.trustManager(trustFactory).build();
Bootstrap bootstrap = new Bootstrap();
final int port = uri.getPort() != -1 ? uri.getPort() : DEFAULT_WSS_PORT;
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), port));
p.addLast(
new HttpClientCodec(),
// Set the max size for the HTTP responses. This only applies to the WebSocket
// handshake response from the server.
new HttpObjectAggregator(32 * 1024),
channelHandler);
}
});
ChannelFuture channelFuture = bootstrap.connect(uri.getHost(), port);
this.channel = channelFuture.channel();
channelFuture.addListener(
new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
eventHandler.onError(future.cause());
}
}
}
);
} catch (Exception e) {
eventHandler.onError(e);
}
}
@Override
public void close() {
checkState(channel != null, "channel not initialized");
try {
channel.close();
} finally {
// The following may leave an active threadDeathWatcher daemon behind. That can be cleaned
// up at a higher level if necessary. See https://github.com/netty/netty/issues/7310.
group.shutdownGracefully();
executorService.shutdown();
}
}
@Override
public void send(String msg) {
checkState(channel != null, "Channel not initialized");
if (!channel.isActive()) {
eventHandler.onError(new EOFException("WebSocket channel became inactive"));
} else {
channel.writeAndFlush(new TextWebSocketFrame(msg));
}
}
/**
* Handles low-level IO events. These events fire on the firebase-websocket-worker thread. We
* notify the {@link WebsocketConnection} on all events, which then hands them off to the
* RunLoop for further processing.
*/
private static class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
private final WebsocketConnection.WSClientEventHandler delegate;
private final WebSocketClientHandshaker handshaker;
WebSocketClientHandler(
URI uri, String userAgent, WebsocketConnection.WSClientEventHandler delegate) {
this.delegate = checkNotNull(delegate, "delegate must not be null");
checkArgument(!Strings.isNullOrEmpty(userAgent), "user agent must not be null or empty");
this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(
uri, WebSocketVersion.V13, null, true,
new DefaultHttpHeaders().add("User-Agent", userAgent));
}
@Override
public void handlerAdded(ChannelHandlerContext context) {
// Do nothing
}
@Override
public void channelActive(ChannelHandlerContext context) {
handshaker.handshake(context.channel());
}
@Override
public void channelInactive(ChannelHandlerContext context) {
delegate.onClose();
}
@Override
public void channelRead0(ChannelHandlerContext context, Object message) {
Channel channel = context.channel();
if (message instanceof FullHttpResponse) {
checkState(!handshaker.isHandshakeComplete());
try {
handshaker.finishHandshake(channel, (FullHttpResponse) message);
delegate.onOpen();
} catch (WebSocketHandshakeException e) {
delegate.onError(e);
}
} else if (message instanceof TextWebSocketFrame) {
delegate.onMessage(((TextWebSocketFrame) message).text());
} else {
checkState(message instanceof CloseWebSocketFrame);
delegate.onClose();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext context, final Throwable cause) {
delegate.onError(cause);
}
}
}