需要源码和服务端代码请点赞关注收藏后评论区留下QQ~~~
一、通过SocketIO传输文本消息
虽然HTTP协议能够满足多数常见的接口交互,但是他属于短连接,每次调用完就自动断开连接,并且HTTP协议区分了服务端和客户端,双方的通信过程是单向的,只有客户端可以请求服务端,服务端无法主动向客户端推送信息,所以它不适合点对点的即时通信功能
即时通信技术需要满足两方面的要求。一是长连接,以便在两台设备之间持续通信,避免频繁的连接断开操作,这样非常浪费资源。二是支持双向交流,既允许A设备主动向B设备发送消息,又允许B设备主动向A设备发送消息。
可是Java 的Socket编程比较繁琐,不仅要自行编写线程通信与IO处理的代码,还要自己定义数据包的内部格式以及解编码,为此出现了第三方的Socket通信框架SocketIO,该框架提供了服务端和客户端的依赖包,大大简化了Socket通信的开发工作量
客户端集成SocketIO添加下面一行依赖即可
implementation 'io.socket:socket.io-client:1.0.0'
接着使用SocketIO提供的Socket工具完成消息的收发操作 常用方法如下
connect 建立Socket连接
connected 判断是否连接上Socket
emit 向服务器提交指定事件消息
on 开始监听服务端推送的事件消息
off 取消监听消息
disconnect 断开Socket连接
close 关闭Socket连接
双向Socket流程如下
图中实线表示代码的调用顺序,虚线表示异步的事件触发
然后确保服务端的SocketServer正在允许(IDEA)环境
服务端会做出应答
代码如下
Java类
package com.example.network; import android.os.Bundle; import android.text.TextUtils; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.example.network.constant.NetConst; import com.example.network.util.DateUtil; import com.example.network.util.SocketUtil; import java.net.URISyntaxException; import io.socket.client.IO; import io.socket.client.Socket; public class SocketioTextActivity extends AppCompatActivity { private static final String TAG = "SocketioTextActivity"; private EditText et_input; // 声明一个编辑框对象 private TextView tv_response; // 声明一个文本视图对象 private Socket mSocket; // 声明一个套接字对象 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_socketio_text); et_input = findViewById(R.id.et_input); tv_response = findViewById(R.id.tv_response); findViewById(R.id.btn_send).setOnClickListener(v -> { String content = et_input.getText().toString(); if (TextUtils.isEmpty(content)) { Toast.makeText(this, "请输入聊天消息", Toast.LENGTH_SHORT).show(); return; } mSocket.emit("send_text", content); // 往Socket服务器发送文本消息 }); initSocket(); // 初始化套接字 } // 初始化套接字 private void initSocket() { // 检查能否连上Socket服务器 SocketUtil.checkSocketAvailable(this, NetConst.BASE_IP, NetConst.BASE_PORT); try { String uri = String.format("http://%s:%d/", NetConst.BASE_IP, NetConst.BASE_PORT); mSocket = IO.socket(uri); // 创建指定地址和端口的套接字实例 } catch (URISyntaxException e) { throw new RuntimeException(e); } mSocket.connect(); // 建立Socket连接 // 等待接收传来的文本消息 mSocket.on("receive_text", (args) -> { String desc = String.format("%s 收到服务端消息:%s", DateUtil.getNowTime(), (String) args[0]); runOnUiThread(() -> tv_response.setText(desc)); }); } @Override protected void onDestroy() { super.onDestroy(); mSocket.off("receive_text"); // 取消接收传来的文本消息 if (mSocket.connected()) { // 已经连上Socket服务器 mSocket.disconnect(); // 断开Socket连接 } mSocket.close(); // 关闭Socket连接 } }
XML文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp" > <EditText android:id="@+id/et_input" android:layout_width="match_parent" android:layout_height="40dp" android:background="@drawable/editext_selector" android:hint="请输入聊天内容" android:textColor="@color/black" android:textSize="17sp" /> <Button android:id="@+id/btn_send" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="发送文本消息" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_response" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/black" android:textSize="17sp" /> </LinearLayout>
二、通过SocketIO传输图片消息
倘若让SocketIO实时传输图片比较困难,因为SocketIO不支持直接传输二进制数据,使得位图对象的字节数据无法作为emit方法的输入参数。所以可以考虑利用JSON对象封装图像信息,把图像的字节数据编码成字符串保存起来
鉴于JSON格式允许容纳多个 字段,同时图片有可能很大,因此建议将图片拆开分段运输,每段标明本次的分段序号、分段长度以及分段数据,由接收方在收到后重新拼接成完整的图像
效果如下
代码如下
Java类
package com.example.network; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.util.Base64; import android.util.Log; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.example.network.bean.ImagePart; import com.example.network.constant.NetConst; import com.example.network.util.BitmapUtil; import com.example.network.util.DateUtil; import com.example.network.util.SocketUtil; import com.google.gson.Gson; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.net.URISyntaxException; import io.socket.client.IO; import io.socket.client.Socket; public class SocketioImageActivity extends AppCompatActivity { private static final String TAG = "SocketioImageActivity"; private ImageView iv_request; // 声明一个图像视图对象 private ImageView iv_response; // 声明一个图像视图对象 private TextView tv_response; // 声明一个文本视图对象 private int CHOOSE_CODE = 3; // 只在相册挑选图片的请求码 private String mFileName; // 图片名称 private Bitmap mBitmap; // 位图对象 private Socket mSocket; // 声明一个套接字对象 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_socketio_image); iv_request = findViewById(R.id.iv_request); iv_response = findViewById(R.id.iv_response); tv_response = findViewById(R.id.tv_response); findViewById(R.id.btn_choose).setOnClickListener(v -> { // 创建一个内容获取动作的意图(准备跳到系统相册) Intent albumIntent = new Intent(Intent.ACTION_GET_CONTENT); albumIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否允许多选 albumIntent.setType("image/*"); // 类型为图像 startActivityForResult(albumIntent, CHOOSE_CODE); // 打开系统相册 }); findViewById(R.id.btn_send).setOnClickListener(v -> { if (mBitmap == null) { Toast.makeText(this, "请先选择图片文件", Toast.LENGTH_SHORT).show(); return; } sendImage(); // 分段传输图片数据 }); initSocket(); // 初始化套接字 } // 初始化套接字 private void initSocket() { // 检查能否连上Socket服务器 SocketUtil.checkSocketAvailable(this, NetConst.BASE_IP, NetConst.BASE_PORT); try { String uri = String.format("http://%s:%d/", NetConst.BASE_IP, NetConst.BASE_PORT); mSocket = IO.socket(uri); // 创建指定地址和端口的套接字实例 } catch (URISyntaxException e) { throw new RuntimeException(e); } mSocket.connect(); // 建立Socket连接 // 等待接收传来的图片数据 mSocket.on("receive_image", (args) -> receiveImage(args)); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK && requestCode == CHOOSE_CODE) { // 从相册返回 if (intent.getData() != null) { // 从相册选择一张照片 Uri uri = intent.getData(); // 获得已选择照片的路径对象 String path = uri.toString(); mFileName = path.substring(path.lastIndexOf("/")+1); // 根据指定图片的uri,获得自动缩小后的位图对象 mBitmap = BitmapUtil.getAutoZoomImage(this, uri); iv_request.setImageBitmap(mBitmap); // 设置图像视图的位图对象 } } } private int mBlock = 50*1024; // 每段的数据包大小 // 分段传输图片数据 private void sendImage() { Log.d(TAG, "sendImage"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 把位图数据压缩到字节数组输出流 mBitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); byte[] bytes = baos.toByteArray(); int count = bytes.length/mBlock + 1; Log.d(TAG, "sendImage length="+bytes.length+", count="+count); // 下面把图片数据经过BASE64编码后发给Socket服务器 for (int i=0; i<count; i++) { Log.d(TAG, "sendImage i="+i); String encodeData = ""; if (i == count-1) { // 是最后一段图像数据 int remain = bytes.length % mBlock; byte[] temp = new byte[remain]; System.arraycopy(bytes, i*mBlock, temp, 0, remain); encodeData = Base64.encodeToString(temp, Base64.DEFAULT); } else { // 不是最后一段图像数据 byte[] temp = new byte[mBlock]; System.arraycopy(bytes, i*mBlock, temp, 0, mBlock); encodeData = Base64.encodeToString(temp, Base64.DEFAULT); } // 往Socket服务器发送本段的图片数据 ImagePart part = new ImagePart(mFileName, encodeData, i, bytes.length); SocketUtil.emit(mSocket, "send_image", part); // 向服务器提交图像数据 } } private String mLastFile; // 上次的文件名 private int mReceiveCount; // 接收包的数量 private byte[] mReceiveData; // 收到的字节数组 // 接收对方传来的图片数据 private void receiveImage(Object... args) { JSONObject json = (JSONObject) args[0]; ImagePart part = new Gson().fromJson(json.toString(), ImagePart.class); if (!part.getName().equals(mLastFile)) { // 与上次文件名不同,表示开始接收新文件 mLastFile = part.getName(); mReceiveCount = 0; mReceiveData = new byte[part.getLength()]; } mReceiveCount++; // 把接收到的图片数据通过BASE64解码为字节数组 byte[] temp = Base64.decode(part.getData(), Base64.DEFAULT); System.arraycopy(temp, 0, mReceiveData, part.getSeq()*mBlock, temp.length); // 所有数据包都接收完毕 if (mReceiveCount >= part.getLength()/mBlock+1) { // 从字节数组中解码得到位图对象 Bitmap bitmap = BitmapFactory.decodeByteArray(mReceiveData, 0, mReceiveData.length); String desc = String.format("%s 收到服务端消息:%s", DateUtil.getNowTime(), part.getName()); runOnUiThread(() -> { // 回到主线程展示图片与描述文字 tv_response.setText(desc); iv_response.setImageBitmap(bitmap); }); } } @Override protected void onDestroy() { super.onDestroy(); mSocket.off("receive_image"); // 取消接收传来的图片数据 if (mSocket.connected()) { // 已经连上Socket服务器 mSocket.disconnect(); // 断开Socket连接 } mSocket.close(); // 关闭Socket连接 } }
XML文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn_choose" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="请选择待发送的图片" android:textColor="@color/black" android:textSize="17sp" /> <ImageView android:id="@+id/iv_request" android:layout_width="match_parent" android:layout_height="180dp" /> <Button android:id="@+id/btn_send" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="发送图片消息" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_response" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="5dp" android:textColor="@color/black" android:textSize="17sp" /> <ImageView android:id="@+id/iv_response" android:layout_width="match_parent" android:layout_height="180dp" /> </LinearLayout>
创作不易 觉得有帮助请点赞关注收藏~~~