websocket基础
webssocket建立握手时发送http请求,当成功后.http转变成ws
大概思路 :ws建立连接 http接口(熟悉的mvc)发送消息(单聊或群发)
调用webservice,向连接发送消息
扩展:事件驱动_异步保存具体信息
netty服务端配置
这段代码属于netty入门的模板代码
我们自定义 HttpHeadersHandler 继承ChannelInboundHandlerAdapter 接口
定义 NettyWebSocketServerHandler extends SimpleChannelInboundHandler 其中TextWebSocketFrame就是传输数据的包装类
实现public void channelRead(ChannelHandlerContext ctx, Object msg) 方法
- pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));转变协议的代码实现
// 服务器启动引导对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.option(ChannelOption.SO_KEEPALIVE, true)
//处理日志
.handler(new LoggingHandler(LogLevel.INFO)) // 为 bossGroup 添加 日志处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//30秒客户端没有向服务器发送心跳则关闭连接
pipeline.addLast(new IdleStateHandler(30, 0, 0));
// 因为使用http协议,所以需要使用http的编码器,解码器
pipeline.addLast(new HttpServerCodec());
// 以块方式写,添加 chunkedWriter 处理器
pipeline.addLast(new ChunkedWriteHandler());
/**
* 说明:
* 1. http数据在传输过程中是分段的,HttpObjectAggregator可以把多个段聚合起来;
* 2. 这就是为什么当浏览器发送大量数据时,就会发出多次 http请求的原因
*/
pipeline.addLast(new HttpObjectAggregator(8192));
//保存用户ip---最开始是一个http 然后升级为ws 起始
pipeline.addLast(new HttpHeadersHandler());
/**
* 说明:
* 1. 对于 WebSocket,它的数据是以帧frame 的形式传递的;
* 2. 可以看到 WebSocketFrame 下面有6个子类
* 3. 浏览器发送请求时: ws://localhost:7000/hello 表示请求的uri
* 4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;
* 是通过一个状态码 101 来切换的
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
// 自定义handler ,处理业务逻辑
pipeline.addLast(new NettyWebSocketServerHandler());
}
});
// 启动服务器,监听端口,阻塞直到启动成功
serverBootstrap.bind(WEB_SOCKET_PORT).sync();
}
基础对象
简单肤浅的理解 channel:连接
-
Channel.pipeline() :
- 返回一个
ChannelPipeline对象,用于处理Channel的事件和数据。
- 返回一个
-
属性值绑定chennel
<T> Attribute<T> attr(AttributeKey<T> var1);
<T> boolean hasAttr(AttributeKey<T> var1);
有上下文的绑定和获取,就可以实现一个工具类->对channel进行特殊绑定
public class NettyUtil {
public static AttributeKey<String> TOKEN = AttributeKey.valueOf("token");
public static AttributeKey<String> IP = AttributeKey.valueOf("ip");
public static AttributeKey<Long> UID = AttributeKey.valueOf("uid");
public static <T> void setAttr(Channel channel, AttributeKey<T> attributeKey, T data) {
Attribute<T> attr = channel.attr(attributeKey);
attr.set(data);
}
public static <T> T getAttr(Channel channel, AttributeKey<T> ip) {
return channel.attr(ip).get();
}
}
处理器的实现
握手阶段
握手阶段为channel绑定相关的的数据(http)
token则是作为参数放在url上
ws协议不能携带请求头,但nginx可以设置请求头,若没有nginx,则直接通过channel获取
@Slf4j
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("loc5----http-IP");
if (msg instanceof FullHttpRequest) {
log.info("loc5 enter");
FullHttpRequest request = (FullHttpRequest) msg;
UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.uri());
// 获取token参数
//上面获取token
String token = Optional.ofNullable(urlBuilder.getQuery()).map(k->k.get("token")).map(CharSequence::toString).orElse("");
NettyUtil.setAttr(ctx.channel(), NettyUtil.TOKEN, token);
// 获取请求路径
//请求头
request.setUri(urlBuilder.getPath().toString());
HttpHeaders headers = request.headers();
String ip = headers.get("X-Real-IP");
if (StringUtils.isEmpty(ip)) {//如果没经过nginx,就直接获取远端地址
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
ip = address.getAddress().getHostAddress();
}
NettyUtil.setAttr(ctx.channel(), NettyUtil.IP, ip);
ctx.pipeline().remove(this);
ctx.fireChannelRead(request);
}else
{
ctx.fireChannelRead(msg);
}
}
}
调试举例
ws://localhost:8090/websocket?token=hello
ws处理类
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
log.info("传输数据");
WSBaseReq wsBaseReq = JSONUtil.toBean(msg.text(), WSBaseReq.class);
WSReqTypeEnum wsReqTypeEnum = WSReqTypeEnum.of(wsBaseReq.getType());
switch (????) {
case LOGIN:
//返回ws数据
channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
case HEARTBEAT://心跳
break;
default:
log.info("未知类型");
}
}
框架父类上的其他生命周期
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.warn("异常发生,异常消息 ={}", cause);
// super.exceptionCaught(ctx, cause);
ctx.channel().close();
}
发现超时后 主要是处理chanel相关的数据下线
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
离线
@Override
public void channelInactive(ChannelHandlerContext ctx)
客户端离线
@Override
public void handlerRemoved(ChannelHandlerContext ctx)
心跳不响应的生命周期