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提高服務端的並發性。