Socket緩沖區探討,是否有拆包的方式?


Socket緩沖區探討
       本文主要探討java網絡套接字傳輸模型,並對如何將NIO應用於服務端,提高服務端的運行能力和降低服務負載。
       1.1 socket套接字緩沖區
      Java提供了便捷的網絡編程模式,尤其在套接字中,直接提供了與網絡進行溝通的輸入和輸出流,用戶對網絡的操作就如同對文件操作一樣簡便。在客戶端與服務端建立Socket連接后,客戶端與服務端間的寫入和寫出流也同時被建立,此時即可向流中寫入數據,也可以從流中讀取數據。在對數據流進行操作時,很多人都會誤以為,客戶端和服務端的read和write應當是對應的,即:客戶端調用一次寫入,服務端必然調用了一次寫出,而且寫入和寫出的字節數應當是對應的。為了解釋上面的誤解,我們提供了Demo-1的示例。
       在Demo-1中服務端先向客戶端輸出了兩次,之后刷新了輸出緩沖區。客戶端先向服務端輸出了一次,然后刷新輸出緩沖,之后調用了一次接收操作。從Demo-1源碼以及后面提供的可能出現的結果可以看出,服務端和客戶端的輸入和輸出並不是對應的,有時一次接收操作可以接收對方幾次發過來的信息,並且不是每次輸出操作對方都需要接收處理。當然了Demo-1的代碼是一種錯誤的編寫方式,沒有任何一個程序員希望編寫這樣的代碼。
Demo-1
package com.upc.upcgrid.guan.chapter02;
 
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
 
import org.junit.Test;
 
public class SocketWriteTest {
    public static final int PORT = 12123;
    public static final int BUFFER_SIZE = 1024;
    //服務端代碼
    @Test
    public void server() throws IOException, InterruptedException{
       ServerSocket ss = new ServerSocket(PORT);
       while(true)
       {
           Socket s = ss.accept();
           //這里向網絡進行兩次寫入
           s.getOutputStream().write("hello ".getBytes());
           s.getOutputStream().write("guanxinquan ".getBytes());
           s.getOutputStream().flush();
           s.close();
       }
    }
   
    //客戶端代碼
    @Test
    public void client() throws UnknownHostException, IOException{
       byte[] buffer;
       Socket s = new Socket("localhost",PORT);//創建socket連接
       s.getOutputStream().write(new byte[BUFFER_SIZE]);
       s.getOutputStream().flush();
       int i = s.getInputStream().read(buffer = new byte[BUFFER_SIZE]);
       System.out.println(new String(buffer,0,i));
      
    }
}
Demo-1可能輸出的結果:
結果1:
hello
結果2:
hello guanxinquan
       為了深入理解網絡發送數據的流程,我們需要對Socket的數據緩沖區有所了解。在創建Socket后,系統會為新創建的套接字分配緩沖區空間。這時套接字已經具有了輸入緩沖區和輸出緩沖區。可以通過Demo-2中的方式來獲取和設置緩沖區的大小。緩沖區大小需要根據具體情況進行設置,一般要低於64K(TCP能夠指定的最大負重載數據量,TCP的窗口大小是由16bit來確定的),增大緩沖區可以增大網絡I/O的性能,而減少緩沖區有助於減少傳入數據的backlog(就是緩沖長度,因此提高響應速度)。對於Socket和SeverSocket如果需要指定緩沖區大小,必須在連接之前完成緩沖區的設定。
Demo-2
package com.upc.upcgrid.guan.chapter02;
 
import java.net.Socket;
import java.net.SocketException;
 
public class SocketBufferTest {
    public static void main(String[] args) throws SocketException {
       //創建一個socket
       Socket socket = new Socket();
       //輸出緩沖區大小
       System.out.println(socket.getSendBufferSize());
       System.out.println(socket.getReceiveBufferSize());
       //重置緩沖區大小
       socket.setSendBufferSize(1024*32);
       socket.setReceiveBufferSize(1024*32);
       //再次輸出緩沖區大小
       System.out.println(socket.getSendBufferSize());
       System.out.println(socket.getReceiveBufferSize());     
    }
}
Demo-2的輸出:
8192
8192
32768
32768
       了解了Socket緩沖區的概念后,需要探討一下Socket的可寫狀態和可讀狀態。當輸出緩沖區未滿時,Socket是可寫的(注意,不是對方啟用接收操作后,本地才能可寫,這是錯誤的理解),因此,當套接字被建立時,即處於可寫如的狀態。對於可讀,則是指緩沖區中有接收到的數據,並且這些數據未完成處理。在socket創建時,並不處於可讀狀態,僅當連接的另一方向本套接字的通道寫入數據后,本套接字方能處於可讀狀態(注意,如果對方套接字已經關閉,那么本地套接字將處於可讀狀態,並且每次調用read后,返回的都是-1)。
       現在應用前面的討論,重新分析一下Demo-1的執行流程,服務端與客戶端建立連接后,服務器端先向緩沖區寫入兩條信息,在第一條信息寫入時,緩沖區並未寫滿,因此在第二條信息輸入時,第一條信息很可能還未發送,因此兩條信息可能同時被傳送到客戶端。另一方面,如果在第二條信息寫入時,第一條已經發送出去,那么客戶端的接收操作僅會獲得第一條信息,因為客戶端沒有繼續接收的操作,因此第二條信息在緩沖區中,將不會被讀取,當socket關閉時,緩沖區將被釋放,未被讀取的數據也就變的無效了。如果對方的socket已經關閉,本地再次調用讀取方法,則讀取方法直接返回-1,表示讀到了文件的尾部。
       對於緩沖區空間的設定,要根據具體情況來定,如果存在大量的長信息(比如文件傳輸),將緩沖區定義的大些,可能更好的利用網絡資源,如果更多的是短信息(比如聊天消息),使用小的緩沖區可能更好些,這樣刷新的速度會更快。一般系統默認的緩沖大小是8*1024。除非對自己處理的情況很清晰,否則請不要隨意更改這個設置。
       由於可讀狀態是在對方寫入數據后或socket關閉時才能出現,因此如果客戶端和服務端都停留在read時,如果沒有任何一方,向對方寫入數據,這將會產生一個死鎖。
       此外,在本地接收操作發起之前,很可能接收緩沖區中已經有數據了,這是一種異步。不要誤以為,本地調用接收操作后,對方才會發送數據,實際數據何時到達,本地不能做出任何假設。
       如果想要將多條輸入的信息區分開,可以使用一些技巧,在文件操作中使用-1表示EOF,就是文件的結束,在網絡傳輸中,也可以使用-1表示一條傳輸語句的結束。Demo-3中給出了一個讀取和寫入操作,在客戶端和服務端對稱的使用這兩個類,可以將每一條信息分析出來。Demo-3中並不是將網絡的傳輸同步,而是分析出緩沖中的數據,將以-1為結尾進行數據划分。如果寫聊天程序可以使用類似的模式。
Demo-3
package com.upc.upcgrid.guan.chapter02;
 
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
 
import org.junit.Test;
 
public class SocketWriteTest {
    public static final int PORT = 12123;
    public static final int BUFFER_SIZE = 1024;
   
    //讀取一條傳入的,以-1為結尾的數據
    public class ReadDatas{
       //數據臨時緩沖用
       private List<ByteBuffer> buffers = new ArrayList<ByteBuffer>();
       private Socket socket;//數據的來源
       public ReadDatas(Socket socket) throws IOException {
           this.socket = socket;
       }
      
       public void read() throws IOException
       {
           buffers.clear();//清空上次的讀取狀態
           InputStream in = socket.getInputStream();//獲取輸入流
           int k = 0;
           byte r = 0;
           while(true)
           {
              ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);//新分配一段數據區
              //如果新數據區未滿,並且沒有讀到-1,則繼續讀取
              for(k = 0 ; k < BUFFER_SIZE ; k++)
              {
                  r = (byte) in.read();//讀取一個數據
                  if(r != -1)//數據不為-1,簡單放入緩沖區
                     buffer.put(r);
                  else{//讀取了一個-1,表示這條信息結束
                     buffer.flip();//翻轉緩沖,以備讀取操作
                     buffers.add(buffer);//將當前的buffer添加到緩沖列表
                     return;
                  }
              }
              buffers.add(buffer);//由於緩沖不足,直接將填滿的緩沖放入緩沖列表
 
           }
          
       }
      
      
       public String getAsString()
       {
           StringBuffer str = new StringBuffer();
           for(ByteBuffer buffer: buffers)//遍歷緩沖列表
           {
              str.append(new String(buffer.array(),0,buffer.limit()));//組織字符串
           }
           return str.toString();//返回生成的字符串
       }
    }
   
    //將一條信息寫出給接收端
    public class WriteDatas{
       public Socket socket;//數據接收端
       public WriteDatas(Socket socket,ByteBuffer[] buffers) throws IOException {
           this.socket = socket;
           write(buffers);
       }
      
       public WriteDatas(Socket socket) {
           this.socket = socket;
       }
      
       public  void write(ByteBuffer[] buffers) throws IOException
       {
           OutputStream out = socket.getOutputStream();//獲取輸出流
           for(ByteBuffer buffer:buffers)
           {
              out.write(buffer.array());//將數據輸出到緩沖區
           }
           out.write(new byte[]{-1});//輸出終結符
           out.flush();//刷新緩沖區
          
       }
      
    }
   
    //服務端代碼
    @Test
    public void server() throws IOException, InterruptedException{
       ServerSocket ss = new ServerSocket(PORT);
       while(true)
       {
           Socket s = ss.accept();
          
           //從網絡連續讀取兩條信息
           ReadDatas read = new ReadDatas(s);
           read.read();
           System.out.println(read.getAsString());
           read.read();
           System.out.println(read.getAsString());
           //向網絡中輸出一條信息
           WriteDatas write = new WriteDatas(s);
           write.write(new ByteBuffer[]{ByteBuffer.wrap("welcome to us ! ".getBytes())});
           //關閉套接字
           s.close();
          
       }
    }
   
   
    //客戶端代碼
    @Test
    public void client() throws UnknownHostException, IOException{
       Socket s = new Socket("localhost",PORT);//創建socket連接
       //連續向服務端寫入兩條信息
       WriteDatas write = new WriteDatas(s,new ByteBuffer[]{ByteBuffer.wrap("ni hao guan xin quan ! ".getBytes())} );
       write.write(new ByteBuffer[]{ByteBuffer.wrap("let's study java network !".getBytes())});      
       //從服務端讀取一條信息
       ReadDatas read = new ReadDatas(s);
       read.read();
       System.out.println(read.getAsString());
       //關閉套接字
       s.close();
    }
}
       在Demo-3中的這種消息處理方式過於復雜,需要理解java底層的緩沖區的知識,還需要編程人員完成消息的組合(在消息末尾添加-1),在Java中可以使用一種簡單的方式完成上述的操作,就是使用java DataInputStream和DataOutputStream提供的方法。Demo-4給出了使用java相關流類完成同步的消息的方法(估計他們與我們Demo-3使用的方式是相似的)。你可以查閱java其它API,可以找到其他的方式。
Demo-4
package com.upc.upcgrid.guan.chapter02;
 
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
 
 
import org.junit.Test;
 
public class SocketDataStream {
    public static final int PORT = 12123;
    @Test
    public void server() throws IOException
    {
       ServerSocket ss = new ServerSocket(PORT);
       while(true)
       {
           Socket s = ss.accept();
           DataInputStream in = new DataInputStream(s.getInputStream());
           DataOutputStream out = new DataOutputStream(s.getOutputStream());
          
           out.writeUTF("hello guan xin quan ! ");
           out.writeUTF("let's study java togethor! ");
          
           System.out.println(in.readUTF());
           s.close();
       }
    }
   
    @Test
    public void client() throws UnknownHostException, IOException
    {
       Socket s = new Socket("localhost",PORT);
       DataInputStream in = new DataInputStream(s.getInputStream());
       DataOutputStream out = new DataOutputStream(s.getOutputStream());
      
       System.out.println(in.readUTF());
       System.out.println(in.readUTF());
       out.writeUTF("welcome to java net world ! ");
       s.close();
    }
}
 
簡單總結:
       上面主要介紹了java Socket通信的緩沖區機制,並通過幾個示例讓您對java Socket的工作原理有了簡單了解。這里需要注意的是可讀狀態和可寫狀態,因為這兩個概念將對下一節的內容理解至關重要。下一節將描述java NIO提高服務端的並發性。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM