標籤:
Socket,又稱為通訊端,Socket是電腦網路通訊的基本的技術之一。如今大多數基於網路的軟體,如瀏覽器,即時通訊工具甚至是P2P下載都是基於Socket實現的。本文會介紹一下基於TCP/IP的Socket編程,並且如何寫一個用戶端/伺服器程式。
餐前甜點
Unix的輸入輸出(IO)系統遵循Open-Read-Write-Close這樣的操作範本。當一個使用者進程進行IO操作之前,它需要調用Open來指定並擷取待操作檔案或裝置讀取或寫入的許可權。一旦IO操作對象被開啟,那麼這個使用者進程可以對這個對象進行一次或多次的讀取或寫入操作。Read操作用來從IO操作對象讀取資料,並將資料傳遞給使用者進程。Write操作用來將使用者進程中的資料傳遞(寫入)到IO操作對象。 當所有的Read和Write操作結束之後,使用者進程需要調用Close來通知系統其完成對IO對象的使用。
在Unix開始支援處理序間通訊(InterProcess Communication,簡稱IPC)時,IPC的介面就設計得類似檔案IO操作介面。在Unix中,一個進程會有一套可以進行讀取寫入的IO描述符。IO描述符可以是檔案,裝置或者是通訊通道(socket通訊端)。一個檔案描述符由三部分組成:建立(開啟socket),讀取寫入資料(接受和發送到socket)還有銷毀(關閉socket)。
在Unix系統中,類BSD版本的IPC介面是作為TCP和UDP協議之上的一層進行實現的。訊息的目的地使用socket地址來表示。一個socket地址是由網路地址和連接埠號碼組成的通訊標識符。
處理序間通訊操作需要一對兒socket。處理序間通訊通過在一個進程中的一個socket與另一個進程中得另一個socket進行資料轉送來完成。當一個訊息執行發出後,這個訊息在發送端的socket中處於排隊狀態,直到下層的網路通訊協定將這些訊息發送出去。當訊息到達接收端的socket後,其也會處於排隊狀態,直到接收端的進程對這條訊息進行了接收處理。
TCP和UDP通訊
關於socket編程我們有兩種通訊協定可以進行選擇。一種是資料報通訊,另一種就是流通訊。
資料報通訊
資料報通訊協定,就是我們常說的UDP(User Data Protocol 使用者資料包通訊協定)。UDP是一種不需連線的協議,這就意味著我們每次發送資料報時,需要同時發送原生socket描述符和接收端的socket描述符。因此,我們在每次通訊時都需要發送額外的資料。
流通訊
流通訊協定,也叫做TCP(Transfer Control Protocol,傳輸控制通訊協定)。和UDP不同,TCP是一種基於串連的協議。在使用流通訊之前,我們必須在通訊的一對兒socket之間建立串連。其中一個socket作為伺服器進行監聽串連請求。另一個則作為用戶端進行串連請求。一旦兩個socket建立好了串連,他們可以單向或雙向進行資料轉送。
讀到這裡,我們多少有這樣的疑問,我們進行socket編程使用UDP還是TCP呢。選擇基於何種協議的socket編程取決於你的具體的用戶端-伺服器端程式的應用情境。下面我們簡單分析一下TCP和UDP協議的區別,或許可以協助你更好地選擇使用哪種。
在UDP中,每次發送資料報時,需要附帶上原生socket描述符和接收端的socket描述符。而由於TCP是基於串連的協議,在通訊的socket對之間需要在通訊之前建立串連,因此會有建立串連這一耗時存在於TCP協議的socket編程。
在UDP中,資料報資料在大小上有64KB的限制。而TCP中也不存在這樣的限制。一旦TCP通訊的socket對建立了串連,他們之間的通訊就類似IO流,所有的資料會按照接受時的順序讀取。
UDP是一種不可靠的協議,發送的資料報不一定會按照其發送順序被接收端的socket接受。然後TCP是一種可靠的協議。接收端收到的包的順序和包在發送端的順序是一致的。
簡而言之,TCP適合於諸如遠程登入(rlogin,telnet)和檔案傳輸(FTP)這類的網路服務。因為這些需要傳輸的資料的大小不確定。而UDP相比TCP更加簡單輕量一些。UDP用來實現即時性較高或者丟包不重要的一些服務。在區域網路中UDP的丟包率都相對比較低。
Java中的socket編程
下面的部分我將通過一些樣本講解一下如何使用socket編寫用戶端和伺服器端的程式。
注意:在接下來的樣本中,我將使用基於TCP/IP協議的socket編程,因為這個協議遠遠比UDP/IP使用的要廣泛。並且所有的socket相關的類都位於java.net包下,所以在我們進行socket編程時需要引入這個包。
用戶端編寫
開啟Socket
如果在用戶端,你需要寫下如下的代碼就可以開啟一個socket。
String host = "127.0.0.1";int port = 8919;Socket client = new Socket(host, port);
上面代碼中,host即用戶端需要串連的機器,port就是伺服器端用來監聽請求的連接埠。在選擇連接埠時,需要注意一點,就是0~1023這些連接埠都已經被系統預留了。這些連接埠為一些常用的服務所使用,比如郵件,FTP和HTTP。當你在編寫伺服器端的代碼,選擇連接埠時,請選擇一個大於1023的連接埠。
寫入資料
接下來就是寫入請求資料,我們從用戶端的socket對象中得到OutputStream對象,然後寫入資料後。很類似檔案IO的處理代碼。
public class ClientSocket { public static void main(String args[]) { String host = "127.0.0.1"; int port = 8919; try { Socket client = new Socket(host, port); Writer writer = new OutputStreamWriter(client.getOutputStream()); writer.write("Hello From Client"); writer.flush(); writer.close(); client.close(); } catch (IOException e) { e.printStackTrace(); } } }
關閉IO對象
類似檔案IO,在讀寫資料完成後,我們需要對IO對象進行關閉,以確保資源的正確釋放。
伺服器端編寫
開啟伺服器端的socket
int port = 8919;ServerSocket server = new ServerSocket(port);Socket socket = server.accept();
上面的代碼建立了一個伺服器端的socket,然後調用accept方法監聽並擷取用戶端的請求socket。accept方法是一個阻塞方法,在伺服器端與用戶端之間建立聯絡之前會一直等待阻塞。
讀取資料
通過上面得到的socket對象擷取InputStream對象,然後安裝檔案IO一樣讀取資料即可。這裡我們將內容列印出來。
public class ServerClient { public static void main(String[] args) { int port = 8919; try { ServerSocket server = new ServerSocket(port); Socket socket = server.accept(); Reader reader = new InputStreamReader(socket.getInputStream()); char chars[] = new char[1024]; int len; StringBuilder builder = new StringBuilder(); while ((len=reader.read(chars)) != -1) { builder.append(new String(chars, 0, len)); } System.out.println("Receive from client message=: " + builder); reader.close(); socket.close(); server.close(); } catch (Exception e) { e.printStackTrace(); } }}
關閉IO對象
還是不能忘記的,最後需要正確地關閉IO對象,以確保資源的正確釋放。
附註一個例子
這裡我們增加一個例子,使用socket實現一個回聲伺服器,就是伺服器會將用戶端發送過來的資料傳回給用戶端。代碼很簡單。
import java.io.*;import java.net.*;public class EchoServer { public static void main(String args[]) { // declaration section: // declare a server socket and a client socket for the server // declare an input and an output stream ServerSocket echoServer = null; String line; DataInputStream is; PrintStream os; Socket clientSocket = null; // Try to open a server socket on port 9999 // Note that we can‘t choose a port less than 1023 if we are not // privileged users (root) try { echoServer = new ServerSocket(9999); } catch (IOException e) { System.out.println(e); } // Create a socket object from the ServerSocket to listen and accept // connections. // Open input and output streams try { clientSocket = echoServer.accept(); is = new DataInputStream(clientSocket.getInputStream()); os = new PrintStream(clientSocket.getOutputStream()); // As long as we receive data, echo that data back to the client. while (true) { line = is.readLine(); os.println(line); } } catch (IOException e) { System.out.println(e); } }}
編譯運行上面的代碼,進行如下請求,就可以看到用戶端請求攜帶的資料的內容。
15:00 $ curl http://127.0.0.1:9999/?111GET /?111 HTTP/1.1User-Agent: curl/7.37.1Host: 127.0.0.1:9999Accept: */*
總結
進行用戶端-伺服器端編程還是比較有趣的,同時在Java中進行socket編程要比其他語言(如C)要簡單快速編寫。
java.net這個包裡麵包含了很多強大靈活的類供開發人員進行網路編程,在進行網路編程中,建議使用這個包下面的API。同時Sun.*這個包也包含了很多的網路編程相關的類,但是不建議使用這個包下面的API,因為這個包可能會改變,另外這個包不能保證在所有的平台都有包含。
原文地址:Sockets programming in Java: A tutorial
QQ群: WEB開發人員官方總群(321386971) 驗證訊息:Admin10000
讀懂Java中的Socket編程