Java開發筆記(一百一十四)利用Socket傳輸文本消息

前面介紹了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開發筆記(序)章節目錄函數

相關文章
相關標籤/搜索