JavaEE —— 网路编程 Socket套接字

UDP数据报套接字编程(User Datagram Protocol)

核心特性 说明(结合之前的 UDP 字典服务器场景)
无连接 客户端与服务器无需提前建立连接,客户端直接发送数据报(DatagramPacket),服务器通过端口监听接收,无需 "三次握手"
面向数据报 数据以独立的 "数据包" 为单位传输,每个数据包包含完整的目标地址和端口,独立处理,不合并、不拆分
不可靠传输 不保证数据到达顺序、完整性,丢失不重传、重复不丢弃、乱序不排序,依赖上层业务处理可靠性(如字典查询可容忍偶尔丢失)
低延迟、高效率 无连接建立 / 关闭、无确认重传等开销,传输速度快,适合对延迟敏感、对可靠性要求不高的场景
无流量 / 拥塞控制 发送方按自身速率发送数据,不考虑接收方处理能力和网络状态

API 介绍

DatagramSocket:

DatagramSocket 是UDP 的Socket,用于发送和接收UDP数据报。

DatagramSocket 构造方法: 就是 打开 '文件'

DatagramSocket 方法:receive 就是读取 ,send 就是写

DatagramPacket

DatagramPacket是UDP Socket发送和接收的数据报。

DatagramPacket表示UDP完整的数据报

DatagramPacket 构造方法:

UDP数据包的载荷数据,就饿可以通过构造方法来指定

DatagramPacket 方法:

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。

InetSocketAddress

InetSocketAddress ( SocketAddress 的子类)构造方法:

注意:真实的服务器响应与请求是不一样的,这个是回显服务器

代码演示:

UDPSevrve实现:
核心思路:

具体实现:

  • DatagramPacket是 Java 中用于表示 UDP 数据报的类。
  • new byte[4096]:创建一个长度为 4096 字节的字节数组,用来存储接收到的 UDP 数据报的载荷部分(即实际传输的有效数据)。
  • length: 4096:指定接收数据的最大长度为 4096 字节,确保接收到的数据不会超出字节数组的容量。
将 UDP 数据报中接收到的二进制数据转换为字符串
  • requestPacket.getData():获取存储 UDP 数据报载荷的字节数组(即接收到的二进制数据)。
  • offset: 0:指定从字节数组的起始位置(第 0 个字节)开始转换。
  • requestPacket.getLength():获取实际接收到的数据长度,确保只转换有效数据部分。
打印 UDP 通信请求与响应日志
receive() 方法的 DatagramPacket 参数是一个典型的"输出型参数":

输出型参数:调用方提供一个"空容器",方法执行后这个容器被填充了数据。

为什么不用手动close 关闭socket?

UDP 服务器中Socket对象的生命周期和资源释放:

  • Socket 对象的生命周期 :在 UDP 服务器中,Socket对象会伴随服务器的整个运行过程,从服务器启动到停止,始终用于处理客户端的 UDP 数据报通信。
  • 资源释放机制 :当服务器进程结束(如关闭服务器程序)时,操作系统会自动释放该进程在 PCB(进程控制块)文件描述符表中占用的所有资源,因此不需要手动调用close方法来释放Socket资源。

PCB 是 进程控制块(Process Control Block) 的缩写,是操作系统用于管理进程的核心数据结构

服务器启动后,客户端还没请求时,服务器逻辑在做什么?

服务器会在 socket.receive(requestPacket) 处阻塞,直到客户端发送 UDP 请求才会继续执行。

代码演示:
java 复制代码
package network;

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

public class UdpEchoServer {
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        // 指定了一个固定端口号, 让服务器来使用.
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        // 启动服务器
        System.out.println("服务器启动");

        while (true) {
            // 循环一次, 就相当于处理一次请求.
            // 处理请求的过程, 典型的服务器都是分成三个步骤的.
            // 1. 读取请求并解析.
            //    DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //    把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求, 计算响应. (服务器最关键的逻辑)
            //    但是此处写的是回显服务器. 这个环节相当于省略了.
            String response = process(request);

            // 3. 把响应返回给客户端
            //    根据 response 构造 DatagramPacket, 发送给客户端.
            //    此处不能使用 response.length()
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            //    此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
            //    需要指定 目的 ip 和 目的端口.
            socket.send(responsePacket);

            // 4. 打印一个日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
                    request, response);
        }
    }

    // 后续如果要写别的服务器, 只修改这个地方就好了.
    // 不要忘记, private 方法不能被重写. 需要改成 public
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}
UDPClient实现:

我们的数据包中,只存储了他自己的IP和端口号,所以客户端这里要记一下服务器的IP和端口号

客户端访问服务器时,IP 和端口是如何分配的?

-目的 IP:服务器的 IP(serverIP)。

-目的端口:服务器的端口(serverport)。

-源 IP:客户端所在主机的 IP。

-源端口:操作系统随机分配的空闲端口。

getByName(): 解析服务器 IP 地址
127.0.0.1(环回 IP)
服务器与客户端之间要联网么?
代码演示:
java 复制代码
package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;

    // UDP 本身不保存对端的信息, 就自己的代码中保存一下
    private String serverIp;
    private int serverPort;

    // 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 从控制台读取用户输入的内容.
            System.out.println("请输入要发送的内容:");
            if (!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();
            // 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.
            //    构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            // 3. 发送数据报
            socket.send(requestPacket);
            // 4. 接收服务器的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 5. 从服务器读取的数据进行解析, 打印出来.
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}
UDP Dict Server 实现:

字典服务器的实现是基于UDPServer实现:

其他的都一样,接收的数据需要处理一下,需要在我们构造方法设置的字典里,寻找有没有英文对应的

只需要重写一下处理请求返回相应的数据

java 复制代码
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer {
    private Map<String, String> dict = new HashMap<String, String>();

    public UdpDictServer(int port) throws SocketException {
        // 调用父类构造方法这句代码, 必须放到子类构造方法的第一行
        super(port);

        dict.put("hello", "你好");
        dict.put("world", "世界");
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("pig", "小猪");
        // 可以添加更多更多的数据.
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request, "没有找到该单词");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9091);
        server.start();
    }
}

TCP数据报套接字编程(Transmission Control Protocol)

概念

1. ServerSocket:TCP 服务端的 "大门"

  • 它是服务端的 "监听接口",负责绑定端口、等待客户端连接
    • ServerSocket(int port) 绑定端口后,就像在这个端口上 "开了一扇门",持续监听是否有客户端来敲门。
    • 调用 accept() 方法时,服务端会 "阻塞等待"------ 直到有客户端发起连接请求,才会返回一个 Socket 对象(相当于给这个客户端 "开了一扇专属的小门")。

2. Socket:客户端与服务端的 "专属通道"

  • 客户端创建 Socket 时,需指定服务端的 IP 和端口(相当于 "主动敲门");
  • 服务端通过 accept() 得到的 Socket,是与该客户端的双向通信通道
    • 服务端和客户端都可以通过这个 Socket 的输入流(getInputStream())读数据,输出流(getOutputStream())写数据。

核心特性(与 UDP 对比,呼应之前的网络通信场景):

  1. 面向连接:通信前需通过 "三次握手" 建立连接,通信结束后通过 "四次挥手" 关闭连接;
  2. 可靠传输:保证数据的完整性、有序性,丢失会重传、重复会丢弃、乱序会排序;
  3. 面向字节流:数据以字节流形式传输,而非独立数据包;
  4. 有流量控制和拥塞控制:避免因发送方速率过快导致接收方处理不及,或网络拥堵;
  5. 延迟略高:因连接建立、确认等机制,传输效率低于 UDP,但可靠性更强。

API 介绍

ServerSocket

ServerSocket 是创建TCP服务端Socket的API。

ServerSocket 构造方法:
ServerSocket 方法:

Socket

Socket 是客户端Socket,或服务端中接收到客⼾端建立连接(accept方法)的请求后,返回的服 务端Socket。 不管是客⼾端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及⽤来与对方收发数据的。

Socket 构造方法:

代码演示

TCPEchoClient:

java 复制代码
package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 客户端在 new Socket 的时候, 就会和服务器建立 TCP 连接.
        // 此时少了服务器 IP 和 端口.
        socket = new Socket(serverIp, serverPort);
        // socket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort));
    }

    public void start() {
        System.out.println("client start!");


        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);

            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);

            while (true) {
                // 1. 从控制台读取用户输入.
                System.out.print("-> ");
                String request = scanner.next();
                // 2. 把请求发送给服务器.
                writer.println(request);
                writer.flush();
                // 3. 从服务器读取响应
                if (!scannerNetwork.hasNext()) {
                    break;
                }
                String response = scannerNetwork.next();
                // 4. 把响应显示到控制台上
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

TCPEchoServer:

java 复制代码
package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("server start!");

        // 不能使用 FixedThreadPool, 线程数目固定.
        ExecutorService executorService = Executors.newCachedThreadPool();

        while (true) {
            // 首先要先接受客户端的连接, 然后才能进行通信.
            // 如果有客户端和服务器建立好了连接, accept 能够返回.
            // 否咋 accept 会阻塞.
            // [有连接]
            Socket socket = serverSocket.accept();
            // 通过这个方法处理这个客户端整个的连接过程.
            // 直接调用 processConnection, 此时就会 "顾此失彼" 一旦进入到 processConnection 方法
            // 就不能再次调用 accept
            // processConnection(socket);

            // 此处创建新线程. 在新线程里, 调用 processConnection
//            Thread t = new Thread(() -> {
//                processConnection(socket);
//            });
//            t.start();

            // 如果当前线程数目进一步增多, 创建销毁进一步频繁, 此时线程创建销毁开销不可忽视了.
            // 使用线程池, 是进一步的改进手段.
            executorService.submit(() -> {
                processConnection(socket);
            });
        }
    }

    private void processConnection(Socket socket) {
        // 在一次连接中, 客户端和服务器之间可能会进行多组数据传输.
        System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
        // [面向字节流] & [全双工]
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {

            Scanner scanner = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);

            while (true) {
                // 处理多次请求/响应的读写操作.
                // 一次循环就是读写一个请求/响应
                // 1. 读取请求并解析 (可以直接使用 Scanner 完成)
                if (!scanner.hasNext()) {
                    // 客户端关闭了连接
                    System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
                    // 假设这里有一系列的逻辑呢??
                    break;
                }
                String request = scanner.next();

                // 2. 根据请求计算响应
                String response = process(request);

                // 3. 把响应写回客户端
                writer.println(response);
                writer.flush();

                // 4. 打印日志
                System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
                        request, response);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 假如在这之前也有一系列逻辑呢?
                socket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

TCPEchoServer:

java 复制代码
package network;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class TcpDictServer extends TcpEchoServer {
    private Map<String, String> dict = new HashMap<>();

    public TcpDictServer(int port) throws IOException {
        super(port);

        dict.put("hello", "你好");
        dict.put("world", "世界");
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
    }

    public String process(String request) {
        return dict.getOrDefault(request, "没有找到该单词");
    }

    public static void main(String[] args) throws IOException {
        TcpDictServer server = new TcpDictServer(9090);
        server.start();
    }
}
相关推荐
j***89461 小时前
Spring Boot整合Redisson的两种方式
java·spring boot·后端
q***96581 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
今天有个Bug1 小时前
Java 读取RTSP、RTMP等网络流、图像、视频指南,易于理解,方便使用
java·推流拉流·javacv·图片视频处理
JavaGuide1 小时前
Spring Boot 4.0 正式发布,真学不动了!
java·spring boot
90后小陈老师1 小时前
用户管理系统 03 搭建基本结构 | Java新手实战 | 最小架构用户管理系统(SpringBoot+Vue3)
java·spring boot·架构
n***84071 小时前
Spring Boot(快速上手)
java·spring boot·后端
组合缺一2 小时前
Solon AI 开发学习3 - chat - 模型配置与请求选项
java·学习·ai·chatgpt·langchain·solon
小兔崽子去哪了2 小时前
Docker部署ZLMediaKit流媒体服务器并自定义配置指南
java·后端·容器
程序猿小蒜2 小时前
基于springboot的人口老龄化社区服务与管理平台
java·前端·spring boot·后端·spring