Http
是无状态的,浏览器每次请求,都是创建一个新连接,传输完毕即断开。双方并不能感知对方的状态。
而WebSocket
是长连接,一次TCP
握手,即可建立持久性的连接,并且双方能感知到对方的状态。
实例要求:
Http
协议是无状态的, 浏览器和服务器间的请求响应一次,下一次会重新创建连接.- 要求:实现基于
webSocket
的长连接的全双工的交互 - 改变
Http
协议多次请求的约束,实现长连接了, 服务器可以发送消息给浏览器 - 客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知
核心代码:
//创建一个通道测试对象(匿名对象)
new ChannelInitializer<SocketChannel>() {
//给管道设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//因为是基于http协议,使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//http是以块方式写的,添加ChunkedWrite处理器
pipeline.addLast(new ChunkedWriteHandler());
//因为http的数据,在传输过程中是分段的,HttpObjectAggregator就是可以将多个段聚合起来
//这就是为什么,当浏览器发送大量数据时,就会发出多次http请求的原因
pipeline.addLast(new HttpObjectAggregator(8192));
//对于webSocket的数据是以帧的形式传递
//可以看到webSocketFrame,下面有六个子类
//浏览器请求时 ws://localhost:7000/hello 表示请求的url,使用的是ws webSocket协议
//WebSocketServerProtocolHandler核心功能将http协议升级为ws协议,保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//自定义的handler,处理业务逻辑
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
}
- HttpServerCodec :Http编码和解码器
- ChunkedWriteHandler :以块的方式写数据
- HttpObjectAggregator(8192) :将多个段聚合起来,最大长度8192
- WebSocketServerProtocolHandler:将http协议升级为WebSocket协议,通过状态码101切换的。101(switch protocols)。构造方法参数是ws的路径
- WebSocket传输数据以frame(帧)的形式传递:
完整代码
服务端:
public class MyServer {
public static void main(String[] args) {
//设置main方法日志级别
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
List<Logger> loggerList = loggerContext.getLoggerList();
loggerList.forEach(logger -> {
logger.setLevel(Level.WARN);
});
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(3);
try {
//2.创建服务器端的启动对象,配置参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
//3.使用链式编程进行设置
serverBootstrap
//设置两个线程组
.group(bossGroup, workerGroup)
//使用NioServerSocketChannel作为服务器的通道实现
.channel(NioServerSocketChannel.class)
//改handler对应的是bossGroup
.handler(new LoggingHandler(LogLevel.INFO))
//给我们的workerGroup的EventLoopGroup对应的管道设置处理器Handler
.childHandler(
//创建一个通道测试对象(匿名对象)
new ChannelInitializer<SocketChannel>() {
//给管道设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//因为是基于http协议,使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//http是以块方式写的,添加ChunkedWrite处理器
pipeline.addLast(new ChunkedWriteHandler());
//因为http的数据,在传输过程中是分段的,HttpObjectAggregator就是可以将多个段聚合起来
//这就是为什么,当浏览器发送大量数据时,就会发出多次http请求的原因
pipeline.addLast(new HttpObjectAggregator(8192));
//对于webSocket的数据是以帧的形式传递
//可以看到webSocketFrame,下面有六个子类
//浏览器请求时 ws://localhost:7000/hello 表示请求的url,使用的是ws webSocket协议
//WebSocketServerProtocolHandler核心功能将http协议升级为ws协议,保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//自定义的handler,处理业务逻辑
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
//启动服务器
ChannelFuture sync = serverBootstrap.bind(8005).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
自定义的handler:
//泛型TextWebSocketFrame类型,表示一个文本帧(frame)
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
System.out.println("服务器端收到消息:"+textWebSocketFrame.text());
//回复浏览器
channelHandlerContext.channel().writeAndFlush(new TextWebSocketFrame("服务器时间 "+ LocalDateTime.now()+" "+textWebSocketFrame.text()));
}
//当web客户端连接后,触发方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//id 表示唯一的值,LongText是唯一的 shortText不是唯一
System.out.println("handlerAdded被调用 "+ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved被调用 "+ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生 "+cause.getMessage());
ctx.close();//关闭连接
}
}
html页面:注意这里的端口和访问路径必须和服务端相同
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
//判断当前浏览器是否支持websocket
if(window.WebSocket) {
//go on
socket = new WebSocket("ws://localhost:8005/hello");
//相当于channelReado, ev 收到服务器端回送的消息
socket.onmessage = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + ev.data;
}
//相当于连接开启(感知到连接开启)
socket.onopen = function (ev) {
var rt = document.getElementById("responseText");
rt.value = "连接开启了.."
}
//相当于连接关闭(感知到连接关闭)
socket.onclose = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "连接关闭了.."
}
} else {
alert("当前浏览器不支持websocket")
}
//发送消息到服务器
function send(message) {
if(!window.socket) { //先判断socket是否创建好
return;
}
if(socket.readyState == WebSocket.OPEN) {
//通过socket 发送消息
socket.send(message)
} else {
alert("连接没有开启");
}
}
</script>
<form onsubmit="return false">
<textarea name="message" style="height: 300px; width: 300px"></textarea>
<input type="button" value="发生消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 300px; width: 300px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
测试:启动服务端和html
关闭服务端:
发送内容:
为什么http协议能升级到ws协议呢
WebSocketServerProtocolHandler
通过一个状态码101