ZetCode

Java Socket

最后修改于 2024 年 1 月 27 日

Java Socket 教程展示了如何在 Java 中使用套接字进行网络编程。套接字编程是底层编程。本教程旨在介绍网络编程,包括这些底层细节。存在更高级别的 API,可能更适合实际任务。 例如,Java 11 引入了 HttpClient,而 Spring 拥有 Webclient。

Java Socket

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

注意: 在网络中,套接字一词有不同的含义。它用于 IP 地址和端口号的组合。

ServerSocket 绑定到一个端口号,该端口号是客户端和服务器同意通信的唯一 ID。

SocketServerSocket 用于 TCP 协议。 DatagramSocketDatagramPacket 用于 UDP 协议。

TCP 更可靠,具有广泛的错误检查,并且需要更多资源。 它被 HTTP、SMTP 或 FTP 等服务使用。 UDP 可靠性差得多,错误检查有限,并且需要更少的资源。 它被 VoIP 等服务使用。

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

Java Socket 时间客户端

有一些服务器提供当前时间。 客户端只需连接到服务器而无需任何命令,服务器会以当前时间作为响应。

注意: 时间服务器会来来去去,因此我们可能需要在 https://www.ntppool.org/en/ 上找到一个可用的服务器。

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

com/zetcode/SocketTimeClient.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);
            }
        }
    }
}

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

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

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

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

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

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

getInputStream 返回此套接字的输入流。 我们从该输入流读取服务器的响应。 套接字之间的通信以字节为单位; 因此,我们使用 InputStreamReader 作为字节和字符之间的桥梁。

int character;
var output = new StringBuilder();

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

    output.append((char) character);
}

System.out.println(output);

由于响应消息很小,我们可以逐个字符地读取它,而不会带来太多的性能损失。

Java Socket Whois 客户端

Whois 是一种基于 TCP 的面向事务的查询/响应协议,广泛用于向 Internet 用户提供信息服务。 它用于查询诸如域名或 IP 地址块所有者之类的信息。

注意: 大多数 whois 服务器仅提供有限的信息(例如,仅适用于选定的域名),并且有关所有者的信息通常由域名注册商匿名化。

Whois 协议使用端口 43。

com/zetcode/WhoisClientEx.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);
                    }
                }
            }
        }
    }
}

在该示例中,我们探测有关域名所有者的信息。

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

    writer.println(domainName);
...

我们获取套接字的输出流并将其包装到 PrintWriter 中。 PrintWriter 会将我们的字符转换为字节。 通过 println,我们将域名写入流。 通过套接字的通信会被缓冲。 PrintWriter 的第二个参数是 autoFlush; 如果设置为 true,则每次 println 后都会刷新缓冲区。

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

    String line;

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

        System.out.println(line);
    }
}

从服务器收到的响应被读取并写入到控制台。

Java Socket GET 请求

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

com/zetcode/JavaSocketGetRequest.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 页面。

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

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

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

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

// 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 文档中描述。

socket.shutdownOutput();

shutdownOutput 禁用此套接字的输出流。 这对于最后关闭连接是必要的。

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

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

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

    System.out.println(outStr);
}

我们逐行读取数据。

socket.shutdownInput();

最后,我们也关闭输入流。

Java Socket HEAD 请求

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

com/zetcode/SocketHeadRequest.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);
                    }
                }
            }
        }
    }
}

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

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

我们发出 HEAD 命令。

writer.println("Connection: close");

在 HTTP 协议版本 1.1 中,除非另有声明,否则所有连接都被认为是持久的(保持活动状态)。 通过将选项设置为 false,我们通知您我们希望在请求/响应周期后完成连接。

Java ServerSocket DateServer

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

com/zetcode/DateServer.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());
                    }
                }
            }
        }
    }
}

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

int port = 8081;

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

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

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

accept 方法侦听要与此套接字建立的连接并接受它。 该方法会阻塞,直到建立连接为止。

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

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

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

get_request.py
#!/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 请求。

$ get_request.py
2019-07-15

Java Socket 客户端/服务器示例

在以下示例中,我们有一个服务器和一个客户端。 服务器反转从客户端发送的文本。 该示例简单且阻塞。 为了改进它,我们需要包含线程。

com/zetcode/ReverseServer.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 命令以关闭连接。

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

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

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 循环。 我们从客户端读取数据并将修改后的内容发送回去。 收到客户端的 bye 命令后,循环结束。 在完成此操作之前,没有其他客户端可以连接到服务器。

com/zetcode/ReverseClient.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"));
                    }
                }
            }
        }
    }
}

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

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 是一种通信协议,它在网络上传输独立的包,不保证到达,也不保证传递顺序。Quote of the Day (QOTD) 是使用 UDP 的一项服务。

以下示例创建一个客户端程序,该程序连接到 QOTD 服务。

com/zetcode/DatagramSocketEx.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);
        }
    }
}

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

var address = InetAddress.getByName(hostname);

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

try (var socket = new DatagramSocket()) {

创建一个 DatagramSocket

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

创建一个 DatagramPacket。 由于 QOTD 服务不需要客户端的数据,因此我们发送一个空的短数组。 每次我们发送一个数据包,我们需要指定数据、地址和端口。

socket.send(request);

使用 send 将数据包发送到其目的地。

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 Socket - 语言参考

在本文中,我们使用套接字创建了网络 Java 程序。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。 我从 2007 年开始撰写编程文章。到目前为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程