使用TCP/IP的套接字(Socket)進行通信
套接字Socket的引入
為了能夠方便地開發網絡應用軟件,由美國伯克利大學在Unix上推出了一種應用程序訪問通信協議的操作系統用調用socket(套接字)。
socket的出現,使程序員可以很方便地訪問TCP/IP,從而開發各種網絡應用的程序。
隨着Unix的應用推廣,套接字在編寫網絡軟件中得到了極大的普及。后來,套接字又被引進了Windows等操作系統中。Java語言也引入了套接字編程模型。
什么是Socket?
Socket是連接運行在網絡上的兩個程序間的雙向通訊的端點。
使用Socket進行網絡通信的過程
服務器程序將一個套接字綁定到一個特定的端口,並通過此套接字等待和監聽客戶的連接請求。
客戶程序根據服務器程序所在的主機名和端口號發出連接請求。
如果一切正常,服務器接受連接請求。並獲得一個新的綁定到不同端口地址的套接字。(不可能有兩個程序同時占用一個端口)。
客戶和服務器通過讀寫套接字進行通訊。
使用ServerSocket和Socket實現服務器端和客戶端的Socket通信。
其中:
左邊ServerSocket類的構造方法可以傳入一個端口值來構建對象。
accept()方法監聽向這個socket的連接並接收連接。它將會阻塞直到連接被建立好。連接建立好后它會返回一個Socket對象。
連接建立好后,服務器端和客戶端的輸入流和輸出流就互為彼此,即一端的輸出流是另一端的輸入流。
總結:使用ServerSocket和Socket實現服務器端和客戶端的Socket通信
(1)建立Socket連接
(2)獲得輸入/輸出流
(3)讀/寫數據
(4)關閉輸入/輸出流
(5)關閉Socket
通信程序測試
建立服務器端和客戶端如下:

package com.example.network; import java.net.ServerSocket; import java.net.Socket; public class TcpServer { public static void main(String[] args) throws Exception { // 創建服務器端的socket對象 ServerSocket ss = new ServerSocket(5000); // 監聽連接 Socket socket = ss.accept(); // 直到連接建立好之后代碼才會往下執行 System.out.println("Connected Successfully!"); } }

package com.example.network; import java.net.Socket; public class TcpClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 5000); } }
然后先運行服務器端,再運行客戶端,可以看到,運行客戶端之后輸出服務器端的后續代碼。
表明連接建立后才會往下執行。
一個比較簡陋的通信程序:

package com.example.network; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class TcpServer { public static void main(String[] args) throws Exception { // 創建服務器端的socket對象 ServerSocket ss = new ServerSocket(5000); // 監聽連接 Socket socket = ss.accept(); // 直到連接建立好之后代碼才會往下執行 System.out.println("Connected Successfully!"); // 獲得服務器端的輸入流,從客戶端接收信息 InputStream is = socket.getInputStream(); // 服務器端的輸出流,向客戶端發送信息 OutputStream os = socket.getOutputStream(); byte[] buffer = new byte[200]; int length = 0; length = is.read(buffer); String str = new String(buffer, 0, length); System.out.println(str); // 服務器端的輸出 os.write("Welcome".getBytes()); // 關閉資源 is.close(); os.close(); socket.close(); } }

package com.example.network; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class TcpClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 5000); // 客戶端的輸出流 OutputStream os = socket.getOutputStream(); // 將信息寫入流,把這個信息傳遞給服務器 os.write("hello world".getBytes()); // 從服務器端接收信息 InputStream is = socket.getInputStream(); byte[] buffer = new byte[200]; int length = is.read(buffer); String str = new String(buffer, 0, length); System.out.println(str); // 關閉資源 is.close(); os.close(); socket.close(); } }
先運行服務器,再運行客戶端。之后可以在服務器和客戶端的控制台上進行輸入操作,另一端將會收到輸入的信息並輸出。
使用線程實現服務器端與客戶端的雙向通信
用兩個線程,一個線程專門用於處理服務器端的讀,另一個線程專門用於處理服務器端的寫。
客戶端同理。
代碼如下,程序共有六個類。
服務器端和其輸入輸出線程:

package com.example.network; import java.net.ServerSocket; import java.net.Socket; public class MainServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(4000); while (true) { // 一直處於監聽狀態,這樣可以處理多個用戶 Socket socket = serverSocket.accept(); // 啟動讀寫線程 new ServerInputThread(socket).start(); new ServerOutputThread(socket).start(); } } }

package com.example.network; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class ServerInputThread extends Thread { private Socket socket; public ServerInputThread(Socket socket) { super(); this.socket = socket; } @Override public void run() { try { // 獲得輸入流 InputStream is = socket.getInputStream(); while (true) { byte[] buffer = new byte[1024]; int length = is.read(buffer); String str = new String(buffer, 0, length); System.out.println(str); } } catch (IOException e) { e.printStackTrace(); } } }

package com.example.network; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; public class ServerOutputThread extends Thread { private Socket socket; public ServerOutputThread(Socket socket) { super(); this.socket = socket; } @Override public void run() { try { OutputStream os = socket.getOutputStream(); while (true) { BufferedReader reader = new BufferedReader( new InputStreamReader(System.in)); String line = reader.readLine(); os.write(line.getBytes()); } } catch (IOException e) { e.printStackTrace(); } } }
客戶端和其輸入輸出線程(其輸入輸出線程和服務器端的完全一樣):

package com.example.network; import java.net.Socket; public class MainClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 4000); new ClientInputThread(socket).start(); new ClientOutputThread(socket).start(); } }

package com.example.network; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class ClientInputThread extends Thread { private Socket socket; public ClientInputThread(Socket socket) { super(); this.socket = socket; } @Override public void run() { try { // 獲得輸入流 InputStream is = socket.getInputStream(); while (true) { byte[] buffer = new byte[1024]; int length = is.read(buffer); String str = new String(buffer, 0, length); System.out.println(str); } } catch (IOException e) { e.printStackTrace(); } } }

package com.example.network; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; public class ClientOutputThread extends Thread { private Socket socket; public ClientOutputThread(Socket socket) { super(); this.socket = socket; } @Override public void run() { try { OutputStream os = socket.getOutputStream(); while (true) { BufferedReader reader = new BufferedReader( new InputStreamReader(System.in)); String line = reader.readLine(); os.write(line.getBytes()); } } catch (IOException e) { e.printStackTrace(); } } }
經測試成功。即從服務器端控制台輸入,可以從客戶端接收到並輸出;也可以反過來,從客戶端控制台輸入,那么服務器端會同時輸出。
多個客戶端的程序實驗
可以啟動多個客戶端,同時與服務器進行交互。這里還是采用上面的MainServer和MainClient及其輸入輸出線程代碼。
這部分做實驗的時候需要使用命令行,因為Eclipse里面每次Run的時候都會重新啟動程序,即想要Run第二個客戶端的時候總是先關閉第一個客戶端(因為它們運行的是同一個程序),這樣,即只能有一個客戶端存在。
在命令行運行的方法如下:
因為源文件帶有包名,所以編譯采用:
javac –d . 源文件名.java
注意d和.之間有一個空格。
可以使用通配符編譯所有的源文件,即使用:
javac –d . *.java
編譯之后執行:
java 完整包名+類名
先啟動服務器程序,之后新開命令行窗口啟動客戶端程序,結果如下:
(一個客戶端時交互正常)
(多個客戶端交互異常)
經實驗,發現在一個服務器多個客戶端的情況下,客戶端可以流暢地向服務器發送信息,但是當服務器發送信息時,就會出現問題,並不是每一個客戶端都能收到信息。
如圖中,當服務器發送語句時,第一個客戶端收到了(並且是發送后多按下一個回車才收到),第二個客戶端沒有收到。
后面試驗了幾個語句都是這樣:
實現服務器支持多客戶機通信
服務器端的程序需要為每一個與客戶機連接的socket建立一個線程,來解決同時通信的問題。
服務器端應該管理一個socket的集合。
即要完成一個功能完善的客戶端和服務器通信程序,代碼還是需要進一步完善的。
參考資料
聖思園張龍老師Java SE系列視頻教程。