Socket,又稱爲套接字,Socket是計算機網絡通訊的基本的技術之一。現在大多數基於網絡的軟件,如瀏覽器,即時通信工具甚至是P2P下載都是基於Socket實現的。本文會介紹一下基於TCP/IP的Socket編程,而且如何寫一個客戶端/服務器程序。java
Unix的輸入輸出(IO)系統遵循Open-Read-Write-Close這樣的操做範本。當一個用戶進程進行IO操做以前,它須要調用Open來指定並獲取待操做文件或設備讀取或寫入的權限。一旦IO操做對象被打開,那麼這個用戶進程能夠對這個對象進行一次或屢次的讀取或寫入操做。Read操做用來從IO操做對象讀取數據,並將數據傳遞給用戶進程。Write操做用來將用戶進程中的數據傳遞(寫入)到IO操做對象。 當全部的Read和Write操做結束以後,用戶進程須要調用Close來通知系統其完成對IO對象的使用。web
在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後,其也會處於排隊狀態,直到接收端的進程對這條消息進行了接收處理。服務器
關於socket編程咱們有兩種通訊協議能夠進行選擇。一種是數據報通訊,另外一種就是流通訊。網絡
數據報通訊協議,就是咱們常說的UDP(User Data Protocol 用戶數據報協議)。UDP是一種無鏈接的協議,這就意味着咱們每次發送數據報時,須要同時發送本機的socket描述符和接收端的socket描述符。所以,咱們在每次通訊時都須要發送額外的數據。app
流通訊協議,也叫作TCP(Transfer Control Protocol,傳輸控制協議)。和UDP不一樣,TCP是一種基於鏈接的協議。在使用流通訊以前,咱們必須在通訊的一對兒socket之間創建鏈接。其中一個socket做爲服務器進行監聽鏈接請求。另外一個則做爲客戶端進行鏈接請求。一旦兩個socket創建好了鏈接,他們能夠單向或雙向進行數據傳輸。less
讀到這裏,咱們多少有這樣的疑問,咱們進行socket編程使用UDP仍是TCP呢。選擇基於何種協議的socket編程取決於你的具體的客戶端-服務器端程序的應用場景。下面咱們簡單分析一下TCP和UDP協議的區別,或許能夠幫助你更好地選擇使用哪一種。curl
在UDP中,每次發送數據報時,須要附帶上本機的socket描述符和接收端的socket描述符。而因爲TCP是基於鏈接的協議,在通訊的socket對之間須要在通訊以前創建鏈接,所以會有創建鏈接這一耗時存在於TCP協議的socket編程。socket
在UDP中,數據報數據在大小上有64KB的限制。而TCP中也不存在這樣的限制。一旦TCP通訊的socket對創建了鏈接,他們之間的通訊就相似IO流,全部的數據會按照接受時的順序讀取。
UDP是一種不可靠的協議,發送的數據報不必定會按照其發送順序被接收端的socket接受。而後TCP是一種可靠的協議。接收端收到的包的順序和包在發送端的順序是一致的。
簡而言之,TCP適合於諸如遠程登陸(rlogin,telnet)和文件傳輸(FTP)這類的網絡服務。由於這些須要傳輸的數據的大小不肯定。而UDP相比TCP更加簡單輕量一些。UDP用來實現實時性較高或者丟包不重要的一些服務。在局域網中UDP的丟包率都相對比較低。
下面的部分我將經過一些示例講解一下如何使用socket編寫客戶端和服務器端的程序。
注意:在接下來的示例中,我將使用基於TCP/IP協議的socket編程,由於這個協議遠遠比UDP/IP使用的要普遍。而且全部的socket相關的類都位於java.net包下,因此在咱們進行socket編程時須要引入這個包。
若是在客戶端,你須要寫下以下的代碼就能夠打開一個socket。
123 |
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的處理代碼。
1234567891011121314151617 |
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對象進行關閉,以確保資源的正確釋放。
123 |
int port = 8919;ServerSocket server = new ServerSocket(port);Socket socket = server.accept(); |
上面的代碼建立了一個服務器端的socket,而後調用accept方法監聽並獲取客戶端的請求socket。accept方法是一個阻塞方法,在服務器端與客戶端之間創建聯繫以前會一直等待阻塞。
經過上面獲得的socket對象獲取InputStream對象,而後安裝文件IO同樣讀取數據便可。這裏咱們將內容打印出來。
12345678910111213141516171819202122 |
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對象,以確保資源的正確釋放。
這裏咱們增長一個例子,使用socket實現一個回聲服務器,就是服務器會將客戶端發送過來的數據傳回給客戶端。代碼很簡單。
1234567891011121314151617181920212223242526272829303132333435363738 |
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); } }} |
編譯運行上面的代碼,進行以下請求,就能夠看到客戶端請求攜帶的數據的內容。
12345 |
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,由於這個包可能會改變,另外這個包不能保證在全部的平臺都有包含。