前言
在原生安卓开发中,socket通信虽然并不是特别常用,但是在Android智能设备比如说工控板或者是在户外工作场景里的某些需求方面,Socket通信仍然是非常重要的一个环节。
本文旨在通过一个简单的demo展示如何使用Netty来实现Android Socket通信的功能,文末附源码
同样的,先来看效果吧
上面呢,就是把socket的连接、通信、断开连接,再次连接等功能都测试了一遍,原谅我的英文,也基本上就是能够把会的单词组一遍,别挑我的语法错误就好 。
关于为什么全部发送的是英文,是因为这个工具我不知道如何解决编码不匹配的问题,我的代码里面全部设置UTF-8的编码,但是我尝试过发送中文,不管是接收还是发送,都是乱码,但是英文和数字是没问题的,我想这应该只是因为编码不匹配导致的,毕竟工具上没有相关的设置,所以我就忽略这个问题了,等大家实际使用的时候,这个问题应该很好解决。
正文
跟上一篇讲MQTT通信一样,我整理这部分内容的时候,如何找一个稳定好用的socket测试软件是一个最大的痛点,在网上找了一圈,发现别人评价的Sokit是一个不错的工具,然后我会把这个工具以及demo的源代码放在文末链接,大家可以根据需要获取。
接下来,正式开始本文的讲解
1.引用依赖
implementation 'io.netty:netty-all:4.1.68.Final'
2.申请网络权限
<uses-permission android:name="android.permission.INTERNET" />
3.布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_connect"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="建立连接" />
<Button
android:id="@+id/btn_disconnect"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="断开连接" />
<EditText
android:id="@+id/et_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入要发送的内容"
android:minHeight="60dp" />
<Button
android:id="@+id/btn_publish"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="发送消息"
android:textAllCaps="false" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<ScrollView
android:id="@+id/scroll_deal"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/tv_deal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="操作:\n"
android:textColor="@color/black" />
</ScrollView>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#dcdcdc" />
<ScrollView
android:id="@+id/scroll_record"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_weight="1">
<TextView
android:id="@+id/tv_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="记录:\n"
android:textColor="@color/black" />
</ScrollView>
</LinearLayout>
<Button
android:id="@+id/btn_clear"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="清空" />
</LinearLayout>
说明,本文中同样使用了viewBinding
build.gradle文件
android {
...
defaultConfig {
...
viewBinding{
enabled true
}
}
}
4.MainActivity
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.swy.socketdemo.databinding.ActivityMainBinding;
import java.text.SimpleDateFormat;
import java.util.Date;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private Channel channel;
private EventLoopGroup group;
private String socketIp = "192.168.79.236";
private int port = 8996;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.btnConnect.setOnClickListener(v -> {
connect();
});
binding.btnDisconnect.setOnClickListener(v -> {
disconnect();
});
binding.btnPublish.setOnClickListener(v -> {
String message = binding.etContent.getText().toString().trim();
if (null == message || message.equals("")) {
Toast.makeText(this, "输入内容不能为空", Toast.LENGTH_SHORT).show();
return;
}
sendMessage(message);
});
binding.btnClear.setOnClickListener(v -> {
binding.etContent.setText("");
binding.tvDeal.setText("操作:\n");
binding.tvRecord.setText("记录:\n");
});
}
private void connect() {
Thread clientThread = new Thread("client-Netty") {
@Override
public void run() {
super.run();
group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));//解码
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));//解码
ch.pipeline().addLast(new NettyClientHandler());
}
});
ChannelFuture future = bootstrap.connect(socketIp, port);
future.addListener((ChannelFutureListener) future1 -> {
if (future1.isSuccess()) {
appendDeal("连接成功");
channel = future1.channel();
} else {
appendDeal("连接失败");
Throwable cause = future1.cause();
cause.printStackTrace();
}
}).sync();
// 等待连接关闭
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
}
};
clientThread.start();
}
private void sendMessage(String message) {
ByteBuf byteBuf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
channel.writeAndFlush(byteBuf);
appendDeal("发送消息成功");
appendMessage(0, message);
}
private void disconnect() {
if (channel != null) {
channel.close();
}
if (group != null) {
group.shutdownGracefully();
}
appendDeal("断开连接");
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
appendDeal("接收消息成功");
appendMessage(1, (String) msg);
}
}
private void appendDeal(String msg) {
runOnUiThread(() -> {
binding.tvDeal.append(getCurrentTime() + " " + msg + "\n");
binding.scrollDeal.post(() -> binding.scrollDeal.fullScroll(View.FOCUS_DOWN));
binding.scrollRecord.post(() -> binding.scrollRecord.fullScroll(View.FOCUS_DOWN));
});
}
//0:发送
//1:接收
private void appendMessage(int type, String msg) {
runOnUiThread(() -> {
if (0 == type) {
binding.tvRecord.append("发送消息:" + msg + "\n");
} else {
binding.tvRecord.append("接收消息:" + msg + "\n");
}
binding.scrollDeal.post(() -> binding.scrollDeal.fullScroll(View.FOCUS_DOWN));
binding.scrollRecord.post(() -> binding.scrollRecord.fullScroll(View.FOCUS_DOWN));
});
}
private String getCurrentTime() {
Date currentTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss");
return sdf.format(currentTime);
}
@Override
protected void onDestroy() {
super.onDestroy();
disconnect();
}
}
写在最后
联调时一定要确保两个客户端在同一网段下,也就是确保IP地址可以访问,然后这个功能的测试最好是不要在AndroidStudio自带的模拟器上测试,因为我刚开始就是在自带的模拟器上测试的,老是报“io.netty.channel.ChannelException: Unable to create Channel from class class io.netty.channel.socket.nio.NioSocketChannel”这个错误,然后一通找原因,说是权限的问题,我看了网络权限申请的没问题,也不需要申请运行时权限,然后我就怀疑是不是模拟器的问题,我就用自己的手机测试,果然,错误就没有了。