WebSocket 是HTML5开始提供的一种在浏览器和服务器间进行全双工通信的协议。目前很多没有使用WebSocket进行客户端服务端实时通信的web应用,大多使用设置规则时间的轮询,或者使用长轮询较多来处理消息的实时推送。这样势必会较大程度浪费服务器和带宽资源,而我们现在要讲的WebSocket正是来解决该问题而出现,使得B/S架构的应用拥有C/S架构一样的实时通信能力。
HTTP协议是半双工协议,也就是说在同一时间点只能处理一个方向的数据传输,同时HTTP消息也是过于庞大,里面包含大量消息头数据,真正在消息处理中很多数据不是必须的,这也是对资源的浪费。
WebSocket在客户端和服务端只需一次请求,就会在客户端和服务端建立一条通信通道,可以实时相互传输数据,并且不会像HTTP那样携带大量请求头等信息。因为WebSocket是基于TCP双向全双工通信的协议,所以支持在同一时间点处理发送和接收消息,做到实时的消息处理。
http
或https
,而是使用过ws
或wss
(一个非安全的,一个安全的,类似前两者之间的差别),请求头里面要附加一个申请协议升级的信息Upgrade: websocket
,还有随机生成一个Sec-WebSocket-Key
的值,及版本信息Sec-WebSocket-Version
等等。服务端收到客户端的请求后,会解析该请求的信息,包括请求协议升级,版本校验,以及将Sec-WebSocket-Key
的加密后以sec-websocket-accept
的值返回给客户端,这样客户端和服务端的连接就建立了。 HTTP轮询和WebSocket生命周期示意图
这里服务端利用Netty的WebSocket开发。这里首先实现服务端启动类,然后自定义处理器来处理WebSocket的消息。
package com.ytao.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* Created by YANGTAO on 2019/11/17 0017.
*/
public class WebSocketServer {
public static String HOST = "127.0.0.1";
public static int PORT = 8806;
public static void startUp() throws Exception {
// 监听端口的线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 处理每一条连接的数据读写的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 启动的引导类
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception{
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));
// 将请求和返回消息编码或解码成http
pipeline.addLast("http-codec", new HttpServerCodec());
// 使http的多个部分组合成一条完整的http
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
// 向客户端发送h5文件,主要是来支持websocket通信
pipeline.addLast("http-chunked", new ChunkedWriteHandler());
// 服务端自定义处理器
pipeline.addLast("handler", new WebSocketServerHandler());
}
})
// 开启心跳机制
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer() {
protected void initChannel(NioServerSocketChannel ch) {
System.out.println("WebSocket服务端启动中...");
}
});
Channel ch = serverBootstrap.bind(HOST, PORT).sync().channel();
System.out.println("WebSocket host: " ch.localAddress().toString().replace("/",""));
ch.closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
startUp();
}
}
上面启动类和HTTP协议的类似,所以较好理解。启动类启动后,我们需要处理WebSocket请求,这里自定义WebSocketServerHandler
。我们在处理中设计的业务逻辑有,如果只有一个连接来发送信息聊天,那么我们就以服务器自动回复,如果存在一个以上,我们就将信息发送给其他人。
package com.ytao.websocket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by YANGTAO on 2019/11/17 0017.
*/
public class WebSocketServerHandler extends SimpleChannelInboundHandler
刚建立连接时,第一次握手有HTTP协议处理,所以WebSocketServerHandler#messageReceived
会判断是HTTP还是WebSocket,如果是HTTP时,交由WebSocketServerHandler#handleHttpRequest
处理,里面会去验证请求,并且处理握手后将消息返回给客户端。如果不是HTTP协议,而是WebSocket协议时,处理交给WebSocketServerHandler#handleWebSocketFrame
处理,进入WebSocket处理后,这里面有判断消息属于哪种类型,里面包括CloseWebSocketFrame
,PingWebSocketFrame
,PongWebSocketFrame
,BinaryWebSocketFrame
,ContinuationWebSocketFrame
,TextWebSocketFrame
,他们都是WebSocketFrame
的子类,并且WebSocketFrame
又继承自DefaultByteBufHolder
。
channelHandlerContextConcurrentHashMap
是缓存WebSocket已连接的信息,因为我们实现的需求要记录连接数量,当有连接关闭时我们要删除以缓存的连接,所以在WebSocketServerHandler#close
中要移除缓存。
最后的发送文本到客户端,根据连接数量判断。如果连接数量不大于1,那么,我们"价值一个亿的AI核心代码"WebSocketServerHandler#answer
来回复客户端消息。否则除了本次接收的连接,消息会发送给其他所有连接的客户端。
客户端使用JS实现WebSocket的操作,目前主流的浏览器基本都支持WebSocket。支持情况如图:
客户端H5的代码实现:
ytao-websocket
连接中.....
信息面板
JS这里实现相对较简单,主要用到:
new WebSocket(URL)
创建WebSocket对象 onopen()
打开连接 onclose()
关闭连接 onmessage
接收消息 send()
发送消息 当断开连接后,客户端这边重新发起连接,直到连接成功为止。
客户端和服务端连接后,我们从日志和请求中可以看到上面所提到的验证信息。客户端:
服务端:
启动服务端后,先实验我们"价值一个亿的AI",只有一个连接用户时,发送信息结果如图:
多个用户连接,这里使用三个连接用户群聊。
用户一:
用户二:
用户三:
到目前为止,WebSocket已帮助我们实现即时通信的需求,相信大家也基本入门了WebSocket的基本使用。
通过本文了解,可以帮助大家入门WebSocket并且解决当前可能存在的一些Web端的通信问题。我曾经在两个项目中也有看到该类解决方案都是通过定时轮询去做的,也或多或少对服务器资源造成一定的浪费。因为WebSocket本身是较复杂的,它提供的API也是比较多,所以在使用过程,要去真正使用好或去优化它,并不是一件很简单的事,也是需要根据现实场景针对性的去做。
个人博客: https://ytao.top
我的公众号 ytao