Java Socket实现局域网聊天的核心原理与代码解析
利用Java Socket技术构建局域网聊天程序,涉及服务端等待连接、客户端发起请求以及双方独立的数据收发线程。通过ServerSocket绑定端口建立会话,并使用BufferedReader和BufferedWriter封装字节流实现字符交换,确保消息实时同步。
架构设计与运行环境
Socket通信的核心在于建立长连接后的双向数据传输。本示例采用经典的C/S(客户端/服务器)模式,具体逻辑如下:
- 服务端 (RunServer):启动后监听特定端口(如
8888),阻塞等待客户端连接。连接建立后,单独开启一个读取线程接收来自客户端的消息。 - 客户端 (RunClient):通过IP地址连接服务器。成功后,开启一个写入线程向服务器发送消息。同时为了保持服务端不堵塞,通常也需要在服务器端或客户端配合处理并发。
- 并发模型:题目要求“可以同时发送消息”,因此在实际应用中,接收和发送应分别由不同的Thread对象管理。本示例展示了基本的流式数据读写,实际生产环境需更完善的异常处理和断开检测。
第1步:搭建服务端监听与接收
服务端的主要任务是创建服务套接字并接管已建立的连接通道。
- 初始化端口:使用
ServerSocket(8888)绑定本地端口。 - 等待连接:调用
serverSocekt.accept()方法,该方法会阻塞当前线程直到有客户端接入。 - 数据流配置:获取连接对象的输入流,将其包装为
InputStreamReader和BufferedReader以支持按行读取文本。
package com.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class RunServer {
// 全局变量记录当前连接的对象
public static Socket socket = null;
/**
* 建立服务器被动连接并开启接收循环
*/
public void startServer() throws IOException {
// 1. 创建服务套接字,监听8888端口
ServerSocket serverSocekt = new ServerSocket(8888);
System.out.println("服务器启动,等待连接...");
// 注:此处原逻辑将接收线程的实例化放在accept之前,
// 但标准流程通常是先 accept 拿到 socket 再处理。
// 原代码结构中 rc 未直接使用,主要依赖主线程进行读取。
// 2. 阻塞等待客户端连接
RunServer.socket = serverSocekt.accept();
System.out.println("客户端已连接");
// 3. 配置输入流
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String str = null;
// 4. 循环读取消息
while ((str = br.readLine()) != null) {
System.out.print("输出信息:" + str);
}
// 5. 资源清理
socket.shutdownInput();
serverSocekt.close();
}
}
第2步:编写客户端连接与发送逻辑
客户端负责主动发起TCP握手,并在连接建立后进行数据的持续推送。
- 用户交互选择:提供菜单选项,确定是主动连接指定IP,还是执行其他操作(本示例中仅保留了主动连接逻辑,因被动等待需复杂的状态机)。这里直接演示
startClient(String ip)路径。 - 建立连接:使用
new Socket(ip, 8888)发起请求。 - 写入流配置:通过
getOutputStream()获取输出流,包装为BufferedWriter。
package com.client;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端实现Runnable接口以便作为线程运行
*/
public class RunClient implements Runnable {
@Override
public void run() {
Scanner input = new Scanner(System.in);
// 简化逻辑:直接演示主动连接
System.out.println("请输入服务器IP:");
try {
startClient(input.next());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通过指定的IP与服务器建立连接并持续发送消息
*/
public void startClient(String ip) throws Exception {
// 1. 发起Socket连接
Socket socket = new Socket(ip, 8888);
System.out.println("连接建立成功...");
// 2. 配置输出流
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
Scanner input = new Scanner(System.in);
System.out.println("准备发送消息,输入任意内容回车发送,输入exit退出。");
// 3. 循环写入
while (true) {
String msg = input.nextLine();
if ("exit".equals(msg)) {
break;
}
bw.write(msg);
bw.newLine(); // 必须换行,否则服务器端的readLine无法捕获结束符
bw.flush(); // 强制刷新缓冲区
}
// 资源关闭建议在实际代码中补充
bw.close();
socket.close();
}
}
第3步:主入口启动与注意事项
在主程序入口实例化服务端并调用启动方法。此时服务端处于阻塞监听状态。
package com.start;
import com.server.RunServer;
public class Go {
public static void main(String[] args) throws Exception {
// 启动服务端
RunServer rs = new RunServer();
rs.startServer();
}
}
在实际测试时,建议使用一台物理机和一台虚拟机,或者两个不同的终端窗口模拟不同节点。如果完全在同一台机器上,且未正确区分进程间的内存隔离,可能会因为回环接口(Loopback)的特性导致消息自发自收。
关键技术点总结
- 缓冲区的必要性:直接使用
InputStream读取二进制字节非常繁琐,通过BufferedReader可以方便地以行(Line)为单位处理文本协议消息。 - flush() 的重要性:客户端写入时必须调用
bw.flush()或设置自动刷新,否则数据会滞留缓冲区,服务器端将无法即时收到消息。 - 并发处理的缺失提醒:上述基础代码主要侧重于流连接的打通。要实现“一边收一边发”的完美体验,客户端和服务端均需各自拥有独立的读线程和写线程,否则会因单线程阻塞在读取操作上而导致另一方向无响应。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













