编写代码
1.创建线程池
一般来说,我们会声明两个线程池,一个线程池用来处理Accept
事件,一个是用于处理消息的读写事件。
// 用来处理Accept事件
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 用来处理消息的读写时间
EventLoopGroup workerGroup = new NioEventLoopGroup();
一般我们只需要监听一个端口,所以bossGroup一般声明为1个线程,bossGroup接收到新的连接后会交给workerGroup处理,workerGroup负责监听这些连接的读写事件。
2.创建启动类
启动类有两个,客户端启动类Bootstrap
,服务端启动类ServerBootstrap
,启动类负责整个Nett 程序的正常运行。
以服务端为例
ServerBootstrap serverBootstrap = new ServerBootstrap();
3.设置线程池
把第一步声明的线程池设置到ServerBootstrap
中, bossGroup
负责监听Accept
连接,workerGroup
负责监听连接的读写数据。
serverBootstrap.group(bossGroup, workerGroup)
4.设置ServerSocketChannel
设置Netty程序以什么样的IO模型运行,以NioServerSocketChannel
为例
serverBootstrap.channel(NioServerSocketChannel.class);
如果程序运行在Linux
系统上,可以使用EpollServerSocketChannel
,它使用的是Linux
系统上的epoll
模型,比select
模型更高效。
这里提到的IO模型是指IO多路复用模型,一共有三个类型:select
、poll
、epoll
。
select 实现多路复用的方式是,将已连接的Socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
epoll 通过两个方面,很好解决了 select/poll 的问题。
第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
原文链接:https://blog.csdn.net/qq_34827674/article/details/115619261
5.设置可选参数
设置Netty中可以使用的参数,这些参数都在ChannelOption
及其子类中。
这里设置了一个SO_BACKLOG
系统参数,它表示的是最大等待连接数量。
serverBootstrap.option(ChannelOption.SO_BACKLOG, 100);
6.设置可选Handler
只能设置一个,它会在SocketChannel
建立起来之前执行,后面再分析它的执行时机。
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO))
7.编写并设置子Handler
Netty中的Handler
分成两种,一种叫做Inbound
,一种叫做Outbound
。
这里简单地写一个Inbound
类型的Handler
,它把接收到数据直接写回给客户端。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 读取数据后写回客户端
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
设置子Handler
设置的是SocketChannel
对应的Handler
,也是只能设置一个,它用于处理SocketChannel
的事件。
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 可以添加多个子Handler
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});
虽然只能设置一个子Handler
,但是Netty提供了一种可以设置多个Handler
的途径,即使用ChannelInitializer
方式,第六步设置 Handler
也可以使用这种方式设置多个Handler
。
这里,我们设置了一个打印日志的LoggingHandler
和一个自定义的EchoServerHandler
。
8.绑定端口
ChannelFuture f = serverBootstrap.bind(PORT).sync();
9.等待服务端端口关闭
等待服务端监听端口关闭,sync () 会阻塞主线程,内部调用的是 Object 的 wait () 方法。
f.channel().closeFuture().sync();
10.关闭线程池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
上面设置了ServerSocketChannel
的类型,而没有设置SocketChannel
的类型。
是因为SocketChannel
是ServerSocketChannel
在接受连接之后创建出来的,所以,并不需要单独再设置它的类型,比如,NioServerSocketChannel
创建出来的肯定是NioSocketChannel
,而EpollServerSocketChannel
创建出来的肯定是 EpollSocketChannel
。
完整代码
服务端代码
/**
* 1.打开命令行窗口: telnet localhost 8001
* 2.进入发送消息模式: Ctrl + ]
* 3.使用send命令发送消息: send hello
*/
public class EchoServer {
static final int PORT = 8001;
public static void main(String[] args) {
// 1. 声明线程池
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 2. 服务端引导器
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 3. 设置线程池
serverBootstrap.group(bossGroup, workerGroup)
// 4. 设置ServerSocketChannel的类型
.channel(NioServerSocketChannel.class)
// 5. 设置参数
.option(ChannelOption.SO_BACKLOG, 100)
// 6. 设置ServerSocketChannel对应的Handler,只能设置一个
.handler(new LoggingHandler(LogLevel.INFO))
// 7. 设置SocketChannel对应的Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 可以添加多个子Handler
// p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});
// 8. 绑定端口
ChannelFuture f = serverBootstrap.bind(PORT).sync();
// 9. 等待服务端监听端口关闭,这里会阻塞主线程
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 10. 优雅地关闭两个线程池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
EchoServerHandler
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 读取数据后写回客户端
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
客户端代码
public class NettyClient {
static final int PORT = 8001;
public static void main(String[] args) throws Exception {
// 工作线程池
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// pipeline.addLast(new LoggingHandler(LogLevel.INFO));
pipeline.addLast(new NettyClientHandler());
}
});
// 连接到服务端
ChannelFuture future = bootstrap.connect(new InetSocketAddress(PORT)).sync();
System.out.println("connect to server success");
// 调用后这里会阻塞
// future.channel().closeFuture().sync();
// 这里实现可以在控制台输入发送信息
Channel channel = future.channel();
Scanner scanner = new Scanner(System.in);
while (true) {
String msg = scanner.nextLine();
channel.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
if ("quit".equals(msg)) {
channel.close();
break;
}
}
} finally {
workerGroup.shutdownGracefully();
}
}
}
NettyClientHandler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(LocalDateTime.now().toString() + " " + ctx.channel() + " " + ((ByteBuf)msg).toString(CharsetUtil.UTF_8));
}
}
Netty实现群聊
NettyChatHolder
public class NettyChatHolder {
static final Map<SocketChannel, Long> userMap = new ConcurrentHashMap<>();
private static AtomicLong userIdList = new AtomicLong(10000);
static void join(SocketChannel socketChannel) {
// 有人加入就给他分配一个id
Long userId = userIdList.addAndGet(1);
send(socketChannel, "userId:" + userId);
for (SocketChannel channel : userMap.keySet()) {
send(channel, userId + " 加入了群聊");
}
// 将当前用户加入到map中
userMap.put(socketChannel, userId);
}
private static void send(SocketChannel socketChannel, String msg) {
try {
ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
ByteBuf writeBuffer = allocator.buffer(msg.getBytes().length);
writeBuffer.writeCharSequence(msg, Charset.defaultCharset());
socketChannel.writeAndFlush(writeBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
static void quit(SocketChannel socketChannel) {
Long userId = userMap.get(socketChannel);
send(socketChannel, "您退出了群聊");
userMap.remove(socketChannel);
for (SocketChannel channel : userMap.keySet()) {
if (channel != socketChannel) {
send(channel, userId + " 退出了群聊");
}
}
}
public static void propagate(SocketChannel socketChannel, String content) {
Long userId = userMap.get(socketChannel);
for (SocketChannel channel : userMap.keySet()) {
if (channel != socketChannel) {
send(channel, userId + ":" + content);
}
}
}
}
NettyChatHandler
public class NettyChatHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketChannel socketChannel = (SocketChannel)ctx.channel();
System.out.println("one conn active: " + socketChannel);
// socketChannel是在ServerBootstrapAcceptor中放到EventLoopGroup中的
NettyChatHolder.join(socketChannel);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
/***
* 如果这里有耗时操作的话, 可以自定义一个线程池来处理这下面的逻辑
*/
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String content = new String(bytes, StandardCharsets.UTF_8);
System.out.println(ctx.channel() + ":" + content);
if (content.equals("quit")) {
ctx.channel().close();
} else {
NettyChatHolder.propagate((SocketChannel) ctx.channel(), content);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
SocketChannel socketChannel = (SocketChannel)ctx.channel();
System.out.println("one conn inactive: " + socketChannel);
NettyChatHolder.quit((SocketChannel) ctx.channel());
}
}
NettyChatServer
public class NettyChatServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8001"));
public static void main(String[] args) {
// 1. 声明线程池
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 2. 服务端引导器
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 3. 设置线程池
serverBootstrap.group(bossGroup, workerGroup)
// 4. 设置ServerSocketChannel的类型
.channel(NioServerSocketChannel.class)
// 5. 设置参数
.option(ChannelOption.SO_BACKLOG, 100)
// 6. 设置ServerSocketChannel对应的Handler,只能设置一个
.handler(new LoggingHandler(LogLevel.INFO))
// 7. 设置SocketChannel对应的Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 可以添加多个子Handler
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new NettyChatHandler());
}
});
// 8. 绑定端口
ChannelFuture f = serverBootstrap.bind(PORT).sync();
// 9. 等待服务端监听端口关闭,这里会阻塞主线程
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 10. 优雅地关闭两个线程池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
Netty版本
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.63.Final</version>
</dependency>