文章目录
- 保活策略
- 集群问题
- 流量整形/流控
- 消息压缩
- 堆外内存优化
- 限制连接数
- IP黑白名单过滤
- 认证授权过滤
- 框架内部日志
保活策略
通常建议在客户端来做心跳,减少服务端压力.
客户端定时发送 ping操作帧
即可
当服务端接收到ping操作帧
后,会自动发送pong帧
。Java_websocket的客户端默认心跳数据包每 60 秒自动发送一次
服务端接收到Ping后会自动发送Pong相关核心代码
// 服务端接收到不同的帧做不同的处理
public void processFrame(WebSocketImpl webSocketImpl, Framedata frame)
throws InvalidDataException {
Opcode curop = frame.getOpcode();
if (curop == Opcode.CLOSING) {
// 接收到 关闭 ,关闭连接
processFrameClosing(webSocketImpl, frame);
} else if (curop == Opcode.PING) {
// 接收到 ping ,传递事件 会自动发个 pong
webSocketImpl.getWebSocketListener().onWebsocketPing(webSocketImpl, frame);
} else if (curop == Opcode.PONG) {
// 接收到 pong , 更新下对应连接的最新pong时间并 传递事件
webSocketImpl.updateLastPong();
// 传递事件
webSocketImpl.getWebSocketListener().onWebsocketPong(webSocketImpl, frame);
} else if (!frame.isFin() || curop == Opcode.CONTINUOUS) {
processFrameContinuousAndNonFin(webSocketImpl, frame, curop);
} else if (currentContinuousFrame != null) {
log.error("Protocol error: Continuous frame sequence not completed.");
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
"Continuous frame sequence not completed.");
} else if (curop == Opcode.TEXT) {
// 文本帧 帮我们转了一手 Charsetfunctions.stringUtf8(frame.getPayloadData())
processFrameText(webSocketImpl, frame);
} else if (curop == Opcode.BINARY) {
// 二进制帧
processFrameBinary(webSocketImpl, frame);
} else {
log.error("non control or continious frame expected");
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
"non control or continious frame expected");
}
}
// 当服务端接收到`ping操作帧`后,会自动发送`pong帧`。
public abstract class WebSocketAdapter implements WebSocketListener {
@Override
public void onWebsocketPing(WebSocket conn, Framedata f) {
conn.sendFrame(new PongFrame((PingFrame) f));
}
集群问题
- 广播(发布订阅)模式方案
- 用rocketmq广播。
- 用redis订阅发布。
- 高可用方案
- 客户端配置多个server地址。
- 每次都跟多个server通信,即每个server多少平等的。
- 共享状态+转发方案
- redis记录会话id:serverId(serverIp+serverPort)
- 每次发送的时候根据会话id判断是自己server node发,还是其他node,如果是自己直接发就行,其他node就给它发送个rpc请求。
流量整形/流控
流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当报文的发送速度过快时,首先在缓冲区进行缓存;再通过流量计量算法的控制下“均匀”地发送这些被缓冲的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。
消息发送保护机制
通过流量整形可以控制发送速度,但是它的控制原理是将待发送的消息封装成Task放入消息队列,等待执行时间到达后继续发送,所以如果业务发送线程不判断channle的可以状态,就可能会导致OOM问题。
java_websocket不支持。。。Netty支持哈,高低水位是需要你在send之前判断isWrite,具体请参考netty
消息压缩
只体会到了 server响应的消息压缩。
请求示例
GET ws://127.0.0.1:8085/ HTTP/1.1\r\n
Host: 127.0.0.1:8085\r\n
Connection: Upgrade\r\n
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
Upgrade: websocket\r\n
Origin: http://www.websocket-test.com\r\n
Sec-WebSocket-Version: 13\r\n
User-Agent: Mozilla/5.0 (Linux; Android 6.0; H60-L01 Build/HDH60-L01) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36\r\n
Accept-Encoding: gzip, deflate, sdch\r\n
Accept-Language: zh-CN,zh;q=0.8\r\n
Sec-WebSocket-Key: S4iljLdlI5qk3jpx2fHU4A==\r\n
// 这里
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n
响应示例
HTTP/1.1 101 \r\n
Server: TooTallNate Java-WebSocket\r\n
Upgrade: websocket\r\n
Connection: upgrade\r\n
Sec-WebSocket-Accept: xwLDQrb5kzxpZDdeTcUd+7diXXU=\r\n
// 这里
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15\r\n
Date: Sun, 09 Oct 2016 23:07:39 GMT\r\n
根据请求中的 Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
来协商是否对传输数据进行deflate压缩。
重点响应头字段 Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
。
// 客户端
private static final Draft perMessageDeflateDraft = new Draft_6455(new PerMessageDeflateExtension());
private static class DeflateClient extends WebSocketClient {
public DeflateClient() throws URISyntaxException {
super(new URI("ws://localhost:" + PORT), perMessageDeflateDraft);
}
// 服务端
private static class DeflateServer extends WebSocketServer {
public DeflateServer() {
super(new InetSocketAddress(PORT), Collections.singletonList(perMessageDeflateDraft));
}
Netty类似如下
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
if (config.isUseCompressionHandler()) {
// 这里使用netty内置的WebSocketServerCompressionHandler
pipeline.addLast(new WebSocketServerCompressionHandler());
}
pipeline.addLast(new HttpServerHandler(xx, config));
}
});
堆外内存优化
当发送量非常大 (如:100000/s),可以使用堆外内存(零拷贝提升性能)来群发进行优化
// sessions 为 List<Session>
String str = "Hello Netty!";
byte[] bytes = str.getBytes();
ByteBuf buf = PooledByteBufAllocator.DEFAULT.buffer(bytes.length).writeBytes(bytes);
try {
for (Session session : sessions) {
if (session.isWritable()) {
session.sendText(buf.retainedDuplicate());
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(buf);
}
限制连接数
如果需要严格限制的话,可以直接使用AtomicInteger类对连接数进行统计,当超过限制时关闭连接
IP黑白名单过滤
在握手时只做一次即可,跟http server一个逻辑。
认证授权过滤
在握手时只做一次即可,跟http server鉴权一个逻辑。
框架内部日志
将此添加到 logback.xml
<logger name="org.java_websocket" level="TRACE">
<appender-ref ref="CONSOLE"/>
</logger>