在開發即時通訊軟件時,輸入流(接收訊息)與輸出流(發送訊息)必須獨立運行。若兩者共用單一線程,程式會陷入「發送後必須等待接收」的阻塞狀態,導致用戶無法同時收發訊息,嚴重影響體驗。引入多線程正是為了解決這一核心衝突。
觀察以下單線程版客戶端代碼:
while(true) {
// 1. 從控制台讀取用戶輸入(阻塞)
String msg = console.readLine();
dos.writeUTF(msg); dos.flush();
// 2. 等待服務器回覆(阻塞)
String info = dis.readUTF();
System.out.println(info);
}
此處存在明顯的串行依賴:用戶必須先輸入內容,程式才會嘗試接收服務器訊息。若對方主動發送訊息,本地客戶端卻卡在 `readLine()` 等待用戶輸入,導致訊息無法即時顯示。
將收發邏輯拆分為兩個獨立線程:
public class Client {
public static void main(String[] args) throws IOException {
Socket client = new Socket("localhost", 8888);
new Thread(new Send(client)).start();
new Thread(new Receive(client)).start();
}
}
public class Send implements Runnable {
private BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
private DataOutputStream dos;
private volatile boolean isRunning = true;
public Send(Socket client) {
try { dos = new DataOutputStream(client.getOutputStream()); }
catch (IOException e) { close(); }
}
private String getMsgFromConsole() {
try { return console.readLine(); }
catch (IOException e) { return ""; }
}
private void send() {
String msg = getMsgFromConsole();
if (msg != null && !msg.isEmpty()) {
try { dos.writeUTF(msg); dos.flush(); }
catch (IOException e) { close(); }
}
}
@Override public void run() {
while (isRunning) send();
}
private void close() {
isRunning = false;
CloseUtil.closeAll(dos, console);
}
}
public class Receive implements Runnable {
private DataInputStream dis;
private volatile boolean isRunning = true;
public Receive(Socket client) {
try { dis = new DataInputStream(client.getInputStream()); }
catch (IOException e) { close(); }
}
private String receive() {
try { return dis.readUTF(); }
catch (IOException e) { close(); return ""; }
}
@Override public void run() {
while (isRunning) System.out.println(receive());
}
private void close() {
isRunning = false;
CloseUtil.closeAll(dis);
}
}
public class CloseUtil {
public static void closeAll(Closeable... ios) {
for (Closeable io : ios) {
if (io != null) try { io.close(); } catch (IOException ignored) {}
}
}
}
服務端同樣採用阻塞式 I/O,接收後原樣返回並加上前綴:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
Socket client = server.accept();
DataInputStream dis = new DataInputStream(client.getInputStream());
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
while (true) {
String msg = dis.readUTF();
System.out.println(msg);
dos.writeUTF("服務器===》" + msg);
dos.flush();
}
}
}
這就是經典的「一連接雙線程」模型,也是所有即時通訊客戶端的基石架構。