传统的请求-应答模式(http)越来越不能满足现实需求,服务器过于被动,而采用轮训或者long poll的方式过于浪费资源,这便有了WebSocket。WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说,二者区别如下。
接下来使用一个小例子来实现服务器往客户端的主动推送功能。
index.html
无标题文档
第一次握手请求由客户端发起,当服务器收到握手请求后,返回响应,这时客户端收到详情并打开socket完成握手,这样就建立了服务器与客户端之间的tcp长连接,对于 WebSocket 来说,它必须依赖HTTP协议的第一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
服务器目录结构如下:
首先看下启动类:
WebsocketApplication.java
package com.jhz.websocket;
import com.jhz.websocket.server.NettyServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebsocketApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebsocketApplication.class, args);
new NettyServer(12345).start();
}
}
在启动类中启动NettyServer(Netty服务器)。
NettyServer.java
package com.jhz.websocket.server;
import com.jhz.websocket.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @author jhz
* @date 18-10-21 下午9:45
*/
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
ch.pipeline().addLast(new WebSocketHandler());
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
}
这里要注意这四个Handler,HttpServerCodec、ChunkedWriteHandler、HttpObjectAggregator、WebSocketServerProtocolHandler,其中HttpServerCodec用于对HttpObject消息进行编码和解码,但是HTTP请求和响应可以有很多消息数据,你需要处理不同的部分,可能也需要聚合这些消息数据,这是很麻烦的。为了解决这个问题,Netty提供了一个聚合器,它将消息部分合并到FullHttpRequest和FullHttpResponse,因此不需要担心接收碎片消息数据,这就是HttpObjectAggregator的作用;ChunkedWriteHandler,允许通过处理ChunkedInput来写大的数据块;而WebSocketServerProtocolHandler是Netty封装好的WebSocket协议处理类,有了它可以少写很多步骤,包括握手的过程,以及url的定义(这里的/ws其实就定义了url指定的后缀)。
WebSocketHandler.java
package com.jhz.websocket.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.Scanner;
/**
* @author jhz
* @date 18-10-21 下午9:51
*/
public class WebSocketHandler extends SimpleChannelInboundHandler{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启!");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开连接,通道关闭!");
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("客户端收到服务器数据:" + msg.text());
Scanner s = new Scanner(System.in);
System.out.println("服务器推送:");
while(true) {
String line = s.nextLine();
if(line.equals("exit")) {
ctx.channel().close();
break;
}
String resp= "(" +ctx.channel().remoteAddress() + ") :" + line;
ctx.writeAndFlush(new TextWebSocketFrame(resp));
}
}
}
可以看到,建立长连接的过程都由WebSocketServerProtocolHandler为我们做完了(但是个人觉得还是要去自己写一次http握手的处理过程,Netty也做了一些封装,非常方便),客户端与服务器之间形成了一个全双工通讯的管道。
DefaultController.java
package com.jhz.websocket.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author jhz
* @date 18-10-21 下午8:25
*/
@Controller
public class DefaultController {
@RequestMapping("/")
public String index(){
return "index";
}
}
application.properties
# 定位模板的目录
spring.mvc.view.prefix=classpath:/templates/
# 给返回的页面添加后缀名
spring.mvc.view.suffix=.html
附上依赖:
buildscript {
ext {
springBootVersion = '2.0.6.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.jhz'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
maven {
name "aliyunmaven"
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-websocket')
testImplementation('org.springframework.boot:spring-boot-starter-test')
compile group: 'io.netty', name: 'netty-all', version: '5.0.0.Alpha2'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.4.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '2.0.4.RELEASE'
}
在前端页面发送123:
在服务器的控制台可以看到已经收到了消息:
在服务器控制台推送消息“456”、“789”,再次查看前端页面:
WebSocket的小Demo便完成了。
在前面的IO章节中,已经对比了使用Netty与传统的NIO方式的区别,Netty是高度封装的NIO框架,用起来会比传统的NIO编程方式方便很多,而其对WebSocket的支持同样为我们带来了极大的便利,WebSocket服务器在接收到客户端消息时需要对其判断,这个消息是http消息还是已经建立tcp连接的WebSocketFrame消息,若是前者,则代表是握手请求,服务器需要对握手请求进行响应,通常的写法如下:
private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
//如果不是WebSocket握手请求消息,那么就返回 HTTP 400 BAD REQUEST 响应给客户端。
if (!req.getDecoderResult().isSuccess()
|| !("websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
//如果是握手请求,那么就进行握手
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
WEB_SOCKET_URL, null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
// 通过它构造握手响应消息返回给客户端,
// 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码,
// 添加WebSocketEncoder和WebSocketDecoder之后,服务端就可以自动对WebSocket消息进行编解码了
handshaker.handshake(ctx.channel(), req);
}
}
而使用WebSocketServerProtocolHandler就能为我们省下很多事了。其实通常使用tomcat不需要我们实现WebSocket,从tomcat7之后就开始支持Websocket了,这里为了进一步的学习一下Netty,但是万一不用Tomcat呢?相对于Tomcat这种Web Server(顾名思义主要是提供Web协议相关的服务的),Netty是一个 是一个Network Server,是处于Web Server更下层的网络框 架,也就是说你可以使用Netty模仿Tomcat做一个提供HTTP服务的Web容器。简而言之,Netty通过使用NIO的很多新特性,对TCP/UDP编程进行了简化和封 装,提供了更容易使用的网络编程接口,让你可以根据自己的需要封装独特的HTTP Server或者FTP Server等.