项目源码地址:地址
这几天和做嵌入式的学长做了一个小项目,学长使用 Socket 来传输包含了各个传感器信息和摄像头图片的 json 字符串,我这边使用 Android 客户端显示和加载信息和图片。
这里使用的 Base64 编码来对图片进行编码,每一张图片都比较大,我们想实现高频率传输图片做成视频的效果。
- 使用 GsonFormat 创建 Bean 类:
先导入 GsonFormat 对 json 字符串的 Bean 类:File---Settings---Plugins,然后搜索 GsonFormat,进行安装,重启 AS:
然后创建 ImageBean.java 文件,在文件里使用默认快捷键 Alt+Insert,然后选择 GsonFormat:
然后输入 json 字符串输入文本框,点击确定,GsonFormat 就会将 ImageBean.java 文件完善:
- 使用 Socket 进行 TCP 的读取输入流和发送输出流,这个项目服务器需要客户端发送一个 byte 数组,来验证客户端身份:
try {
// 创建 Socket,需要传入两个参数,第一个是 IP 地址,第二个是端口号
// 这里我们的 IP 地址是本地服务器的,所有小伙伴们可以自己写一个简单的服务器程序
Socket socket = new Socket("192.168.1.105",8000);
// 获取输出流
OutputStream outputStream = socket.getOutputStream();
// 创建一个 OutputStreamWriter 来写输出流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
// 这里我们使用 BufferWriter 来写,更加方便
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
// 服务器需要验证客户端身份,客服端发送大小为 5 的 byte 数组
byte[] buff = new byte[5];
// 服务器规定 buff[3]=0x20,buff[4]='\0'
buff[3] = 0x20;
buff[4] = '\0';
// 写入数据
bufferedWriter.write(new String(buff));
bufferedWriter.flush();
// 获取输入流,和输出流一样,我们也要使用 InputStreamReader 和 BufferedReader
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 服务器端会在每一个 json 字符串完成后发送一个 \t,所有我们使用 readLine()
String line = null;
while ((line = bufferedReader.readLine())!=null){
// 这里我们先打印获取的 json 字符串
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
我们把它放到一个 Java 文件中测试一下,打印结构如下:
- 导入 RxJava 的依赖库和 Gson 的依赖库:
RxJava:compile 'io.reactivex:rxjava:1.3.8'
Gson:compile 'com.google.code.gson:gson:2.8.5'
- 使用 RxJava 和 Gson 对获取得到的 json 字符串进行操作,我们将上面的代码放到
Observable.unsafeCreate(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
// 调用其 onStart() 方法,初始化数据或者做一些准备
subscriber.onStart();
try {
// 创建 Socket,需要传入两个参数,第一个是 IP 地址,第二个是端口号
// 这里我们的 IP 地址是本地服务器的,所有小伙伴们可以自己写一个简单的服务器程序
Socket socket = new Socket("192.168.1.105", 8000);
...
String line = null;
while ((line = bufferedReader.readLine())!=null){
// 这里我们先打印获取的 json 字符串
System.out.println(line);
// 调用 onNext 方法,当被观察者状态变化(即发送数据)
// 则进行事件操作
subscriber.onNext(line);
}
// 调用 onCompleted() 方法
subscriber.onCompleted();
}catch (IOException e){
e.printStackTrace();
}
}
})
/*
RxJava 支持线程调度,能将操作切换到其它线程
Schedulers.immediate():当前线程
Schedulers.newThread():新线程
Schedulers.io() :I/O 操作的线程(线程无限的内部线程池)
Schedulers.computation() :计算线程(大小为 CPU 数的内部线程池)
AndroidSchedulers.mainThread():Android 的主线程
*/
// 这里将被观察者(即 Socket 通信)放到子线程
.subscribeOn(Schedulers.io())
// 将观察者(即响应事件)切换到计算线程
.observeOn(Schedulers.computation())
/*
RxJava 支持类型转换
就是将事件序列中的对象或整个序列进行加工处理,转换成不同的事件或事件序列
可以使用 map() 和 flatMap() 进行类型转换
感兴趣的同学可以去看看
*/
// 因为 onNext() 进行事件响应时是传入的 String 类型的 json 字符串
// map() 方法实现了一个 Funcl 的匿名内部类
// 其构造方法有两个参数,第一个参数代表要转换的,第二个代表转换成的
// 所以这里我们先将 String 转换成 ImageBean 类型
.map(new Func1<String, ImageBean>() {
@Override
public ImageBean call(String s) {
// 使用 Gson 将 json 字符串转换为 Bean 的对象
ImageBean bean = gson.fromJson(s,ImageBean.class);
return bean;
}
})
// 将观察者切换到 Android 主线程
.observeOn(AndroidSchedulers.mainThread())
.map(new Func1<ImageBean, ImageBean>() {
@Override
public ImageBean call(ImageBean imageBean) {
// 因为我们这里主要做一些图片解析
// 所有如果需要我们可以在这里对 Json 字符串中基本类型进行操作
return imageBean;
}
})
.observeOn(Schedulers.computation())
// 因为我们的 Json 字符串比较复杂,摄像头图片的数据在
// ImageBean 的内部类 UsbCamDataBean 中,所以我们先进行类型转换
.map(new Func1<ImageBean, ImageBean.UsbCamDataBean>() {
@Override
public ImageBean.UsbCamDataBean call(ImageBean imageBean) {
// 因为我们可能不止一个摄像头,所以用集合保存的
// get(0) 获取第一个摄像头的数据,小伙伴们不用管
return imageBean.getUsb_cam_data().get(0);
}
})
// 我们的图片使用的是 Base64 编码,所以这里先将它的数据转换成 byte 数组
.map(new Func1<ImageBean.UsbCamDataBean, byte[]>() {
@Override
public byte[] call(ImageBean.UsbCamDataBean usbCamDataBean) {
byte[] data = Base64.decode(usbCamDataBean.getData(),Base64.DEFAULT);
return data;
}
})
// 再将其转换为 Bitmap
.map(new Func1<byte[], Bitmap>() {
@Override
public Bitmap call(byte[] bytes) {
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.length);
return bitmap;
}
})
// 然后在 Android 主线程进行事件订阅
// 完成其 onStart()、onCompleted()、onError()、onNext() 方法的实现
// 这四个方法不用多讲,大家看名字应该就知道是干嘛的
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<Bitmap>() {
@Override
public void onStart() {
super.onStart();
Toast.makeText(MainActivity.this,"解析開始",Toast.LENGTH_SHORT).show();
}
@Override
public void onCompleted() {
Toast.makeText(MainActivity.this,"解析結束",Toast.LENGTH_SHORT).show();
}
@Override
public void onError(Throwable e) {
// 进行发生错误后的操作
// 比如判断异常类型进行相关操作
}
@Override
public void onNext(Bitmap bitmap) {
// 加载图片
imageView.setImageBitmap(bitmap);
}
});
现在按理来说整个项目是能够完美运行的,但是我们的 Json 接受发生了错误。
服务端是学长用 C 语言写的,为了达到效果,每秒可能会传送几十上百条 Json 字符串,而服务端的 Json 字符串破解和发送也是单独线程,并且有锁。现在有一个问题就是,我们通过 debug 发现,客户端接收的 Json 字符串会出现重复和半截,大概如图中的情况:
我们降低了服务端的发送频率,也将客户端接收频率降低,还是会出现这种情况,现在我们还没有解决这个问题,也想各位小伙伴能够指导指导。
。。。
后面这个问题解决了,开始的时候 C 语言那边从客户端往服务器发送数据时是直接读取的缓存区,有多少就发送多少。可能是当频率太快时缓存区还没有刷新,所以导致的这个问题。
后面通过发送一个长度,当从客户端发送至服务器时,先发送一个长度,只从缓存区读取该长度的字符发送,再经过优化,后面发生错误的几率已经非常小了,然后移动端再捕获错误 json 无法解析导致的 Bean 为空的异常,因为频率比较快,这样程序不会崩溃而且也看不出来异常。
。。。
项目的源代码在最上面,虽然最新版还没有 push 上去,但是主要的功能实现和注解都有,大家有兴趣可以看一看,这里放一张效果图(那个,我的 GIF 转换工具不能给它旋转,在电脑里面一编辑就不是 GIF 图了,大家将就看一下):