Java网络编程全解析:从基础协议到Socket实战,吃透网络通信

2 阅读26分钟

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步:

  1. 服务器启动:监听指定端口,等待客户端连接;

  2. 客户端启动:指定服务器的IP地址和端口号,发起连接请求;

  3. 连接建立:服务器接受客户端连接,双方通过输入流/输出流进行数据传输,传输完成后关闭连接。

补充:另一种常见模型是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编程核心流程

  1. 服务器端:① 创建ServerSocket,监听指定端口;② 调用accept(),阻塞等待客户端连接;③ 连接成功后,获取Socket的输入流/输出流,与客户端通信;④ 通信完成后,关闭输入流、输出流、Socket、ServerSocket;

  2. 客户端:① 创建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编程核心流程

  1. 服务器端:① 创建DatagramSocket,绑定指定端口;② 创建DatagramPacket(接收缓冲区);③ 调用receive(),阻塞等待接收数据;④ 接收数据后,可通过DatagramPacket获取发送方的IP和端口,回复数据;⑤ 关闭DatagramSocket;

  2. 客户端:① 创建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/