杰瑞科技汇

Java Socket编程代码如何实现?

目录

  1. 核心概念
    • Socket (套接字)
    • ServerSocket (服务器套接字)
    • TCP vs. UDP
  2. TCP Socket 编程
    • 通信模型
    • 服务器端代码示例
    • 客户端代码示例
    • 如何运行
    • 多线程服务器进阶
  3. UDP Socket 编程
    • 通信模型
    • 发送方代码示例
    • 接收方代码示例
    • 如何运行
  4. 重要注意事项
    • 异常处理
    • 资源关闭 (try-with-resources)
    • IO 流与字节流

核心概念

Socket (套接字)

Socket 是网络编程的 API,它提供了两个程序在网络上进行通信的端点,你可以把它想象成一个电话插孔,一旦你把电话线插进去,就可以进行通话了,在 Java 中,java.net.Socket 类代表一个客户端套接字,java.net.ServerSocket 类代表一个服务器端套接字。

Java Socket编程代码如何实现?-图1
(图片来源网络,侵删)

TCP vs. UDP

在选择使用哪种协议时,需要理解它们之间的根本区别:

特性 TCP (传输控制协议) UDP (用户数据报协议)
连接性 面向连接 (Connection-oriented) 无连接 (Connectionless)
可靠性 可靠,通过确认、重传、排序等机制确保数据无差错、不丢失、不重复且按序到达。 不可靠,不保证数据包的顺序或是否到达,可能会丢失或重复。
速度 较慢,因为需要建立连接和维护状态。 非常快,没有连接和确认的开销。
开销 较高,有头部开销和连接维护开销。 较低,头部开销很小。
适用场景 要求可靠传输的应用,如文件传输、网页浏览、电子邮件。 对速度要求高、能容忍少量丢包的应用,如视频会议、在线游戏、DNS查询。

TCP Socket 编程

TCP 是最常用的 Socket 编程方式,因为它提供了可靠的数据传输。

通信模型

  1. 服务器端:

    • 创建一个 ServerSocket 并绑定到一个特定的端口(8888),开始监听客户端连接。
    • 调用 accept() 方法,该方法会阻塞(暂停执行),直到一个客户端连接到来。
    • accept() 返回时,它会返回一个新的 Socket 对象,这个对象代表与特定客户端的连接。
    • 通过这个 Socket 对象的输入流和输出流与客户端进行双向通信。
    • 通信结束后,关闭 SocketServerSocket
  2. 客户端:

    Java Socket编程代码如何实现?-图2
    (图片来源网络,侵删)
    • 创建一个 Socket 对象,指定服务器的 IP 地址和端口号。
    • 如果服务器正在监听,Socket 构造函数会尝试连接到服务器,连接成功后继续执行。
    • 通过这个 Socket 对象的输入流和输出流与服务器进行双向通信。
    • 通信结束后,关闭 Socket

服务器端代码示例 (TCPServer.java)

这个服务器会等待一个客户端连接,然后接收客户端发送的消息,并将其转换为大写后返回给客户端。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
    public static void main(String[] args) {
        int port = 8888; // 服务器监听的端口
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,等待客户端连接...");
            // accept() 方法会阻塞,直到有客户端连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 获取输入流,用于读取客户端发送的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // 获取输出流,用于向客户端发送数据
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
            String inputLine;
            // 循环读取客户端发送的数据
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 将消息转换为大写并发送回客户端
                String response = inputLine.toUpperCase();
                out.println(response);
                System.out.println("已向客户端发送: " + response);
                // 如果客户端发送 "bye",则结束通信
                if ("bye".equalsIgnoreCase(inputLine)) {
                    break;
                }
            }
            System.out.println("客户端断开连接。");
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

客户端代码示例 (TCPClient.java)

这个客户端连接到服务器,发送一条消息,然后接收并打印服务器返回的响应。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class TCPClient {
    public static void main(String[] args) {
        String hostname = "localhost"; // 或服务器的IP地址
        int port = 8888;
        try (
            // 创建一个Socket连接到指定主机和端口
            Socket socket = new Socket(hostname, port);
            // 获取输出流,用于向服务器发送数据
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 获取输入流,用于读取服务器返回的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 用于从控制台读取用户输入
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器。");
            System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
            String userInput;
            // 循环读取用户输入
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入发送给服务器
                out.println(userInput);
                // 从服务器读取响应
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
                System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
            }
        } catch (UnknownHostException e) {
            System.err.println "不知道的主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

如何运行

  1. 编译代码:

    javac TCPServer.java TCPClient.java
  2. 启动服务器:

    java TCPServer

    你会看到控制台输出: 服务器已启动,等待客户端连接...

  3. 启动客户端: 打开一个新的终端窗口,运行客户端:

    java TCPClient

    你会看到客户端输出: 已连接到服务器。

  4. 进行通信:

    • 在客户端的控制台输入 hello world,然后按回车。
    • 客户端会收到服务器返回的 HELLO WORLD
    • 在客户端输入 bye,然后按回车,客户端和服务器都会关闭连接。

多线程服务器进阶

上面的服务器一次只能处理一个客户端,当处理多个客户端时,必须使用多线程。

服务器端改进代码 (MultiThreadTCPServer.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class MultiThreadTCPServer {
    public static void main(String[] args) {
        int port = 8888;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("多线程服务器已启动,等待客户端连接...");
            while (true) {
                // accept() 阻塞,等待新客户端
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
                // 为每个客户端创建一个新的线程来处理
                ClientHandler handler = new ClientHandler(clientSocket);
                new Thread(handler).start();
            }
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
        }
    }
}
// 客户端处理线程
class ClientHandler implements Runnable {
    private final Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        try (
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
        ) {
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println("来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
                out.println("服务器收到了: " + inputLine.toUpperCase());
                if ("bye".equalsIgnoreCase(inputLine)) {
                    break;
                }
            }
        } catch (IOException e) {
            // 客户端断开连接时会抛出异常,这里可以打印日志
            System.out.println("客户端 " + clientSocket.getInetAddress() + " 断开连接。");
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

UDP Socket 编程

UDP 无需建立连接,数据以数据报的形式发送,速度更快但不保证顺序和可靠性。

通信模型

  1. 发送方:

    • 创建一个 DatagramSocket
    • 创建一个 DatagramPacket,包含要发送的数据、目标 IP 地址和端口号。
    • 通过 DatagramSocketsend() 方法发送数据包。
  2. 接收方:

    • 创建一个 DatagramSocket 并绑定到特定端口。
    • 创建一个空的 DatagramPacket 用于接收数据。
    • 通过 DatagramSocketreceive() 方法接收数据,该方法会阻塞直到数据包到达。
    • 从接收到的 DatagramPacket 中提取数据。

发送方代码示例 (UDPSender.java)

import java.net.*;
import java.nio.charset.StandardCharsets;
public class UDPSender {
    public static void main(String[] args) {
        String hostname = "localhost";
        int port = 9999;
        String message = "这是一条UDP消息!";
        try {
            // 创建一个DatagramSocket,系统会分配一个可用端口
            DatagramSocket socket = new DatagramSocket();
            // 将消息转换为字节数组
            byte[] buffer = message.getBytes(StandardCharsets.UTF_8);
            // 创建要发送的数据包,指定目标地址和端口
            InetAddress address = InetAddress.getByName(hostname);
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);
            // 发送数据包
            socket.send(packet);
            System.out.println("消息已发送: " + message);
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接收方代码示例 (UDPReceiver.java)

import java.net.*;
import java.nio.charset.StandardCharsets;
public class UDPReceiver {
    public static void main(String[] args) {
        int port = 9999;
        byte[] buffer = new byte[1024]; // 创建一个缓冲区用于接收数据
        try {
            // 创建一个DatagramSocket并绑定到指定端口
            DatagramSocket socket = new DatagramSocket(port);
            System.out.println("UDP接收器已启动,监听端口 " + port);
            // 创建一个空的数据包用于接收数据
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            // receive() 方法会阻塞,直到收到数据包
            socket.receive(packet);
            // 从数据包中提取数据
            String receivedMessage = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
            System.out.println("收到消息: " + receivedMessage);
            // 获取发送方的地址和端口
            InetAddress senderAddress = packet.getAddress();
            int senderPort = packet.getPort();
            System.out.println("来自: " + senderAddress.getHostAddress() + ":" + senderPort);
            socket.close();
        } catch (SocketException e) {
            System.err.println("Socket错误: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("I/O错误: " + e.getMessage());
        }
    }
}

如何运行

  1. 编译:

    javac UDPSender.java UDPReceiver.java
  2. 启动接收方:

    java UDPReceiver

    输出: UDP接收器已启动,监听端口 9999,然后程序会等待。

  3. 启动发送方: 打开新终端,运行:

    java UDPSender

    发送方会发送消息并退出。

  4. 查看结果: 接收方终端会打印出收到的消息和发送方的信息。


重要注意事项

异常处理

网络编程充满了不确定性,因此必须妥善处理各种 IOException,如连接失败、网络中断、端口被占用等,通常使用 try-catch 块来捕获这些异常。

资源关闭 (try-with-resources)

Socket、InputStream、OutputStream 等资源都实现了 AutoCloseable 接口,强烈推荐使用 try-with-resources 语句,这样可以确保无论是否发生异常,资源都会被自动关闭,避免资源泄漏。

// 推荐写法
try (Socket socket = new Socket(...);
     BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
     PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
    // ... 使用流进行通信 ...
} catch (IOException e) {
    // ... 处理异常 ...
} // in, out, socket 会在这里自动关闭

IO 流与字节流

Socket 的 getInputStream()getOutputStream() 返回的是字节流 (InputStream/OutputStream),对于文本数据,通常需要将其包装成字符流 (Reader/Writer),InputStreamReaderOutputStreamWriter(或 PrintWriter),并指定字符编码(如 UTF-8),以确保在不同平台间正确处理文本。

分享:
扫描分享到社交APP
上一篇
下一篇