TCP 与 UDP 下的 Socket 系统调用

1、DatagramSocket#connet方法

在Java中,DatagramSocket 专门用于 UDP(User Datagram Protocol,用户数据报协议)的API。 大家都知道UDP协议是面向无连接的 ,但是用于UDP的DatagramSocket类中却有connet()方法,实在令人费解:

java 复制代码
public void connect(InetAddress address, int port) {
        try {
            connectInternal(address, port);
        } catch (SocketException se) {
            throw new Error("connect failed", se);
        }
 }
 
 public void connect(SocketAddress addr) throws SocketException {
        if (addr == null)
            throw new IllegalArgumentException("Address can't be null");
        if (!(addr instanceof InetSocketAddress))
            throw new IllegalArgumentException("Unsupported address type");
        InetSocketAddress epoint = (InetSocketAddress) addr;
        if (epoint.isUnresolved())
            throw new SocketException("Unresolved address");
        connectInternal(epoint.getAddress(), epoint.getPort());
  }

connet()方法共有两个重载的方法,这两个方法的本质是一样的,SocketAddress其实是把InnetAddressport封装在一起。并且Java 的 DatagramSocket.connect() 方法最终确实会调用到底层操作系统的 connect() 系统调用。难道是说UDP也是支持面向连接的吗?

其实不是这样的,connect() 系统调用的作用取决于它操作的套接字类型。

  • TCP 套接字调用 connect():触发网络活动(三次握手),建立一个虚拟电路连接。
  • UDP 套接字调用 connect():执行一个本地操作,记录一个默认地址,不产生任何网络流量

内核中处理 connect() 系统调用的函数大致会这样实现:

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    // 1. 根据sockfd找到内核中对应的socket结构体
    struct socket *sock = get_socket_by_fd(sockfd);

    // 2. 检查socket的类型,执行不同的分支
    switch (sock->type) {
        case SOCK_STREAM: // TCP套接字
            // 触发TCP三次握手协议
            initiate_tcp_handshake(sock, addr);
            break;

        case SOCK_DGRAM:  // UDP套接字
            // 仅仅是将对端地址保存到sock->peer_addr中
            sock->peer_addr = copy_address(addr);
            // !!! 没有网络活动 !!!
            break;

        default:
            return -EINVAL; // 无效的socket类型
    }
    return 0; // 成功
}

所以DatagramSocket.connect() 的作用不是建立网络连接,而是执行一个本地操作 ,为UDP套接字设置一个"默认的通信对端",从而带来性能优化、安全过滤和错误处理三大好处。 调用 connect(InetAddress address, int port) 方法后,会发生以下几件事:

1. 设定默认目标地址 (Primary Effect)

  • 作用 :将传入的 addressport 设置为该 DatagramSocket 的默认目标地址。
  • 后果 :此后,当你调用 send(DatagramPacket p) 方法时,数据包将被发送到这个默认地址,而无视你在 DatagramPacket 对象中设置的目标地址 。如果你尝试在 DatagramPacket 中设置一个不同的地址,send() 方法会抛出 IllegalArgumentException
  • 代码示例
Java 复制代码
    DatagramSocket socket = new DatagramSocket();
    InetAddress serverAddr = InetAddress.getByName("example.com");
    
    socket.connect(serverAddr, 8888); // 设置默认对端为 example.com:8888
    
    // 即使packet里没设置地址,也会发到 example.com:8888
    DatagramPacket packet = new DatagramPacket(data, data.length);
    socket.send(packet); // 目的地:example.com:8888

2. 启用数据包过滤 (Security & Convenience)

  • 作用:内核会为这个已"连接"的套接字设置一个过滤器。
  • 后果 :此后,当你调用 receive(DatagramPacket p) 方法时,只有源地址是 connect 所设地址的数据包才会被接收。来自其他任何地址的数据包都会被操作系统内核静默丢弃。
  • 好处:这大大提升了安全性,你的应用程序无需再处理无关或恶意的数据包,代码逻辑更简单。

3. 激活异步错误接收 (Reliability)

  • 作用:这是最关键但也最不为人知的作用。它允许套接字接收来自网络的异步错误信号(如ICMP错误)。
  • 后果 :如果因为你发送的数据包导致网络产生错误(例如,目标端口没有任何应用程序监听),对方主机会返回一个ICMP"端口不可达"报文。对于一个未连接 的UDP套接字,这个错误无法被关联,因此会被操作系统丢弃,你的程序完全不知情。对于一个已连接 的UDP套接字,操作系统可以将此错误与你的套接字关联,导致后续的 send()receive() 调用抛出 PortUnreachableException 之类的 SocketException
  • 好处:让你的程序能够感知到通信故障,实现了基本的"可观察性",而不是在静默中失败。

4. 允许使用便捷方法 (API Convenience)

  • 作用 :连接后,你可以不再使用笨重的 DatagramPacket 来接收数据(虽然仍然可以),但更重要的是,它允许使用 send()receive() 而不必每次都构造地址。
  • 注意 :Java的API设计是,无论是否连接,发送和接收主要仍然通过 DatagramPacket 对象。这个好处在Java中不如在C语言中直接使用 send()/recv() 系统调用那么明显,但底层逻辑一致。

2、Socket 核心系统调用作用总览

在Java的Socket API中,每个方法的底层基本都对应响应的系统调用,系统调用共同构成了一套完整的网络通信接口,但其行为会根据协议(TCP/UDP)和角色(服务器/客户端)有所不同。 下表是所有核心系统调用的作用总结,并标明了它们在不同场景下的使用情况。

系统调用 主要使用方 核心作用 TCP 场景下的含义 UDP 场景下的含义
socket() 服务器/客户端 这是所有网络通信的第一步。创建通信端点 。它向操作系统内核申请并分配了一个 socket 资源,指定了通信域(如 IPv4 AF_INET)和服务类型(如面向连接的流服务 SOCK_STREAM 对应 TCP,或数据报服务 SOCK_DGRAM 对应 UDP)。返回一个文件描述符 (fd,在 Unix/Linux 中,一切皆文件,socket 也被视为一种特殊的文件,后续所有操作都通过这个 fd 进行)。 创建了一个可用于建立连接的端点。 创建了一个可用于发送/接收数据报的端点。
bind() 服务器 (必须) / 客户端 (可选) socket() 创建的通信资源绑定到一个具体的IP地址和端口号上。 服务器声明在哪个端口提供服务。客户端通常由系统自动分配临时端口。 同上。服务器绑定知名端口,客户端通常不手动绑定。
listen() TCP 服务器 (必须) 将socket置于被动监听状态。通知内核开始接受连接请求并设置连接队列大小。 启动监听,准备接受客户端的连接请求 (SYN)。 不适用。UDP 是无连接的,无需监听。
accept() TCP 服务器 (必须) 从已完成连接队列中取出一个连接。返回一个专用于此连接的新socket。 接受一个客户端的 TCP 连接(完成三次握手后)。这是一个阻塞调用 不适用。UDP 没有连接的概念。
connect() TCP 客户端 (必须) / UDP (可选) 指定默认的远程通信对端 发起TCP三次握手,主动与服务器建立连接。 执行一个本地操作 。仅为socket设置一个默认目标地址,不产生网络流量。用于优化性能和过滤数据。
send() / write() 服务器/客户端 通过已连接的socket发送数据 通过已建立的TCP连接发送数据。 connect() 设置的默认地址发送数据报。
recv() / read() 服务器/客户端 通过已连接的socket接收数据 从已建立的TCP连接接收数据。 接收来自 connect() 设置的默认地址的数据报。
sendto() UDP (主要) 向指定地址发送数据 也可用于TCP,但通常被 send() 取代。 UDP的核心发送函数。每次调用都必须指定目标地址。
recvfrom() UDP (主要) 接收数据并获取发送方的地址 也可用于TCP,但通常被 recv() 取代。 UDP的核心接收函数。返回数据的同时告知数据来自谁。
close() 服务器/客户端 关闭连接,释放资源 触发 TCP 四次挥手,优雅地终止连接。 关闭socket,释放端口。

总结:

  1. socket()bind()奠基,完成了通信端点的创建和寻址。
  2. listen()accept()TCP 服务器的核心,实现了"迎接客户"的连接接收模型。
  3. connect()TCP 客户端的核心 (用于建立连接),也是 UDP 的优化工具(用于指定默认对端)。
  4. send()/recv()sendto()/recvfrom()数据传输的工具。前者用于"连接"场景,后者用于"无连接"或需要知道对端地址的场景。
  5. close()收尾,负责优雅地终止通信并释放资源。

理解每个调用的作用及其在协议栈中的对应阶段,是掌握网络编程的关键。这套系统调用接口是 Unix "一切皆文件" 哲学的完美体现,通过一个文件描述符 (fd) 抽象了复杂的网络操作。

相关推荐
鬼火儿2 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin2 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧4 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧4 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧4 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧4 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧4 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧4 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧4 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang5 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构