在Android中使用Netty-Socket互相发送图片和文字
源码地址: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