最近開發中使用到將字符串和圖片同時傳輸的功能。我這邊是Android端,要接收服務器端發送來的信息和圖片。由於服務器端不是一個web servser,所以圖片和字符串信息要混雜着傳送。比較麻煩,花了一些時間解決這個問題。特記錄。
網絡上關於圖片的傳輸一般有兩種方式,一個是通過base64編碼,一個就是通過發送端先發送圖片大小,在發送圖片,接收端根據圖片大小讀取規定大小的數據保存到文件。由於base64會增加數據量,身為一個Android程序員,我並不想這么實現,所以我實現第二種方式。
首先說一下通信協議:我定義的一條完整的數據如下:(text1)(image_start)(image_file_name)(image_file_name_end)(image_file_length)(image)(text2)(message_end)
(text1)和(text2):圖片文件可能是在一條完整數據的中部,所以圖片文件前端和后端都可能存在文本數據
(image_start):圖片文件數據開始的標識,在程序中我使用的是(image:)。
(image_file_name):圖片文件的名稱,其實增加這個內容,剛開始的時候只要是想獲得文件的后綴名從而確定圖片文件的類型,后來干脆就直接把圖片文件的名稱接收算了。。。。
(imge_file_name_end):圖片文件名結束的標識,在windows系統中,文件名中不能存在?,所以在程序中,我使用的是(?);
(image_file_length):圖片文件的長度,這部分是8個字節的內容。在java中,獲取文件的長度,返回的是一個Long類型的數據,所以發送的時候,需要將Long類型轉化為一個8字節的數據,接收時,要根據這8字節的數據,轉化為Long類型。
(image):具體的圖片文件信息。
(message_end):一條完整的消息結束的標識,在程序中我使用的是(over)。
首先是發送圖片的類,我在demo中,發送圖片的是客戶端。類寫的比較簡單。
1 package util; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.InputStream; 6 import java.io.OutputStream; 7 import java.net.Socket; 8 9 public class SendImage 10 { 11 private Socket socket; 12 private OutputStream os; 13 /** 14 * 圖片開始的標識 15 */ 16 private final String IMAGE_START = "image:"; 17 /** 18 * 一條完整信息結束的標識 19 */ 20 private final String MESSAGE_END = "over"; 21 /** 22 * 文件名結束的表示 23 */ 24 private final String FILE_NAME_END = "?"; 25 26 public SendImage(String ip, int port) throws Exception 27 { 28 socket = new Socket(ip, port); 29 os = socket.getOutputStream(); 30 } 31 32 public void send() 33 { 34 try 35 { 36 File imageFile = new File("F:/1.jpg"); 37 InputStream is = new FileInputStream(imageFile); 38 39 long fileLength = imageFile.length(); 40 System.out.println("圖片長度:" + fileLength); 41 42 /*發送第一部分的文本信息,對應text1*/ 43 os.write("要開始發送圖片了哦".getBytes()); 44 45 /*發送圖片開始的標識,對應image_start*/ 46 os.write(IMAGE_START.getBytes()); 47 48 /*發送圖片文件名稱,對應image_file_name*/ 49 os.write(imageFile.getName().getBytes()); 50 51 /*發送圖片文件名稱結束的標識,對應image_file_name_end*/ 52 os.write(FILE_NAME_END.getBytes()); 53 54 /*發送圖片文件的長度,對應image_file_length*/ 55 byte[] bs = longToBytes(fileLength); 56 os.write(bs); 57 58 /*發送圖片文件,對應image*/ 59 int length; 60 byte[] b = new byte[1024]; 61 while ((length = is.read(b)) > 0) 62 { 63 os.write(b, 0, length); 64 } 65 66 /*發送第二部分文本信息,對應text2*/ 67 os.write("圖片發送結束了".getBytes()); 68 69 /*發送一條完整信息結束的標識,對應message_end*/ 70 os.write(MESSAGE_END.getBytes()); 71 } 72 catch (Exception e) 73 { 74 e.printStackTrace(); 75 } 76 } 77 78 public void close() 79 { 80 try 81 { 82 os.close(); 83 socket.close(); 84 } 85 catch (Exception e) 86 { 87 88 } 89 } 90 91 /** 92 * 將長整型轉換為byte數組 93 * @param n 94 * @return 95 */ 96 public static byte[] longToBytes(long n) 97 { 98 byte[] b = new byte[8]; 99 b[7] = (byte) (n & 0xff); 100 b[6] = (byte) (n >> 8 & 0xff); 101 b[5] = (byte) (n >> 16 & 0xff); 102 b[4] = (byte) (n >> 24 & 0xff); 103 b[3] = (byte) (n >> 32 & 0xff); 104 b[2] = (byte) (n >> 40 & 0xff); 105 b[1] = (byte) (n >> 48 & 0xff); 106 b[0] = (byte) (n >> 56 & 0xff); 107 return b; 108 } 109 110 public static void main(String[] args) 111 { 112 try 113 { 114 SendImage send = new SendImage("127.0.0.1", 40123); 115 116 /*測試多次發送*/ 117 send.send(); 118 send.send(); 119 send.send(); 120 121 send.close(); 122 } 123 catch (Exception e) 124 { 125 e.printStackTrace(); 126 } 127 } 128 }
圖片接受端,使用的是服務器端來接收圖片。類依然寫的很簡單。
1 package util; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.net.ServerSocket; 8 import java.net.Socket; 9 10 public class ReceiveImage2 11 { 12 private ServerSocket ss; 13 private Thread listenThread; 14 /** 15 * 圖片開始的標識 16 */ 17 private final String IMAGE_START = "image:"; 18 /** 19 * 一條完整信息結束的標識 20 */ 21 private final String MESSAGE_END = "over"; 22 /** 23 * 文件名結束的表示 24 */ 25 private final String FILE_NAME_END = "?"; 26 /** 27 * 默認的編碼,我的是UTF-8,大家可以更改成自己的編碼 28 */ 29 private final String DEFAULT_ENCODE = "UTF-8"; 30 /** 31 * ISO編碼 32 */ 33 private final String ISO_ENCODE = "ISO-8859-1"; 34 35 public ReceiveImage2() throws Exception 36 { 37 ss = new ServerSocket(40123); 38 39 listenThread = new Thread(new Runnable() 40 { 41 @Override 42 public void run() 43 { 44 listen(); 45 } 46 }); 47 48 listenThread.start(); 49 } 50 51 /** 52 * 監聽鏈接 53 */ 54 private void listen() 55 { 56 while (!ss.isClosed()) 57 { 58 try 59 { 60 final Socket s = ss.accept(); 61 new Thread(new Runnable() 62 { 63 @Override 64 public void run() 65 { 66 read(s); 67 } 68 }).start(); 69 } 70 catch (IOException e) 71 { 72 e.printStackTrace(); 73 } 74 } 75 } 76 77 /** 78 * 讀取信息 79 * 80 * @param socket 81 * 客戶端鏈接過來的socket 82 */ 83 private void read(Socket socket) 84 { 85 try 86 { 87 InputStream is = socket.getInputStream(); 88 StringBuffer sb = new StringBuffer(); 89 int imageName = 0; 90 while (!socket.isClosed()) 91 { 92 int imageStart; 93 while ((imageStart = sb.indexOf(IMAGE_START)) < 0) 94 readToBuffer(is, sb); 95 96 System.out.println("開始讀取第一部分文本信息"); 97 String text1 = sb.substring(0, imageStart); 98 text1 = new String(text1.getBytes(ISO_ENCODE), DEFAULT_ENCODE); 99 System.out.println("第一部分文本信息:" + text1); 100 sb.delete(0, imageStart + IMAGE_START.length()); 101 102 System.out.println("開始讀取文件名稱"); 103 int file_name_end; 104 while ((file_name_end = sb.indexOf(FILE_NAME_END)) < 0) 105 readToBuffer(is, sb); 106 String file_name = new String(sb.substring(0, file_name_end).getBytes(ISO_ENCODE), DEFAULT_ENCODE); 107 System.out.println("文件名稱:" + file_name); 108 sb.delete(0, file_name_end + FILE_NAME_END.length()); 109 110 System.out.println("開始讀取文件長度"); 111 while (sb.length() < 8) 112 readToBuffer(is, sb); 113 String imageLengthString = sb.substring(0, 8); 114 byte[] imageLengthByteArray = imageLengthString.getBytes(ISO_ENCODE); 115 long imageLength = bytesToLong(imageLengthByteArray); 116 System.out.println("文件長度:" + imageLength); 117 sb.delete(0, 8); 118 119 System.out.println("開始讀取文件"); 120 byte[] image = sb.toString().getBytes(ISO_ENCODE); 121 FileOutputStream fos = new FileOutputStream(new File("F:/接收文件" + imageName + file_name)); 122 if (imageLength > image.length) 123 { 124 System.out.println("文件只有部分在數組中"); 125 fos.write(image); 126 System.out.println("已經寫了" + image.length + "還需要寫" + (imageLength - image.length)); 127 writeImage(is, fos, imageLength - image.length); 128 sb.delete(0, sb.length()); 129 } 130 else 131 { 132 System.out.println("文件已經在數組中"); 133 fos.write(image, 0, (int) imageLength); 134 sb.delete(0, (int) imageLength); 135 } 136 fos.close(); 137 imageName++; 138 System.out.println("文件已經保存"); 139 140 int end; 141 while ((end = sb.indexOf(MESSAGE_END)) < 0) 142 { 143 readToBuffer(is, sb); 144 } 145 146 String text2 = new String(sb.substring(0, end).getBytes(ISO_ENCODE), DEFAULT_ENCODE); 147 System.out.println("第二部分文本信息:" + text2); 148 sb.delete(0, end + MESSAGE_END.length()); 149 } 150 } 151 catch (Exception e) 152 { 153 e.printStackTrace(); 154 } 155 finally 156 { 157 try 158 { 159 socket.close(); 160 } 161 catch (IOException e) 162 { 163 164 } 165 System.out.println("線程結束"); 166 } 167 168 } 169 170 /** 171 * 將輸入流中的數據讀取到stringbuffer中,一次最多讀取1024個長度 172 * 173 * @param is 174 * 輸入流 175 * @param sb 176 * 圖片文件輸出流 177 * @throws Exception 178 */ 179 private void readToBuffer(InputStream is, StringBuffer sb) throws Exception 180 { 181 int readLength; 182 byte[] b = new byte[1024]; 183 184 readLength = is.read(b); 185 if (readLength == -1) 186 throw new RuntimeException("讀取到了-1,說明Socket已經關閉"); 187 String s = new String(b, 0, readLength, ISO_ENCODE); 188 sb.append(s); 189 } 190 191 /** 192 * 從輸入流中讀取圖片信息到圖片文件輸出流中 193 * 194 * @param is 195 * 輸入流 196 * @param fos 197 * 圖片文件輸出流 198 * @param length 199 * 需要讀取的數據長度 200 * @throws Exception 201 */ 202 private void writeImage(InputStream is, FileOutputStream fos, long length) throws Exception 203 { 204 byte[] imageByte = new byte[1024]; 205 int oneTimeReadLength; 206 207 for (long readLength = 0; readLength < length;) 208 { 209 if (readLength + imageByte.length <= length) 210 { 211 System.out.println("剩余的字節數大於1024,將盡可能多的讀取內容"); 212 oneTimeReadLength = is.read(imageByte); 213 } 214 else 215 { 216 System.out.println("剩余的字節數小於1024,將只讀取" + (length - readLength) + "字節"); 217 oneTimeReadLength = is.read(imageByte, 0, (int) (length - readLength)); 218 } 219 220 if (oneTimeReadLength == -1) 221 throw new RuntimeException("讀取文件時,讀取到了-1,說明Socket已經結束"); 222 System.out.println("實際讀取長度" + oneTimeReadLength + "字節"); 223 224 readLength += oneTimeReadLength; 225 226 fos.write(imageByte, 0, oneTimeReadLength); 227 System.out.println("繼續追加" + readLength + "字節長度"); 228 } 229 } 230 231 /** 232 * 將byte數組轉化為Long類型 233 * 234 * @param array 235 * @return 236 */ 237 public static long bytesToLong(byte[] array) 238 { 239 return ((((long) array[0] & 0xff) << 56) | (((long) array[1] & 0xff) << 48) | (((long) array[2] & 0xff) << 40) 240 | (((long) array[3] & 0xff) << 32) | (((long) array[4] & 0xff) << 24) 241 | (((long) array[5] & 0xff) << 16) | (((long) array[6] & 0xff) << 8) | (((long) array[7] & 0xff) << 0)); 242 } 243 244 public static void main(String[] args) 245 { 246 try 247 { 248 new ReceiveImage2(); 249 } 250 catch (Exception e) 251 { 252 e.printStackTrace(); 253 } 254 } 255 }
客戶端並沒有什么太難理解的地方,按照規定的格式發送內容就可以了。下面主要說一說服務器端接收圖片。
首先先說一下關於Long類型轉字節數組和字節數組轉Long類型,我是直接使用別人已經實現的方法。鏈接如下:http://www.cnblogs.com/devinzhang/archive/2012/09/28/2707605.html。
因為圖片文件是二進制信息,所以傳統的使用一個標志符來標志一個圖片文件的結束並不合適,因為圖片文件的二進制信息中,可能存在這個結束標識符,這樣就會造成圖片文件內容不全的狀況(base64轉碼可以解決這個問題,但是我們不討論base64)。所以,我們在文件內容之前先發送文件的消息,我們根據接收來大小來確定圖片文件的長度。
在代碼中,之所以使用到了ISO編碼,是因為UTF-8是可變長度的編碼,原來的字節數組就被改變了。而ISO8859-1通常叫做Latin-1,Latin-1包括了書寫所有西方歐洲語言不可缺少的附加字符,其中0~127的字符與ASCII碼相同,它是單字節的編碼方式,這樣第二種方式生成的String里的字節數組就跟原來的字節數組一樣。在new String使用其他編碼如GBK,GB2312的話一樣也會導致字節數組發生變化,因此要想獲取String里單字節數組,就應該使用ISO8859-1編碼。
例如
1 byte[] b = new byte[8]; 2 b[6] = 5; 3 b[7] = -4; 4 5 for (int i = 0; i < b.length; i++) 6 { 7 System.out.println(b[i]); 8 } 9 10 System.out.println("---------華麗麗的分割線----------------"); 11 String s = null; 12 s = new String(b); 13 14 byte[] b2 = null; 15 b2 = s.getBytes(); 16 17 for (int i = 0; i < b2.length; i++) 18 { 19 System.out.println(b2[i]); 20 }
輸出的結果:
使用ISO編碼
1 byte[] b = new byte[8]; 2 b[6] = 5; 3 b[7] = -4; 4 5 for (int i = 0; i < b.length; i++) 6 { 7 System.out.println(b[i]); 8 } 9 10 System.out.println("---------華麗麗的分割線----------------"); 11 String s = null; 12 s = new String(b,"ISO-8859-1"); 13 14 byte[] b2 = null; 15 b2 = s.getBytes("ISO-8859-1"); 16 17 for (int i = 0; i < b2.length; i++) 18 { 19 System.out.println(b2[i]); 20 }
結果如下:
下面,講一講接收端的接收一次的邏輯。
1.會接收一些數據,如果這些數據中不存在圖片文件開始的標識,則繼續讀取。當讀取的數據中存在圖片文件開始的標識,則將圖片文件開始標識前的信息輸出,並且刪除以及刪除圖片文件開始標識。
2.判斷stringbuffer中是否存在文件名稱結束的標識,如果沒有,則繼續讀取信息,否則保存並輸出文件名稱。刪除stringbuffer中圖片名稱和圖片名稱結束的標識。
3.判斷stringbuffer中是否有8個以上的字節數據,如果沒有,則繼續讀取,否則獲取8個字節的數據作為圖片文件的長度。刪除stringbuffer中圖片文件的長度。
4.判斷stringbuffer的長度是否大於圖片文件長度,如果大於,說明圖片文件內容都在stringbuffer中,則從stringbuffer中提取圖片文件大小的長度作為文件信息,否則將stringbuffer中的內容輸出到文件輸出流,在從Socket輸入流中讀取剩余的圖片文件內容。刪除stringbuffer中圖片文件內容。
5.判斷stringbuffer中是否存在一條完整信息結束的標識,如果沒有,則繼續讀取,否則截取完整信息結束標識前的信息輸出。
文筆稀爛,大家湊活着看,如果看不懂我寫的東西,就看代碼吧。