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) 抽象了复杂的网络操作。

相关推荐
知其然亦知其所以然3 小时前
MySQL性能暴涨100倍?其实只差一个“垂直分区”!
后端·mysql·面试
往事随风去3 小时前
惊!多线程编程竟成内存杀手:90%程序员不知道的OOM陷阱
java·后端
间彧3 小时前
@Transactional(readOnly=true)与MVCC隔离级别的关联机制
后端
TZOF3 小时前
TypeScript的新类型(五):tuple元组
前端·后端·typescript
TZOF3 小时前
TypeScript的object大小写的区别
前端·后端·typescript
TZOF3 小时前
TypeScript的对象如何进行类型声明
前端·后端·typescript
用户5965906181343 小时前
Moq 是mock库
后端
用户5965906181343 小时前
AutoMappe包及用法
后端