在Android中使用Netty-Socket互相发送图片和文字

  |   0 评论   |   0 浏览   |   夜雨飘零

源码地址:https://github.com/yeyupiaoling/NettySendPhoto (里面包含了两个 APP,使用 Android Studio 选择两个应用安装在不同的手机上)

服务端

首先是服务端,服务端的应用在 server 下,其中最重要的是 NettyServerUtil.java,这里包含了服务的启动和发送数据,这数据包括文字和图像。

这段代码是启动 Netty 服务的,其中非常重要的是 ch.pipeline().addLast(new ByteArrayEncoder());ch.pipeline().addLast(new ByteArrayDecoder());,因为我们要传输的都是基于 byte[] 的,同时还要 LineBasedFrameDecoder 设置最大包的长度。

    public void start() {
        new Thread() {
            @Override
            public void run() {
                super.run();
                bossGroup = new NioEventLoopGroup(1);
                workerGroup = new NioEventLoopGroup();
                try {
                    ServerBootstrap b = new ServerBootstrap();
                    b.group(bossGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)
                            .localAddress(new InetSocketAddress(Const.TCP_PORT))
                            .childOption(ChannelOption.SO_KEEPALIVE, true)
                            .childOption(ChannelOption.SO_REUSEADDR, true)
                            .childOption(ChannelOption.TCP_NODELAY, true)
                            .childHandler(new ChannelInitializer<SocketChannel>() {
                                @Override
                                public void initChannel(SocketChannel ch) throws Exception {
                                    if (!TextUtils.isEmpty(packetSeparator)) {
                                        ByteBuf delimiter = Unpooled.buffer();
                                        delimiter.writeBytes(packetSeparator.getBytes());
                                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(Const.MAX_PACKET_LONG, delimiter));
                                    } else {
                                        ch.pipeline().addLast(new LineBasedFrameDecoder(Const.MAX_PACKET_LONG));
                                    }
                                    ch.pipeline().addLast(new ByteArrayEncoder());
                                    ch.pipeline().addLast(new ByteArrayDecoder());

                                    ch.pipeline().addLast(new ServerHandler(listener));
                                }
                            });

                    // Bind and start to accept incoming connections.
                    ChannelFuture f = b.bind().sync();
                    Log.e(TAG, " started and listen on " + f.channel().localAddress());
                    listener.onStartServer();
                    f.channel().closeFuture().sync();
                } catch (Exception e) {
                    Log.e(TAG, e.getLocalizedMessage());
                    e.printStackTrace();
                } finally {
                    listener.onStopServer();
                    workerGroup.shutdownGracefully();
                    bossGroup.shutdownGracefully();
                }
            }
        }.start();

    }

这个是发送文本消息,每个的文本消息都要加上包分割符,否则会出现粘包的情况。

    public void sendTextToClient(String msg, MessageStateListener listener) {
        boolean flag = channel != null && channel.isActive();
        // 要加上分割符
        msg = msg + Const.PACKET_SEPARATOR;
        byte[] data = msg.getBytes(StandardCharsets.UTF_8);

        ByteBuf buf = Unpooled.copiedBuffer(data);
        if (flag) {
            channel.writeAndFlush(buf).addListener((ChannelFutureListener) channelFuture -> {
                listener.isSendSuccess(channelFuture.isSuccess());
            });
        }
    }

这里是发送图像数据的,我们把图像转换成 Base64 字符串,使用 Base64 字符串作为数据发送。在发送图像之前,我们要告诉客户端我们下面要发送图像数据,并说明数据大小,最后发送图像数据。

    public void sendPhotoToClient(byte[] data, MessageStateListener listener) {
        boolean flag = channel != null && channel.isActive();
        if (flag) {
            String base64 = Base64.encodeToString(data, Base64.DEFAULT).replace("\n", "");
            String m = base64 + Const.PACKET_SEPARATOR;
            byte[] bb = m.getBytes(StandardCharsets.UTF_8);

            // 首先发送通知客户端接下来发生的是图片数据,并告诉大小
            String msg = "Photo_Size:" + base64.length() + Const.PACKET_SEPARATOR;
            byte[] d = msg.getBytes(StandardCharsets.UTF_8);
            ByteBuf b1 = Unpooled.copiedBuffer(d);
            channel.writeAndFlush(b1).awaitUninterruptibly();

            // 发送图片数据
            ByteBuf buf = Unpooled.copiedBuffer(bb);
            channel.writeAndFlush(buf).awaitUninterruptibly();

            listener.isSendSuccess(true);
            return;
        }
        listener.isSendSuccess(false);
    }

接收数据在 ServerHandler.java,因为客户端会一直发送心跳数据,我们要忽略这些心跳数据。然后判断是否为通知发送图像数据,不是图像数据就直接把数据通知给 Activity,否则就根据图像的数据大小,一直接收数据,直到数据跟通知的一样就解码 Base64,转成 byte[],最后发送给 Activity。

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, byte[] data) throws Exception {
        String msg = new String(data, StandardCharsets.UTF_8);
        // 客户端发送来的心跳数据
        if (msg.equals(Const.HEART_BEAT_DATA)) {
            return;
        }

        // 匹配获取图片大小
        Pattern pattern = Pattern.compile(Const.PHOTO_SIZE_TEMPLATE);
        Matcher matcher = pattern.matcher(msg);
        // 判断是否能获取图片大小
        if (matcher.find()) {
            photoSize = Integer.parseInt(matcher.group(1));
            isPhoto = true;
            return;
        }

        // 判断本次数据是否为图片数据
        if (isPhoto) {
            // 记录图片数据
            stringBuffer.append(msg);
            // 当记录的图片书等于指定大小就结束接收图片数据
            if (stringBuffer.length() == photoSize) {
                byte[] photo =  Base64.decode(stringBuffer.toString(), Base64.DEFAULT);
                mListener.onPhotoMessage(photo, ctx.channel().id().asShortText());
                photoSize = 0;
                isPhoto = false;
                // 清空数据
                stringBuffer.delete(0, stringBuffer.length());
            }
        } else {
            mListener.onTextMessage(data, ctx.channel().id().asShortText());
        }
    }

客户端

客户端的应用在 app 下,其中最重要的是 NettyClientUtil.java,这里包含了服务的启动和发送数据,这数据包括文字和图像。

其中使用 IdleStateHandler() 发送心跳数据,保证一直连接。同时还设置 ch.pipeline().addLast(new ByteArrayEncoder());ch.pipeline().addLast(new ByteArrayDecoder());,因为我们要传输的都是基于 byte[] 的,同时还要 LineBasedFrameDecoder 设置最大包的长度。

    private void connectServer() {
        synchronized (NettyClientUtil.this) {
            ChannelFuture channelFuture = null;
            if (!isConnect) {
                isConnecting = true;
                group = new NioEventLoopGroup();
                Bootstrap bootstrap = new Bootstrap().group(group)
                        .option(ChannelOption.TCP_NODELAY, true)  //屏蔽Nagle算法试图
                        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                        .channel(NioSocketChannel.class)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel ch) throws Exception {
                                if (isSendHeartBeat) {
                                    // 5s未发送数据,回调userEventTriggered
                                    ch.pipeline().addLast("ping", new IdleStateHandler(0, heartBeatInterval, 0, TimeUnit.SECONDS));
                                }

                                //黏包处理,需要客户端、服务端配合
                                if (!TextUtils.isEmpty(packetSeparator)) {
                                    ByteBuf delimiter = Unpooled.buffer();
                                    delimiter.writeBytes(packetSeparator.getBytes());
                                    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(maxPacketLong, delimiter));
                                } else {
                                    ch.pipeline().addLast(new LineBasedFrameDecoder(maxPacketLong));
                                }
                                ch.pipeline().addLast(new ByteArrayEncoder());
                                ch.pipeline().addLast(new ByteArrayDecoder());

                                ch.pipeline().addLast(new ClientHandler(listener, mIndex, isSendHeartBeat, heartBeatData, packetSeparator));
                            }
                        });

                try {
                    channelFuture = bootstrap.connect(host, tcpPort).addListener((ChannelFutureListener) channelFuture1 -> {
                        if (channelFuture1.isSuccess()) {
                            Log.e(TAG, "连接成功");
                            reconnectNum = MAX_CONNECT_TIMES;
                            isConnect = true;
                            channel = channelFuture1.channel();
                        } else {
                            Log.e(TAG, "连接失败");
                            isConnect = false;
                        }
                        isConnecting = false;
                    }).sync();

                    // Wait until the connection is closed.
                    channelFuture.channel().closeFuture().sync();
                    Log.e(TAG, " 断开连接");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    isConnect = false;
                    listener.onClientStatusConnectChanged(Const.STATUS_CONNECT_CLOSED, mIndex);
                    if (null != channelFuture) {
                        if (channelFuture.channel() != null && channelFuture.channel().isOpen()) {
                            channelFuture.channel().close();
                        }
                    }
                    group.shutdownGracefully();
                    reconnect();
                }
            }
        }
    }

这个是发送文本消息,每个的文本消息都要加上包分割符,否则会出现粘包的情况。

    public void sendTextToServer(String msg, MessageStateListener listener) {
        boolean flag = channel != null && channel.isActive();
        // 要加上分割符
        msg = msg + Const.PACKET_SEPARATOR;
        byte[] data = msg.getBytes(StandardCharsets.UTF_8);

        ByteBuf buf = Unpooled.copiedBuffer(data);
        if (flag) {
            channel.writeAndFlush(buf).addListener((ChannelFutureListener) channelFuture -> {
                listener.isSendSuccess(channelFuture.isSuccess());
            });
        }
    }

这里是发送图像数据的,我们把图像转换成 Base64 字符串,使用 Base64 字符串作为数据发送。在发送图像之前,我们要告诉客户端我们下面要发送图像数据,并说明数据大小,最后发送图像数据。

    public void sendPhotoToServer(byte[] data, MessageStateListener listener) {
        boolean flag = channel != null && channel.isActive();
        if (flag) {
            // 将图像打包成Base64的字符串
            String base64 = Base64.encodeToString(data, Base64.DEFAULT).replace("\n", "");
            String m = base64 + Const.PACKET_SEPARATOR;
            byte[] bb = m.getBytes(StandardCharsets.UTF_8);

            // 首先发送通知客户端接下来发生的是图片数据,并告诉大小
            String msg = "Photo_Size:" + base64.length() + Const.PACKET_SEPARATOR;
            byte[] d = msg.getBytes(StandardCharsets.UTF_8);
            ByteBuf b1 = Unpooled.copiedBuffer(d);
            channel.writeAndFlush(b1).awaitUninterruptibly();

            // 发送图片数据
            ByteBuf buf = Unpooled.copiedBuffer(bb);
            channel.writeAndFlush(buf).awaitUninterruptibly();

            listener.isSendSuccess(true);
            return;
        }
        listener.isSendSuccess(false);
    }

接收数据在 ClientHandler.java,首先判断是否为通知发送图像数据,不是图像数据就直接把数据通知给 Activity,否则就根据图像的数据大小,一直接收数据,直到数据跟通知的一样就解码 Base64,转成 byte[],最后发送给 Activity。

    protected void channelRead0(ChannelHandlerContext channelHandlerContext, byte[] data) {
        // 将数据转成字符串
        String msg = new String(data, StandardCharsets.UTF_8);

        // 匹配获取图片大小
        Pattern pattern = Pattern.compile(Const.PHOTO_SIZE_TEMPLATE);
        Matcher matcher = pattern.matcher(msg);
        // 判断是否能获取图片大小
        if (matcher.find()) {
            photoSize = Integer.parseInt(matcher.group(1));
            isPhoto = true;
            return;
        }
        // 判断本次数据是否为图片数据
        if (isPhoto) {
            // 记录图片数据
            stringBuffer.append(msg);
            // 当记录的图片书等于指定大小就结束接收图片数据
            if (stringBuffer.length() == photoSize) {
                byte[] photo = Base64.decode(stringBuffer.toString(), Base64.DEFAULT);
                listener.onPhotoMessage(photo, index);
                photoSize = 0;
                isPhoto = false;
                // 清空数据
                stringBuffer.delete(0, stringBuffer.length());
            }
        } else {
            listener.onTextMessage(data, index);
        }
    }

标题:在Android中使用Netty-Socket互相发送图片和文字
作者:夜雨飘零
地址:https://blog.doiduoyi.com/articles/1609047128110.html

评论

发表评论