【0.Netty相关的知识专栏都帮你整理好了,根据自己的情况,自行选择学习,点击我即可快速跳转】
【1.初识Netty&使用Netty实现简单的客户端与服务端的通信操作&Netty框架中一些重要的类以及方法的解析】
【2.基于Netty实现Http通信、实现UDP单播和广播通信、代码案例实战学习】
WebSocket ——一种在2011 年被互联网工程任务组(IETF)标准化的协议。
WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的交互序列,那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。
WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。简单地说,WebSocket提供了“在一个单个的TCP连接上提供双向的通信+结合WebSocket API+它为网页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。”,但是最终它们仍然属于扩展性受限的变通之法。
WebSocket 在客户端和服务器之间提供了真正的双向数据交换。WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。
HTML5中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。
采用新的协议,后端需要单独实现
客户端并不是所有浏览器都支持
Connection 必须设置 Upgrade,表示客户端希望连接升级。
Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
Upgrade: websocket
Connection: Upgrade,依然是固定的,告诉客户端即将升级的是 Websocket 协议,而不是mozillasocket,lurnarsocket或者shitsocket。
Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。
Sec-WebSocket-Protocol 则是表示最终使用的协议。
/**
WebSocket客户端的示例。
要运行此示例,需要兼容的WebSocket服务器。
因此,可以通过运行WebSocketServer来启动WebSocket服务器,
*/
public final class WebSocketClient {
static final String URL
= System.getProperty("url",
"ws://127.0.0.1:8080/websocket");
static final String SURL
= System.getProperty("url",
"wss://127.0.0.1:8443/websocket");
public static void main(String[] args) throws Exception {
URI uri = new URI(URL);
String scheme = uri.getScheme() == null? "ws" : uri.getScheme();
final String host =
uri.getHost() == null? "127.0.0.1" : uri.getHost();
final int port = uri.getPort();
if (!"ws".equalsIgnoreCase(scheme)
&& !"wss".equalsIgnoreCase(scheme)) {
System.err.println("Only WS(S) is supported.");
return;
}
final boolean ssl = "wss".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
EventLoopGroup group = new NioEventLoopGroup();
try {
// Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
// If you change it to V00, ping is not supported and remember to change
// HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
final WebSocketClientHandler handler =
new WebSocketClientHandler(
WebSocketClientHandshakerFactory
.newHandshaker(
uri, WebSocketVersion.V13,
null,
true,
new DefaultHttpHeaders()));
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(),
host, port));
}
p.addLast(
//http协议为握手必须
new HttpClientCodec(),
new HttpObjectAggregator(8192),
//支持WebSocket数据压缩
WebSocketClientCompressionHandler.INSTANCE,
handler);
}
});
//连接服务器
Channel ch = b.connect(uri.getHost(), port).sync().channel();
//等待握手完成
handler.handshakeFuture().sync();
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in));
while (true) {
String msg = console.readLine();
if (msg == null) {
break;
} else if ("bye".equals(msg.toLowerCase())) {
ch.writeAndFlush(new CloseWebSocketFrame());
ch.closeFuture().sync();
break;
} else if ("ping".equals(msg.toLowerCase())) {
WebSocketFrame frame = new PingWebSocketFrame(
Unpooled.wrappedBuffer(new byte[] { 8, 1, 8, 1 }));
ch.writeAndFlush(frame);
} else {
WebSocketFrame frame = new TextWebSocketFrame(msg);
ch.writeAndFlush(frame);
}
}
} finally {
group.shutdownGracefully();
}
}
}
class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
//负责和服务器进行握手
private final WebSocketClientHandshaker handshaker;
//握手的结果
private ChannelPromise handshakeFuture;
public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
//当前Handler被添加到ChannelPipeline时,
// new出握手的结果的实例,以备将来使用
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
handshakeFuture = ctx.newPromise();
}
//通道建立,进行握手
@Override
public void channelActive(ChannelHandlerContext ctx) {
handshaker.handshake(ctx.channel());
}
//通道关闭
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("WebSocket Client disconnected!");
}
//读取数据
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel ch = ctx.channel();
//握手未完成,完成握手
if (!handshaker.isHandshakeComplete()) {
try {
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
System.out.println("WebSocket Client connected!");
handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException e) {
System.out.println("WebSocket Client failed to connect");
handshakeFuture.setFailure(e);
}
return;
}
//握手已经完成,升级为了websocket,不应该再收到http报文
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"Unexpected FullHttpResponse (getStatus=" + response.status() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
//处理websocket报文
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
System.out.println("WebSocket Client received message: " + textFrame.text());
} else if (frame instanceof PongWebSocketFrame) {
System.out.println("WebSocket Client received pong");
} else if (frame instanceof CloseWebSocketFrame) {
System.out.println("WebSocket Client received closing");
ch.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
if (!handshakeFuture.isDone()) {
handshakeFuture.setFailure(cause);
}
ctx.close();
}
}
/**
* 类说明:
*/
public final class WebSocketServer {
/*创建 DefaultChannelGroup,用来保存所
有已经连接的 WebSocket Channel,群发和一对一功能可以用上*/
private final static ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
static final boolean SSL = false;//是否启用ssl
/*通过ssl访问端口为8443,否则为8080*/
static final int PORT
= Integer.parseInt(
System.getProperty("port", SSL? "8443" : "8080"));
public static void main(String[] args) throws Exception {
/*SSL配置*/
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(),
ssc.privateKey()).build();
} else {
sslCtx = null;
}
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(sslCtx,channelGroup));
Channel ch = b.bind(PORT).sync().channel();
System.out.println("打开浏览器访问: " +
(SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
class WebSocketServerInitializer
extends ChannelInitializer<SocketChannel> {
private final ChannelGroup group;
/*websocket访问路径*/
private static final String WEBSOCKET_PATH = "/websocket";
private final SslContext sslCtx;
public WebSocketServerInitializer(SslContext sslCtx,ChannelGroup group) {
this.sslCtx = sslCtx;
this.group = group;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
/*增加对http的支持*/
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
/*Netty提供,支持WebSocket应答数据压缩传输*/
pipeline.addLast(new WebSocketServerCompressionHandler());
/*Netty提供,对整个websocket的通信进行了初始化(发现http报文中有升级为websocket的请求)
,包括握手,以及以后的一些通信控制*/
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH,
null, true));
/*浏览器访问时展示index页面*/
pipeline.addLast(new ProcessWsIndexPageHandler(WEBSOCKET_PATH));
/*对websocket的数据进行处理*/
pipeline.addLast(new ProcesssWsFrameHandler(group));
}
}
/**
* 对http请求,将index的页面返回给前端
*/
class ProcessWsIndexPageHandler
extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String websocketPath;
public ProcessWsIndexPageHandler(String websocketPath) {
this.websocketPath = websocketPath;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx,
FullHttpRequest req) throws Exception {
// 处理错误或者无法解析的http请求
if (!req.decoderResult().isSuccess()) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
}
//只允许Get请求
if (req.method() != GET) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
return;
}
// 发送index页面的内容
if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {
//生成WebSocket的访问地址,写入index页面中
String webSocketLocation
= getWebSocketLocation(ctx.pipeline(), req,
websocketPath);
System.out.println("WebSocketLocation:["+webSocketLocation+"]");
//生成index页面的具体内容,并送往浏览器
ByteBuf content
= MakeIndexPage.getContent(
webSocketLocation);
FullHttpResponse res = new DefaultFullHttpResponse(
HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE,
"text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, req, res);
} else {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
/*发送应答*/
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req,
FullHttpResponse res) {
// 错误的请求进行处理 (code<>200).
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 发送应答.
ChannelFuture f = ctx.channel().writeAndFlush(res);
//对于不是长连接或者错误的请求直接关闭连接
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
/*根据用户的访问,告诉用户的浏览器,WebSocket的访问地址*/
private static String getWebSocketLocation(ChannelPipeline cp,
HttpRequest req,
String path) {
String protocol = "ws";
if (cp.get(SslHandler.class) != null) {
protocol = "wss";
}
return protocol + "://" + req.headers().get(HttpHeaderNames.HOST)
+ path;
}
}
/**
* 对websocket的数据进行处理
*/
class ProcesssWsFrameHandler
extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ChannelGroup group;
public ProcesssWsFrameHandler(ChannelGroup group) {
this.group = group;
}
private static final Logger logger
= LoggerFactory.getLogger(ProcesssWsFrameHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx,
WebSocketFrame frame) throws Exception {
//判断是否为文本帧,目前只处理文本帧
if (frame instanceof TextWebSocketFrame) {
// Send the uppercase string back.
String request = ((TextWebSocketFrame) frame).text();
logger.info("{} received {}", ctx.channel(), request);
ctx.channel().writeAndFlush(
new TextWebSocketFrame(request.toUpperCase(Locale.CHINA)));
/*群发实现:一对一道理一样*/
group.writeAndFlush(new TextWebSocketFrame(
"Client " + ctx.channel() + " say:"+request.toUpperCase(Locale.CHINA)));
} else {
String message = "unsupported frame type: "
+ frame.getClass().getName();
throw new UnsupportedOperationException(message);
}
}
/*重写 userEventTriggered()方法以处理自定义事件*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx,
Object evt) throws Exception {
/*检测事件,如果是握手成功事件,做点业务处理*/
if (evt == WebSocketServerProtocolHandler
.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
//通知所有已经连接的 WebSocket 客户端新的客户端已经连接上了
group.writeAndFlush(new TextWebSocketFrame(
"Client " + ctx.channel() + " joined"));
//将新的 WebSocket Channel 添加到 ChannelGroup 中,
// 以便它可以接收到所有的消息
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
}
/**
* 生成index页面的内容
*/
final class MakeIndexPage {
private static final String NEWLINE = "\r\n";
public static ByteBuf getContent(String webSocketLocation) {
return Unpooled.copiedBuffer(
"Web Socket Test "
+ NEWLINE +
"" + NEWLINE +
"" + NEWLINE +
" + NEWLINE +
" +
"value=\"Hello, World!\"/>" +
"
+ NEWLINE +
" οnclick=\"send(this.form.message.value)\" />"
+ NEWLINE +
"Output
" + NEWLINE +
""
+ NEWLINE +
"" + NEWLINE +
"" + NEWLINE +
"" + NEWLINE, CharsetUtil.US_ASCII);
}
}
服务器启动测试
打开浏览器测试
服务端显示通信地址
客户端测试连接
其它客户端同样可收到加入通信的消息
客户端1发送消息
其它客户端收到信息
客户端2发送消息
其它客户端接收消息
再打开一个客户端3
其它客户端看到客户端3加入通信界面
服务端看到新的客户端加入
好了,更多的测试同理,感兴趣的同学可以自己实现并进行测试。
到现在为止,【基于Netty实现WebSocket通信代码&基于WebSocket通信实现简单的群聊天室案例实战学习】就学习到这里,更多Netty相关的知识持续创作中。