WebSocket与HTTP的区别:
HTTP协议是用在应用层的, 基于TCP协议的基础上,是单向的。 HTTP协议也需要建立三次握手才能够才能发送消息, HTTP连接分为短,长连接, 短连接每次请求都需要三次握手才能发送消息。长连接是在一定的期限内保持连接。保持TCP连接不断开。 客服端与服务端进行通信必须由客服端先发起, 客服端想要实时获取服务端信息就得不断发送长连接。
WebSocket实现了多路复用, 他是全双工通信。 服务端和客服端可以同时发送消息, 并且建立连接后服务端可以主动推送消息到客服端。 而且消息可以不用携带head部分信息。与http的长连接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。
实现网页版聊天:SpringBoot + netty(HTTP协议升级为WebSocket)
1. NettyServer:
package com.leiyu.groupchat.netty;
import com.leiyu.groupchat.config.NettyConfig;
import com.leiyu.groupchat.handler.WebSocketChannelInitializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
@Component
public class NettyServer implements Runnable{
private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private EventLoopGroup workerGroup = new NioEventLoopGroup();
@Autowired
NettyConfig nettyConfig;
@Autowired
WebSocketChannelInitializer webSocketChannelInit;
/*** 资源关闭--在容器销毁是关闭 */
@PreDestroy
public void close() {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
@Override
public void run() {
try {
// 创建启动类
ServerBootstrap boot = new ServerBootstrap();
boot.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(webSocketChannelInit) ;
// 启动
ChannelFuture future = boot.bind(nettyConfig.getPort()).sync();
System.out.println("--Netty服务端启动成功---");
future.channel().closeFuture().sync() ;
} catch (InterruptedException e) {
e.printStackTrace();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
2. WebSocketChannelInitializer实现了ChannelInitializer
@Component
public class WebSocketChannelInitializer extends ChannelInitializer {
@Autowired
private NettyConfig nettyConfig ;
@Autowired
private WebSocketHandler webSocketHandler ;
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
//对http协议的支持.
pipeline.addLast(new HttpServerCodec()) ;
// 对大数据流的支持
pipeline.addLast(new ChunkedWriteHandler()) ;
//post请求分三部分. request line / request header / message body
//HttpObjectAggregator将多个信息转化成单一的request或者response对象
pipeline.addLast(new HttpObjectAggregator(8000)) ;
// 将http协议升级为ws协议. websocket的支持
pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath())) ;
// 自定义处理handler
pipeline.addLast(webSocketHandler) ;
}
}
3. WebSocketHandler实现SimpleChannelInboundHandler<TextWebSocketFrame>
/**
* 自定义处理类
* TextWebSocketFrame: websocket数据是帧的形式处理
*/
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
List<Channel> list = new ArrayList<Channel>() ;
/**
* 通 道就绪事件
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
list.add(channel) ;
// 可以直到谁来连接, 通知上线
System.out.println("上线了...");
}
/**
* 通知下线
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
list.remove(channel) ;
}
/**
* 读就绪事件
* @param ctx
* @param o
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame o) throws Exception {
Channel channel = ctx.channel();
String msg = o.text();
System.out.println("msg:" + msg);
for (Channel ch:list) {
if(ch != channel) {
ch.writeAndFlush(new TextWebSocketFrame(msg)) ;
}
}
}
/**
* 异常关闭
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
Channel channel = ctx.channel();
list.remove(channel) ;
}
}
4. SpringBoot启动类:
@SpringBootApplication
public class GroupChatApplication implements CommandLineRunner {
@Autowired
private NettyServer nettyServer ;
public static void main(String[] args) {
SpringApplication.run(GroupChatApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
new Thread(nettyServer).start();
}
}
5. 前端重要部分js代码:
var ws = new WebSocket("ws://localhost:8081/chat") ;
ws.onopen = function () {
console.log("连接成功") ;
}
ws.onmessage = function (evt) {
showMessage(evt.data) ;
}
ws.onclose = function () {
console.log("关闭") ;
}
ws.onerror = function () {
console.log("连接异常") ;
}
这样基于SpringBoot+Netty+WebSocket网页版群聊就实现了, 想要完整代码的, 评论留言!
分析一下实现群聊的重要逻辑:
1. 在WebSocketHandler里面, 我们的通道就绪事件触发的channelActive()方法可以得到每次客服端连接服务端的channel, 得到每个用户的channel我们需要保存在Map集合中, 当一个channel发出信息时, 服务端只需要遍历集合,排除自己, 发送消息即可。下线以及异常关闭, 都需要移除channel.
2. 除了自定义handler, 我们往pipline中添加, 对HTTP协议支持的HttpServerCodec,对大数据流支持的ChunkedWriteHandler, 把post请求封装成一个resquest、response接收的HttpObjectAggregator, 对HTTP协议升级成webSocket的WebSocketServerProtocolHandler
3. 在启动内实现CommandLineRunner, 让容器启动后单独开一条线程运行Netty服务器
在这里我想问下各位网友能分析下用户与用户之间的私聊怎么实现?