目录
- 核心概念
- Socket (套接字)
- ServerSocket (服务器套接字)
- TCP vs. UDP
- TCP Socket 编程
- 通信模型
- 服务器端代码示例
- 客户端代码示例
- 如何运行
- 多线程服务器进阶
- UDP Socket 编程
- 通信模型
- 发送方代码示例
- 接收方代码示例
- 如何运行
- 重要注意事项
- 异常处理
- 资源关闭 (try-with-resources)
- IO 流与字节流
核心概念
Socket (套接字)
Socket 是网络编程的 API,它提供了两个程序在网络上进行通信的端点,你可以把它想象成一个电话插孔,一旦你把电话线插进去,就可以进行通话了,在 Java 中,java.net.Socket 类代表一个客户端套接字,java.net.ServerSocket 类代表一个服务器端套接字。

TCP vs. UDP
在选择使用哪种协议时,需要理解它们之间的根本区别:
| 特性 | TCP (传输控制协议) | UDP (用户数据报协议) |
|---|---|---|
| 连接性 | 面向连接 (Connection-oriented) | 无连接 (Connectionless) |
| 可靠性 | 可靠,通过确认、重传、排序等机制确保数据无差错、不丢失、不重复且按序到达。 | 不可靠,不保证数据包的顺序或是否到达,可能会丢失或重复。 |
| 速度 | 较慢,因为需要建立连接和维护状态。 | 非常快,没有连接和确认的开销。 |
| 开销 | 较高,有头部开销和连接维护开销。 | 较低,头部开销很小。 |
| 适用场景 | 要求可靠传输的应用,如文件传输、网页浏览、电子邮件。 | 对速度要求高、能容忍少量丢包的应用,如视频会议、在线游戏、DNS查询。 |
TCP Socket 编程
TCP 是最常用的 Socket 编程方式,因为它提供了可靠的数据传输。
通信模型
-
服务器端:
- 创建一个
ServerSocket并绑定到一个特定的端口(8888),开始监听客户端连接。 - 调用
accept()方法,该方法会阻塞(暂停执行),直到一个客户端连接到来。 - 当
accept()返回时,它会返回一个新的Socket对象,这个对象代表与特定客户端的连接。 - 通过这个
Socket对象的输入流和输出流与客户端进行双向通信。 - 通信结束后,关闭
Socket和ServerSocket。
- 创建一个
-
客户端:
(图片来源网络,侵删)- 创建一个
Socket对象,指定服务器的 IP 地址和端口号。 - 如果服务器正在监听,
Socket构造函数会尝试连接到服务器,连接成功后继续执行。 - 通过这个
Socket对象的输入流和输出流与服务器进行双向通信。 - 通信结束后,关闭
Socket。
- 创建一个
服务器端代码示例 (TCPServer.java)
这个服务器会等待一个客户端连接,然后接收客户端发送的消息,并将其转换为大写后返回给客户端。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
int port = 8888; // 服务器监听的端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,等待客户端连接...");
// 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);
String inputLine;
// 循环读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 将消息转换为大写并发送回客户端
String response = inputLine.toUpperCase();
out.println(response);
System.out.println("已向客户端发送: " + response);
// 如果客户端发送 "bye",则结束通信
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
System.out.println("客户端断开连接。");
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
客户端代码示例 (TCPClient.java)
这个客户端连接到服务器,发送一条消息,然后接收并打印服务器返回的响应。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class TCPClient {
public static void main(String[] args) {
String hostname = "localhost"; // 或服务器的IP地址
int port = 8888;
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("请输入要发送的消息 (输入 'bye' 退出):");
String userInput;
// 循环读取用户输入
while ((userInput = stdIn.readLine()) != null) {
// 将用户输入发送给服务器
out.println(userInput);
// 从服务器读取响应
String response = in.readLine();
System.out.println("服务器响应: " + response);
if ("bye".equalsIgnoreCase(userInput)) {
break;
}
System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
}
} catch (UnknownHostException e) {
System.err.println "不知道的主机: " + hostname);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O error: " + e.getMessage());
e.printStackTrace();
}
}
}
如何运行
-
编译代码:
javac TCPServer.java TCPClient.java
-
启动服务器:
java TCPServer
你会看到控制台输出:
服务器已启动,等待客户端连接... -
启动客户端: 打开一个新的终端窗口,运行客户端:
java TCPClient
你会看到客户端输出:
已连接到服务器。 -
进行通信:
- 在客户端的控制台输入
hello world,然后按回车。 - 客户端会收到服务器返回的
HELLO WORLD。 - 在客户端输入
bye,然后按回车,客户端和服务器都会关闭连接。
- 在客户端的控制台输入
多线程服务器进阶
上面的服务器一次只能处理一个客户端,当处理多个客户端时,必须使用多线程。
服务器端改进代码 (MultiThreadTCPServer.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class MultiThreadTCPServer {
public static void main(String[] args) {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,等待客户端连接...");
while (true) {
// accept() 阻塞,等待新客户端
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新的线程来处理
ClientHandler handler = new ClientHandler(clientSocket);
new Thread(handler).start();
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
}
}
}
// 客户端处理线程
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(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);
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
out.println("服务器收到了: " + inputLine.toUpperCase());
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
// 客户端断开连接时会抛出异常,这里可以打印日志
System.out.println("客户端 " + clientSocket.getInetAddress() + " 断开连接。");
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
UDP Socket 编程
UDP 无需建立连接,数据以数据报的形式发送,速度更快但不保证顺序和可靠性。
通信模型
-
发送方:
- 创建一个
DatagramSocket。 - 创建一个
DatagramPacket,包含要发送的数据、目标 IP 地址和端口号。 - 通过
DatagramSocket的send()方法发送数据包。
- 创建一个
-
接收方:
- 创建一个
DatagramSocket并绑定到特定端口。 - 创建一个空的
DatagramPacket用于接收数据。 - 通过
DatagramSocket的receive()方法接收数据,该方法会阻塞直到数据包到达。 - 从接收到的
DatagramPacket中提取数据。
- 创建一个
发送方代码示例 (UDPSender.java)
import java.net.*;
import java.nio.charset.StandardCharsets;
public class UDPSender {
public static void main(String[] args) {
String hostname = "localhost";
int port = 9999;
String message = "这是一条UDP消息!";
try {
// 创建一个DatagramSocket,系统会分配一个可用端口
DatagramSocket socket = new DatagramSocket();
// 将消息转换为字节数组
byte[] buffer = message.getBytes(StandardCharsets.UTF_8);
// 创建要发送的数据包,指定目标地址和端口
InetAddress address = InetAddress.getByName(hostname);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);
// 发送数据包
socket.send(packet);
System.out.println("消息已发送: " + message);
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
接收方代码示例 (UDPReceiver.java)
import java.net.*;
import java.nio.charset.StandardCharsets;
public class UDPReceiver {
public static void main(String[] args) {
int port = 9999;
byte[] buffer = new byte[1024]; // 创建一个缓冲区用于接收数据
try {
// 创建一个DatagramSocket并绑定到指定端口
DatagramSocket socket = new DatagramSocket(port);
System.out.println("UDP接收器已启动,监听端口 " + port);
// 创建一个空的数据包用于接收数据
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// receive() 方法会阻塞,直到收到数据包
socket.receive(packet);
// 从数据包中提取数据
String receivedMessage = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("收到消息: " + receivedMessage);
// 获取发送方的地址和端口
InetAddress senderAddress = packet.getAddress();
int senderPort = packet.getPort();
System.out.println("来自: " + senderAddress.getHostAddress() + ":" + senderPort);
socket.close();
} catch (SocketException e) {
System.err.println("Socket错误: " + e.getMessage());
} catch (IOException e) {
System.err.println("I/O错误: " + e.getMessage());
}
}
}
如何运行
-
编译:
javac UDPSender.java UDPReceiver.java
-
启动接收方:
java UDPReceiver
输出:
UDP接收器已启动,监听端口 9999,然后程序会等待。 -
启动发送方: 打开新终端,运行:
java UDPSender
发送方会发送消息并退出。
-
查看结果: 接收方终端会打印出收到的消息和发送方的信息。
重要注意事项
异常处理
网络编程充满了不确定性,因此必须妥善处理各种 IOException,如连接失败、网络中断、端口被占用等,通常使用 try-catch 块来捕获这些异常。
资源关闭 (try-with-resources)
Socket、InputStream、OutputStream 等资源都实现了 AutoCloseable 接口,强烈推荐使用 try-with-resources 语句,这样可以确保无论是否发生异常,资源都会被自动关闭,避免资源泄漏。
// 推荐写法
try (Socket socket = new Socket(...);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
// ... 使用流进行通信 ...
} catch (IOException e) {
// ... 处理异常 ...
} // in, out, socket 会在这里自动关闭
IO 流与字节流
Socket 的 getInputStream() 和 getOutputStream() 返回的是字节流 (InputStream/OutputStream),对于文本数据,通常需要将其包装成字符流 (Reader/Writer),InputStreamReader 和 OutputStreamWriter(或 PrintWriter),并指定字符编码(如 UTF-8),以确保在不同平台间正确处理文本。
