Android使用socket進(jìn)行二進(jìn)制流數(shù)據(jù)傳輸
引言
使用socket流傳輸二進(jìn)制流數(shù)據(jù),比如文件或者視頻圖片等等信息的時(shí)候,我們通常使用tcp協(xié)議傳輸,因?yàn)閠cp協(xié)議可以保證二進(jìn)制流按序到達(dá),并且保證交付,這樣子就可以保證我們傳輸二進(jìn)制流的完整性。
使用tcp協(xié)議進(jìn)行二進(jìn)制流傳輸?shù)臅r(shí)候通常會有兩個(gè)問題:
由于tcp進(jìn)行信息傳輸?shù)臅r(shí)候是沒有邊界的,所以可能會產(chǎn)生粘包半包問題。所謂粘包就是指接收的一段數(shù)據(jù)包含了下一段數(shù)據(jù)的信息,所謂半包就是指一段數(shù)據(jù)沒有接收完整,實(shí)際上都是邊界不明確產(chǎn)生的問題。
并且在傳輸一段很大的二進(jìn)制流數(shù)據(jù)的時(shí)候,我們可能需要對超大的二進(jìn)制流分段處理,也就是分段來傳輸。
在輸出端將二進(jìn)制流分段,輸入端在接收到各個(gè)片段后再將整個(gè)流信息拼接起來,就構(gòu)成了完整的傳輸流程。
為了解決上述的兩個(gè)問題,同時(shí)也為了能夠統(tǒng)一這個(gè)傳輸流程,我們需要自定義一個(gè)簡單的傳輸協(xié)議。
簡單的自定義協(xié)議
我們自定義一個(gè)簡單的通信協(xié)議,協(xié)議一共傳輸兩種信息,第一種是文字,第二種是二進(jìn)制流(其實(shí)文字也可以用二進(jìn)制流表示),傳輸過程如下圖所示。

我們定義的簡單通信協(xié)議規(guī)則如下
1.首先發(fā)送一個(gè)字節(jié)的信息(就是圖中的type),表示一段消息的開始,同時(shí)也表明了后面二進(jìn)制數(shù)據(jù)的類型(文字信息還是二進(jìn)制流數(shù)據(jù))
2.每一個(gè)chunk都由三個(gè)字節(jié)的長度信息和相應(yīng)的二進(jìn)制流信息組成,接收方在接收到三個(gè)字節(jié)的長度信息后,繼續(xù)使用相應(yīng)大小的緩沖區(qū)接收后面的流數(shù)據(jù)
3.當(dāng)接收到三個(gè)字節(jié)的000的時(shí)候表示數(shù)據(jù)接收完成,接收方將二進(jìn)制流數(shù)據(jù)拼接起來即可
我們規(guī)定一個(gè)最大的分段長度,一旦發(fā)送的數(shù)據(jù)超過這個(gè)分段長度就需要進(jìn)行分段發(fā)送。
發(fā)送的代碼示例如下
// 發(fā)送文件
public void sendFile(int size) {
new Thread(()->{
try {
// 表示發(fā)送文件
outputStream.write("2".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
for(int i=0; i<size/SocketUtil.MAX_CHUNK+1; i++) {
StringBuffer sb = new StringBuffer();
if (i!=size / SocketUtil.MAX_CHUNK) {
for (int j = 0; j < SocketUtil.MAX_CHUNK; j++) {
sb.append('a');
}
}
else if(i==size/SocketUtil.MAX_CHUNK && size%SocketUtil.MAX_CHUNK==0) {
break;
}
else {
for (int j = 0; j < size % SocketUtil.MAX_CHUNK; j++) {
sb.append('a');
}
}
try {
SocketUtil.sendInfo("[客戶端]發(fā)送一個(gè)數(shù)據(jù)包,大小" + sb.toString().getBytes().length + "B");
// 發(fā)送chunk的長度
outputStream.write(SocketUtil.intToStr(sb.toString().getBytes().length).getBytes());
// 發(fā)送chunk塊
outputStream.write(sb.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
// 最后發(fā)送000表示結(jié)束
try {
outputStream.write("000".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}接收二進(jìn)制流的代碼示例如下
// 讀取二進(jìn)制信息
public static byte[] readBytes(InputStream inputStream, String log) {
byte[] len = new byte[3];
byte[] allbytes = new byte[10000];
int idx = 0;
try {
inputStream.read(len);
// 然后再根據(jù)讀取的長度信息讀取二進(jìn)制流
// 只要不是最后一個(gè)二進(jìn)制流就繼續(xù)讀取
while (SocketUtil.parseLen(len) != 0) {
byte[] temp = new byte[SocketUtil.parseLen(len)];
inputStream.read(temp);
idx = SocketUtil.appendBytes(allbytes, temp, idx);
String info = "[" + log + "]接收一個(gè)數(shù)據(jù)包,大小" + SocketUtil.parseLen(len) + "B";
SocketUtil.sendInfo(info);
inputStream.read(len);
}
} catch (IOException e) {
e.printStackTrace();
}
return SocketUtil.getNewArr(allbytes, idx);
}其實(shí)我理解的所謂的通信協(xié)議,就是發(fā)送方和接收方都遵守的某種規(guī)則,按照這種規(guī)則發(fā)送和接收數(shù)據(jù)就可以保證數(shù)據(jù)的完整性。
完整的代碼
這段代碼只有四個(gè)java文件,非常簡單,只是一個(gè)極簡的通信協(xié)議模型。

首先來看一下界面定義
<?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"
tools:context=".MainActivity">
<EditText
android:id="@+id/text_file_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="請輸入待發(fā)送文件大小"/>
<Button
android:id="@+id/button_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="發(fā)送文件"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="消息欄:"/>
<TextView
android:id="@+id/text_info"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>然后是MainActivity
public class MainActivity extends AppCompatActivity {
private EditText text_file_size;
private Button button_send;
private TextView text_info;
private BroadcastReceiver broadcastReceiver;
private SocketClient socketClient;
private SocketServer socketServer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SocketUtil.context = MainActivity.this;
// 初始化控件
initView();
// 注冊廣播接收器
register();
socketServer = new SocketServer();
socketClient = new SocketClient();
}
private void initView() {
text_file_size = findViewById(R.id.text_file_size);
button_send = findViewById(R.id.button_send);
button_send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Integer size = Integer.parseInt(text_file_size.getText().toString());
socketClient.sendFile(size);
}
});
text_info = findViewById(R.id.text_info);
text_info.setMovementMethod(new ScrollingMovementMethod());
}
private void register() {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String info = intent.getStringExtra("info");
text_info.append(info + "\n");
}
};
IntentFilter filter = new IntentFilter("main.info");
registerReceiver(broadcastReceiver, filter);
}
}我們將需要重復(fù)用到的一些代碼都放到工具類中
public class SocketUtil {
public static Context context;
// 一次最多傳輸多少字節(jié)
public static int MAX_CHUNK = 100;
public static void sendInfo(String info) {
Intent intent = new Intent("main.info");
intent.putExtra("info", info);
context.sendBroadcast(intent);
}
// 讀取二進(jìn)制信息
public static byte[] readBytes(InputStream inputStream, String log) {
byte[] len = new byte[3];
byte[] allbytes = new byte[10000];
int idx = 0;
try {
inputStream.read(len);
// 然后再根據(jù)讀取的長度信息讀取二進(jìn)制流
// 只要不是最后一個(gè)二進(jìn)制流就繼續(xù)讀取
while (SocketUtil.parseLen(len) != 0) {
byte[] temp = new byte[SocketUtil.parseLen(len)];
inputStream.read(temp);
idx = SocketUtil.appendBytes(allbytes, temp, idx);
String info = "[" + log + "]接收一個(gè)數(shù)據(jù)包,大小" + SocketUtil.parseLen(len) + "B";
SocketUtil.sendInfo(info);
inputStream.read(len);
}
} catch (IOException e) {
e.printStackTrace();
}
return SocketUtil.getNewArr(allbytes, idx);
}
// 將int轉(zhuǎn)成String
public static String intToStr(int len) {
StringBuffer sb = new StringBuffer();
if(len < 100) {
sb.append("0");
}
else if (len < 10) {
sb.append("00");
}
sb.append(Integer.toString(len));
return sb.toString();
}
public static int parseLen(byte[] len) {
return Integer.parseInt(new String(len, 0, len.length));
}
public static int appendBytes(byte[] arr1, byte[] arr2, int st) {
for(int i=st; i<arr2.length; i++) {
arr1[i] = arr2[i-st];
}
return arr2.length+st;
}
public static byte[] getNewArr(byte[] arr, int idx) {
byte[] newarr = new byte[idx];
for(int i=0; i<idx; i++) {
newarr[i] = arr[i];
}
return newarr;
}
}最后是定義我們的客戶端和服務(wù)端
public class SocketClient {
private final String HOST = "localhost";
private final int PORT = 50055;
private Socket socket = null;
private OutputStream outputStream = null;
private InputStream inputStream = null;
public SocketClient() {
conn();
while(socket == null) {}
SocketUtil.sendInfo("服務(wù)端連接成功...");
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
// 連接服務(wù)端
private void conn() {
new Thread(()->{
try {
socket = new Socket(HOST, PORT);
inputStream = socket.getInputStream();
while(true) {
// 接收服務(wù)端消息0
byte[] type = new byte[1];
inputStream.read(type);
if (new String(type, 0, 1).equals("1")) {
byte[] infobytes = SocketUtil.readBytes(inputStream, "客戶端");
String info = "[客戶端]接收消息:" + new String(infobytes, 0, infobytes.length);
SocketUtil.sendInfo(info);
SocketUtil.sendInfo("====================================");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
// 發(fā)送文件
public void sendFile(int size) {
new Thread(()->{
try {
// 表示發(fā)送文件
outputStream.write("2".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
for(int i=0; i<size/SocketUtil.MAX_CHUNK+1; i++) {
StringBuffer sb = new StringBuffer();
if (i!=size / SocketUtil.MAX_CHUNK) {
for (int j = 0; j < SocketUtil.MAX_CHUNK; j++) {
sb.append('a');
}
}
else if(i==size/SocketUtil.MAX_CHUNK && size%SocketUtil.MAX_CHUNK==0) {
break;
}
else {
for (int j = 0; j < size % SocketUtil.MAX_CHUNK; j++) {
sb.append('a');
}
}
try {
SocketUtil.sendInfo("[客戶端]發(fā)送一個(gè)數(shù)據(jù)包,大小" + sb.toString().getBytes().length + "B");
// 發(fā)送chunk的長度
outputStream.write(SocketUtil.intToStr(sb.toString().getBytes().length).getBytes());
// 發(fā)送chunk塊
outputStream.write(sb.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
// 最后發(fā)送000表示結(jié)束
try {
outputStream.write("000".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}public class SocketServer {
private final int PORT = 50055;
private ServerSocket serverSocket = null;
public SocketServer() {
// 啟動服務(wù)端監(jiān)聽
start();
while(serverSocket == null) {}
SocketUtil.sendInfo("服務(wù)端啟動...");
}
// 啟動服務(wù)端監(jiān)聽程序
private void start() {
new Thread(()->{
try {
serverSocket = new ServerSocket(PORT);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
while(true) {
byte[] type = new byte[1];
inputStream.read(type);
String typeinfo = new String(type, 0, 1);
if(typeinfo.equals("2")) {
byte[] file = SocketUtil.readBytes(inputStream, "服務(wù)端");
String filetxt = new String(file, 0, file.length);
String info = "[服務(wù)端]接收完文件,大小" + file.length + "B" + "\n";
info = info + "[服務(wù)端]具體內(nèi)容如下:" + "\n" + filetxt;
SocketUtil.sendInfo(info);
// 給客戶端發(fā)送一個(gè)響應(yīng)信息表示接收成功
String typetxt = "1";
outputStream.write(typetxt.getBytes());
String successinfo = "文件接收成功";
String lentxt = SocketUtil.intToStr(successinfo.getBytes().length);
outputStream.write(lentxt.getBytes());
outputStream.write(successinfo.getBytes());
outputStream.write("000".getBytes());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}上述代碼中,服務(wù)端只負(fù)責(zé)接收二進(jìn)制流,客戶端只負(fù)責(zé)發(fā)送二進(jìn)流,并且服務(wù)端在接收完二進(jìn)制流數(shù)據(jù)后,會給服務(wù)端返回一個(gè)表示接收成功的文字信息。

結(jié)語
以上就是一個(gè)極簡的自定義通信協(xié)議模型,這個(gè)協(xié)議非常簡單,并且功能非常單一,可以根據(jù)上述的邏輯自定義通信協(xié)議以符合各種需求。
到此這篇關(guān)于Android使用socket進(jìn)行二進(jìn)制流數(shù)據(jù)傳輸?shù)奈恼戮徒榻B到這了,更多相關(guān)Android二進(jìn)制流數(shù)據(jù)傳輸內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中AlertDialog用法實(shí)例分析
這篇文章主要介紹了Android中AlertDialog用法,結(jié)合實(shí)例形式簡單分析了AlertDialog的基本調(diào)用與功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-01-01
Android動畫工具類的封裝實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于一次Android動畫工具類的封裝實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Android自定義view實(shí)現(xiàn)半圓環(huán)效果
這篇文章主要為大家詳細(xì)介紹了Android自定義view實(shí)現(xiàn)半圓環(huán)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
Android中分析Jetpack?Compose動畫內(nèi)部的實(shí)現(xiàn)原理
這篇文章主要介紹了Android中分析Jetpack?Compose動畫內(nèi)部的實(shí)現(xiàn)原理,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-09-09
android 點(diǎn)擊EditText始終不彈出軟件鍵盤實(shí)現(xiàn)代碼
這篇文章主要介紹了android 點(diǎn)擊EditText始終不彈出軟件鍵盤實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2016-11-11
Android 實(shí)現(xiàn)電話來去自動錄音的功能
本文主要介紹Android 電話自動錄音功能的開發(fā),這里提供實(shí)現(xiàn)代碼和實(shí)現(xiàn)效果圖,有需要的小伙伴可以參考下2016-08-08
Android編程創(chuàng)建桌面快捷方式的常用方法小結(jié)【2種方法】
這篇文章主要介紹了Android編程創(chuàng)建桌面快捷方式的常用方法,結(jié)合實(shí)例形式總結(jié)分析了2種常見的實(shí)現(xiàn)方法與相關(guān)操作技巧,需要的朋友可以參考下2017-02-02

