【Java】网络编程:TCP与UDP协议详解

51 阅读8分钟

目录

  1. TCP/UDP协议概述
  2. Java网络编程基础类
  3. TCP网络传输
  4. UDP网络传输
  5. 常见异常处理

一、TCP/UDP协议概述

协议定义

  • TCP(Transmission Control Protocol):传输控制协议/网间协议
  • UDP(User Datagram Protocol):用户数据包协议
  • 层级:均属于传输层协议

TCP协议特点

TCP协议是面向连接、可靠的、基于字节流的通信协议。

主要特征:

  1. 面向连接的传输
    • 两个使用TCP的主机在传输数据前必须先建立TCP连接
    • 使用三次握手建立连接
  2. 可靠性强
    • 确保传输数据的正确性
    • 不出现丢失或乱序
  3. 端到端通信
    • TCP连接通常是一个客户端和一个服务器端
    • 不支持广播和多播
  4. 字节流方式
    • 以字节为单位传输字节序列
    • 字节流的解释由应用层负责

UDP协议特点

UDP是一种无连接的传输协议,提供面向事务的简单不可靠传输服务。

主要特征:

  1. 无连接
    • 传输数据前不建立连接
    • 发送端只管发送,不关心接收状态
    • 接收端只读取收到的信息
  2. 无连接状态维护
    • 不需要维护连接状态
    • 一台服务器可同时向多个客户端传输相同消息
  3. 开销小
    • UDP信息包头部仅8个字节(TCP为20个字节)
    • 额外开销很小
  4. 高吞吐量
    • 不受拥塞控制算法调节
    • 只受应用软件生成数据速率、传输带宽、主机性能限制
  5. 尽力而为交付
    • 不保证可靠交付
    • 主机不需要维持复杂的连接状态表
  6. 面向报文
    • 保留报文边界
    • 不拆分也不合并报文
    • 应用程序需要选择合适的报文大小

二、Java网络编程基础类

InetAddress类

表示互联网协议IP地址的类。

主要方法:
方法返回类型描述
equals(Object obj)boolean将此对象与指定对象比较
getAddress()byte[]返回此InetAddress对象的原始IP地址
getAllByName(String host)static InetAddress[]返回指定主机名的所有IP地址数组
getByAddress(byte[] addr)static InetAddress根据原始IP返回InetAddress对象
getByAddress(String host, byte[] addr)static InetAddress根据主机名和IP创建InetAddress
getByName(String host)static InetAddress根据主机名获取IP地址
getHostAddress()String返回IP地址字符串
getHostName()String返回主机名称(如未注册则返回IP地址)
getLocalHost()static InetAddress返回本地主机
toString()String返回"主机名/IP地址"格式字符串

InetSocketAddress类

实现IP套接字地址,封装了IP和端口号。

三、TCP网络传输

核心概念

TCP传输建立时必须有客户端和服务端两个端点:

  • 客户端:Socket
  • 服务端:ServerSocket

由于TCP是面向连接的协议,Socket建立时会自动开启流对象进行数据操作。

Socket类(客户端)

实现客户端套接字,是两台机器间通信的端点。

构造方法:

Socket()
Socket(InetAddress address, int port)    // 连接指定IP和端口的服务端
Socket(String host, int port)            // 连接指定主机名和端口

常用方法:

方法描述
bind(SocketAddress bindpoint)绑定到本地地址
close()关闭套接字
connect(SocketAddress endpoint)将此套接字连接到服务器
getInetAddress()获取套接字连接的地址
getPort()获取套接字连接的端口
getInputStream()获取输入流
getOutputStream()获取输出流

ServerSocket类(服务端)

实现服务器套接字,通过accept方法获取连接的Socket对象。

构造方法:

ServerSocket()
ServerSocket(int port)                   // 监听指定端口

常用方法:

方法描述
accept()监听并接受连接(阻塞式方法)
bind(SocketAddress endpoint)绑定到特定地址
close()关闭套接字
getInetAddress()获取套接字地址
getLocalPort()返回监听端口

TCP传输完整示例

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class TCPExample {
    
    // 客户端和服务器需要分别封装成两个线程
    public static void main(String[] args) throws IOException {
        // 开启服务端线程
        new Thread(new ServerSide()).start();
        // 开启客户端线程
        new Thread(new ClientSide()).start();
    }
}

// 客户端线程
class ClientSide implements Runnable {
    public void run() {
        Socket s;
        Scanner sc = new Scanner(System.in);
        BufferedWriter bufw = null;
        BufferedReader bufr;
        
        try {
            // 开启客户端连接本地11111端口
            s = new Socket("127.0.0.1", 11111);
            
            // 获取连接的输入输出流
            bufw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            bufr = new BufferedReader(new InputStreamReader(s.getInputStream()));
            
            // 客户端循环输入并获取服务端返回的字符串
            while (sc.hasNext()) {
                String scannerIn = sc.next();
                
                // 将键盘录入写入Socket流中
                bufw.write(scannerIn);
                bufw.newLine();  // 服务端需要判断行结尾
                bufw.flush();
                
                // 如果输入q就退出循环关闭客户端
                if (scannerIn.equals("q")) {
                    System.out.println("客户端关闭");
                    return;
                }
                
                // 打印从流中读取的服务端返回数据
                System.out.println("返回的数据:" + bufr.readLine());
            }
            
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 服务端线程
class ServerSide implements Runnable {
    public void run() {
        ServerSocket ss = null;
        Socket s;
        
        try {
            // 初始化服务器套接字,监听11111端口
            ss = new ServerSocket(11111);
            
            // 获取客户端发来的套接字
            s = ss.accept();
            
            // 获取连接的输入输出流对象
            BufferedReader bufr = new BufferedReader(new InputStreamReader(s.getInputStream()));
            BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            
            String line;
            while ((line = bufr.readLine()) != null) {
                System.out.print("服务端接收数据:" + line + "  ");
                
                // 如果接收到q就关闭服务器
                if (line.equals("q")) {
                    System.out.println("服务器关闭");
                    return;
                }
                
                // 反转字符串
                StringBuilder sb = new StringBuilder(line);
                sb.reverse();
                String str1 = new String(sb);
                
                // 将反转的字符串写入服务端的输出流中返回给客户端
                bufw.write(str1);
                bufw.newLine();
                bufw.flush();
                
                System.out.println("已返回");
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、UDP网络传输

核心概念

UDP传输只需一个类:DatagramSocket

  • 没有开启流对象
  • 只是把数据封装成DatagramPacket对象进行发送或接收

DatagramSocket类

表示可接收可发送数据包的套接字(UDP套接字)。

构造方法:

DatagramSocket()                                    // 绑定到任何可用端口
DatagramSocket(int port)                           // 绑定到指定端口
DatagramSocket(SocketAddress bindaddr)             // 绑定到指定地址
DatagramSocket(int port, InetAddress laddr)        // 绑定到指定端口和地址

常用方法:

方法返回类型描述
bind(SocketAddress addr)void将DatagramSocket绑定到特定地址和端口
send(DatagramPacket p)void从此套接字发送数据包
receive(DatagramPacket p)void从此套接字接收数据包(阻塞式方法)
close()void关闭套接字
getPort()int返回此套接字的端口
isBound()boolean返回套接字的绑定状态

DatagramPacket类

表示数据报包(UDP在网络层中的传输单元)。

重要说明:

  • 构造时必须设置数据缓冲区buf
  • 发送时将buf封装到包中
  • 接收时将包中的数据存入buf中
  • 有地址参数的构造方法用于发送数据

构造方法:

// 用于接收数据
DatagramPacket(byte[] buf, int length)

// 用于发送数据
DatagramPacket(byte[] buf, int length, InetAddress address, int port)

常用方法:

方法返回类型描述
getAddress()InetAddress返回数据报的目标/源IP地址
getData()byte[]返回数据缓冲区
getLength()int返回数据长度
getPort()int返回端口号
getSocketAddress()SocketAddress获取套接字地址
setData(byte[] buf)void设置数据缓冲区
setAddress(InetAddress iaddr)void设置地址
setPort(int iport)void设置端口

UDP传输示例

UDP发送端:

import java.net.*;

class UdpSend {
    public static void main(String[] args) throws Exception {
        // 1. 建立DatagramSocket服务
        DatagramSocket ds = new DatagramSocket();
        
        // 2. 提供数据并封装到DatagramPacket
        byte[] buf = "abcdefg".getBytes();
        DatagramPacket dp = new DatagramPacket(buf, buf.length,
                InetAddress.getByName("192.168.1.5"), 10000);
        
        // 3. 通过DatagramSocket发送数据包
        ds.send(dp);
        
        // 4. 关闭资源
        ds.close();
    }
}

UDP接收端:

class UdpReceive {
    public static void main(String[] args) throws Exception {
        // 1. 定义DatagramSocket服务,绑定监听端口10000
        DatagramSocket ds = new DatagramSocket(10000);
        
        // 2. 定义数据缓冲区
        byte[] buf = new byte[1024];
        
        // 3. 定义DatagramPacket接收数据
        DatagramPacket dp = new DatagramPacket(buf, buf.length);
        
        // 4. 接收数据包
        ds.receive(dp);
        
        // 5. 提取数据
        String data = new String(dp.getData(), 0, dp.getLength());
        System.out.println(data);
        
        // 6. 关闭资源
        ds.close();
    }
}

UDP应用场景

UDP具有TCP无法比拟的速度优势:

  1. 优势:不属于连接性协议,消耗资源小,处理速度快
  2. 适用场景:
    • 音频视频传输
    • 网络直播
    • 视频聊天
    • 在线游戏
    • DNS查询
  3. 特点:即使丢失少量数据包,也不会对接收结果产生太大影响

五、常见异常处理

网络编程常见异常类型:

异常类型描述常见原因
java.net.UnknownHostException未知主机异常主机名无法解析、DNS配置问题
java.net.SocketException套接字异常网络连接问题、端口被占用
java.io.IOExceptionIO异常输入输出流操作失败
java.net.BindException绑定端口异常端口已被占用、权限不足

异常处理最佳实践:

try {
    // 网络操作代码
    Socket socket = new Socket("127.0.0.1", 8080);
    // 其他操作...
} catch (UnknownHostException e) {
    System.err.println("无法解析主机: " + e.getMessage());
} catch (IOException e) {
    System.err.println("网络IO异常: " + e.getMessage());
} catch (Exception e) {
    System.err.println("其他异常: " + e.getMessage());
} finally {
    // 确保资源被正确关闭
    if (socket != null) {
        try {
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结

TCP vs UDP对比

特性TCPUDP
连接性面向连接无连接
可靠性可靠传输不保证可靠
速度相对较慢快速
开销较大(20字节头部)较小(8字节头部)
应用场景文件传输、网页浏览、邮件视频直播、在线游戏、DNS
数据格式字节流数据报

Java网络编程要点

  1. TCP编程:使用Socket和ServerSocket,需要处理流操作
  2. UDP编程:使用DatagramSocket和DatagramPacket,操作数据包
  3. 异常处理:网络编程必须妥善处理各种网络异常
  4. 资源管理:及时关闭Socket连接和流对象,避免资源泄露
  5. 线程安全:多客户端场景下需要考虑线程安全问题

通过理解TCP和UDP的特性差异,选择合适的协议来满足不同的应用需求,是网络编程的关键。