Java网络编程入门:从基础原理到实践(二)

目录

[1. 网络编程基础:搞懂设备通信的底层逻辑](#1. 网络编程基础:搞懂设备通信的底层逻辑)

[1.1 为啥需要网络编程?------ 让设备 "互通有无"](#1.1 为啥需要网络编程?—— 让设备 “互通有无”)

[1.2 什么是网络编程?------ 给数据 "定规矩、找路线"](#1.2 什么是网络编程?—— 给数据 “定规矩、找路线”)

[1.3 网络编程的基本概念:理清通信里的角色和流程](#1.3 网络编程的基本概念:理清通信里的角色和流程)

[1.3.1 发送端和接收端 ------ 数据的 "发信人" 和 "收信人"](#1.3.1 发送端和接收端 —— 数据的 “发信人” 和 “收信人”)

[1.3.2 请求和响应 ------ 通信的 "一问一答"](#1.3.2 请求和响应 —— 通信的 “一问一答”)

[1.3.3 客户端和服务端 ------ 稳定的 "需求方" 和 "服务方"](#1.3.3 客户端和服务端 —— 稳定的 “需求方” 和 “服务方”)

[1.3.4 常见的客户端服务端模型](#1.3.4 常见的客户端服务端模型)

[2. Socket 套接字:网络通信的 "连接器"](#2. Socket 套接字:网络通信的 “连接器”)

[2.1 概念:网络通信的 "电话"](#2.1 概念:网络通信的 “电话”)

[2.2 分类:两种通信 "风格](#2.2 分类:两种通信 “风格)

[2.2.1 数据报套接字](#2.2.1 数据报套接字)

[2.2.2 流套接字](#2.2.2 流套接字)

[2.3 Java数据报套接字通信模型](#2.3 Java数据报套接字通信模型)

[2.4 Java流套接字通信模型](#2.4 Java流套接字通信模型)

[2.5 Socket 编程注意事项:避坑要点](#2.5 Socket 编程注意事项:避坑要点)

[3. UDP 数据报套接字编程:简单高效的 "快传" 实践](#3. UDP 数据报套接字编程:简单高效的 “快传” 实践)

[3.1 API 介绍:核心工具](#3.1 API 介绍:核心工具)

[3.1.1 DatagramSocket](#3.1.1 DatagramSocket)

[3.1.2 DatagramPacket](#3.1.2 DatagramPacket)

[3.1.3 InetSocketAddress](#3.1.3 InetSocketAddress)

[3.2 代码示例:UDP 通信全流程](#3.2 代码示例:UDP 通信全流程)

[3.2.1 UDP Echo Server](#3.2.1 UDP Echo Server)

[3.2.2 UDP Echo Client](#3.2.2 UDP Echo Client)

[3.2.3 3. UDP Dict Serve 字典服务器](#3.2.3 3. UDP Dict Serve 字典服务器)

[4. TCP 流套接字编程:可靠传输的 "保障"](#4. TCP 流套接字编程:可靠传输的 “保障”)

[4.1 API 介绍:构建可靠连接](#4.1 API 介绍:构建可靠连接)

[4.1.1 ServerSocket](#4.1.1 ServerSocket)

[4.1.2 Socket](#4.1.2 Socket)

[​编辑4.2 代码示例:TCP 通信实践](#编辑4.2 代码示例:TCP 通信实践)

[4.2.1 TCP Echo Server](#4.2.1 TCP Echo Server)

[4.2.2 TCP Echo Client](#4.2.2 TCP Echo Client)

[5. 长短连接:按需选择通信模式](#5. 长短连接:按需选择通信模式)

[5.1 短连接与长连接的定义](#5.1 短连接与长连接的定义)

[5.2 短连接与长连接的核心区别](#5.2 短连接与长连接的核心区别)

[5.2.1 连接建立与关闭的耗时差异](#5.2.1 连接建立与关闭的耗时差异)

[5.2.2 主动请求的发起方差异](#5.2.2 主动请求的发起方差异)

[5.2.3 适用场景差异](#5.2.3 适用场景差异)

[5.3 长连接的 "扩展痛点" 与优化方向](#5.3 长连接的 “扩展痛点” 与优化方向)

[5.3.1 BIO 长连接的资源瓶颈](#5.3.1 BIO 长连接的资源瓶颈)

[5.3.2 NIO 优化长连接的思路](#5.3.2 NIO 优化长连接的思路)

[5.4 总结:按需选型,平衡效率与资源](#5.4 总结:按需选型,平衡效率与资源)


网络编程听着高深,其实就是解决 "设备之间咋传数据 " 的问题。想象一下,你手机刷短视频,本质是手机(++客户端++ )和短视频服务器(++服务端++)在传数据;玩联机游戏,是你电脑和游戏服务器、队友设备在传数据。这篇就把网络编程最基础的逻辑和核心工具 Socket,掰开揉碎了讲,保证像唠家常一样好懂 。

1. 网络编程基础:搞懂设备通信的底层逻辑

1.1 为啥需要网络编程?------ 让设备 "互通有无"

用手机点外卖,手机得把 "我要点宫保鸡丁" 的需求发给外卖平台服务器;玩联机游戏,你操控角色的动作得传给队友的设备;智能手表测的心率数据,得传到手机 App 上显示......网络编程就是让这些 "需求、动作、数据",能在不同设备(或同一设备的不同程序)之间准确、高效传递的技术

**简单说:**没有网络编程,所有跨设备的功能全废!手机只能当计算器,电脑连不上网页,智能设备数据也传不出去,所有的网络资源都无法访问,世界直接 "断网瘫痪" 。

  • 所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
  • 而所有的网络资源,都是通过网络编程来进行数据传输的。

1.2 什么是网络编程?------ 给数据 "定规矩、找路线"

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。

当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。

特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。

但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

  • 进程A:编程来获取网络资源
  • 进程B:编程来提供网络资源

网络编程的核心,就是控制程序按照 "网络协议"(比如 TCP、UDP),把数据打包、发送、接收、解析。举个例子:你用微信发消息 "吃了吗",手机里的微信程序会:

  1. 打包:把文字转成符合网络协议的 "数据包"(类似把信装进信封,写上收件人地址);
  2. 发送:通过网络(WiFi、基站)把数据包传输出去(类似快递小哥把信运到目的地);
  3. 接收:对方的微信程序收到数据包(类似收件人拿到信);
  4. 解析:把数据包还原成 "吃了吗" 的文字(类似拆信封读内容)。

整个过程,就是网络编程在 "暗中操作",让数据能跨设备 "跑来跑去"。

1.3 网络编程的基本概念:理清通信里的角色和流程

这些概念看着抽象,对应生活场景就秒懂了,一个个看:

1.3.1 发送端和接收端 ------ 数据的 "发信人" 和 "收信人"

  • 发送端 :数据的发送方进程,称为发送端,它是主动发数据的一方。比如你给朋友发微信,你手机就是发送端;游戏里你开枪,你的设备就是发送端(把 "开枪动作" 发出去)。发送端主机即网络通信的源主机。
  • 接收端 :数据的**接收方进程,**称为接收端,它是被动收数据的一方。朋友的手机收你的微信、队友的设备收你 "开枪动作",它们就是接收端。接收端主机即网络通信中的目的主机。
  • **收发端:**发送端和接收端两端,也简称为收发端

注意:发送端和接收端只是相对的,角色会切换!只是一次网络数据传输产生数据流向后的概念。比如你和朋友互相发消息,你们的手机会轮流当 "发送端" 和 "接收端",像打乒乓球一样来回传数据。

1.3.2 请求和响应 ------ 通信的 "一问一答"

一般来说,获取一个网络资源,涉及到两次网络数据传输:

• 第一次:请求数据的发送。

• 第二次:响应数据的发送。

请求(Request):发送端主动提的 "需求"。比如你打开抖音,抖音 App 会给服务器发 "请求":"给我推荐点搞笑视频";你登录微信,微信 App 会发 "请求":"验证这个账号密码对不对"。

响应(Response):接收端针对请求的 "回复"。服务器收到抖音的请求,回复 "这是搞笑视频列表";收到微信登录请求,回复 "密码正确,登录成功"(或 "密码错误,失败" )

生活 analogy:你去餐厅(客户端)喊 "来份牛肉面"(请求),服务员(服务端)回复 "好的,马上做"(响应),就是典型的 "请求 - 响应"。

1.3.3 客户端和服务端 ------ 稳定的 "需求方" 和 "服务方"

  • 客户端(Client) :主动找别人获取服务的程序 / 设备。手机 App(抖音、微信)、电脑上的浏览器(Chrome、Edge)、智能手表的 App,都是客户端。特点是 "按需连接":需要服务时才主动连服务器,不用服务时就 "躺平"。
  • 服务端(Server) :长期在线、专门给别人提供服务的程序 / 设备。抖音的后台服务器、微信的认证服务器、游戏的对战服务器,都是服务端。特点是 "7×24 小时待命":不管有没有客户端找它,它都开着提供服务,随时准备响应请求。

**再举个生活例子:**你用美团 App(客户端)点外卖,美团的服务器(服务端)负责接收订单、分配骑手,就是 "客户端 - 服务端" 的典型交互。

1.3.4 常见的客户端服务端模型

最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

  1. 客户端先发送请求到服务端
  2. 服务端根据请求数据,执行相应的业务处理
  3. 服务端返回响应:发送业务处理结果
  4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
  • C/S 模型(客户端 / 服务端 ):需安装专门客户端软件(如微信 App )。优点是功能定制强,能利用本地资源;缺点是客户端需手动更新 。
  • B/S 模型(浏览器 / 服务端 ):通过浏览器访问(如知乎网页版 )。优点是使用方便、跨设备易访问;缺点是受浏览器功能限制,复杂交互体验弱于 C/S 。

2. Socket 套接字:网络通信的 "连接器"

2.1 概念:网络通信的 "电话"

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元,是程序实现网络通信的基础载体。基于Socket套接字的网络程序开发就是网络编程。把它想象成 "网络电话"。程序通过 Socket 建立与其他设备的连接,在连接上发送和接收数据,就像通过电话线路实现双方通话。无论是 UDP 还是 TCP 通信,都得依靠 Socket 搭建数据传输的通道 。

2.2 分类:两种通信 "风格

2.2.1 数据报套接字

使用传输层UDP协议,UDP,即User Datagram Protocol(用户数据报协议),传输层协议。类似 "发短信",发送端把数据打包成 "数据报" 直接发,不管接收端状态。

优点:传输速度快,无需建立连接开销;

缺点:可能丢数据、数据无序。适合实时性要求高、容忍少量丢包的场景(如在线视频直播、游戏实时位置同步 )

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 有接收缓冲区,无发送缓冲区
  • 大小受限:一次最多传输64k

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。

2.2.2 流套接字

使用传输层TCP协议,TCP,即Transmission Control Protocol(传输控制协议),传输层协议。如同 "打电话",通信前需三次握手建立连接 ,保证数据可靠、有序传输,传输完四次挥手断开连接

优点:数据传输可靠;

缺点:建立 / 断开连接有额外耗时,速度稍慢。适合文件传输(需完整数据 )、登录验证(关键数据不能丢 )等场景 。

  • 有连接
  • 可靠传输
  • 面向字节流
  • 有接收缓冲区,也有发送缓冲区
  • 大小不限

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。

2.3 Java数据报套接字通信模型

对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。

java中使用UDP协议通信。主要基于 DatagramSocket(收发器 )和 DatagramPacket(数据报载体 )实现。发送时,把数据转字节数组,构建 DatagramPacket 并指定目标地址 / 端口,通过 DatagramSocket 发送;接收时,创建空 DatagramPacket 当缓冲区,用 DatagramSocket 接收,再解析数据 。对于一次发送及接收UDP数据报的流程如下:

以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:

2.4 Java流套接字通信模型

java中使用TCP通信协议,主要依赖 ServerSocket(服务端 "大门",监听客户端连接 )和 Socket(客户端 / 服务端连接后的数据通道 )。服务端用 ServerSocket 绑定端口监听,如 ServerSocket serverSocket = new ServerSocket(9999); ;客户端用 Socket 连服务端,如 Socket clientSocket = new Socket("127.0.0.1", 9999); 。连接建立后,通过 Socket 的输入输出流(像水管 )收发数据,保证数据有序、可靠传输 。

2.5 Socket 编程注意事项:避坑要点

在 Socket 编程开发中,这些关键问题得留意,避免踩坑:

  1. 客户端与服务端部署场景
    开发调试时,常在同一主机开两个进程模拟客户端、服务端,但真实环境里,客户端和服务端一般分属不同主机 。开发要考虑跨主机通信的网络差异(如防火墙、网段限制),别只依赖本地调试逻辑。

  2. 目标标识:IP + 端口
    数据传输得明确 "终点",目的 IP 定位目标主机,目的端口号 定位主机内接收数据的进程。编程时要准确设置这两个参数,否则数据发错地方,通信直接失败。

  3. 套接字类型与协议层
    Socket 编程用 流套接字(基于 TCP)数据报套接字(基于 UDP) 开发,对应传输层 TCP/UDP 协议。但光有传输层还不够,应用层协议得自己设计(比如定义数据包格式、指令含义),这部分后续会详细讲怎么规划。

  4. 端口占用冲突问题 如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下: 此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。

解决端口被占用的问题:

  • 如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
  • 如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。

3. UDP 数据报套接字编程:简单高效的 "快传" 实践

3.1 API 介绍:核心工具

3.1.1 DatagramSocket

UDP 通信的 "收发器",用于发送和接收UDP数据报。服务端常绑定固定端口(如 DatagramSocket serverSocket = new DatagramSocket(8888); ),方便客户端定位;客户端一般不绑定固定端口(DatagramSocket clientSocket = new DatagramSocket(); ),由系统分配 。

构造方法:

常用方法:

3.1.2 DatagramPacket

"数据报载体",包含数据、目标地址(发送时 )或源地址(接收时 )。发送时,构建 DatagramPacket 需指定数据字节数组、目标地址 / 端口;接收时,创建空 DatagramPacket 当缓冲区,用 DatagramSocket 接收后解析数据 。

构造方法:

常用方法:

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

3.1.3 InetSocketAddress

InetSocketAddress 是 SocketAddress 的子类。

构造方法:

3.2 代码示例:UDP 通信全流程

3.2.1 UDP Echo Server

java 复制代码
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. 创建一个数据包对象,用来接收客户端发来的数据
            // 就像准备一个"收件盒",指定最大能装4096字节的数据
            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. 把响应结果发回给客户端
            // 准备一个"发件盒",装着要发送的内容,以及客户端的地址和端口
            DatagramPacket responsePacket = new DatagramPacket(
                response.getBytes(),  // 要发送的内容(转换为字节数组)
                response.getBytes().length,  // 内容的长度
                requestPacket.getSocketAddress()  // 客户端的地址和端口(从请求中获取)
            );
            
            // 发送响应数据
            socket.send(responsePacket);

            // 打印日志,记录通信详情
            System.out.printf(
                "[客户端 %s:%d] 收到: %s, 回复: %s\n",
                requestPacket.getAddress().toString(),  // 客户端IP地址
                requestPacket.getPort(),  // 客户端端口
                request,  // 客户端发送的内容
                response  // 服务器回复的内容
            );
        }
    }

    // 处理请求的方法
    // 这里实现的是回显功能:输入什么,就返回什么
    public String process(String request) {
        return request;
    }

    // 程序入口:启动服务器
    public static void main(String[] args) throws IOException {
        // 创建服务器实例,指定端口号9090
        UdpEchoServer server = new UdpEchoServer(9090);
        // 启动服务器
        server.start();
    }
}

3.2.2 UDP Echo Client

java 复制代码
public class UdpEchoClient {
    // 客户端的通信接口,就像打电话用的"手机"
    private DatagramSocket socket = null;
    // 服务器的IP地址,类似对方的"电话号码"
    private String serverIp;
    // 服务器的端口号,类似对方"手机上的某个APP"
    private int serverPort;

    // 客户端构造方法:需要知道服务器的IP和端口才能连接
    // IP地址格式是"点分十进制",比如"192.168.1.1"
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        // 初始化客户端的通信接口(不用指定端口,系统会自动分配一个)
        socket = new DatagramSocket();
    }

    // 客户端启动方法:开始和服务器通信
    public void start() throws IOException {
        System.out.println("客户端启动成功!可以开始发送消息了...");
        // 创建Scanner对象,用于读取用户在控制台输入的内容
        Scanner scanner = new Scanner(System.in);
        
        // 循环处理:不断读取用户输入并和服务器交互
        while (true) {
            // 显示提示符号,告诉用户可以输入内容了
            System.out.print("-> ");
            
            // 检查用户是否还有输入(如果输入结束就退出循环)
            if (!scanner.hasNext()) {
                break;
            }
            
            // 读取用户输入的内容(这就是要发给服务器的请求)
            String request = scanner.next();
            
            // 构造要发送的数据包:相当于把消息装进"信封"
            DatagramPacket requestPacket = new DatagramPacket(
                request.getBytes(),  // 要发送的内容(转成字节数组)
                request.getBytes().length,  // 内容的长度
                InetAddress.getByName(serverIp),  // 服务器的IP地址(把字符串转成网络地址)
                serverPort  // 服务器的端口号
            );
            
            // 发送数据包:相当于把信封"寄出去"
            socket.send(requestPacket);
            
            // 准备接收服务器的回复:创建一个"收件盒"
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            
            // 等待接收服务器的回复:这一步会"卡住"直到收到消息
            socket.receive(responsePacket);
            
            // 把服务器回复的字节数据转成字符串
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            
            // 在控制台显示服务器的回复内容
            System.out.println(response);
        }
    }

    // 程序入口:启动客户端
    public static void main(String[] args) throws IOException {
        // 创建客户端实例,连接到本机(127.0.0.1)的9090端口服务器
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        // 启动客户端
        client.start();
    }
}

3.2.3 3. UDP Dict Serve 字典服务器

思路: 客户端发单词,服务器查字典(用 Map 存单词 - 翻译 )返回翻译。只需要重写 process。比如初始化 Map<String, String> dict = new HashMap<>(); ,存入 dict.put("apple", "苹果"); 等。服务器接收单词后,在 dict 中查找,把翻译当响应发回;客户端发请求、收翻译并展示。通过这个示例,能更灵活理解 UDP 套接字的应用 。

4. TCP 流套接字编程:可靠传输的 "保障"

4.1 API 介绍:构建可靠连接

4.1.1 ServerSocket

ServerSocket 是创建TCP服务端Socket的API。是服务端 "大门",监听客户端连接。创建时绑定端口,如ServerSocket serverSocket = new ServerSocket(9090);然后通过 serverSocket.accept() 阻塞等客户端连接,有连接时返回 Socket 用于通信 。

构造方法:

其它方法:

4.1.2 Socket

Socket 是客户端发起连接或服务端通过 ServerSocket.accept () 响应连接后得到的通信端点,双方建立连接后,用其保存的对端信息收发数据,客户端创建时需指定服务端 IP 和端口 ,服务端通过该 Socket 的输入输出流交互 。

构造方法:

其它方法:

4.2 代码示例:TCP 通信实践

4.2.1 TCP Echo Server

java 复制代码
public class TcpEchoServer {
    // 服务器的"总机",负责接听客户端的连接请求
    ServerSocket serverSocket = null;

    // 构造方法:创建服务器并指定端口号(就像给总机分配一个电话号码)
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    // 启动服务器的方法
    public void start() throws IOException {
        System.out.println("服务器启动成功!等待客户端连接...");
        
        // 创建一个线程池,用来处理多个客户端的请求(相当于多个接线员)
        // newCachedThreadPool表示可以根据需要自动创建新线程
        ExecutorService pool = Executors.newCachedThreadPool();
        
        // 服务器一直运行,不断接收新的客户端连接
        while (true) {
            // 等待客户端连接(总机接听电话)
            // 这一步会"卡住",直到有客户端来连接
            Socket clientSocket = serverSocket.accept();
            
            // 收到连接后,交给线程池处理(安排一个接线员专门服务这个客户端)
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    // 处理单个客户端连接的方法(接线员和客户的通话过程)
    private void processConnection(Socket clientSocket) {
        // 打印客户端上线信息:包含客户端的IP和端口
        System.out.printf("[%s:%d] 客户端上线啦\n", 
                clientSocket.getInetAddress(), 
                clientSocket.getPort());
        
        // try-with-resources语法:自动关闭资源,不用手动写close()
        try (
            // 获取输入流:用来读取客户端发送的消息(相当于听客户说话)
            InputStream inputStream = clientSocket.getInputStream();
            // 获取输出流:用来向客户端发送消息(相当于跟客户说话)
            OutputStream outputStream = clientSocket.getOutputStream()
        ) {
            // 创建Scanner对象,方便读取输入流中的文本
            Scanner scanner = new Scanner(inputStream);
            
            // 循环处理客户端的请求
            while (true) {
                // 判断客户端是否还有数据发送(如果没有,说明客户端要下线了)
                if (!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线啦\n", 
                            clientSocket.getInetAddress(), 
                            clientSocket.getPort());
                    break;
                }
                
                // 1. 读取客户端发送的请求内容
                String request = scanner.next();
                
                // 2. 处理请求(回显服务器直接返回相同内容)
                String response = process(request);
                
                // 3. 把响应写回给客户端
                // 创建PrintWriter方便写入文本
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                // 刷新缓冲区:确保数据立刻发送出去(不然可能留在缓存里)
                printWriter.flush();
                
                // 打印日志:记录这次通信的详情
                System.out.printf("[%s:%d] 收到: %s, 回复: %s\n",
                        clientSocket.getInetAddress(),
                        clientSocket.getPort(),
                        request,
                        response);
            }
            
        } catch (IOException e) {
            // 捕获并打印异常信息(比如网络中断等问题)
            e.printStackTrace();
        }
    }

    // 处理请求的核心方法(回显逻辑:输入什么返回什么)
    private String process(String request) {
        return request;
    }

    // 程序入口:启动服务器
    public static void main(String[] args) throws IOException {
        // 创建服务器实例,绑定9090端口
        TcpEchoServer server = new TcpEchoServer(9090);
        // 启动服务器
        server.start();
    }
}

4.2.2 TCP Echo Client

java 复制代码
public class TcpEchoClient {
    // 客户端的"电话",用来和服务器建立连接并通话
    private Socket clientSocket = null;

    // 构造方法:初始化客户端,需要知道服务器的IP和端口(相当于知道对方的电话号码)
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 连接服务器:就像拨打指定的电话号码
        clientSocket = new Socket(serverIp, serverPort);
    }

    // 启动客户端:开始和服务器通信
    public void start() {
        System.out.println("客户端启动成功!可以开始聊天啦...");

        // try-with-resources语法:自动关闭输入输出流,不用手动关闭
        try (
            // 输入流:用来接收服务器发过来的消息(相当于耳朵,听服务器说话)
            InputStream inputStream = clientSocket.getInputStream();
            // 输出流:用来向服务器发送消息(相当于嘴巴,跟服务器说话)
            OutputStream outputStream = clientSocket.getOutputStream()
        ) {
            // 扫描器1:用来读取用户在控制台输入的内容(从键盘读)
            Scanner scannerConsole = new Scanner(System.in);
            // 扫描器2:用来读取服务器发送过来的消息(从网络读)
            Scanner scannerNetwork = new Scanner(inputStream);
            // 打印器:用来向服务器发送消息(包装输出流,方便写文本)
            PrintWriter writer = new PrintWriter(outputStream);

            // 循环聊天:不断读取用户输入并发送给服务器,再接收服务器回复
            while (true) {
                // 显示提示符号,告诉用户可以输入内容了
                System.out.printf("-> ");

                // 检查用户是否还有输入(如果没有输入,就退出循环)
                if (!scannerConsole.hasNext()) {
                    break;
                }

                // 1. 读取用户在控制台输入的内容(要发给服务器的消息)
                String request = scannerConsole.next();

                // 2. 把消息发送给服务器
                writer.println(request);
                // 刷新缓冲区:确保消息立刻发出去(不然可能存在缓存里没发送)
                writer.flush();

                // 3. 等待并读取服务器的回复
                String response = scannerNetwork.next();

                // 4. 在控制台显示服务器的回复内容
                System.out.println(response);
            }

        } catch (IOException e) {
            // 捕获并打印异常(比如网络断开等问题)
            e.printStackTrace();
        } finally {
            // 最后一定要关闭客户端的"电话",释放资源
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 程序入口:启动客户端
    public static void main(String[] args) throws IOException {
        // 创建客户端实例,连接到本机(127.0.0.1)的9090端口服务器
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        // 启动客户端,开始通信
        client.start();
    }
}

5. 长短连接:按需选择通信模式

在 TCP 通信体系里,连接的建立与关闭时机,直接界定了短连接和长连接两种模式。合理选用,能让程序在效率、资源占用间找到平衡,适配不同业务场景。

5.1 短连接与长连接的定义

TCP 传输数据依赖先建立连接,"何时关闭连接" 是区分短、长连接的核心

  • 短连接:每次完成 "接收数据 + 返回响应" 后,立即关闭连接 。如同 "一锤子买卖",一次连接仅支撑单次收发数据,下次交互需重新建连。
  • 长连接:保持连接状态不关闭,允许双方持续收发数据 。像 "持续对话",一次建连可支撑多次数据交互,直至主动断开或网络异常。

5.2 短连接与长连接的核心区别

对比短、长连接,从建连开销到适用场景,差异显著:

5.2.1 连接建立与关闭的耗时差异

  • 短连接:每次请求 - 响应,都要经历 "建连→传数据→关连" 全流程 。频繁交互时,建连、关连的耗时会叠加,拖慢整体效率。
  • 长连接:仅首次需完整建连,后续请求 - 响应直接复用已连通道 。省掉重复建连、关连的耗时,高频交互场景下效率优势明显。

5.2.2 主动请求的发起方差异

  • 短连接:通常由客户端主动发起请求,服务端被动响应 。典型如浏览器访问静态网页,客户端发请求,服务端回数据后关连,服务端很少主动 "推送"。
  • 长连接:支持双向主动通信 。既允许客户端主动发请求(如聊天时发消息 ),也支持服务端主动推送数据(如即时通讯的新消息通知、实时行情更新 )。

5.2.3 适用场景差异

  • 短连接 :适配客户端请求频率低的场景 。如浏览新闻网页(单次请求即可获取内容 )、查询静态数据接口(查天气、物流信息 ),无需持续保持连接,"即用即关" 省资源。
  • 长连接 :专为客户端与服务端高频通信设计 。像聊天室(持续收发消息 )、实时游戏(同步玩家操作、状态 )、金融行情推送(秒级更新数据 ),依赖长连接实现低延迟、高实时性交互。

5.3 长连接的 "扩展痛点" 与优化方向

长连接虽高效,但若基于传统 BIO(同步阻塞 IO )实现,会面临系统资源占用过高问题:

5.3.1 BIO 长连接的资源瓶颈

BIO 模式下,每个长连接对应一个阻塞线程 。连接需持续 "阻塞等待" 数据,若有 1 万长连接,服务端需创建 1 万线程,内存、线程切换开销会直接 "压垮" 系统。

5.3.2 NIO 优化长连接的思路

为解决 BIO 缺陷,Java 引入 NIO(同步非阻塞 IO ) ,通过 Selector(多路复用器 ) 实现:

  • 少量线程 管理大量连接,线程无需阻塞等待,而是由 Selector 监听 "哪些连接有数据可读 / 可写",按需处理。
  • 极大降低资源消耗,支撑高并发长连接场景(如百万级在线的即时通讯系统 ),主流框架(Netty )也基于 NIO 封装,简化长连接开发。

5.4 总结:按需选型,平衡效率与资源

短连接 "简单轻量",适合低频交互;长连接 "高效持久",适配高频、实时场景 。实际开发中,需结合业务需求:

  • 若做新闻网站、静态数据接口,选短连接省心省力;
  • 若开发聊天、实时游戏,长连接是必选项,且建议基于 NIO/Netty 优化资源占用。
    理解长短连接的本质差异,才能让网络通信既高效又稳定,贴合业务需求 。
相关推荐
丨千纸鹤丨2 分钟前
高可用集群Keepalived
linux·服务器·网络
北极光SD-WAN组网1 小时前
工业互联网时代,如何通过混合SD-WAN提升煤炭行业智能化网络安全
网络·安全·web安全
charlie1145141912 小时前
快速入门Socket编程——封装一套便捷的Socket编程——导论
linux·网络·笔记·面试·网络编程·socket
东风西巷3 小时前
X-plore File Manager v4.34.02 修改版:安卓设备上的全能文件管理器
android·网络·软件需求
liulilittle3 小时前
C++ Proactor 与 Reactor 网络编程模式
开发语言·网络·c++·reactor·proactor
我很好我还能学3 小时前
【计算机网络 篇】TCP基本认识和TCP三次握手相关问题
运维·服务器·网络
苏州向日葵4 小时前
篇五 网络通信硬件之PHY,MAC, RJ45
网络·嵌入式硬件
程序员编程指南4 小时前
Qt 网络编程进阶:WebSocket 通信
c语言·网络·c++·qt·websocket
作业Y4 小时前
第1章第2章笔记
网络
宇宙核5 小时前
【内网穿透】使用FRP实现内网与公网Linux/Ubuntu服务器穿透&项目部署&多项目穿透方案
运维·服务器·网络