# Java 套接字教程

原文：http://zetcode.com/java/socket/

Java 套接字教程展示了如何使用套接字在 Java 中进行网络编程。 套接字编程是低级的。 本教程的目的是介绍包括这些低级详细信息的网络编程。 有些高级 API 可能更适合实际任务。 例如，Java 11 引入了`HttpClient`，而 Spring 具有`Webclient`。

## Java 套接字

在编程中，套接字是网络上运行的两个程序之间的通信端点。 套接字类用于在客户端程序和服务器程序之间创建连接。 `Socket`代表客户端套接字，`ServerSocket`代表服务器套接字。

> **注意**：在网络中，“套接字”一词具有不同的含义。 它用于 IP 地址和端口号的组合。

`ServerSocket`绑定到端口号，这是通过客户端和服务器同意进行通信的唯一 ID。

`Socket`和`ServerSocket`用于 TCP 协议。 `DatagramSocket`和`DatagramPacket`用于 UDP 协议。

TCP 更可靠，具有大量错误检查并需要更多资源。 HTTP，SMTP 或 FTP 等服务使用它。 UDP 的可靠性要差得多，错误检查的能力也有限，所需资源也更少。 VoIP 等服务使用它。

`DatagramSocket`是用于发送和接收数据报包的套接字。 数据报包由`DatagramPacket`类表示。 在数据报套接字上发送或接收的每个数据包都经过单独寻址和路由。 从一台机器发送到另一台机器的多个数据包可能会以不同的方式路由，并且可能以任何顺序到达。

## Java 套接字时间客户端

是提供当前时间的服务器。 客户端无需任何命令即可直接连接到服务器，服务器以当前时间作为响应。

> **注意**：时间服务器来来往往，因此我们可能需要在 <https://www.ntppool.org/en/> 上找到可用的服务器。

在我们的示例中，我们选择了瑞典的服务器。

`com/zetcode/SocketTimeClient.java`

```java
package com.zetcode;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

// time servers come and go; we might need to 
// find a functioning server on https://www.ntppool.org/en/

public class SocketTimeClient {

    public static void main(String[] args) throws IOException {

        var hostname = "3.se.pool.ntp.org";
        int port = 13;

        try (var socket = new Socket(hostname, port)) {

            try (var reader = new InputStreamReader(socket.getInputStream())) {

                int character;
                var output = new StringBuilder();

                while ((character = reader.read()) != -1) {

                    output.append((char) character);
                }

                System.out.println(output);
            }
        }
    }
}

```

该示例连接到时间服务器并接收当前时间。

```java
var hostname = "3.se.pool.ntp.org";
int port = 13;

```

这是瑞典的时间服务器； 13 端口是白天服务的标准端口。

```java
try (var socket = new Socket(hostname, port)) {

```

流客户端套接字已创建。 它连接到命名主机上的指定端口号。 使用 Java 的`try-with-resources`语句自动关闭套接字。

```java
try (var reader = new InputStreamReader(socket.getInputStream())) {

```

`getInputStream()`返回此套接字的输入流。 我们从此输入流中读取服务器的响应。 套接字之间的通信以字节为单位； 因此，我们将`InputStreamReader`用作字节和字符之间的桥梁。

```java
int character;
var output = new StringBuilder();

while ((character = reader.read()) != -1) {

    output.append((char) character);
}

System.out.println(output);

```

由于响应消息很小，因此我们可以逐个字符地读取它，而对性能的影响很小。

## Java 套接字 Whois 客户端

Whois 是基于 TCP 的面向事务的查询/响应协议，被广泛用于向互联网用户提供信息服务。 它用于查询域名或 IP 地址块所有者等信息。

> **注意**：大多数 Whois 服务器仅提供有限的信息（例如，仅针对选定的域名），并且有关域名所有者的信息通常由域名注册机构匿名。

Whois 协议使用端口 43。

`com/zetcode/WhoisClientEx.java`

```java
package com.zetcode;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

// probing whois.iana.org might give the right
// whois server

public class WhoisClientEx {

    public static void main(String[] args) throws IOException {

        var domainName = "example.com";
        var whoisServer = "whois.nic.me";
        int port = 43;

        try (var socket = new Socket(whoisServer, port)) {

            try (var writer = new PrintWriter(socket.getOutputStream(), true)) {

                writer.println(domainName);

                try (var reader = new BufferedReader(
                        new InputStreamReader(socket.getInputStream()))) {

                    String line;

                    while ((line = reader.readLine()) != null) {

                        System.out.println(line);
                    }
                }
            }
        }
    }
}

```

在该示例中，我们探查了有关域名所有者的信息。

```java
try (var writer = new PrintWriter(socket.getOutputStream(), true)) {

    writer.println(domainName);
...

```

我们获得套接字的输出流，并将其包装到`PrintWriter`中。 `PrintWriter`会将我们的字符转换为字节。 使用`println()`，将域名写入流中。 通过套接字的通信被缓冲。 `PrintWriter`的第二个参数是`autoFlush`； 如果设置为`true`，则在每个`println()`之后将刷新缓冲区。

```java
try (var reader = new BufferedReader(
        new InputStreamReader(socket.getInputStream()))) {

    String line;

    while ((line = reader.readLine()) != null) {

        System.out.println(line);
    }
}

```

来自服务器的响应被读取并写入控制台。

## Java 套接字 GET 请求

在下面的示例中，我们创建一个 GET 请求。 HTTP GET 请求用于检索特定资源。

`com/zetcode/JavaSocketGetRequest.java`

```java
package com.zetcode;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class SocketGetRequest {

    public static void main(String[] args) throws IOException {

        try (var socket = new Socket("webcode.me", 80)) {

            try (var wtr = new PrintWriter(socket.getOutputStream())) {

                // create GET request
                wtr.print("GET / HTTP/1.1\r\n");
                wtr.print("Host: www.webcode.me\r\n");
                wtr.print("\r\n");
                wtr.flush();
                socket.shutdownOutput();

                String outStr;

                try (var bufRead = new BufferedReader(new InputStreamReader(
                        socket.getInputStream()))) {

                    while ((outStr = bufRead.readLine()) != null) {

                        System.out.println(outStr);
                    }

                    socket.shutdownInput();
                }
            }
        }
    }
}

```

该示例从网站检索 HTML 页面。

```java
try (var socket = new Socket("webcode.me", 80)) {

```

我们在端口 80 上的指定网页上打开一个套接字。HTTP 协议使用端口 80。

```java
try (var wtr = new PrintWriter(socket.getOutputStream())) {

```

我们将在协议上发布文本命令； 因此，我们为套接字输出流创建一个`PrintWriter`。 由于我们没有将`autoFlush`选项设置为`true`，因此我们需要手动刷新缓冲区。

```java
// create GET request
wtr.print("GET / HTTP/1.1\r\n");
wtr.print("Host: www.webcode.me\r\n");
wtr.print("\r\n");
wtr.flush();

```

我们创建一个 HTTP GET 请求，该请求检索指定网页的主页。 请注意，文本命令以`\r\n`（CRLF）字符完成。 这些是必需的通信详细信息，在 RFC 2616 文档中进行了描述。

```java
socket.shutdownOutput();

```

`shutdownOutput`禁用此套接字的输出流。 最后必须关闭连接。

```java
try (var bufRead = new BufferedReader(new InputStreamReader(
    socket.getInputStream()))) {

```

对于服务器响应，我们打开一个套接字输入流，并使用`InputStreamReader`将字节转换为字符。 我们还缓冲读取操作。

```java
while ((outStr = bufRead.readLine()) != null) {

    System.out.println(outStr);
}

```

我们逐行读取数据。

```java
socket.shutdownInput();

```

最后，我们也关闭了输入流。

## Java 套接字 HEAD 请求

在下一个示例中，我们使用 Java 套接字创建 HEAD 请求。 HEAD 方法与 GET 方法相同，不同之处在于服务器在响应中不返回消息正文。 它仅返回标头。

`com/zetcode/SocketHeadRequest.java`

```java
package com.zetcode;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class SocketHeadRequest {

    public static void main(String[] args) throws IOException {

        var hostname = "webcode.me";
        int port = 80;

        try (var socket = new Socket(hostname, port)) {

            try (var writer = new PrintWriter(socket.getOutputStream(), true)) {

                writer.println("HEAD / HTTP/1.1");
                writer.println("Host: " + hostname);
                writer.println("User-Agent: Console Http Client");
                writer.println("Accept: text/html");
                writer.println("Accept-Language: en-US");
                writer.println("Connection: close");
                writer.println();

                try (var reader = new BufferedReader(new InputStreamReader(
                        socket.getInputStream()))) {

                    String line;

                    while ((line = reader.readLine()) != null) {

                        System.out.println(line);
                    }
                }
            }
        }
    }
}

```

该示例检索指定网页的标题。

```java
writer.println("HEAD / HTTP/1.1");

```

我们发出 HEAD 命令。

```java
writer.println("Connection: close");

```

在 HTTP 协议版本 1.1 中，除非另有声明，否则所有连接均被视为持久连接（保持活动状态）。 通过将选项设置为`false`，我们通知我们要在请求/响应周期之后完成连接。

## Java `ServerSocket`日期服务器

下面的示例使用`ServerSocket`创建一个非常简单的服务器。 `ServerSocket`创建绑定到指定端口的服务器套接字。

`com/zetcode/DateServer.java`

```java
package com.zetcode;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.time.LocalDate;

public class DateServer {

    public static void main(String[] args) throws IOException {

        int port = 8081;

        try (var listener = new ServerSocket(port)) {

            System.out.printf("The started on port %d%n", port);

            while (true) {

                try (var socket = listener.accept()) {

                    try (var pw = new PrintWriter(socket.getOutputStream(), true)) {

                        pw.println(LocalDate.now());
                    }
                }
            }
        }
    }
}

```

该示例创建一个返回当前日期的服务器。 最后必须手动终止该程序。

```java
int port = 8081;

try (var listener = new ServerSocket(port)) {

```

在端口 8081 上创建一个服务器套接字。

```java
try (var socket = listener.accept()) {

```

`accept()`方法监听与此套接字建立的连接并接受它。 该方法将阻塞，直到建立连接为止。

```java
try (var pw = new PrintWriter(socket.getOutputStream(), true)) {

    pw.println(LocalDate.now());
}

```

我们将当前日期写入套接字输出流。

`get_request.py`

```java
#!/usr/bin/env python3

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

    s.connect(("localhost" , 8081))
    s.sendall(b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: text/html\r\n\r\n")
    print(str(s.recv(4096),  'utf-8'))

```

我们有一个 Python 脚本向服务器发出 GET 请求。

```java
$ get_request.py
2019-07-15

```

这是输出。

## Java 套接字客户端/服务器示例

在下面的示例中，我们有一个服务器和一个客户端。 服务器反转从客户端发送的文本。 这个例子很简单而且很阻塞。 为了改善它，我们需要包括线程。

`com/zetcode/ReverseServer.java`

```java
package com.zetcode;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;

// This server communicates only with one client at a time.
// It must disconnect from a client first to communicate
// with another client. It receives a bye command from a client
// to close a connection.

public class ReverseServer {

    public static void main(String[] args) throws IOException {

        int port = 8081;

        try (var serverSocket = new ServerSocket(port)) {

            System.out.println("Server is listening on port " + port);

            while (true) {

                try (var socket = serverSocket.accept()) {

                    System.out.println("client connected");

                    try (var reader = new BufferedReader(new InputStreamReader(
                                socket.getInputStream()));
                         var writer = new PrintWriter(socket.getOutputStream(), true)) {

                        String text;

                        do {

                            text = reader.readLine();

                            if (text != null) {

                                var reversed = new StringBuilder(text).reverse().toString();
                                writer.println("Server: " + reversed);

                                System.out.println(text);
                            }
                        } while (!"bye".equals(text));

                        System.out.println("client disconnected");
                    }
                }
            }
        }
    }
}

```

`ReverseServer`将反向字符串发送回客户端。 一次仅与一个客户端通信。 它必须首先与客户端断开连接才能与另一个客户端通信。 它从客户端收到一个`bye`命令以关闭连接。

```java
try (var reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    var writer = new PrintWriter(socket.getOutputStream(), true)) {

```

我们有一个套接字输入流，用于读取客户端数据，以及套接字输出流，用于将响应发送回客户端； 输出流和连接已关闭。

```java
do {

    text = reader.readLine();

    if (text != null) {

        var reversed = new StringBuilder(text).reverse().toString();
        writer.println("Server: " + reversed);

        System.out.println(text);
    }
} while (!"bye".equals(text));

```

为单个客户端创建了一个`do-while`循环。 我们从客户端读取数据，然后将修改后的内容发送回去。 收到客户端的再见命令后，循环结束。 在此之前，没有其他客户端可以连接到服务器。

`com/zetcode/ReverseClient.java`

```java
package com.zetcode;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

// the client must send a bye command to
// inform the server to close the connection

public class ReverseClient {

    public static void main(String[] args) throws IOException {

        var hostname = "localhost";
        int port = 8081;

        try (var socket = new Socket(hostname, port)) {

            try (var writer = new PrintWriter(socket.getOutputStream(), true)) {

                try (var scanner = new Scanner(System.in)) {

                    try (var reader = new BufferedReader(new InputStreamReader(
                            socket.getInputStream()))) {

                        String command;

                        do {

                            System.out.print("Enter command: ");

                            command = scanner.nextLine();

                            writer.println(command);

                            var data = reader.readLine();
                            System.out.println(data);

                        } while (!command.equals("bye"));
                    }
                }
            }
        }
    }
}

```

客户端将文本数据发送到服务器。

```java
do {

    System.out.print("Enter command: ");

    command = scanner.nextLine();

    writer.println(command);

    var data = reader.readLine();
    System.out.println(data);

} while (!command.equals("bye"));

```

我们从控制台读取输入，并将其发送到服务器。 当我们发送`bye`命令时，`while`循环完成，该命令通知服务器可以关闭连接。

## Java `DatagramSocket`示例

UDP 是一种通信协议，它通过网络传输独立的数据包，不保证到达且也不保证传递顺序。 使用 UDP 的一项服务是每日报价（QOTD）。

下面的示例创建一个连接到 QOTD 服务的客户端程序。

`com/zetcode/DatagramSocketEx.java`

```java
package com.zetcode;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

// DatagramSocket provides network communication via UDP protocol
// The Quote of the Day (QOTD) service is a member of the Internet protocol
// suite, defined in RFC 865

public class DatagramSocketEx {

    public static void main(String[] args) throws IOException {

        var hostname = "djxmmx.net";
        int port = 17;

        var address = InetAddress.getByName(hostname);

        try (var socket = new DatagramSocket()) {

            var request = new DatagramPacket(new byte[1], 1, address, port);
            socket.send(request);

            var buffer = new byte[512];
            var response = new DatagramPacket(buffer, buffer.length);
            socket.receive(response);

            var quote = new String(buffer, 0, response.getLength());
            System.out.println(quote);
        }
    }
}

```

该示例从报价服务检索报价并将其打印到终端。

```java
var address = InetAddress.getByName(hostname);

```

我们从主机名获得 IP 地址。

```java
try (var socket = new DatagramSocket()) {

```

创建了`DatagramSocket`。

```java
var request = new DatagramPacket(new byte[1], 1, address, port);

```

创建了`DatagramPacket`。 由于 QOTD 服务不需要来自客户端的数据，因此我们发送了一个空的小型数组。 每次发送数据包时，我们都需要指定数据，地址和端口。

```java
socket.send(request);

```

数据包通过`send()`发送到其目的地。

```java
var buffer = new byte[512];
var response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);

```

我们收到来自服务的数据包。

```java
var quote = new String(buffer, 0, response.getLength());
System.out.println(quote);

```

我们将接收到的字节转换为字符串并打印。

在本教程中，我们创建了带有套接字的网络 Java 程序。 您可能也对相关教程感兴趣： [Java HTTP GET/POST 请求教程](/java/getpostrequest/)， [Java `InputStreamReader`教程](/java/inputstreamreader/)， [Java Servlet 教程](/java/servlet/)和 [Java 教程](/lang/java/) 。

列出[所有 Java 教程](/all/#java)。