Java网络编程全解析:从基础协议到Socket实战,吃透网络通信
在Java开发中,网络编程是实现“跨设备通信”的核心技术,广泛应用于客户端/服务器(C/S)架构、分布式系统、网络接口调用等场景——无论是即时通讯、文件上传下载,还是接口对接,都离不开网络编程的支持。
Java提供了完善的网络编程API,封装了底层的TCP/IP协议细节,开发者无需深入理解复杂的网络协议,只需调用相关类和方法,就能快速实现跨设备的数据传输。本文将从网络编程基础、核心协议(TCP/UDP)、Socket编程、实战案例,到避坑指南,层层递进。
一、先搞懂:Java网络编程的核心基础
Java网络编程的本质是“通过网络协议,实现不同设备(或同一设备不同进程)之间的数据传输”。在开始编程之前,必须先掌握3个核心基础概念,这是理解所有网络编程的前提。
1.1 核心概念:IP地址、端口号、协议
网络通信的本质是“找到目标设备→找到设备上的目标程序→按约定规则传输数据”,而IP地址、端口号、协议,正是实现这一过程的三大核心。
(1)IP地址:设备的“身份证”
IP地址(Internet Protocol Address)是互联网中每台设备的唯一标识,用于确定“数据要发送到哪台设备”。常见的IP地址分为两类:
-
IPv4:32位地址,格式为“点分十进制”(如192.168.1.1),是目前最常用的IP地址格式,但地址资源有限;
-
IPv6:128位地址,格式为“冒分十六进制”(如2001:0db8:85a3:0000:0000:8a2e:0370:7334),用于解决IPv4地址耗尽的问题。
注意事项:127.0.0.1 是本地回环地址,用于测试本机内部的网络通信(数据不会经过网卡,仅在本机进程间传输);localhost 是127.0.0.1的域名映射,二者等价。
(2)端口号:程序的“门牌号”
一台设备上会运行多个网络程序(如浏览器、QQ、服务器),端口号用于区分“设备上的哪个程序接收数据”,本质是设备上的逻辑端口。
-
端口号范围:0~65535(16位整数);
-
知名端口(0~1023):系统预留端口,如80端口(HTTP协议)、443端口(HTTPS协议)、21端口(FTP协议),不建议开发者使用;
-
动态端口(1024~65535):开发者可使用的端口,用于自定义网络程序,注意避免端口冲突。
注意事项:一个端口号同一时间只能被一个进程占用,若启动程序时提示“端口被占用”,需关闭占用该端口的进程,或更换端口号。
(3)网络协议:通信的“规则”
网络协议是不同设备之间通信的“约定规则”,规定了数据的传输格式、传输速率、错误处理等细节。Java网络编程主要基于TCP/IP协议簇,核心是TCP和UDP两个协议,二者是开发中最常用的协议。
1.2 Java网络编程的核心模型:客户端/服务器(C/S)
Java网络编程主要采用“客户端(Client)/服务器(Server)”模型,核心流程固定为3步:
-
服务器启动:监听指定端口,等待客户端连接;
-
客户端启动:指定服务器的IP地址和端口号,发起连接请求;
-
连接建立:服务器接受客户端连接,双方通过输入流/输出流进行数据传输,传输完成后关闭连接。
补充:另一种常见模型是B/S(浏览器/服务器),本质是“客户端为浏览器”的特殊C/S模型,Java Web开发(如SSM、Spring Boot)就属于B/S模型,而本文重点讲解通用的C/S模型Socket编程。
二、核心协议:TCP与UDP(重中之重)
TCP和UDP是Java网络编程中最核心的两个协议,二者特性不同、适用场景不同,必须牢记它们的区别,避免用错场景。
2.1 TCP协议:可靠的、面向连接的协议
TCP(Transmission Control Protocol,传输控制协议)是一种“面向连接、可靠、有序、面向字节流”的协议,核心特点是“保证数据的可靠传输”——数据传输前必须建立连接,传输过程中会进行错误检测、重传机制,确保数据不丢失、不重复、有序到达。
TCP的核心特性
-
面向连接:数据传输前,客户端和服务器必须通过“三次握手”建立连接,传输完成后通过“四次挥手”关闭连接;
-
可靠传输:采用重传机制、校验和、流量控制、拥塞控制,确保数据不丢失、不重复;
-
有序传输:数据按发送顺序到达接收方;
-
面向字节流:数据以字节为单位传输,没有固定的数据包大小;
-
开销较大:连接建立、维护、关闭都需要消耗资源,传输效率低于UDP。
2.2 UDP协议:不可靠的、无连接的协议
UDP(User Datagram Protocol,用户数据报协议)是一种“无连接、不可靠、无序、面向数据报”的协议,核心特点是“追求传输效率”——数据传输前无需建立连接,直接发送数据,不保证数据的可靠传输(可能丢失、重复、无序),但传输开销小、速度快。
UDP的核心特性
-
无连接:数据传输前无需建立连接,发送方直接发送数据,接收方被动接收;
-
不可靠传输:不进行错误检测、不重传丢失的数据,数据可能丢失、重复、无序到达;
-
面向数据报:数据以“数据报”为单位传输,每个数据报有固定的大小(最大65535字节);
-
开销小:无需维护连接,传输速度快,资源消耗少;
-
支持广播/组播:可向多个设备同时发送数据(如局域网广播)。
2.3 TCP与UDP的核心区别(必记)
| 对比维度 | TCP协议 | UDP协议 |
|---|---|---|
| 连接方式 | 面向连接(三次握手建立连接) | 无连接(直接发送) |
| 可靠性 | 可靠(不丢失、不重复、有序) | 不可靠(可能丢失、重复、无序) |
| 传输方式 | 面向字节流 | 面向数据报 |
| 开销 | 较大(维护连接、重传等) | 较小(无连接、无重传) |
| 适用场景 | 文件传输、即时通讯、接口调用(需可靠传输) | 视频直播、语音通话、广播(追求效率,可容忍少量数据丢失) |
注意事项:开发中选择TCP还是UDP,核心看“是否需要可靠传输”——若数据丢失会导致严重问题(如文件传输、转账),用TCP;若追求速度,可容忍少量数据丢失(如直播、游戏),用UDP。
三、Java网络编程核心API(Socket相关类)
Java提供了java.net包,封装了TCP和UDP编程的核心类,无需开发者手动实现底层协议,只需调用这些类的方法,就能快速实现网络通信。以下是最常用的核心类,按TCP和UDP分类讲解。
3.1 TCP编程核心类(重点)
TCP编程需要两个核心类:ServerSocket(服务器端)和Socket(客户端),二者配合使用,完成连接建立和数据传输。
(1)ServerSocket:服务器端类
作用:监听指定端口,等待客户端连接,接受客户端的连接请求后,返回一个Socket对象,通过该对象与客户端进行数据传输。
常用构造方法:
-
ServerSocket\(int port\):创建ServerSocket对象,监听指定端口(port); -
ServerSocket\(int port, int backlog\):指定端口和最大等待连接数(backlog,默认50),超过最大连接数的请求会被拒绝。
常用方法:
-
Socket accept\(\):阻塞等待客户端连接,连接成功后返回Socket对象(用于与该客户端通信); -
void close\(\):关闭服务器,释放端口资源; -
int getLocalPort\(\):获取服务器监听的端口号。
注意事项:accept()方法是“阻塞方法”——调用后会一直等待客户端连接,直到有客户端发起连接,才会继续执行后续代码。
(2)Socket:客户端类(也用于服务器端与客户端通信)
作用:客户端通过Socket对象发起连接请求,连接到指定IP地址和端口的服务器;服务器端通过accept()返回的Socket对象,与对应的客户端通信。
常用构造方法:
-
Socket\(String host, int port\):创建Socket对象,连接到指定主机(host,IP地址或域名)和端口(port)的服务器; -
Socket\(InetAddress address, int port\):通过InetAddress对象(封装IP地址)连接服务器。
常用方法:
-
InputStream getInputStream\(\):获取输入流,用于读取服务器发送的数据; -
OutputStream getOutputStream\(\):获取输出流,用于向服务器发送数据; -
void close\(\):关闭Socket,释放连接资源; -
InetAddress getInetAddress\(\):获取服务器的IP地址; -
int getPort\(\):获取服务器的端口号。
注意事项:1. 客户端创建Socket时,若服务器未启动或IP/端口错误,会抛出ConnectException(连接异常);2. Socket的输入流和输出流必须在关闭Socket前关闭,避免资源泄漏;3. 数据传输完成后,需主动关闭Socket,否则会占用端口资源。
3.2 UDP编程核心类
UDP编程无需建立连接,核心类是DatagramSocket(用于发送/接收数据报)和DatagramPacket(用于封装数据报)。
(1)DatagramSocket:发送/接收数据报的核心类
作用:用于发送和接收UDP数据报,客户端和服务器端都需要使用该类。
常用构造方法:
-
DatagramSocket\(\):创建DatagramSocket对象,随机分配一个动态端口(客户端常用); -
DatagramSocket\(int port\):创建DatagramSocket对象,绑定指定端口(服务器端常用,需监听固定端口)。
常用方法:
-
void send\(DatagramPacket p\):发送数据报(DatagramPacket对象); -
void receive\(DatagramPacket p\):阻塞接收数据报,将接收的数据存入DatagramPacket对象; -
void close\(\):关闭DatagramSocket,释放端口资源。
(2)DatagramPacket:封装UDP数据报的类
作用:封装UDP传输的数据、目标IP地址、目标端口号,是UDP数据传输的载体。
常用构造方法:
-
DatagramPacket\(byte\[\] buf, int length, InetAddress address, int port\):用于发送数据——封装数据(buf)、数据长度(length)、目标IP(address)、目标端口(port); -
DatagramPacket\(byte\[\] buf, int length\):用于接收数据——指定接收数据的缓冲区(buf)和缓冲区大小(length)。
常用方法:
-
byte\[\] getData\(\):获取接收的数据(字节数组); -
int getLength\(\):获取实际接收的数据长度; -
InetAddress getAddress\(\):获取发送方的IP地址(接收数据时使用); -
int getPort\(\):获取发送方的端口号(接收数据时使用)。
注意事项:1. UDP数据报的最大长度是65535字节,超过该长度会导致数据被截断;2. receive()方法是阻塞方法,会一直等待接收数据;3. UDP无连接,发送方无法确认接收方是否收到数据,需手动实现确认机制(如接收方收到数据后回复确认信息)。
3.3 辅助类:InetAddress
作用:封装IP地址和域名,用于获取IP地址、判断IP类型等。
常用静态方法:
-
InetAddress getByName\(String host\):通过域名(如www.baidu.com)或IP地址,获取InetAddress对象; -
InetAddress getLocalHost\(\):获取本机的InetAddress对象(本机IP地址); -
String getHostAddress\(\):获取IP地址字符串(如192.168.1.1); -
String getHostName\(\):获取域名(若没有域名,返回IP地址)。
四、实战入门:TCP编程(最常用,重点掌握)
TCP编程是Java网络编程的重点,适用于需要可靠传输的场景(如文件传输、接口调用)。以下是TCP编程的完整流程,分为服务器端和客户端,代码可直接复用,同时补充关键注意事项。
4.1 TCP编程核心流程
-
服务器端:① 创建ServerSocket,监听指定端口;② 调用accept(),阻塞等待客户端连接;③ 连接成功后,获取Socket的输入流/输出流,与客户端通信;④ 通信完成后,关闭输入流、输出流、Socket、ServerSocket;
-
客户端:① 创建Socket,连接服务器(指定IP和端口);② 获取Socket的输入流/输出流,与服务器通信;③ 通信完成后,关闭输入流、输出流、Socket。
4.2 实战案例:TCP客户端与服务器端通信(简单文本交互)
场景:客户端向服务器端发送一条文本消息,服务器端接收消息后,回复一条确认消息,双方通信完成后关闭连接。
4.2.1 服务器端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* TCP服务器端
*/
public class TcpServer {
public static void main(String[] args) {
// 1. 定义服务器端口(1024~65535之间,避免端口冲突)
int port = 8888;
ServerSocket serverSocket = null;
Socket clientSocket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
// 2. 创建ServerSocket,监听指定端口
serverSocket = new ServerSocket(port);
System.out.println("TCP服务器已启动,监听端口:" + port + ",等待客户端连接...");
// 3. 阻塞等待客户端连接(accept()是阻塞方法)
clientSocket = serverSocket.accept();
System.out.println("客户端已连接,客户端IP:" + clientSocket.getInetAddress().getHostAddress());
// 4. 获取输入流,读取客户端发送的消息(字符流,处理中文)
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
// 获取输出流,向客户端发送回复消息
out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
// 5. 读取客户端消息
String clientMsg = in.readLine();
System.out.println("收到客户端消息:" + clientMsg);
// 6. 向客户端发送回复消息
out.println("服务器已收到消息:" + clientMsg);
} catch (IOException e) {
// 捕获IO异常(端口被占用、客户端连接异常等)
e.printStackTrace();
} finally {
// 7. 关闭资源(顺序:先关闭输入流、输出流,再关闭客户端Socket,最后关闭服务器Socket)
try {
if (in != null) in.close();
if (out != null) out.close();
if (clientSocket != null) clientSocket.close();
if (serverSocket != null) serverSocket.close();
System.out.println("服务器资源已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4.2.2 客户端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
/**
* TCP客户端
*/
public class TcpClient {
public static void main(String[] args) {
// 1. 服务器IP地址(本机测试用127.0.0.1)和端口号(与服务器一致)
String serverIp = "127.0.0.1";
int serverPort = 8888;
Socket socket = null;
PrintWriter out = null;
BufferedReader in = null;
try {
// 2. 创建Socket,连接服务器(若服务器未启动,会抛出ConnectException)
socket = new Socket(serverIp, serverPort);
System.out.println("已连接到服务器,服务器IP:" + serverIp + ",端口:" + serverPort);
// 3. 获取输出流,向服务器发送消息(字符流,处理中文)
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
// 获取输入流,读取服务器的回复消息
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
// 4. 向服务器发送消息
String msg = "Hello TCP Server,我是Java客户端!";
out.println(msg);
System.out.println("向服务器发送消息:" + msg);
// 5. 读取服务器的回复消息
String serverMsg = in.readLine();
System.out.println("收到服务器回复:" + serverMsg);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 6. 关闭资源(顺序:先关闭输出流、输入流,再关闭Socket)
try {
if (out != null) out.close();
if (in != null) in.close();
if (socket != null) socket.close();
System.out.println("客户端资源已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4.2.3 运行说明与注意事项
-
运行顺序:必须先启动服务器端,再启动客户端;若先启动客户端,会抛出“连接拒绝”异常;
-
中文处理:使用InputStreamReader和OutputStreamWriter指定编码(UTF-8),避免中文乱码;
-
资源关闭:必须按“输入流/输出流 → Socket → ServerSocket”的顺序关闭,避免资源泄漏;
-
阻塞方法:服务器端的accept()、客户端/服务器端的readLine()都是阻塞方法,会一直等待直到有数据或连接;
-
端口冲突:若启动服务器端时提示“BindException: Address already in use”,说明端口被占用,需更换端口号(如8889)。
4.3 进阶:TCP服务器端支持多客户端连接(线程池优化)
上面的案例中,服务器端只能处理一个客户端连接,处理完一个后就会关闭。实际开发中,服务器需要同时处理多个客户端连接,此时需要使用“多线程”——每接收一个客户端连接,就启动一个线程处理该客户端的通信,同时使用线程池优化线程资源(避免线程过多导致内存溢出)。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* TCP多线程服务器端(线程池优化)
*/
public class TcpMultiThreadServer {
// 线程池(固定线程数,避免线程过多)
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
int port = 8888;
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
System.out.println("多线程TCP服务器已启动,监听端口:" + port + ",等待客户端连接...");
// 循环监听客户端连接(无限循环,持续接收客户端)
while (true) {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接,客户端IP:" + clientSocket.getInetAddress().getHostAddress());
// 提交任务到线程池,由线程处理该客户端的通信
THREAD_POOL.submit(new ClientHandler(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (serverSocket != null) serverSocket.close();
THREAD_POOL.shutdown(); // 关闭线程池
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 客户端处理线程:每个客户端对应一个线程,处理与客户端的通信
*/
static class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
// 获取输入流和输出流
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
String clientMsg;
// 循环读取客户端消息(客户端不关闭,就持续接收)
while ((clientMsg = in.readLine()) != null) {
System.out.println("收到客户端[" + clientSocket.getInetAddress().getHostAddress() + "]消息:" + clientMsg);
// 回复客户端
out.println("服务器已收到消息:" + clientMsg);
}
} catch (IOException e) {
System.out.println("客户端[" + clientSocket.getInetAddress().getHostAddress() + "]连接断开");
} finally {
// 关闭当前客户端的资源
try {
if (in != null) in.close();
if (out != null) out.close();
if (clientSocket != null) clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注意事项:1. 线程池的核心线程数需根据服务器性能调整(如5~10个),避免线程过多导致资源耗尽;2. 客户端断开连接时,服务器端的readLine()会返回null,此时线程会退出并关闭资源;3. 无限循环监听客户端连接,服务器会一直运行,需手动关闭。
五、实战进阶:UDP编程(无连接,适用于高速传输)
UDP编程无需建立连接,流程简单,适用于追求效率、可容忍少量数据丢失的场景(如视频直播、广播)。以下是UDP客户端与服务器端的实战案例,代码可直接复用。
5.1 UDP编程核心流程
-
服务器端:① 创建DatagramSocket,绑定指定端口;② 创建DatagramPacket(接收缓冲区);③ 调用receive(),阻塞等待接收数据;④ 接收数据后,可通过DatagramPacket获取发送方的IP和端口,回复数据;⑤ 关闭DatagramSocket;
-
客户端:① 创建DatagramSocket;② 创建DatagramPacket(封装数据、目标IP和端口);③ 调用send()发送数据;④ (可选)创建DatagramPacket接收服务器回复;⑤ 关闭DatagramSocket。
5.2 实战案例:UDP客户端与服务器端通信
场景:客户端向服务器端发送一条文本消息,服务器端接收消息后,向客户端回复一条确认消息(UDP无连接,回复时需指定客户端的IP和端口)。
5.2.1 服务器端代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* UDP服务器端
*/
public class UdpServer {
public static void main(String[] args) {
// 1. 服务器端口(与客户端一致)
int port = 9999;
DatagramSocket datagramSocket = null;
try {
// 2. 创建DatagramSocket,绑定指定端口
datagramSocket = new DatagramSocket(port);
System.out.println("UDP服务器已启动,监听端口:" + port + ",等待接收数据...");
// 3. 创建接收数据的缓冲区(字节数组),大小建议与客户端发送的缓冲区一致
byte[] receiveBuf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuf, receiveBuf.length);
// 4. 阻塞接收数据(receive()是阻塞方法)
datagramSocket.receive(receivePacket);
// 5. 解析接收的数据、发送方的IP和端口
String clientMsg = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");
InetAddress clientIp = receivePacket.getAddress(); // 客户端IP
int clientPort = receivePacket.getPort(); // 客户端端口
System.out.println("收到客户端[" + clientIp.getHostAddress() + ":" + clientPort + "]消息:" + clientMsg);
// 6. 向客户端回复消息
String replyMsg = "服务器已收到UDP消息:" + clientMsg;
byte[] replyBuf = replyMsg.getBytes("UTF-8");
// 创建发送的数据报(指定回复数据、客户端IP、客户端端口)
DatagramPacket replyPacket = new DatagramPacket(replyBuf, replyBuf.length, clientIp, clientPort);
datagramSocket.send(replyPacket);
System.out.println("已向客户端回复消息");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 7. 关闭DatagramSocket
if (datagramSocket != null) {
datagramSocket.close();
System.out.println("服务器资源已关闭");
}
}
}
}
5.2.2 客户端代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* UDP客户端
*/
public class UdpClient {
public static void main(String[] args) {
// 1. 服务器IP和端口
String serverIp = "127.0.0.1";
int serverPort = 9999;
DatagramSocket datagramSocket = null;
try {
// 2. 创建DatagramSocket(客户端无需绑定固定端口,随机分配)
datagramSocket = new DatagramSocket();
// 3. 准备发送的数据
String msg = "Hello UDP Server,我是Java客户端!";
byte[] sendBuf = msg.getBytes("UTF-8");
// 4. 创建发送的数据报(封装数据、服务器IP、服务器端口)
InetAddress serverInet = InetAddress.getByName(serverIp);
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, serverInet, serverPort);
// 5. 发送数据
datagramSocket.send(sendPacket);
System.out.println("向服务器发送消息:" + msg);
// 6. 接收服务器的回复消息
byte[] receiveBuf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuf, receiveBuf.length);
datagramSocket.receive(receivePacket); // 阻塞接收
// 7. 解析回复消息
String serverMsg = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");
System.out.println("收到服务器回复:" + serverMsg);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 8. 关闭DatagramSocket
if (datagramSocket != null) {
datagramSocket.close();
System.out.println("客户端资源已关闭");
}
}
}
}
5.2.3 注意事项
-
UDP无连接:客户端发送数据前无需等待服务器启动,若服务器未启动,数据会丢失,客户端不会报错;
-
数据报大小:发送的数据报最大不能超过65535字节,超过会被截断,需手动分割数据;
-
中文处理:发送和接收时需指定编码(UTF-8),避免中文乱码;
-
回复机制:UDP无内置确认机制,若需要确认数据是否收到,需手动实现(如服务器接收后回复,客户端未收到回复则重发);
-
端口绑定:服务器端必须绑定固定端口,客户端无需绑定,随机分配即可。
六、实战综合案例:TCP文件传输(开发高频)
TCP可靠传输的核心应用场景之一是“文件传输”(如图片、文档、视频等)。以下是完整的TCP文件传输案例:客户端读取本地文件,发送给服务器端,服务器端接收文件后,保存到本地指定目录,实现文件的远程传输。
6.1 服务器端代码(接收文件)
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* TCP文件传输服务器端(接收文件)
*/
public class TcpFileServer {
public static void main(String[] args) {
int port = 8888;
ServerSocket serverSocket = null;
Socket clientSocket = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 1. 启动服务器,监听端口
serverSocket = new ServerSocket(port);
System.out.println("文件传输服务器已启动,监听端口:" + port + ",等待客户端连接...");
// 2. 接收客户端连接
clientSocket = serverSocket.accept();
System.out.println("客户端已连接,开始接收文件...");
// 3. 获取输入流(接收客户端发送的文件数据)
bis = new BufferedInputStream(clientSocket.getInputStream());
// 4. 创建输出流(将接收的数据写入本地文件)
String savePath = "D:/server_receive/test.jpg"; // 本地保存路径
bos = new BufferedOutputStream(new FileOutputStream(savePath));
// 5. 读取文件数据并写入本地
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,提升效率
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush();
System.out.println("文件接收完成,保存路径:" + savePath);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 6. 关闭资源
try {
if (bos != null) bos.close();
if (bis != null) bis.close();
if (clientSocket != null) clientSocket.close();
if (serverSocket != null) serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
6.2 客户端代码(发送文件)
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* TCP文件传输客户端(发送文件)
*/
public class TcpFileClient {
public static void main(String[] args) {
String serverIp = "127.0.0.1";
int serverPort = 8888;
String filePath = "D:/test.jpg"; // 要发送的文件路径
Socket socket = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 1. 连接服务器
socket = new Socket(serverIp, serverPort);
System.out.println("已连接到文件服务器,开始发送文件...");
// 2. 创建输入流(读取本地文件)
bis = new BufferedInputStream(new FileInputStream(filePath));
// 3. 获取输出流(向服务器发送文件数据)
bos = new BufferedOutputStream(socket.getOutputStream());
// 4. 读取文件数据并发送
byte[] buffer = new byte[1024 * 8];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush();
// 关闭输出流的写入端,告知服务器文件已发送完成
socket.shutdownOutput();
System.out.println("文件发送完成!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5. 关闭资源
try {
if (bos != null) bos.close();
if (bis != null) bis.close();
if (socket != null) socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
6.3 关键注意事项
-
文件路径:客户端的文件路径必须存在(否则抛出FileNotFoundException),服务器端的保存目录必须存在(否则抛出FileNotFoundException);
-
缓冲区大小:建议设置较大的缓冲区(如8KB、16KB),提升文件传输效率;
-
shutdownOutput():客户端发送完文件后,需调用socket.shutdownOutput(),告知服务器端“文件已发送完成”,否则服务器端的read()会一直阻塞,等待数据;
-
文件类型:该案例支持所有类型的文件(图片、视频、文档等),因为使用字节流传输,不依赖文件格式;
-
异常处理:需处理文件不存在、端口冲突、连接异常等多种IO异常,确保程序健壮性。
七、Java网络编程避坑指南(开发高频,必看)
Java网络编程容易出现连接失败、数据丢失、资源泄漏、中文乱码等问题,以下是6个高频坑点,结合实例说明,帮你避开陷阱。
坑点1:服务器端未启动,客户端直接连接
客户端创建Socket时,若服务器端未启动或IP/端口错误,会抛出ConnectException(Connection refused)异常。
坑点2:端口被占用,导致服务器启动失败
服务器端创建ServerSocket时,若指定的端口已被其他进程占用,会抛出BindException(Address already in use)异常。
坑点3:忘记关闭流和Socket,导致资源泄漏
IO流和Socket都是稀缺资源,使用后未关闭,会占用端口、内存等资源,长期运行会导致程序崩溃。
坑点4:TCP通信中,服务器端未处理多客户端连接
简单的TCP服务器端(单线程)只能处理一个客户端连接,处理完一个后就会关闭,无法同时处理多个客户端,需使用多线程+线程池优化。
坑点5:UDP数据报过大,导致数据被截断
UDP数据报的最大长度是65535字节,超过该长度会被截断,导致数据丢失或损坏。
坑点6:中文乱码(未指定编码格式)
TCP/UDP通信中,若发送和接收时未指定编码格式(如UTF-8),会导致中文乱码(默认编码可能为GBK)。
八、面试高频:Java网络编程相关面试题(附答案)
Java网络编程是基础面试的高频考点,尤其是TCP/UDP的区别、Socket编程流程、资源释放等,以下是5道最常考的面试题,附简洁答案(面试直接用)。
面试题1:TCP和UDP协议的区别是什么?适用场景分别是什么?
答:区别:① 连接方式:TCP面向连接(三次握手),UDP无连接;② 可靠性:TCP可靠(不丢失、不重复、有序),UDP不可靠;③ 传输方式:TCP面向字节流,UDP面向数据报;④ 开销:TCP开销大,UDP开销小。
适用场景:TCP适用于文件传输、即时通讯、接口调用等需要可靠传输的场景(数据丢失会导致严重问题);UDP适用于视频直播、语音通话、广播等追求效率、可容忍少量数据丢失的场景。
面试题2:Java中TCP编程的核心类是什么?分别说明其作用。
答:核心类有两个,分别是ServerSocket(服务器端)和Socket(客户端/通信端)。① ServerSocket:用于服务器端,监听指定端口,阻塞等待客户端连接,接受连接后返回Socket对象,用于与客户端通信;② Socket:用于客户端时,发起连接请求(指定服务器IP和端口);用于服务器端时,由ServerSocket的accept()方法返回,对应单个客户端,通过其获取输入流/输出流实现数据传输。
面试题3:TCP的三次握手和四次挥手分别是什么?作用是什么?
答:① 三次握手(建立连接):客户端发送SYN请求 → 服务器回复SYN+ACK确认 → 客户端发送ACK确认;作用是确保客户端和服务器双方都能正常接收和发送数据,避免无效连接。② 四次挥手(关闭连接):客户端发送FIN请求关闭 → 服务器回复ACK确认 → 服务器发送FIN请求关闭 → 客户端回复ACK确认;作用是确保双方都已完成数据传输,安全释放连接资源,避免数据丢失。
面试题4:Java网络编程中,如何避免资源泄漏?
答:核心是及时关闭稀缺资源,主要有两种方式:① 手动关闭:使用try-finally块,按“输入流/输出流 → Socket/DatagramSocket → ServerSocket”的顺序关闭资源,确保无论是否出现异常,资源都能释放;② 自动关闭:使用JDK7+的try-with-resources语法,将需要关闭的资源(如Socket、流)声明在try括号内,程序结束后自动关闭,简化代码且更安全。
面试题5:UDP编程中,为什么会出现数据丢失?如何解决?
答:数据丢失的原因:① UDP是不可靠协议,无重传机制,数据发送后无法确认接收方是否收到;② 数据报超过65535字节被截断;③ 网络拥堵导致数据丢失。解决方案:① 手动实现确认机制(接收方收到数据后回复确认消息,发送方未收到确认则重发);② 分割大数据报,分多次发送(每次不超过65535字节);③ 合理设置缓冲区大小,减少网络拥堵带来的影响。
九、总结
Java网络编程的核心是“基于TCP/UDP协议,通过Socket相关API实现跨设备数据传输”,核心要点可总结为3点:① 基础认知:掌握IP地址、端口号、网络协议的核心作用,理解C/S模型的通信流程;② 协议区分:牢记TCP和UDP的特性与适用场景,避免用错开发场景;③ 实战落地:熟练使用ServerSocket、Socket、DatagramSocket等核心类,掌握TCP/UDP基础通信、多客户端连接、文件传输等实战场景,避开资源泄漏、中文乱码等高频坑点。
Java网络编程全解析:从基础协议到Socket实战,吃透网络通信 在Java开发中,网络编程是实现“跨设备通信”的核心技术,广泛应用于客户端/服务器(C/