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


前面介紹了HTTP協議的網絡通信,包括接口調用、文件下載和文件上傳,這些功能固然已經覆蓋了常見的聯網操作,可是HTTP協議擁有專門的通信規則,這些規則一方面有利於維持正常的數據交互,另一方面不可避免地缺少靈活性,比如下列條條框框就難以逾越:
1、HTTP連接屬於短連接,每次訪問操作結束之后,客戶端便會關閉本次連接。下次還想訪問接口的話,就得重新建立連接,要是頻繁發生數據交互的話,反復的連接和斷開將造成大量的資源消耗。
2、在HTTP連接中,服務端總是被動接收消息,無法主動向客戶端推送消息。倘若客戶端不去請求服務端,服務端就沒法發送即時消息。
3、每次HTTP調用都屬於客戶端與服務端之間的一對一交互,完全與第三者無關(比如另一個客戶端),這種技術手段無法滿足類似QQ聊天那種群發消息的要求。
4、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:判斷套接字是否關閉。

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,很高興認識你

 

同時觀察到下面的服務端日志:

12:40:12.543 Thread-0 接收文本的Socket服務已啟動
12:41:15.970 Thread-1 收到客戶端消息:Hello World
12:41:15.971 Thread-1 服務端返回消息:hi,很高興認識你

 

根據以上的客戶端日志以及服務端日志,可知通過Socket成功實現了文本傳輸功能。



更多Java技術文章參見《Java開發筆記(序)章節目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM