用Netty实现WebSocket协议的实战应用开发指南
上一篇聊了一下使用Netty进行HTTP协议的应用开发,今儿就来说一下HTTP协议的弊端以及WebSocket协议的开发使用。
1、HTTP协议的弊端
(1)半双工模式:同一时刻,只能有一个方向的数据传输,不能同时传输。
(2)消息冗长复杂:包括了一大堆的消息头、消息体等,相比于其它二进制通信传输,冗长而繁琐。
2、WebSocket协议
WebSocket提供了一种浏览器与服务器间进行全双工通信的网络技术,浏览器与服务器之间只需要做一个握手动作,之后就形成了一条快速通道,两者可以互相传输数据。WebSocket是基于TCP全双工进行消息传递,相比于HTTP半双工,性能得到很大的提升。
特点:
(1)单一的TCP连接,采用全双工模式通信
(2)无头部信息、cookie和身份认证
(3)服务端可以主动传递消息给客户端,不需要客户端轮询
(4)通过“PING/PONG”保持心跳检测
WebSocket进行握手的请求还是HTTP请求,只是在请求头上多了几个标识表明此请求是WebSocket握手请求:
其中Upgrade:websocket就是表明此请求为WebSocket握手请求。
3、Netty之WebSocket协议开发使用
这边我们开发一个WebSocket服务端,服务端在接收到客户端请求之后,发送当前时间给客户端的示例。需要处理的是HTTP握手请求以及消息接受处理。
服务端程序
public class WSServer {
public static void main(String[] args) {
new WSServer().start();
}
private void start() {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap server = new ServerBootstrap();
server.group(boss,worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//将请求和应答消息编码或解码成http消息
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
//支持异步发送大的码流
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new WSServerHandler());
}
})
.childOption(ChannelOption.SO_KEEPALIVE,true);
ChannelFuture sync = server.bind(8888).sync();
System.out.println("WebSocket服务端已启动,端口号为8888");
sync.channel().closeFuture().sync();
} catch (Exception e) {
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
服务端添加了几个处理器,分别是
(1)HttpServerCodec:将请求和应答消息编码或解码成HTTP消息。 (2)HttpObjectAggregator:将HTTP消息的多个部分组合成一条完整的HTTP消息
(3)ChunkedWriteHandler:支持异步发送大的码流。
接下来看一下自定义的处理器:
public class WSServerHandler extends SimpleChannelInboundHandler<Object> {
private static final Log logger = LogFactory.getLog(WSServerHandler.class);
private WebSocketServerHandshaker handshaker;
@Override
protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
//如果是握手请求
if (msg instanceof FullHttpRequest) {
handlerHttpRequest(ctx,(FullHttpRequest)msg);
} else if (msg instanceof WebSocketFrame) {//如果是WebSocket消息接受
handlerWebSocketFrame(ctx,(WebSocketFrame)msg);
}
}
private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
//判断是否是关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return ;
}
//判断是否是ping指令
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
//只支持文本信息
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(frame.getClass().getName()+" frame type not support ");
}
String text = ((TextWebSocketFrame) frame).text();
logger.info("服务端接收到客户端信息为:"+text);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ctx.writeAndFlush(new TextWebSocketFrame("欢迎使用Netty WebSocket服务,目前时间为:"+ format.format(new Date())));
}
private void handlerHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
//如果http解码失败,返回http异常
//判断是否是WebSocket握手请求
if (!req.decoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
sentHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
//构造握手响应返回
WebSocketServerHandshakerFactory wsFactory =
new WebSocketServerHandshakerFactory("ws://localhost:8888", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(),req);
}
}
private void sentHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
if (res.status().code() != 200) {
ByteBuf byteBuf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
res.content().writeBytes(byteBuf);
byteBuf.release();
setContentLength(res,res.content().readableBytes());
}
//如果是非keep-alive连接,关闭连接
ChannelFuture future = ctx.channel().writeAndFlush(res);
if (!isKeepAlive(req) || res.status().code()!=200) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
}
(1)首先在messageReceived方法中判断是HTTP握手请求还是WebSocket接入。
(2)如果是HTTP握手请求,则判断是否是WebSocket的握手请求,判断方法是请求头中是否有Upgrade:websocket这个消息,如果是WebSocket握手请求,则构建握手响应返回。
(3)如果是WebSocket接入,判断是关闭指令还是ping指令,也可以判断消息是否是文本消息,然后构建TextWebSocketFrame对象返回给客户端。
测试
这里推荐使用网上已有的WebSocket测试工具,不推荐自己写前端代码测试,因为麻烦。
启动WebSocket服务端:
在测试工具中输入ws://localhost:8888进行连接:
连接成功后,就可以发信息了:
推荐阅读
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
用Netty实现WebSocket协议的实战应用开发指南
-
go语言Socket编程-Socket编程 什么是Socket Socket,英文含义是插座、插孔,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。 套接字通讯原理示意 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 网络应用程序设计模式 C/S模式 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。 B/S模式 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。 优缺点 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。 C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。 B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。 简单的C/S模型通信 Server端:Listen函数 func Listen(network, address string) (Listener, error) network:选用的协议:TCP、UDP, 如:“tcp”或 “udp” address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000” Listener 接口: type Listener interface { Accept (Conn, error) Close error Addr Addr } Conn 接口: type Conn interface { Read(b byte) (n int, err error) Write(b byte) (n int, err error) Close error LocalAddr Addr RemoteAddr Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 参看 [<u>https://studygolang.com/pkgdoc</u>](https://studygolang.com/pkgdoc) 中文帮助文档中的demo: 示例代码:TCP服务器.go package main import ( "net" "fmt" ) func main { // 创建监听 listener, err:= net.Listen("tcp", ":8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close // 主协程结束时,关闭listener fmt.Println("服务器等待客户端建立连接...") // 等待客户端连接请求 conn, err := listener.Accept if err != nil { fmt.Println("accept err:", err) return } defer conn.Close // 使用结束,断开与客户端链接 fmt.Println("客户端与服务器连接建立成功...") // 接收客户端数据 buf := make(byte, 1024) // 创建1024大小的缓冲区,用于read n, err := conn.Read(buf) if err != nil { fmt.Println("read err:", err) return } fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。 }