Android使用socket進行二進制流數(shù)據(jù)傳輸
引言
使用socket流傳輸二進制流數(shù)據(jù),比如文件或者視頻圖片等等信息的時候,我們通常使用tcp協(xié)議傳輸,因為tcp協(xié)議可以保證二進制流按序到達,并且保證交付,這樣子就可以保證我們傳輸二進制流的完整性。
使用tcp協(xié)議進行二進制流傳輸?shù)臅r候通常會有兩個問題:
由于tcp進行信息傳輸?shù)臅r候是沒有邊界的,所以可能會產(chǎn)生粘包半包問題。所謂粘包就是指接收的一段數(shù)據(jù)包含了下一段數(shù)據(jù)的信息,所謂半包就是指一段數(shù)據(jù)沒有接收完整,實際上都是邊界不明確產(chǎn)生的問題。
并且在傳輸一段很大的二進制流數(shù)據(jù)的時候,我們可能需要對超大的二進制流分段處理,也就是分段來傳輸。
在輸出端將二進制流分段,輸入端在接收到各個片段后再將整個流信息拼接起來,就構成了完整的傳輸流程。
為了解決上述的兩個問題,同時也為了能夠統(tǒng)一這個傳輸流程,我們需要自定義一個簡單的傳輸協(xié)議。
簡單的自定義協(xié)議
我們自定義一個簡單的通信協(xié)議,協(xié)議一共傳輸兩種信息,第一種是文字,第二種是二進制流(其實文字也可以用二進制流表示),傳輸過程如下圖所示。
我們定義的簡單通信協(xié)議規(guī)則如下
1.首先發(fā)送一個字節(jié)的信息(就是圖中的type),表示一段消息的開始,同時也表明了后面二進制數(shù)據(jù)的類型(文字信息還是二進制流數(shù)據(jù))
2.每一個chunk都由三個字節(jié)的長度信息和相應的二進制流信息組成,接收方在接收到三個字節(jié)的長度信息后,繼續(xù)使用相應大小的緩沖區(qū)接收后面的流數(shù)據(jù)
3.當接收到三個字節(jié)的000的時候表示數(shù)據(jù)接收完成,接收方將二進制流數(shù)據(jù)拼接起來即可
我們規(guī)定一個最大的分段長度,一旦發(fā)送的數(shù)據(jù)超過這個分段長度就需要進行分段發(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ā)送一個數(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表示結束 try { outputStream.write("000".getBytes()); } catch (IOException e) { e.printStackTrace(); } }).start(); }
接收二進制流的代碼示例如下
// 讀取二進制信息 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ù)讀取的長度信息讀取二進制流 // 只要不是最后一個二進制流就繼續(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 + "]接收一個數(shù)據(jù)包,大小" + SocketUtil.parseLen(len) + "B"; SocketUtil.sendInfo(info); inputStream.read(len); } } catch (IOException e) { e.printStackTrace(); } return SocketUtil.getNewArr(allbytes, idx); }
其實我理解的所謂的通信協(xié)議,就是發(fā)送方和接收方都遵守的某種規(guī)則,按照這種規(guī)則發(fā)送和接收數(shù)據(jù)就可以保證數(shù)據(jù)的完整性。
完整的代碼
這段代碼只有四個java文件,非常簡單,只是一個極簡的通信協(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); } }
我們將需要重復用到的一些代碼都放到工具類中
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); } // 讀取二進制信息 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ù)讀取的長度信息讀取二進制流 // 只要不是最后一個二進制流就繼續(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 + "]接收一個數(shù)據(jù)包,大小" + SocketUtil.parseLen(len) + "B"; SocketUtil.sendInfo(info); inputStream.read(len); } } catch (IOException e) { e.printStackTrace(); } return SocketUtil.getNewArr(allbytes, idx); } // 將int轉成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; } }
最后是定義我們的客戶端和服務端
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("服務端連接成功..."); try { outputStream = socket.getOutputStream(); } catch (IOException e) { e.printStackTrace(); } } // 連接服務端 private void conn() { new Thread(()->{ try { socket = new Socket(HOST, PORT); inputStream = socket.getInputStream(); while(true) { // 接收服務端消息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ā)送一個數(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表示結束 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() { // 啟動服務端監(jiān)聽 start(); while(serverSocket == null) {} SocketUtil.sendInfo("服務端啟動..."); } // 啟動服務端監(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, "服務端"); String filetxt = new String(file, 0, file.length); String info = "[服務端]接收完文件,大小" + file.length + "B" + "\n"; info = info + "[服務端]具體內(nèi)容如下:" + "\n" + filetxt; SocketUtil.sendInfo(info); // 給客戶端發(fā)送一個響應信息表示接收成功 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(); } }
上述代碼中,服務端只負責接收二進制流,客戶端只負責發(fā)送二進流,并且服務端在接收完二進制流數(shù)據(jù)后,會給服務端返回一個表示接收成功的文字信息。
結語
以上就是一個極簡的自定義通信協(xié)議模型,這個協(xié)議非常簡單,并且功能非常單一,可以根據(jù)上述的邏輯自定義通信協(xié)議以符合各種需求。
到此這篇關于Android使用socket進行二進制流數(shù)據(jù)傳輸?shù)奈恼戮徒榻B到這了,更多相關Android二進制流數(shù)據(jù)傳輸內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android自定義View 實現(xiàn)鬧鐘喚起播放鬧鐘鈴聲功能
這篇文章主要介紹了Android自定義View 實現(xiàn)鬧鐘喚起播放鬧鐘鈴聲的效果,本文通過實例代碼給大家詳解,需要的朋友可以參考下2016-12-12Android中ListView的幾種常見的優(yōu)化方法總結
Android中的ListView應該算是布局中幾種最常用的組件之一,本篇文章主要做了三種優(yōu)化總結,有興趣的可以了解一下。2017-02-02Flutter?Widget?之StatefulBuilder構建方法詳解
這篇文章主要為大家介紹了Flutter?Widget?之StatefulBuilder構建方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11Android 通過jni返回Mat數(shù)據(jù)類型方法
今天小編就為大家分享一篇Android 通過jni返回Mat數(shù)據(jù)類型方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08TabLayout關聯(lián)ViewPager后不顯示文字的解決方法
這篇文章主要為大家詳細介紹了TabLayout關聯(lián)ViewPager后不顯示文字的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11