socket編程注意事項

Socket(套接字)位於iso模型中的傳輸層之上,應用層(包括表示層和會話層)之下,它是操作系統向外暴露的一些api,使用socket編程,只需要調用這些接口即可。Socket的實現原理和接口的具體使用就不詳細描述,本文主要是對本人在使用socket傳輸數據時遇到的問題和需要注意的事項進行了分析和總結。主要包括以下這些事項:

  1. 數據丟失
  2. 死鎖
  3. 傳輸文本文檔時,編碼問題
  4. 數據傳輸加密

    通過代碼來描述遇到的上述問題和注意事項,首先客戶端代碼如下:

package com.idt.socket;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class SocketClient {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Socket socket = null;
        DataOutputStream dos = null;
        DataInputStream dis = null;
        FileInputStream fis = null;
        try {
            System.out.println("socket客戶端執行:");
            socket = new Socket("199.66.65.120", 8082);
            dos = new DataOutputStream(socket.getOutputStream());
            dis = new DataInputStream(socket.getInputStream());
            File file = new File("E:\\教程\\socket\\data\\測試數據.jpg");
            String fileName = file.getName();
            long totalLen = file.length();
            System.out.println("發送文件名稱:"+fileName);
            System.out.println("發送文件大小:"+totalLen);
            fis = new FileInputStream(file);
            byte[] b = new byte[1024];
            int len = 0;
            int sendLen = 0;            
            dos.writeInt(fileName.getBytes("utf-8").length);//文件名稱字節長度
            dos.write(fileName.getBytes("utf-8"));//文件名字節
            dos.writeInt((int)totalLen);//文件內容字節長度
            //循環寫入文件流
            while ((len=(fis.read(b, 0, b.length)))>0) {                
                dos.write(b, 0, len);               
                dos.flush();
                sendLen +=len;
                System.out.println("已發送百分比:"+sendLen*100/totalLen+"%("+sendLen+")");                
            }
        } catch (UnknownHostException e) {
            // TODO: handle exception
            e.printStackTrace();
        } catch(IOException e){
            e.printStackTrace();
        }finally {
            try{
                if(socket != null){
                    socket.close();
                }
                if(dos != null){
                    dos.close();
                }
                if(dis != null){
                    dis.close();
                }
                if(fis != null){
                    fis.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

}

服務端代碼:

package com.idt.demo.socket;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        DataOutputStream dos = null;
        DataInputStream dis = null;
        Socket socket = null;
        FileOutputStream fos = null;
        try {
            System.out.println("執行socket服務端:");
            ServerSocket serverSocket = new ServerSocket(8082);
            socket = serverSocket.accept();
            dos = new DataOutputStream(socket.getOutputStream());
            dis = new DataInputStream(socket.getInputStream());
            //讀取文件名稱長度
            int fileNameLen = dis.readInt();
            byte[] fileNameByte = new byte[fileNameLen];
            dis.read(fileNameByte,0,fileNameByte.length);
            String fileName = new String(fileNameByte, "utf-8");
            System.out.println("接收文件名稱:"+fileName);
            int fileLen = dis.readInt();
            System.out.println("接收文件總長度:"+fileLen);
            byte[] fileByte = new byte[fileLen];
            int readLen = dis.read(fileByte, 0, fileByte.length);   
            System.out.println("讀取的長度:"+readLen);
            fos = new FileOutputStream(new File("E:\\教程\\socket\\receive\\"+fileName));
            fos.write(fileByte);
            fos.flush();
        } catch (IOException e) {
            // TODO: handle exception
            e.printStackTrace();
        }finally{
            try {
                if(dos != null){
                    dos.close();
                }
                if(dis != null){
                    dis.close();
                }
                if(socket != null){
                    socket.close();
                }
                if(fos !=null){
                    fos.close();
                }
            } catch (IOException e2) {
                // TODO: handle exception
                e2.printStackTrace();
            }
        }

    }

}

客戶端執行結果打印:

socket客戶端執行:
發送文件名稱:測試數據.jpg
發送文件大小:349008
已發送百分比:0%(1024)
已發送百分比:0%(2048)
已發送百分比:0%(3072)
已發送百分比:1%(4096)
已發送百分比:1%(5120)
已發送百分比:1%(6144)
已發送百分比:2%(7168)
已發送百分比:2%(8192)
已發送百分比:2%(9216)
已發送百分比:2%(10240)
已發送百分比:3%(11264)
已發送百分比:3%(12288)
已發送百分比:3%(13312)
已發送百分比:4%(14336)
已發送百分比:4%(15360)
已發送百分比:4%(16384)
已發送百分比:4%(17408)
已發送百分比:5%(18432)
已發送百分比:5%(19456)
已發送百分比:5%(20480)
已發送百分比:6%(21504)
已發送百分比:6%(22528)
已發送百分比:6%(23552)
已發送百分比:7%(24576)
已發送百分比:7%(25600)
已發送百分比:7%(26624)
java.net.SocketException: Connection reset by peer: socket write error
    at java.net.SocketOutputStream.socketWrite0(Native Method)
    at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109)
    at java.net.SocketOutputStream.write(SocketOutputStream.java:153)
    at java.io.DataOutputStream.write(DataOutputStream.java:107)
    at com.idt.socket.SocketClient.main(SocketClient.java:37)

服務端執行結果:

執行socket服務端:
接收文件名稱:測試數據.jpg
接收文件總長度:349008
讀取的長度:2048

執行過程就是將“測試數據.jpg”文件,從客戶端發送到服務端,文件大小是340K。Socket傳輸完畢後,服務端接受的的“測試數據.jpg”大小與客戶端的大小相同(也是340K),但是文件打不開,說明數據沒有傳輸成功。分析代碼,發現文件字節長度是349008,但是在服務端只收到了2048個字節,看下面這句代碼:

int readLen = dis.read(fileByte, 0, fileByte.length);

上面這句代碼,只是將覆蓋了fileByte數組的前2048個元素,後面的元素爲初始化值,即0。下面這句代碼:

fos.write(fileByte);//

將fileByte字節數組寫入到文件中,前2048個字節與客戶端文件內容保持一致,後面的字節就完全不一樣了。將上面代碼替換成:

fos.write(fileByte,0,readLen);//

此時,服務端和客戶端文件不在一致,而是變小了。
再分析客戶端打印日誌,客戶端socket 寫到26624個字節後,開始報socket write error錯誤,此處有兩個疑問:
1. 爲什麼客戶端不再繼續write,而是報socket write error錯誤?
2. 客戶端寫了26624個字節,服務端爲何只接收了2048個字節?
帶着這兩個疑問,搜索了相關的文章,發現《asynSocket源碼解析之三》這篇文章針對這兩個疑問講的非常透徹。
Socket的寫和讀實際上是寫入各自的寫緩衝區,從讀緩衝區中獲取內容,傳輸層(iso協議)負責將客戶端的寫緩衝區的包運輸到服務端讀緩衝區中。
在java socket源碼中有兩個方法:

public synchronized int getReceiveBufferSize()//獲取發送緩衝區大小
public synchronized int getSendBufferSize() throws SocketException//獲取接收緩衝區大小

執行這兩個方法,會得出java socket默認的發送緩衝區和接收緩衝區大小均爲8192。
從以上分析可知,socket傳輸過程就是:客戶端執行while循環不停地向發送緩衝區寫數據,傳輸層負責將客戶端的發送緩衝區中的數據運輸到服務器的接收緩衝區中,服務端執行read方法從接收緩衝區獲取數據。
分析第一個問題,數據丟失,原因就是服務端只讀了一次,剩餘的字節沒有讀取,導出文件傳輸不完全,客戶端報socket write error錯誤,就是因此客戶端發送緩衝區和服務器接收緩衝區均已寫滿,無法再寫入,因此報此錯誤。
我們通過計算對此問題進行深入分析下,客戶端發送了26624個字節,服務端寫向文件中寫入了2048個字節,假設接收緩衝區和發送緩衝區都已填滿,那麼字節數是16384,由於文件名稱的字節較少,可以忽略不計,客戶端發送的字節減去服務端向文件寫入的字節,再減去緩衝區的字節個數,還剩下8192個字節,那麼這8192個字節去哪裏了?仔細閱讀那篇文章,裏面有一句“但是socket其實可用buffer要比實際設置的大。(這個實際大小和設置大小具體啥關係,還不是太清楚)”,也許就是這個原因吧。
服務端socket需要循環讀取,才能完全接收文件,修改服務端代碼:

public static void main(String[] args) {
        // TODO Auto-generated method stub
        DataOutputStream dos = null;
        DataInputStream dis = null;
        Socket socket = null;
        FileOutputStream fos = null;
        try {
            System.out.println("執行socket服務端:");
            ServerSocket serverSocket = new ServerSocket(8082);
            socket = serverSocket.accept();
            dos = new DataOutputStream(socket.getOutputStream());
            dis = new DataInputStream(socket.getInputStream());
            //讀取文件名稱長度
            int fileNameLen = dis.readInt();
            byte[] fileNameByte = new byte[fileNameLen];
            dis.read(fileNameByte,0,fileNameByte.length);
            String fileName = new String(fileNameByte, "utf-8");
            System.out.println("接收文件名稱:"+fileName);
            int fileLen = dis.readInt();
            System.out.println("接收文件總長度:"+fileLen);
            //byte[] fileByte = new byte[fileLen];
            //int readLen = dis.read(fileByte, 0, fileByte.length);
            fos = new FileOutputStream(new File("E:\\教程\\socket\\receive\\"+fileName));
            byte[] b = new byte[1024];
            int readLen = 0;
            while((readLen = dis.read(b, 0, b.length))!=-1){
                fos.write(b,0,readLen);
                fos.flush();
            }
            //System.out.println("讀取的長度:"+readLen);                 
        } catch (IOException e) {
            // TODO: handle exception
            e.printStackTrace();
        }finally{
            try {
                if(dos != null){
                    dos.close();
                }
                if(dis != null){
                    dis.close();
                }
                if(socket != null){
                    socket.close();
                }
                if(fos !=null){
                    fos.close();
                }
            } catch (IOException e2) {
                // TODO: handle exception
                e2.printStackTrace();
            }
        }

    }

再次運行代碼,服務端可以正常接收文件,客戶端不再報錯。但是,服務器接收完文件後,希望給客戶端返回一個是否接收成功的標識,修改客戶端代碼,在while循環後面加上:

String rtnMsg = dis.readUTF();
System.out.println("發送結果:" + rtnMsg);

修改服務端代碼,也是在while循環後面加上:

dos.writeUTF("succesful");

執行代碼,客戶端並沒有打印發送成功結果,再次運行服務端,提示jvm端口被佔用,說明服務端socket還在運行,socket還沒有關閉。什麼原因呢?
調試代碼發現,在服務端while循環一直在執行,無法向下執行到dos.writeUTF("succesful")這句代碼,所以客戶端無法讀取任何信息。服務端while循環一直執行的原因是什麼呢?肯定是socket的輸出流讀取不到-1這個標誌,也就是客戶端沒有向服務端發送-1標誌,socket在什麼情況下發送-1這個標誌呢?搜索資料發現這篇文章《s.shutdownOutput();這行代碼的牛逼之處》針對這個問題講的非常好。Socket發送-1標誌的兩種方法:

1.  socket.close();
2.  socket.shutdownoutput();

第一種方法會造成socket關閉,在socket傳輸過程中不能使用。因此使用第二方法是合理的。在客戶端while循環後面writeUTF方法之前加上方法二的代碼。執行代碼,客戶端成功打印發送成功日誌。
問題解決,還要繼續分析下,客戶端代碼在沒加讀取服務端返回結果的代碼之間,爲什麼可以正常執行呢?服務端沒有出現while循環一直執行呢?結合上面,非常好理解,之前沒有添加readUTF方法,客戶端執行完while循環後繼續向下執行,當執行到socket.close()時,服務端接收到-1標誌,結束while循環,向下執行直到程序結束。當在客戶端代碼中添加readUTF方法後,該方法會一直等待服務端向它發送信息,而服務端沒有收到客戶端向它發送的-1的標誌,就會一直執行while循環,無法向下執行writeUTF方法,即無法向客戶端發送信息,這樣就造成兩邊相互等待的結果,即死鎖現象。
當使用socket傳輸文本文件時,一定要注意編碼的問題,如果客戶端和服務端的編碼的編碼不一致,就會造成亂碼的問題。解決亂碼採用如下的代碼形式:

DataInputStream dataIS = new DataInputStream(clientSocket.getInputStream());  
InputStreamReader inSR = new InputStreamReader(dataIS, "UTF-8");

DataOutputStream dataOS = new DataOutputStream(clientSocket.getOutputStream());  
OutputStreamWriter outSW = new OutputStreamWriter(dataOS, "UTF-8"); 

輸入流的編碼要與當前系統的編碼一致,輸出流的編碼要與接收方系統編碼一致。目前,一般情況下系統編碼都是utf-8,無需考慮編碼問題,直接使用上述的代碼即可。
數據傳輸必須考慮數據的安全性,數據加密是保證數據完整性的有效手段,java中實現的了若干加密算法,如:
1. Base64 嚴格地說是,屬於編碼格式;
2. Md5(message digest algorithm 5,信息摘要算法)
3. Sha(secure hash algorithm,安全散列算法)
4. Hmac(hash message authentication code,散列消息鑑別碼)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章