使用Netty实现最简单的聊天

Netty是对NIO的封装。虽然其使用比Java BIO(也就是传统的基于流或字符阻塞型的数据读写)也要复杂。不过效率高。没有那么多废话。本片的目标是使用Netty实现一个聊天功能。功能的完善、需要大家在亲自操刀。

服务器端

Netty服务器端的一般写法。

package com.example.gch;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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;

public class Server {
    public static void main(String[] args){
        Server server = new Server(8080);
    }
    private ServerSocketChannel serverSocketChannel;


    public Server(int serverPort) {
        bind(serverPort);
    }

    private void bind(final int serverPort) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                //服务端要建立两个group,一个负责接收客户端的连接,一个负责处理数据传输
                //连接处理group
                EventLoopGroup boss = new NioEventLoopGroup();
                //事件处理group
                EventLoopGroup worker = new NioEventLoopGroup();
                ServerBootstrap bootstrap = new ServerBootstrap();
                // 绑定处理group
                bootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
                        //保持连接数
                        .option(ChannelOption.SO_BACKLOG, 128)
                        //保持连接
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        //处理新连接
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                // 增加任务处理
                                ChannelPipeline p = socketChannel.pipeline();
//                                p.addLast(new SimpleServerHandler());
                                p.addLast(new ServerHandler());
                            }
                        })
                ;

                //绑定端口,同步等待成功
                ChannelFuture future;
                try {
                    future = bootstrap.bind(serverPort).sync();
                    if (future.isSuccess()) {
                        serverSocketChannel = (ServerSocketChannel) future.channel();
                        System.out.println("服务端开启成功");
                    } else {
                        System.out.println("服务端开启失败");
                    }

                    //等待服务监听端口关闭,就是由于这里会将线程阻塞,导致无法发送信息,所以我这里开了线程
                    future.channel().closeFuture().sync();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //优雅地退出,释放线程池资源
                    boss.shutdownGracefully();
                    worker.shutdownGracefully();
                }
            }
        });
        thread.start();
    }

    public void sendMessage(Object msg) {
        if (serverSocketChannel != null) {
            serverSocketChannel.writeAndFlush(msg);
        }
    }
}

使用NettyConfig 记录所有的客户端连接的Channel。

package com.example.gch;

import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * 存储整个工程的全局配置
 *
 * @author NewBies
 */
public class NettyConfig {

    /**
     * 存储每一个客户端接入进来时的channel对象
     */
    public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}

和客户端交互定义的地方(PS.需要进行着重开发)

在**channelRead**中有对单聊和多聊的不同发送方式
package com.example.gch;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Iterator;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 客户端与服务端创建连接的时候调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端与服务端连接开始...");
        NettyConfig.group.add(ctx.channel());
    }

    /**
     * 客户端与服务端断开连接时调用
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端与服务端连接关闭...");
        NettyConfig.group.remove(ctx.channel());
    }

    /**
     * 服务端接收客户端发送过来的数据结束之后调用
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
        System.out.println("信息接收完毕...");
    }

    /**
     * 工程出现异常的时候调用
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 服务端处理客户端websocket请求的核心方法,这里接收了客户端发来的信息
     */
    @Override
    public void channelRead(ChannelHandlerContext channelHandlerContext, Object info) throws Exception {
        ByteBuf result = (ByteBuf) info;
        byte[] result1 = new byte[result.readableBytes()];
        result.readBytes(result1);
        System.out.println("我是服务端,我接受到了:" + new String(result1));
        //服务端使用这个就能向 每个连接上来的客户端群发消息
//        NettyConfig.group.writeAndFlush(info);
        channelHandlerContext.writeAndFlush(info);

        Iterator<Channel> iterator = NettyConfig.group.iterator();
        while (iterator.hasNext()) {
            //打印出所有客户端的远程地址
            System.out.println((iterator.next()).remoteAddress());
        }
        try {
            Thread.sleep(3000);
            // 向客户端发送消息
            String response = "hello client";
            // 在当前场景下,发送的数据必须转换成ByteBuf数组
            ByteBuf encoded = channelHandlerContext.alloc().buffer(4 * response.length());
            encoded.writeBytes(response.getBytes());
            //单聊
//            channelHandlerContext.write(encoded);
//            channelHandlerContext.flush();
            //群聊
            NettyConfig.group.writeAndFlush(encoded);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

客户端应用

这是Netty客户端一般写法

package com.example.gch;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
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.NioSocketChannel;

public class Client {
    public static void main(String[] args) {
        Client bootstrap = new Client(8080, "localhost");
    }

    private int port;
    private String host;
    private SocketChannel socketChannel;

    public Client(int port, String host) {
        this.host = host;
        this.port = port;
        start();
    }

    private void start() {

        Thread thread = new Thread(new Runnable() {
            public void run() {
                EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.channel(NioSocketChannel.class)
                        // 保持连接
                        .option(ChannelOption.SO_KEEPALIVE, true)
                        // 有数据立即发送
                        .option(ChannelOption.TCP_NODELAY, true)
                        // 绑定处理group
                        .group(eventLoopGroup).remoteAddress(host, port)
                        .handler(new SimpleClientHandler());
                // 进行连接
                ChannelFuture future;
                try {
                    future = bootstrap.connect(host, port).sync();
                    // 判断是否连接成功
                    if (future.isSuccess()) {
                        // 得到管道,便于通信
                        socketChannel = (SocketChannel) future.channel();
                        System.out.println("客户端开启成功...");
                    } else {
                        System.out.println("客户端开启失败...");
                    }
                    // 等待客户端链路关闭,就是由于这里会将线程阻塞,导致无法发送信息,所以我这里开了线程
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //优雅地退出,释放相关资源
                    eventLoopGroup.shutdownGracefully();
                }
            }
        });

        thread.start();
    }

}

这里是客户端处理服务器连接的方法

package com.example.gch;


import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class SimpleClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("SimpleClientHandler.channelRead");
        ByteBuf result = (ByteBuf) msg;
        byte[] result1 = new byte[result.readableBytes()];
        result.readBytes(result1);
        System.out.println("Server said:" + new String(result1));
        result.release();
        //write to server.
//        try {
//            Thread.sleep(3000);
//            // 向客户端发送消息
//            String response = "hello server!";
//            // 在当前场景下,发送的数据必须转换成ByteBuf数组
//            ByteBuf encoded = ctx.alloc().buffer(4 * response.length());
//            encoded.writeBytes(response.getBytes());
//            ctx.write(encoded);
//            ctx.flush();
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 当出现异常就关闭连接
        cause.printStackTrace();
        ctx.close();
    }


    // 连接成功后,向server发送消息
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String msg = "hello Server!";
        ByteBuf encoded = ctx.alloc().buffer(4 * msg.length());
        encoded.writeBytes(msg.getBytes());
        ctx.write(encoded);
        ctx.flush();
    }
}

项目地址

https://github.com/guchuanhang/HttpLongDemo 服务器端&客户端

https://github.com/guchuanhang/HttpLongAndroidDemo 安卓客户端

欢迎大家fork&star!

对于标志用户谁是谁,可以在刚和服务器建立连接的时候,发送自己的身份信息。在服务器端进行user-channel映射。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Spring Boot可以很方便地整合Netty实现聊天功能。Netty是一个高性能的网络编程框架,可以用来实现各种网络应用,包括聊天室。 在Spring Boot中,可以使用Netty的ChannelHandler来处理客户端连接、消息接收和发送等操作。可以使用Spring Boot的WebSocket支持来实现浏览器与服务器之间的实时通信。 具体实现步骤如下: 1. 引入Netty和WebSocket的依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 创建Netty服务器 创建一个Netty服务器,监听指定的端口,处理客户端连接和消息接收等操作。可以使用Spring Boot的@Configuration注解和@Bean注解来创建Netty服务器。 ``` @Configuration public class NettyConfig { @Value("${netty.port}") private int port; @Autowired private ChannelInitializer<SocketChannel> channelInitializer; @Bean public EventLoopGroup bossGroup() { return new NioEventLoopGroup(); } @Bean public EventLoopGroup workerGroup() { return new NioEventLoopGroup(); } @Bean public ServerBootstrap serverBootstrap() { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup(), workerGroup()) .channel(NioServerSocketChannel.class) .childHandler(channelInitializer); return serverBootstrap; } @Bean public ChannelFuture channelFuture() throws InterruptedException { return serverBootstrap().bind(port).sync(); } } ``` 其中,@Value("${netty.port}")注解用来读取配置文件中的端口号,@Autowired注解用来注入ChannelInitializer,用于处理客户端连接和消息接收等操作。 3. 创建ChannelInitializer 创建一个ChannelInitializer,用于初始化Netty的ChannelPipeline,添加ChannelHandler来处理客户端连接和消息接收等操作。 ``` @Component public class ChatServerInitializer extends ChannelInitializer<SocketChannel> { @Autowired private ChatServerHandler chatServerHandler; @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new WebSocketServerProtocolHandler("/chat")); pipeline.addLast(chatServerHandler); } } ``` 其中,HttpServerCodec用于处理HTTP请求和响应,HttpObjectAggregator用于将HTTP请求和响应合并为一个完整的消息,WebSocketServerProtocolHandler用于处理WebSocket握手和消息传输,chatServerHandler用于处理客户端连接和消息接收等操作。 4. 创建ChannelHandler 创建一个ChannelHandler,用于处理客户端连接和消息接收等操作。 ``` @Component @ChannelHandler.Sharable public class ChatServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { channels.add(ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { channels.remove(ctx.channel()); } @Override protected void channelRead(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { String message = msg.text(); channels.writeAndFlush(new TextWebSocketFrame(message)); } } ``` 其中,@ChannelHandler.Sharable注解用于标记该ChannelHandler可以被多个Channel共享,channels用于保存所有连接的Channel,channelActive方法用于添加新的Channel,channelInactive方法用于移除已关闭的Channel,channelRead方法用于处理接收到的消息,将消息广播给所有连接的客户端。 5. 创建WebSocket客户端 在前端页面中创建WebSocket客户端,连接到Netty服务器,发送和接收消息。 ``` var socket = new WebSocket("ws://localhost:808/chat"); socket.onopen = function(event) { console.log("WebSocket connected."); }; socket.onmessage = function(event) { console.log("Received message: " + event.data); }; socket.onclose = function(event) { console.log("WebSocket closed."); }; function sendMessage() { var message = document.getElementById("message").value; socket.send(message); } ``` 其中,WebSocket连接的URL为ws://localhost:808/chat,onopen方法用于在连接建立时输出日志,onmessage方法用于在接收到消息时输出日志,onclose方法用于在连接关闭时输出日志,sendMessage方法用于发送消息。 6. 运行程序 运行Spring Boot程序,访问前端页面,即可实现聊天功能。 以上就是使用Spring Boot整合Netty实现聊天功能的步骤。 ### 回答2: Spring Boot是一个用来简化创建Spring应用程序的框架,而Netty是一个高性能的网络编程框架。将它们整合起来可以实现一个简单聊天应用程序。 首先,我们需要创建一个基于Spring Boot的项目。可以使用Spring Initializer工具来快速生成一个基本的Spring Boot项目骨架。在pom.xml文件中添加Netty的依赖,这样项目就可以使用Netty库。 接下来,创建一个Netty的服务器类,用于处理客户端的连接和消息。在这个类中,我们需要实现Netty的ChannelInboundHandlerAdapter接口来处理连接和消息的事件。在连接建立时,可以将连接信息保存到一个Map中,以便后续消息的转发。当接收到消息时,可以根据消息内容将消息转发给指定的客户端。 在Spring Boot的配置文件中,我们需要配置Netty服务器的监听端口和服务器线程池的大小等参数。可以使用Spring Boot的注解来标记需要进行自动配置的类和方法。 在Spring Boot的控制器中,我们可以添加一个用于接收用户发送消息的API接口。当这个接口被调用时,它会将用户发送消息转发给Netty服务器,然后由服务器进行处理和转发。 此外,可以根据实际需求添加一些其他的功能,比如用户身份验证、消息加解密等。可以在Netty服务器的代码中添加相应的逻辑来实现这些功能。 最后,我们可以使用Spring Boot的打包工具将项目打包成一个可执行的Jar文件。然后可以将这个Jar文件部署到服务器上,并启动它来运行这个聊天应用程序。 总之,通过将Spring Boot和Netty整合起来,我们可以快速构建一个简单聊天应用程序。Spring Boot提供了便捷的开发和配置方式,而Netty能够提供高性能的网络通信能力,这使得我们可以很容易地实现一个高效的聊天系统。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值