Bootstrap 意思是引导,一个 Netty 应用通常由一个Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类
个人理解:
- 凡是方法名带
child
都是workGroup的配置,不带就是BossGroup的配置- 一个参数的group方法客户端使用,两个参数的group方法服务端使用
Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件
NioSocketChannel
,异步的客户端 TCP Socket
连接。NioServerSocketChannel
,异步的服务器端 TCP Socket
连接。NioDatagramChannel
,异步的 UDP
连接。NioSctpChannel
,异步的客户端 Sctp
连接。NioSctpServerChannel
,异步的 Sctp 服务器端连接,这些通道涵盖了UDP 和TCP网络IO以及文件 IO。出入站Handler
ChannelInboundHandler
用于处理入站I/O事件。ChannelOutboundHandler
用于处理出站I/O操作。适配器
ChannelInboundHandlerAdapter
用于处理入站I/O事件。ChannelOutboundHandlerAdapter
用于处理出站I/O操作。ChannelDuplexHandler
用于处理入站和出站事件。出站
的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler,并被这些Handler处理**,反之则称为入站
的**我们经常需要自定义一 个 Handler 类去继承 ChannelInboundHandlerA dapter,然后通过重写 相应方法实现业务逻辑, 我们接下来看看一般都 需要重写哪些方法
ChannelPipeline 是一个重点:
ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截inbound或者outbound 的事件和操作,相当于一个贯穿 Netty 的链。(也可以这样理解:ChannelPipeline 是 保存 ChannelHandler 的 List,用于处理或拦截Channel 的入站事件和出站操作)
ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下
ChannelHandlerContex
t组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个ChannelHandler我们手动添加如下图两个handler
以下示例是从头结点往后开始梳理:即找到pipeline后依次寻找next
如果是从尾结点往前梳理将以下示例倒叙即可:即即找到pipeline后依次寻找prev
通过debug我们发现Pipeline其实是DefaultChannelPipeline类型的双向链表
一个头DefaultChannelPipeline
我们发现第一个不是我们手动添加的,而是一个叫
DefaultChannelPipeline$HeadContext#0
的ChannelPipeline
第三个DefaultChannelPipeline的name是我们创建的TestServerInitializer
第三个DefaultChannelPipeline的name是我们手动添加的第一个handler
第四个DefaultChannelPipeline的name是我们手动添加的第二个handler
第五个DefaultChannelPipeline的name为DefaultChannelPipeline$TailContext#0
,并且next为null
ChannelHandlerContext
中 包 含 一 个 具 体 的事件处理器ChannelHandler,同 时ChannelHandlerContext 中也绑定了对应的pipeline 和Channel 的信息,方便对 ChannelHandler进行调用.close()
,关闭通道flush()
,刷新writeAndFlush(Object msg)
, 将 数 据写到ChannelPipeline中当前ChannelHandler 的下一个 ChannelHandler 开始处理(出站)Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。
对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。
通道一直保持连接活动状态
EventLoopGroup
是一组 EventLoop 的抽象,Netty 为了更好的利用多核CPU资源,一般会有多个 EventLoop 同时工作,每个 EventLoop 维护着一个Selector 实例。
EventLoopGroup 提供 next 接口
,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如:BossEventLoopGroup 和WorkerEventLoopGroup。
通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop线程。BossEventLoop 负责接收客户端的连接并将SocketChannel 交给WorkerEventLoopGroup 来进行 IO 处理,如下图所示:
Netty 提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类
(类似于NIO中的ByteBuffer但有区别)
代码总结:
底层数据存储还是数组
在netty 的buffer中,不需要使用flip 进行反转
因为底层维护了
readerindex
和writerIndex
和capacity
所以不用进行反转
- readerindex:读指针
- writerIndex:写指针
- capacity:数组大小
buffer.getByte(i)和buffer.readByte()区别
- buffer.getByte(i)会获取指定索引的数据,所以readerindex和writerIndex不会改变
- buffer.readByte()是从头依次读取数据所以readerindex会增加
通过 readerindex 和 writerIndex 和 capacity, 将buffer分成三个区域
- 0—readerindex 已经读取的区域
- readerindex—writerIndex , 可读的区域
- writerIndex – capacity, 可写的区域
package site.zhourui.nioAndNetty.netty.buf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class NettyByteBuf01 {
public static void main(String[] args) {
//创建一个ByteBuf
//说明
//1. 创建 对象,该对象包含一个数组arr , 是一个byte[10]
//2. 在netty 的buffer中,不需要使用flip 进行反转
// 底层维护了 readerindex 和 writerIndex
//3. 通过 readerindex 和 writerIndex 和 capacity, 将buffer分成三个区域
// 0---readerindex 已经读取的区域
// readerindex---writerIndex , 可读的区域
// writerIndex -- capacity, 可写的区域
ByteBuf buffer = Unpooled.buffer(10);
//填充数据
for (int i = 0; i < 10; i++) {
buffer.writeByte(i);
}
System.out.println("capacity="+buffer.capacity());
//输出
for (int i = 0; i < buffer.capacity(); i++) {
// System.out.println(buffer.getByte(i));
System.out.println(buffer.readByte());
}
System.out.println("执行完毕");
}
}
当i等于0时readerindex等于0
当i等于1时readerindex等于1
但是当我们使用getByte(i)方法时,当i等于1时readerindex等于0(不会改变)
验证底层存储数据为数组,capacity为数组长度
package site.zhourui.nioAndNetty.netty.buf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.Charset;
public class NettyByteBuf02 {
public static void main(String[] args) {
//创建ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));
//使用相关的方法
if(byteBuf.hasArray()) { // true
byte[] content = byteBuf.array();
//将 content 转成字符串
System.out.println(new String(content, Charset.forName("utf-8")));
System.out.println("byteBuf=" + byteBuf);
System.out.println(byteBuf.arrayOffset()); // 0
System.out.println(byteBuf.readerIndex()); // 0
System.out.println(byteBuf.writerIndex()); // 12
System.out.println(byteBuf.capacity()); // 36
//System.out.println(byteBuf.readByte()); //
System.out.println(byteBuf.getByte(0)); // 104
int len = byteBuf.readableBytes(); //可读的字节数 12
System.out.println("len=" + len);
//使用for取出各个字节
for(int i = 0; i < len; i++) {
System.out.println((char) byteBuf.getByte(i));
}
//按照某个范围读取
System.out.println(byteBuf.getCharSequence(0, 4, Charset.forName("utf-8")));
System.out.println(byteBuf.getCharSequence(4, 6, Charset.forName("utf-8")));
}
}
}
执行结果
- byteBuf实际类型为
UnpooledByteBufAllocator
capacity()
返回此缓冲区可以包含的字节数(八位字节)getByte(0)
获取此缓冲区中指定绝对index处的一个字节。readableBytes()
返回等于(this.writerIndex - this.readerIndex)的可读字节数。getCharSequence()
按照某个范围读取
实例要求:
- 编写一个 Netty 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 实现多人群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
- 目的:进一步理解Netty非阻塞网络编程机制
代码介绍:
- 和之前我们写的服务端没有啥区别,只不过在
initChannel
时在自定义hander之前加入StringDecoder
和StringEecoder
package site.zhourui.nioAndNetty.netty.groupchat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class GroupChatServer {
private int port;//监听端口
public GroupChatServer(int port) {
this.port=port;
}
public static void main(String[] args) throws InterruptedException {
new GroupChatServer(8888).run();
}
//编写run方法,处理客户端的请求
private void run() throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup(8);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childOption(ChannelOption.SO_KEEPALIVE,true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline加入解码器
pipeline.addLast("decoder",new StringDecoder());
//向pipeline加入编码器
pipeline.addLast("encoder",new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new GroupChatServerHandler());
}
});
System.out.println("netty 服务器启动");
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
//监听关闭
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
代码介绍:
package site.zhourui.nioAndNetty.netty.groupchat;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.text.SimpleDateFormat;
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
//public static List channels = new ArrayList();
//使用一个hashmap 管理
//public static Map channels = new HashMap();
//定义一个channle 组,管理所有的channel
//GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//表示channel 处于活动状态, 提示 xx上线
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " 上线了~");
}
//表示channel 处于不活动状态, 提示 xx离线了
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " 离线了~");
}
//handlerAdded 表示连接建立,一旦连接,第一个被执行
//将当前channel 加入到 channelGroup
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//将该客户加入聊天的信息推送给其它在线的客户端
/*
该方法会将 channelGroup 中所有的channel 遍历,并发送 消息,
我们不需要自己遍历
*/
channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new java.util.Date()) + " \n");
channelGroup.add(channel);
}
//断开连接, 将xx客户离开信息推送给当前在线的客户
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 离开聊天" + sdf.format(new java.util.Date()) + " \n");
System.out.println("channelGroup size" + channelGroup.size());
}
//读取数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
//获取到当前channel
Channel channel = ctx.channel();
//这时我们遍历channelGroup, 根据不同的情况,回送不同的消息
channelGroup.forEach(ch->{
if (channel!=ch){//不是当前的channel,转发消息
ch.writeAndFlush("[客户]" + channel.remoteAddress() + " 发送了消息" + msg + "\n");
}else {//回显自己发送的消息给自己
ch.writeAndFlush("[自己]发送了消息" + msg + "\n");
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- 和之前我们写的客户端没有啥区别,只不过在
initChannel
时在自定义hander之前加入StringDecoder
和StringEecoder
- 通过scanner读取用户输入的信息发送给channelFuture获取到的channel
package site.zhourui.nioAndNetty.netty.groupchat;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Scanner;
public class GroupChatClient {
//属性
private final String host;
private final int port;
public GroupChatClient(String host, int port) {
this.host = host;
this.port = port;
}
public void run() throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//得到pipeline
ChannelPipeline pipeline = ch.pipeline();
//加入相关handler
pipeline.addLast("decoder",new StringDecoder());
pipeline.addLast("encoder",new StringEncoder());
//加入自定义的handler
pipeline.addLast(new GroupChatClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
//得到channel
Channel channel = channelFuture.channel();
System.out.println("-------" + channel.localAddress()+ "--------");
//客户端需要输入信息,创建一个扫描器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String msg = scanner.nextLine();
//通过channel 发送到服务器端
channel.writeAndFlush(msg+"\r\n");
}
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new GroupChatClient("127.0.0.1",8888).run();
}
}
只打印服务端发送的消息
package site.zhourui.nioAndNetty.netty.groupchat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim());
}
}
启动服务端
启动一个客户端
启动两个客户端
端口为7607的客户端发送消息
此处服务端没做任何处理所以没有打印其他数据
关闭端口为7765的客户端
当channel端口连接时channelGroup会自动将该channel从channelGroup中移除
要实现点对点的私聊就需要有用户,并且注册
发送消息就不能简单的使用ChannelGroup来发送消息,我们使用map来管理channel
我们可以在用户登录成功的时候,将用户的id和channel加入到map中,后续私聊的时,只需要知道需要和哪个用户私聊(用户id),就可以拿到channel,并实现私聊
- 服务端启动,客户端建立连接,连接的目的是互相发送消息。
- 如果客户端在工作,服务端一定能收到数据,如果客户端空闲,服务端会出现资源浪费。
- 服务端需要一种检测机制,验证客户端的活跃状态,不活跃则关闭。
实例要求:
- 编写一个 Netty心跳检测机制案例, 当服务器超过3秒没有读时,就提示读空闲
- 当服务器超过5秒没有写操作时,就提示写空闲
- 实现当服务器超过7秒没有读或者写操作时,就提示读写空闲
代码介绍:
IdleStateHandler
是netty 提供的处理空闲状态的处理器- long
readerIdleTime
: 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接- long
writerIdleTime
: 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接- long
allIdleTime
: 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接- 文档说明
triggers an {@link IdleStateEvent} when a {@link Channel} has not performed read, write, or both operation for a while.- 当 IdleStateEvent 触发后 , 就会传递给管道 的下一个handler去处理通过调用(触发)下一个handler 的
userEventTiggered
, 在该方法中去处理 IdleStateEvent(读空闲,写空闲,读写空闲)
package site.zhourui.nioAndNetty.netty.heartbeat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class MyServer {
public static void main(String[] args) throws Exception{
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty 提供 IdleStateHandler
/*
说明
1. IdleStateHandler 是netty 提供的处理空闲状态的处理器
2. long readerIdleTime : 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
3. long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
4. long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
5. 文档说明
triggers an {@link IdleStateEvent} when a {@link Channel} has not performed
* read, write, or both operation for a while.
* 6. 当 IdleStateEvent 触发后 , 就会传递给管道 的下一个handler去处理
* 通过调用(触发)下一个handler 的 userEventTiggered , 在该方法中去处理 IdleStateEvent(读空闲,写空闲,读写空闲)
*/
pipeline.addLast(new IdleStateHandler(3,5,7,TimeUnit.SECONDS));
pipeline.addLast(new MyServerHandler());
}
});
//启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
//加入一个对空闲检测进一步处理的handler(自定义)
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
- 需要继承
ChannelInboundHandlerAdapter
- 因为
IdleStateHandler
发送心跳检测后会将事件交给下一个handler 的userEventTiggered
,所以要重写该方法自定义处理
package site.zhourui.nioAndNetty.netty.heartbeat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent){
//将 evt 向下转型 IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
String eventType =null;
switch (event.state()){
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
System.out.println("服务器做相应处理..");
//如果发生空闲,我们关闭通道
// ctx.channel().close();
}
}
}
此处我们没有写对应的客户端,我们之间使用11.3章节的客户端(只要端口一致就没问题)
为什么要使用WebSocket实现长链接?:
- Http协议是无状态的, 浏览器和服务 器间的请求响应一次,下一次会重 新创建连接.
实例要求:
- 实现基于webSocket的长连接 的全双工的交互
- 改变Http协议多次请求的约束,实 现长连接了, 服务器可以发送消息 给浏览器
- 客户端浏览器和服务器端会相互感 知,比如服务器关闭了,浏览器会 感知,同样浏览器关闭了,服务器 会感知
代码说明:
主要就是initChannel里的区别
HttpServerCodec
:因为基于http协议,使用http的编码和解码器
ChunkedWriteHandler
:是以块方式写,添加ChunkedWriteHandler处理器
HttpObjectAggregator
: http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
- 这就是为什么,当浏览器发送大量数据时,就会发出多次http请求
WebSocketServerProtocolHandler
:
WebSocketServerProtocolHandler
核心功能是将 http协议升级为 ws协议 , 保持长连接是通过一个 状态码 101
package site.zhourui.nioAndNetty.netty.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class MyServer {
public static void main(String[] args) throws InterruptedException {
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//因为基于http协议,使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//是以块方式写,添加ChunkedWriteHandler处理器
pipeline.addLast(new ChunkedWriteHandler());
/*
说明
1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
2. 这就就是为什么,当浏览器发送大量数据时,就会发出多次http请求
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/*
说明
1. 对应websocket ,它的数据是以 帧(frame) 形式传递
2. 可以看到WebSocketFrame 下面有六个子类
3. 浏览器请求时 ws://localhost:7000/hello 表示请求的uri
4. WebSocketServerProtocolHandler 核心功能是将 http协议升级为 ws协议 , 保持长连接
5. 是通过一个 状态码 101
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//自定义的handler ,处理业务逻辑
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
//启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
代码介绍:
- 因为我们发送的数据是文本所以使用
TextWebSocketFrame(文本帧)
package com.atguigu.netty.websocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.time.LocalDateTime;
//这里 TextWebSocketFrame 类型,表示一个文本帧(frame)
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("服务器收到消息 " + msg.text());
//回复消息
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text()));
}
//当web客户端连接后, 触发方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一
System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText());
System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText());
}
@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(); //关闭连接
}
}
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<script>
var socket;
//判断当前浏览器是否支持websocket
if(window.WebSocket) {
//go on
socket = new WebSocket("ws://localhost:8888/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>
开启服务端
浏览器打开客户端
因为asShortText只是asLongText的后面一小段所以LongText 是唯一的 ShortText 不是唯一
关闭服务端
客户端也能检测到服务端的状态
重新开启服务端端和客户端
刷新浏览器(模拟客户端先关闭再重新连接)
服务端也能检测到客户端的状态
客户端发送消息
http如何升级为websocket协议?