前面介紹了HTTP協議的網絡通訊,包括接口調用、文件下載和文件上傳,這些功能當然已經覆蓋了常見的聯網操做,但是HTTP協議擁有專門的通訊規則,這些規則一方面有利於維持正常的數據交互,另外一方面不可避免地缺乏靈活性,好比下列條條框框就難以逾越:
一、HTTP鏈接屬於短鏈接,每次訪問操做結束以後,客戶端便會關閉本次鏈接。下次還想訪問接口的話,就得從新創建鏈接,要是頻繁發生數據交互的話,反覆的鏈接和斷開將形成大量的資源消耗。
二、在HTTP鏈接中,服務端老是被動接收消息,沒法主動向客戶端推送消息。假若客戶端不去請求服務端,服務端就無法發送即時消息。
三、每次HTTP調用都屬於客戶端與服務端之間的一對一交互,徹底與第三者無關(好比另外一個客戶端),這種技術手段沒法知足相似QQ聊天那種羣發消息的要求。
四、HTTP鏈接須要搭建專門的HTTP服務器,這樣的服務端比較重,不適合兩個設備終端之間的簡單信息傳輸。
誠然HTTP協議作不到如此靈活多變的地步,勢必要在更基礎的層次去實現變化無窮的場景。在Java編程中,網絡通訊的基本操做單元實際上是套接字Socket,它自己不是什麼協議,而是一種支持TCP/IP協議的通訊接口。建立Socket鏈接的時候,容許指定當前的傳輸層協議,當Socket鏈接的雙方握手確認連上以後,此時採用的是TCP協議;當Socket鏈接的雙方未確認連上就自顧自地發送數據,此時採用的是UDP協議。在TCP協議的實現過程當中,每次創建Socket鏈接至少須要一對套接字,其中一個運行於客戶端,用的是Socket類;另外一個運行於服務端,用的是ServerSocket類。
Socket工具雖然主要用於客戶端,但服務端一般也保留一份客戶端的Socket備份,它描述了兩邊對套接字處理的通常行爲。下面是Socket類的主要方法說明:
connect:鏈接指定IP和端口。該方法用於客戶端鏈接服務端,成功連上以後才能開展數據交互。
getInputStream:獲取套接字的輸入流,輸入流用於接收對方發來的數據。
getOutputStream:獲取套接字的輸出流,輸出流用於向對方發送數據。
isConnected:判斷套接字是否連上。
close:關閉套接字。套接字關閉以後將沒法再傳輸數據。
isClosed:判斷套接字是否關閉。html
ServerSocket僅用於服務端,它的構造函數可指定偵聽指定端口,從而及時響應客戶端的鏈接請求。下面是ServerSocket的主要方法說明:
accept:開始接收客戶端的鏈接。一旦有客戶端連上,就返回該客戶端的套接字對象。若要持續偵聽鏈接,得在循環語句中調用該方法。
close:關閉服務端的套接字。
isClosed:判斷服務端的套接字是否關閉。編程
因爲套接字屬於長鏈接,只要鏈接的雙方未調用close方法,也沒退出程序運行,那麼理論上都處於已鏈接的狀態。既然是長時間鏈接,在此期間的任什麼時候刻均可能發送和接收數據,爲此套接字的客戶端須要給每一個鏈接分配兩個線程,其中一個線程專門用來向服務端發送信息,而另外一個線程專門用於從服務端接收信息。而服務端須要循環調用accept方法,以便持續偵聽客戶端的套接字請求,一旦接到某個客戶端的鏈接請求,就開啓一個分線程單獨處理該客戶端的信息交互。
接下來看個利用Socket傳輸文本消息的例子,爲方便起見,每次只傳輸一行文本。因爲要求I/O流支持讀寫一行文本,所以採用的輸入流成員爲緩存讀取器BufferedReader,輸出流成員爲打印流PrintStream,其中前者的readLine方法可以讀出一行文本,後者的println方法可以寫入一行文本。據此編寫的套接字客戶端主要代碼示例以下:緩存
//定義一個文本發送任務 public class SendText implements Runnable { // 如下爲Socket服務器的IP和端口,根據實際狀況修改 private static final String SOCKET_IP = "192.168.1.8"; private static final int TEXT_PORT = 51000; // 文本傳輸專用端口 private BufferedReader mReader; // 聲明一個緩存讀取器對象 private PrintStream mWriter; // 聲明一個打印流對象 private String mRequest = ""; // 待發送的文本內容 @Override public void run() { Socket socket = new Socket(); // 建立一個套接字對象 try { // 命令套接字鏈接指定地址的指定端口,超時時間爲3秒 socket.connect(new InetSocketAddress(SOCKET_IP, TEXT_PORT), 3000); // 根據套接字的輸入流構建緩存讀取器 mReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 根據套接字的輸出流構建打印流對象 mWriter = new PrintStream(socket.getOutputStream()); // 利用Lambda表達式簡化Runnable代碼。啓動一條子線程從服務器讀取文本消息 new Thread(() -> handleRecv()).start(); } catch (Exception e) { e.printStackTrace(); } } // 發送文本消息 public void sendText(String text) { mRequest = text; // 利用Lambda表達式簡化Runnable代碼。啓動一條子線程向服務器發送文本消息 new Thread(() -> handleSend(text)).start(); } // 處理文本發送事件。爲了不多線程併發產生衝突,這裏添加了synchronized使之成爲同步方法 private synchronized void handleSend(String text) { PrintUtils.print("向服務器發送消息:"+text); try { mWriter.println(text); // 往打印流對象中寫入文本消息 } catch (Exception e) { e.printStackTrace(); } } // 處理文本接收事件。爲了不多線程併發產生衝突,這裏添加了synchronized使之成爲同步方法 private synchronized void handleRecv() { try { String response; // 持續從服務器讀取文本消息 while ((response = mReader.readLine()) != null) { PrintUtils.print("服務器返回消息:"+response); } } catch (Exception e) { e.printStackTrace(); } } }
至於套接字的服務端,在accept方法偵聽到客戶端鏈接以後,使用的I/O流依然爲緩存讀取器BufferedReader與打印流PrintStream,爲方便觀察客戶端和服務端的交互過程,服務端準備在接收客戶端消息以後馬上返回一行文本,從而告知客戶端已經收到消息了。據此編寫的套接字服務端主要代碼示例以下:服務器
//定義一個文本接收任務 public class ReceiveText implements Runnable { private static final int TEXT_PORT = 51000; // 文本傳輸專用端口 @Override public void run() { PrintUtils.print("接收文本的Socket服務已啓動"); try { // 建立一個服務端套接字,用於監聽客戶端Socket的鏈接請求 ServerSocket server = new ServerSocket(TEXT_PORT); while (true) { // 持續偵聽客戶端的鏈接 // 收到了某個客戶端的Socket鏈接請求,並得到該客戶端的套接字對象 Socket socket = server.accept(); // 啓動一個服務線程負責與該客戶端的交互操做 new Thread(new ServerTask(socket)).start(); } } catch (Exception e) { e.printStackTrace(); } } // 定義一個伺候任務,好生招待這位顧客 private class ServerTask implements Runnable { private Socket mSocket; // 聲明一個套接字對象 private BufferedReader mReader; // 聲明一個緩存讀取器對象 public ServerTask(Socket socket) throws IOException { mSocket = socket; // 根據套接字的輸入流構建緩存讀取器 mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); } @Override public void run() { try { String request; // 循環不斷地從Socket中讀取客戶端發送過來的文本消息 while ((request = mReader.readLine()) != null) { PrintUtils.print("收到客戶端消息:" + request); // 根據套接字的輸出流構建打印流對象 PrintStream ps = new PrintStream(mSocket.getOutputStream()); String response = "hi,很高興認識你"; PrintUtils.print("服務端返回消息:" + response); ps.println(response); // 往打印流對象中寫入文本消息 } } catch (Exception e) { e.printStackTrace(); } } } }
接着服務端程序開啓Socket專用的文本接收線程,線程啓動代碼以下所示:網絡
// 啓動一個文本接收線程 new Thread(new ReceiveText()).start();
而後客戶端程序也開啓Socket鏈接的文本發送線程,並命令該線程前後發送兩條文本消息,消息發送代碼以下所示:多線程
// 發送文本消息 private static void testSendText() { SendText task = new SendText(); // 建立一個文本發送任務 new Thread(task).start(); // 爲文本發送任務開啓分線程 task.sendText("你好呀"); // 命令該線程發送文本消息 task.sendText("Hello World"); // 命令該線程發送文本消息 }
最後完整走一遍流程,先運行服務端的測試程序,再運行客戶端的測試程序,觀察到的客戶端日誌以下:併發
12:41:15.967 Thread-3 向服務器發送消息:Hello World 12:41:15.972 Thread-2 服務器返回消息:hi,很高興認識你
同時觀察到下面的服務端日誌:socket
12:40:12.543 Thread-0 接收文本的Socket服務已啓動 12:41:15.970 Thread-1 收到客戶端消息:Hello World 12:41:15.971 Thread-1 服務端返回消息:hi,很高興認識你
根據以上的客戶端日誌以及服務端日誌,可知經過Socket成功實現了文本傳輸功能。ide
更多Java技術文章參見《Java開發筆記(序)章節目錄》函數