Java Socket 编程完整教程
目录
- 什么是 Socket?
- 网络基础回顾
- Java Socket 编程核心类
- 第一部分:基于 TCP 的 Socket 编程(可靠、面向连接)
- 1 通信流程概述
- 2 编写一个简单的 Echo 服务器
- 3 编写对应的客户端
- 4 运行与测试
- 5 多线程服务器处理多个客户端
- 第二部分:基于 UDP 的 Socket 编程(不可靠、无连接)
- 1 通信流程概述
- 2 编写一个简单的 UDP Echo 服务器
- 3 编写对应的客户端
- 4 运行与测试
- 关键知识点与最佳实践
- 1 I/O 流管理
- 2 异常处理
- 3 关闭资源
- 4 阻塞与非阻塞 I/O (NIO)
什么是 Socket?
你可以把 Socket(套接字) 想象成一个电话。

- 电话机:就是你的程序(客户端或服务器)。
- 电话号码:IP 地址,用来在网络中唯一标识一台设备。
- 分机号:就是端口号,用来在同一台设备上区分不同的应用程序。
- 通话线路:就是网络连接。
Socket 就是网络编程中,应用程序为了进行网络通信而使用的一个“端点”,它封装了复杂的底层网络协议(如 TCP/IP),使得开发者可以方便地进行网络数据收发。
Java 通过 java.net 包提供了强大的 Socket 编程 API。
网络基础回顾
在开始编码前,需要了解两个核心概念:
- IP 地址:网络中设备的唯一标识,如
168.1.100或2001:0db8:85a3::8a2e:0370:7334,在 Java 中,常用InetAddress类来表示。 - 端口号:一个 16 位的整数(0-65535),用于区分同一台主机上运行的不同服务,HTTP 服务通常使用 80 端口,HTTPS 使用 443 端口。
- 协议:网络通信的规则,Socket 编程主要涉及两种:
- TCP (Transmission Control Protocol):面向连接、可靠的协议,在数据传输前,客户端和服务器需要先建立一个连接(三次握手),数据会按顺序、无丢失地到达,适用于要求高可靠性的场景,如文件传输、网页浏览。
- UDP (User Datagram Protocol):无连接、不可靠的协议,发送方直接把数据包(Datagram)发出去,不保证对方一定能收到,也不保证顺序,适用于对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏。
Java Socket 编程核心类
| 类/接口 | 描述 |
|---|---|
java.net.Socket |
客户端 Socket,客户端通过创建 Socket 对象来发起连接请求。 |
java.net.ServerSocket |
服务器端 Socket,服务器通过创建 ServerSocket 对象来监听客户端的连接请求。 |
java.net.InetAddress |
表示 IP 地址,没有构造方法,通过静态方法 getByName() 获取实例。 |
java.net.SocketImpl |
Socket 的具体实现类,一般由 JVM 自动创建。 |
java.io.InputStream |
输入流,用于从 Socket 中读取数据。 |
java.io.OutputStream |
输出流,用于向 Socket 中写入数据。 |
java.io.BufferedReader / java.io.InputStreamReader |
用于包装输入流,可以方便地按行读取文本数据。 |
java.io.PrintWriter |
用于包装输出流,可以方便地写入文本数据并自动处理换行符。 |
第一部分:基于 TCP 的 Socket 编程(可靠、面向连接)
TCP 是最常用的 Socket 编程方式,我们以一个经典的 Echo(回声)服务 为例:客户端发送一条消息,服务器原样返回这条消息。

1 通信流程概述
服务器端:
- 创建
ServerSocket对象,并绑定一个端口号,开始监听。 - 调用
accept()方法,阻塞等待客户端连接,一旦有客户端连接,accept()返回一个新的Socket对象,代表与该客户端的连接。 - 通过这个新的
Socket对象获取InputStream和OutputStream。 - 从
InputStream读取客户端发送的数据。 - 将读取到的数据通过
OutputStream写回给客户端。 - 关闭与当前客户端的连接(
Socket和相关的流)。 - 循环执行第 2 步,等待下一个客户端连接。
客户端:
- 创建
Socket对象,指定服务器的 IP 地址和端口号,发起连接,这个过程是阻塞的,直到连接成功或超时。 - 连接成功后,通过
Socket对象获取InputStream和OutputStream。 - 通过
OutputStream向服务器发送数据。 - 从
InputStream读取服务器返回的数据。 - 关闭
Socket和相关的流。
2 编写一个简单的 Echo 服务器
TCPEchoServer.java
import java.io.*;
import java.net.*;
public class TCPEchoServer {
public static void main(String[] args) {
int port = 6789; // 定义服务器监听的端口号
// try-with-resources 语句可以自动关闭资源
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器启动,监听端口 " + port + "...");
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 获取输入流和输出流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // true表示自动刷新缓冲区
String inputLine;
// 从客户端读取数据,直到客户端关闭连接
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
out.println(inputLine); // 将消息回写给客户端
}
System.out.println("客户端断开连接。");
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
3 编写对应的客户端
TCPEchoClient.java

import java.io.*;
import java.net.*;
public class TCPEchoClient {
public static void main(String[] args) {
String hostname = "localhost"; // 或服务器的IP地址
int port = 6789;
try (
// 创建Socket连接服务器
Socket socket = new Socket(hostname, port);
// 获取输入流和输出流
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 用于从控制台读取用户输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器。");
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
String userInput;
// 循环读取用户输入并发送
while ((userInput = stdIn.readLine()) != null) {
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
out.println(userInput); // 发送消息到服务器
// 读取服务器返回的回声
String response = in.readLine();
System.out.println("服务器回声: " + response);
}
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
System.exit(1);
} catch (IOException e) {
System.err.println "I/O error: " + e.getMessage());
System.exit(1);
}
}
}
4 运行与测试
-
先运行服务器:
java TCPEchoServer
你会看到控制台输出:
服务器启动,监听端口 6789... -
再运行客户端(可以在另一个终端窗口中运行):
java TCPEchoClient
客户端控制台会提示:
已连接到服务器。和请输入要发送的消息... -
在客户端输入消息,"Hello, Server!",然后按回车。
- 客户端会收到回声:
服务器回声: Hello, Server! - 服务器端会打印:
收到客户端消息: Hello, Server!
- 客户端会收到回声:
-
在客户端输入
exit并回车,程序退出。
5 多线程服务器处理多个客户端
上面的服务器一次只能处理一个客户端,当它 accept() 一个客户端后,必须等该客户端断开连接后才能处理下一个,为了能同时处理多个客户端,我们需要使用多线程。
MultiThreadTCPEchoServer.java
import java.io.*;
import java.net.*;
public class MultiThreadTCPEchoServer implements Runnable {
private final Socket clientSocket;
public MultiThreadTCPEchoSocket(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
) {
System.out.println("处理新连接: " + clientSocket.getInetAddress().getHostAddress());
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("收到来自 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
out.println(inputLine);
}
} catch (IOException e) {
// 客户端正常断开会触发 SocketException, 不算错误
if (!(e instanceof SocketException)) {
System.err.println("处理客户端时出错: " + e.getMessage());
}
} finally {
try {
clientSocket.close();
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
int port = 6789;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器启动,监听端口 " + port + "...");
while (true) { // 无限循环,持续接受连接
Socket clientSocket = serverSocket.accept();
// 为每个客户端连接创建一个新线程
new Thread(new MultiThreadTCPEchoServer(clientSocket)).start();
}
}
}
}
第二部分:基于 UDP 的 Socket 编程(不可靠、无连接)
UDP 编程模型更简单,没有客户端和服务器的严格区分,双方都是平等的“数据包发送/接收方”。
1 通信流程概述
发送方:
- 创建
DatagramSocket对象(可以指定端口号,也可以不指定,让系统分配)。 - 创建
DatagramPacket对象,包含要发送的数据、接收方的 IP 地址和端口号。 - 调用
DatagramSocket的send()方法发送数据包。
接收方:
- 创建
DatagramSocket对象,并绑定一个端口号进行监听。 - 创建一个空的
DatagramPacket对象,用于接收数据。 - 调用
DatagramSocket的receive()方法阻塞等待数据包,接收到的数据会填充到空的DatagramPacket中。 - 从
DatagramPacket中解析出数据、发送方地址和端口。
2 编写一个简单的 UDP Echo 服务器
UDPEchoServer.java
import java.io.*;
import java.net.*;
public class UDPEchoServer {
public static void main(String[] args) throws IOException {
int port = 9876; // UDP服务器端口
byte[] buffer = new byte[1024]; // 数据包缓冲区
try (DatagramSocket serverSocket = new DatagramSocket(port)) {
System.out.println("UDP服务器启动,监听端口 " + port + "...");
while (true) { // 持续接收数据包
// 创建一个空的DatagramPacket用于接收数据
DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
// receive()方法会阻塞,直到收到数据包
serverSocket.receive(receivePacket);
// 从数据包中提取数据
String receivedMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("收到来自 " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort() + " 的消息: " + receivedMessage);
// 创建回显数据包
DatagramPacket sendPacket = new DatagramPacket(
receivedMessage.getBytes(),
receivedMessage.getBytes().length,
receivePacket.getAddress(),
receivePacket.getPort()
);
// 发送回显数据包
serverSocket.send(sendPacket);
}
}
}
}
3 编写对应的客户端
UDPEchoClient.java
import java.io.*;
import java.net.*;
public class UDPEchoClient {
public static void main(String[] args) throws IOException {
String hostname = "localhost";
int port = 9876;
byte[] sendData;
byte[] receiveData = new byte[1024];
try (
// 创建DatagramSocket,系统会分配一个可用端口
DatagramSocket clientSocket = new DatagramSocket();
) {
// 从控制台读取用户输入
BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
String sentence;
while ((sentence = inFromUser.readLine()) != null) {
if ("exit".equalsIgnoreCase(sentence)) {
break;
}
sendData = sentence.getBytes();
// 创建要发送的数据包
DatagramPacket sendPacket = new DatagramPacket(
sendData,
sendData.length,
InetAddress.getByName(hostname),
port
);
// 发送数据包
clientSocket.send(sendPacket);
// 创建用于接收的空数据包
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
// receive()方法会阻塞,直到收到服务器的响应
clientSocket.receive(receivePacket);
// 解析并打印服务器返回的消息
String modifiedSentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("服务器回声: " + modifiedSentence);
}
}
}
}
4 运行与测试
运行方式与 TCP 类似,先启动服务器,再启动客户端,你会发现通信是即时的,且不需要像 TCP 那样先建立连接。
关键知识点与最佳实践
1 I/O 流管理
- 字节流 vs. 字符流:
Socket的getInputStream()和getOutputStream()返回的是原始的字节流,对于文本数据,最好使用InputStreamReader包装成字符流,再用BufferedReader进行缓冲,以高效地按行读写。 PrintWriter的自动刷新:在创建PrintWriter时,将第二个参数设为true(new PrintWriter(socket.getOutputStream(), true)),这样每次调用println()方法后,输出流会自动刷新,确保数据能立即发送出去,这对于交互式应用非常重要。
2 异常处理
网络操作充满了不确定性,必须妥善处理各种 IOException,
SocketException:连接被重置、连接超时等。BindException:端口被占用。ConnectException:连接被拒绝(服务器未启动或地址错误)。UnknownHostException:无法解析主机名。
3 关闭资源
- 顺序:先关闭外层流,再关闭内层流(Socket),但
Socket的close()方法会自动关闭其关联的输入/输出流,所以通常只需关闭Socket即可。 try-with-resources:这是 Java 7 引入的语法糖,强烈推荐使用,它能确保在try代码块执行完毕后,自动调用close()方法关闭实现了AutoCloseable接口(如Socket,ServerSocket,InputStream,OutputStream等)的资源,即使在try块中发生了异常。
4 阻塞与非阻塞 I/O (NIO)
传统的 Socket I/O 是阻塞式的,即 accept(), read(), write() 等方法在没有数据时会一直等待,这会占用线程资源。
对于高并发的场景,Java NIO (New I/O) 提供了非阻塞的解决方案。
- 核心概念:
- Channel (通道):类似流,但可以双向读写。
- Buffer (缓冲区):数据都存放在 Buffer 中。
- Selector (选择器):一个线程可以管理多个 Channel,通过
Selector查询哪些 Channel 已经准备好进行 I/O 操作。
- 优点:用少量线程就能管理成千上万的连接,极大地提高了系统的吞吐量和扩展性。
- 缺点:编程模型比传统的 BIO 复杂。
如果你需要构建高性能的网络服务(如聊天服务器、RPC 框架),应该学习 NIO,著名的 Netty 框架就是对 Java NIO 的高级封装。
| 特性 | TCP (Socket/ServerSocket) | UDP (DatagramSocket/DatagramPacket) |
|---|---|---|
| 连接 | 面向连接,需要先建立连接 | 无连接,直接发送数据包 |
| 可靠性 | 可靠,保证数据顺序和完整性 | 不可靠,可能丢失、重复或乱序 |
| 速度 | 较慢,因为需要建立连接和维护状态 | 较快,开销小 |
| 应用场景 | 文件传输、网页浏览、邮件 | 视频会议、DNS、在线游戏 |
| 编程模型 | 流式,像读写文件 | 数据包式,像发快递 |
| Java类 | Socket, ServerSocket, InputStream, OutputStream |
DatagramSocket, DatagramPacket |
通过本教程,你应该已经掌握了 Java Socket 编程的基础,从简单的 TCP 回声服务开始,逐步理解其工作原理,然后尝试实现自己的客户端/服务器应用,并最终探索更高级的 NIO 技术,网络编程是一个实践性很强的领域,多动手是最好的学习方式。
