Java 中的套接字类型

2025年3月17日 | 阅读 14 分钟

套接字是 Java 网络支持的核心概念。套接字范式是 20 世纪 80 年代初 4.2BSD Berkeley UNIX 版本的一部分。因此,使用了 Berkeley 套接字这个名称。套接字是现代网络的基础,因为套接字允许一台计算机同时运行多个客户端,并提供多种类型的信息。这是通过使用端口实现的,端口是特定机器上的一个编号套接字。现在,在客户端连接到服务器之前,服务器进程被称为“监听”一个端口。服务器可以托管连接到同一端口号的多个客户端,尽管每个会话都是不同的。为了控制多客户端连接,服务器进程必须是多线程的,或者具有其他多路复用并发 I/O 的方式。

Types of Sockets in Java

套接字连接通过协议进行。互联网协议 (IP) 是一种低级路由协议,它将数据分成小数据包,并将它们发送到网络上的地址。但在其中不能保证数据包能送达目的地。为了可靠地传输数据,还有一个称为 **传输控制协议** (TCP) 的更高级协议,它能有效地将这些数据包串联起来,并根据需要进行排序和重传。第三种称为 **用户数据报协议** (UDP) 的协议可以直接用于支持快速、离线、不可靠的数据包传输。

建立连接后,一个更高级的协议会确保这一点,这取决于你使用的是哪个端口。TCP/IP 保留了前 1,024 个端口供特定协议使用。如果你花足够的时间上网冲浪,其中许多你都会很熟悉。端口号 23 用于 Telnet;21 用于 FTP;43 用于 whois;25 用于电子邮件;80 用于 HTTP;79 用于 finger;119 用于 netnews - 以此类推。客户端如何与端口交互由每个协议决定。

Java 中的套接字类型

Java 支持以下三种类型的套接字

  • 流套接字 (Stream Sockets)
  • 数据报套接字 (Datagram Sockets)
  • 原始套接字 (Raw Sockets)

流套接字 (Stream Sockets)

**流套接字** 允许进程使用 TCP 进行通信。流套接字提供双向、可靠、有序、非复制的数据流,没有记录边界。它是一种 **面向连接** 的套接字。一旦建立连接,就可以将这些套接字作为字节流进行读写。套接字类型是 **SOCK_STREAM**。

当我们希望通过可靠的连接在两个网络程序之间发送和接收多个消息时,就会使用流套接字。流套接字依赖 TCP 来确保消息能够无误地到达目的地。实际上,IP 数据包很可能在网络上传输时丢失或出错。无论哪种情况,接收端的 TCP 都会连接到发送端的 TCP 并重新发送该 IP 数据包。这会在两个流套接字之间建立一个可靠的连接。

流套接字在客户端/服务器程序中起着必要的作用。客户端程序(即需要访问某些服务的网络感知程序)尝试创建一个流套接字对象,该对象连接到服务器程序的宿主机的 IP 地址和服务器程序的端口号(即提供服务的网络感知程序)。客户端程序的流套接字初始化代码会将 IP 地址和端口号传递给客户端宿主机的网络管理软件。

该软件通过 IP 地址和端口号(通过 IP)传递给服务器宿主机上的 NIC。服务器宿主机上的网络管理软件会尝试从 NIC 读取数据(通过 IP),并尝试验证服务器程序是否在指定端口上监听连接请求。如果服务器程序正在监听,服务器宿主机上的网络管理软件会向客户端宿主机上的网络管理软件发送一个正向确认。作为对客户端程序流套接字初始化代码的回应,会为客户端程序建立一个端口号,该端口号(通过客户端和服务器宿主机上的网络管理软件)会传递给服务器程序的流套接字(该流套接字使用该号码来标识将发送消息的客户端程序),从而完成流套接字对象的初始化。

如果服务器程序没有在端口上监听,服务器宿主机上的网络管理软件会向客户端宿主机上的网络管理软件发送一个否定确认。作为回应,客户端程序流套接字初始化代码会抛出一个异常对象,并且不会建立通信通道(即不会创建流套接字对象)。

TCP/IP 客户端套接字

TCP/IP 套接字用于在 Internet 主机之间实现可靠的双向、持久、点对点的流连接。Java I/O 系统可以使用套接字连接到本地系统或其他 Internet 系统上的其他程序。需要注意的是,applet 会建立一个反向套接字连接到加载 applet 的宿主机。此限制存在是因为通过防火墙加载的 applet 访问任意系统是危险的。Java 中有两种类型的 TCP 套接字。

一种用于 **服务器**,一种用于 **客户端**。ServerSocket 类设计为“监听器”,在执行任何操作之前等待客户端连接。所以 ServerSocket 用于服务器。Socket 类用于客户端。它设计用于连接到服务器套接字并启动协议交换。这是因为客户端套接字在 Java 应用程序中最常使用。创建 Socket 对象会自动建立客户端和服务器之间的连接。没有方法或构造函数会明确地公开有关设置此连接的详细信息。

以下是用于创建客户端套接字的两个构造函数

  1. **Socket(String hostName, int port) throws UnknownHostException, IOException:** 创建一个连接到指定主机和端口的套接字。
  2. **Socket(InetAddress ipAddress, int port) throws IOException:** 使用预先存在的 InetAddress 对象和端口创建套接字。

Socket 定义了多个实例方法。例如,Socket 始终可以使用以下方法检查关联的地址和端口信息

  1. **InetAddress getInetAddress( ):** 返回与 Socket 对象关联的 InetAddress。如果套接字未连接,则返回 null。
  2. **int getPort( ):** 返回调用 Socket 对象连接到的远程端口。如果套接字未连接,则返回 0。
  3. **int getLocalPort( ):** 返回调用 Socket 对象绑定的本地端口。如果套接字未绑定,则返回 -1。
  4. **InputStream getInputStream( ) throws IOException:** 返回与调用套接字关联的 InputStream。
  5. **OutputStream getOutputStream( ) throws IOException:** 返回与调用套接字关联的 OutputStream。
  6. **connect( ):** 允许你指定新连接
  7. **isConnected( ):** 如果套接字已连接到服务器,则返回 true。
  8. **isBound( ):** 如果套接字已绑定到地址,则返回 true。
  9. **isClosed( ):** 如果套接字已关闭,则返回 true。

以下程序提供了一个简单的套接字示例。它打开与 InterNIC 服务器的“whois”端口(端口 43)的连接,将命令行参数发送到套接字,并打印返回的数据。InterNIC 将尝试按注册的 Internet 域名查找参数,然后发送该站点的 IP 地址和联系信息。

流套接字示例

WhoisClient.java

输出

Types of Sockets in Java

程序的工作方式如下。首先,构造套接字以编译主机名“internic.net”和端口号 43,这是一个处理 43 端口 whois 请求的在线站点。此外,在套接字上打开了输入和输出流。然后,构造一个包含我们想要获取信息的网站名称的字符串。通过从套接字输入来读取响应,并将结果显示出来。

数据报套接字 (Datagram Sockets)

**数据报套接字** 允许进程使用 UDP 进行通信。数据报套接字支持双向消息流。它是一种无连接套接字。数据报套接字上的进程可能以与发送顺序不同的顺序接收消息,也可能接收到重复的消息。数据的记录边界会得到保留。套接字类型是 **SOCK_DGRAM**。

TCP/IP 式的网络对于大多数网络需求来说都是合适的。它提供了序列化、可预测、可靠的数据包流。但这并非没有代价。TCP 包含许多先进的算法来处理拥挤网络上的拥塞控制,以及对数据包丢失的悲观预期。这导致了一种效率较低的数据传输方式。数据报提供了一种替代方案。

数据报是传输于机器之间的大量信息。一旦数据报被发布以实现其预期目的,就不能保证它会到达,甚至不能保证有人会去接收它。同样,当接收到数据报时,也不能保证它在传输过程中没有损坏,或者发送它的人是否还在那里接收响应。

Java 通过使用两个类在 UDP 协议之上实现了数据报:DatagramPacket 对象是数据容器,而 DatagramSocket 是用于发送或接收 DatagramPackets 的机制。

DatagramSocket

DatagramSocket 定义了四个公共构造函数。它们如下所示

  • **DatagramSocket( ) throws SocketException** - 创建一个绑定到本地计算机上任何可用端口的数据报套接字。
  • **DatagramSocket(int port) throws SocketException** - 创建一个绑定到 port 参数指定的端口的数据报套接字。
  • **DatagramSocket(int port, InetAddress ipAddress) throws SocketException** - 构造一个绑定到指定端口和 InetAddress 的 DatagramSocket。
  • **DatagramSocket(SocketAddress address) throws SocketException** - 构造一个绑定到指定 SocketAddress 的 DatagramSocket。SocketAddress 是一个抽象类,由具体类 InetSocketAddress 实现。InetSocketAddress 封装了 IP 地址和端口号。所有这些在创建套接字时如果发生错误都可能抛出 SocketException。

DatagramSocket 定义了许多方法。其中两个最重要的方法是 send() 和 receive(),如下所示

  • **void send(DatagramPacket packet) throws IOException** - 将数据包发送到数据包指定的端口。
  • **void receive(DatagramPacket packet) throws IOException** - 等待从数据包指定的端口接收数据包并返回结果。

其他方法可以让你访问与 DatagramSocket 关联的各种属性。

以下是一些示例

  • **InetAddress getInetAddress( ):** 如果套接字已连接,则返回地址。否则,返回 null。
  • **int getLocalPort( ):** 返回本地端口的编号。
  • **int getPort( ):** 返回套接字连接到的端口的编号。如果套接字未连接到端口,则返回 -1。
  • **boolean isBound( ):** 如果套接字已绑定到地址,则返回 true。否则返回 false。
  • **boolean isConnected( ):** 如果套接字已连接到服务器,则返回 true。否则返回 false。
  • **void setSoTimeout(int millis) throws SocketException:** 将超时设置为 millis 参数指定的毫秒数。

DatagramPacket

DatagramPacket 定义了几个构造函数。其中四个如下所示

  • **DatagramPacket(byte data[ ], int size):** 指定一个用于接收数据的缓冲区和数据包的大小。用于通过 DatagramSocket 接收数据。
  • **DatagramPacket(byte data[ ], int offset, int size):** 允许我们指定缓冲区中存储数据的偏移量。
  • **DatagramPacket(byte data[ ], int size, InetAddress ipAddress, int port):** 指定目标地址和端口,DatagramSocket 使用这些信息来确定数据包中的数据将在何处发送。
  • **DatagramPacket(byte data[ ], int offset, int size, InetAddress ipAddress, int port):** 从指定偏移量开始传输数据。

DatagramPacket 定义了几个方法,包括此处显示的方法,这些方法可以访问数据包的地址和端口号,以及原始数据及其长度。通常,get 方法用于接收到的数据包,set 方法用于

将要发送的数据包。

  • **InetAddress getAddress( ):** 返回源地址(对于正在接收的数据报)或目标地址(对于正在发送的数据报)。
  • **byte[ ] getData( ):** 返回数据报中的数据字节数组。主要用于在数据报接收后检索数据。
  • **int getLength( ):** 返回 getData() 方法返回的字节数组中有效数据的长度。这可能不等于整个字节数组的长度。
  • **int getOffset( ):** 返回数据的起始索引。
  • **int getPort( ):** 返回端口号。
  • **void setAddress(InetAddress ipAddress):** 设置数据包将发送到的地址。地址由 ipAddress 指定。
  • **void setData(byte[ ] data):** 将数据设置为 data,偏移量设置为零,长度设置为 data 中字节数。
  • **void setData(byte[ ] data, int idx, int size):** 将数据设置为 data,偏移量设置为 idx,长度设置为 size。
  • **void setLength(int size):** 将数据包的长度设置为 size。
  • **void setPort(int port):** 将端口设置为 port。

数据报套接字示例

以下示例实现了一个非常简单的网络通信客户端和服务器。消息在服务器的窗口中输入,并通过网络传输到客户端,然后在客户端显示。

WriteServer.java

此示例应用程序受限于使用 DatagramSocket 构造函数在本地计算机上的两个端口之间运行。要使用该程序,在一个窗口中运行 java WriteServer;这将作为客户端。然后运行 java WriteServer 1;这将作为服务器。在服务器窗口中输入的任何内容都会在接收到换行符后发送到客户端窗口。

原始套接字 (Raw Sockets)

**原始套接字** 提供 ICMP(Internet 控制消息协议)访问。这些套接字通常是面向数据报的,但它们的具体性质取决于协议提供的接口。原始套接字不适合大多数应用程序。它用于支持开发新的通信协议或访问现有协议更复杂的功能。只有 root 进程才能使用原始套接字。套接字类型是 **SOCK_RAW**。

Internet 的主要部分是地址。Internet 上的每台计算机设备都有自己的地址。Internet 地址是网络上每台计算机的唯一标识符。最初,所有 Internet 地址都是 32 位值,分为四个 8 位值。IPv4(Internet Protocol,版本 4)指定了这种类型的地址。但是,一种名为 IPv6(Internet Protocol,版本 6)的新地址系统已经投入使用。IPv6 使用 128 位值来表示地址,该地址被组织成八个 16 位片段。虽然 IPv6 有几个原因和优点,但最主要的一个是它支持比 IPv4 大得多的地址空间。为了与 IPv4 保持向后兼容性,IPv6 地址在其低 32 位中可以包含一个有效的 IPv4 地址。这样,IPv4 就与 IPv6 向后兼容。幸运的是,如果你使用 Java,无论我们使用的是 IPv4 还是 IPv6 地址,这些细节都由 Java 为我们处理,我们通常不必担心。正如 IP 地址的数字定义了网络顺序一样,Internet 地址的名称,即域名,定义了机器在命名空间中的位置。例如,“www.javatpoint.com”位于 COM 域(保留的商业网站);它被称为“javatpoint”(按组织名称),而“www”标识了一个 Web 应用程序服务器。Internet 域名由域名服务 (DNS) 映射到 IP 地址。这允许用户使用域名,但 Internet 是使用 IP 地址工作的。

Java 中的接口和网络类通过扩展已建立的流 I/O 接口并累积构建跨网络 I/O 对象所需的功能来支持 TCP/IP。Java 同时支持 TCP 和 UDP 协议族。对于跨网络可靠的流式 I/O,使用 TCP。UDP 支持一种更简单、因此更快、面向点对点数据报的模型。

Rocksaw 是一个简单的 API,它使用 Java 中的 IPv4 和 IPv6 来运行 I/O 网络。它提供 ICMP 访问。ICMP 是一种工作在网络层的协议,用于网络设备诊断网络通信问题。它确定数据是否及时到达预定目的地。它对于错误报告和测试至关重要。ICMP 是一种无连接协议:设备在发送 ICMP 消息之前不需要与另一台设备建立连接。

Rocksaw 是 Java 中多平台原始套接字编程的事实标准 API。它已作为商业产品和定制企业应用程序的一部分部署在多个计算节点上。RockSaw 的当前版本可在以下 32 位和 64 位平台上编译:Linux、Windows(使用 Cygwin/MinGW/Winsock 或 Visual C++)、FreeBSD 和 Darwin/Mac OS X。它应该可以在使用 GNU 工具链的其他 POSIX 系统上编译。原始套接字用于生成/接收内核不支持的特定类型的数据包。

原始套接字的一个简单示例是 PING。Ping 通过发送 ICMP 回显数据包(Internet Message Control Protocol,一种除了 TCP 或 UDP 之外的 IP 协议)工作。内核具有响应回显/ping 数据包的内置代码。它必须符合 TCP/IP 规范。没有代码来创建这些数据包,因为它们不需要。因此,与其创建一个新的系统调用并在内核中编写关联的代码来实现这一点,“ping 数据包生成器”是一个用户空间程序。它格式化一个 ICMP 回显数据包,并通过 SOCK_RAW 发送出去,等待响应。这就是为什么 ping 以 set-uid root 身份运行的原因。