跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 网络编程:TCP 流套接字编程

TCP 协议与 UDP 协议在连接性、可靠性及传输单位上的区别。介绍 ServerSocket 和 Socket 的构造方法与常用 API。通过回显服务器示例展示如何使用线程池处理多客户端连接,利用输入输出流进行数据收发。强调 PrintWriter 需调用 flush 刷新缓存,以及 Scanner 接收数据依赖空白符的特性,确保客户端与服务端通信正常。

imJackJia发布于 2026/3/16更新于 2026/7/837 浏览
Java 网络编程:TCP 流套接字编程

TCP 和 UDP 的区别

在传输层,TCP 协议是有连接的,可靠传输,面向字节流,全双工。 而 UDP 协议是无连接的,不可靠传输,面向数据报,全双工。

有连接和无连接的区别是在进行网络通信的时候,通信双方有没有保存对端的地址信息。即假设 A 和 B 进行通信,A 保存了 B 的地址信息,B 也保存了 A 的地址信息,此时双方都知道和谁建立了连接,这就是有连接的通信。在之前的 UDP 数据报套接字编程中就提到过 UDP 是无连接的,所以在发送数据报的时候要加上对端的信息,防止丢包。

可靠传输是通过各种手段来防止丢包的出现,而不可靠传输则没有做任何处理直接把数据报传输过去。但是可靠传输不意味着能 100% 把数据报完整无误地传输给对方,只是尽可能降低丢包发生的概率,并且可靠传输是要使用很多手段来保持的,所以付出的代价相比于不可靠传输要大。

面向字节流就是以字节为单位来进行数据的传输,面向数据报就是以数据报为单位进行数据的传输。

全双工就是通信的双发可以同时给对方发送数据,但是半双工是指双方只有一方可以发送数据。

TCP 流套接字 API 介绍

ServerSocket

ServerSocket 是 TCP 服务端 Socket 的 API。

构造方法:

方法名说明
ServerSocket(int port)创建一个 TCP 服务端流套接字 Socket,并绑定端口号

ServerSocket 方法:

方法名返回值说明
accept()Socket开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待
close()void关闭此套接字

Socket

Socket 是客户端 Socket 或者是服务端那边收到客户端建立连接的请求(通过 accept() 方法)返回的 Socket 对象。

不管是客户端还是服务端的 Socket 对象,他们都保留了对端的地址信息,这也是 TCP 协议有连接的体现。

Socket 构造方法:

方法名说明
Socket(String host, int port)创建一个客户端流套接字 Socket,并于对应 IP 的主机对应的端口的进程建立连接

Socket 方法:

方法名返回值说明
getInetAddress()InetAddress返回套接字所连接的地址
getInputStream()InputStream返回此套接字的输入流
getOutputStream()OutputStream返回此套接字的输出流

回显服务器

首先在回显服务器的构造方法里初始化我们的 ServerSocket。

public class TcpEchoServer {
    private ServerSocket serverSocket;

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

    public void start() throws IOException {
        System.out.println("服务器启动...");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            // 与客户端建立连接
            Socket clientSocket = serverSocket.accept();
            // 处理客户端发出的多个请求
            executorService.submit(() -> {
                try {
                    processClient(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    private void processClient(Socket clientSocket) throws IOException {
        // 获取输入流和输出流
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
            // 对输入流和输出流进行进一步的封装,方便我们的使用
            Scanner scanner = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while (true) {
                if (!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 解析请求
                String request = scanner.next();
                // 计算响应
                String response = process(request);
                // 发送响应
                writer.println(response);
                // 因为此时的响应数据还在缓存区里,所以需要使用 flush 来将内存的数据发送出去
                writer.flush();
                System.out.printf("[%s:%d] request:%s response:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 当请求处理完的时候记得关闭服务器与客户端的连接,防止资源泄漏
            clientSocket.close();
        }
    }

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

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

客户端

首先在客户端构造方法建立与服务器的连接:

public class TcpEchoClient {
    private Socket socket;

    public TcpEchoClient(String serverIP, int port) throws IOException {
        // 与服务器建立连接
        socket = new Socket(serverIP, port);
    }

    public void start() {
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            // 对输入流和输出流进行进一步的封装
            Scanner scanner = new Scanner(System.in);
            Scanner scanner2 = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while (true) {
                // 发送多个请求和接收多个响应
                if (!scanner.hasNext()) {
                    break;
                }
                // 发送请求
                String request = scanner.next();
                writer.println(request);
                writer.flush();
                // 接收响应
                String response = scanner2.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

细节说明

在我们使用 PrintWriter 的 writer.println(xxx) 之后,我们的数据其实还保留在缓存区中,也就是还没发出去,我们需要通过 flush() 方法来刷新缓存区的数据,才能将数据真正发送到对端去。

我们不可以使用 writer.print 这种没有自动添加换行符的方法,因为我们在接收数据的时候,使用的是 Scanner 的 next() 方法,next() 是要接收到空白符(包括换行符,制表符,翻页符…)才停止接收的。如果你使用 print 来发送数据,这时候的数据是没有带任何空白符的,那么就不会停止接收数据而是继续等待空白符的到来,这时候服务器就无法处理客户端的请求。

你在客户端的控制台输入的回车不算进数据的换行符里,控制台输入的回车时,只是将数据交给了客户端程序,并不会自动将这些数据转换为网络流中的换行符。 换一句话说,控制台的回车只是结束你在控制台的输入,并不会自动在数据末尾加上换行符。

目录

  1. TCP 和 UDP 的区别
  2. TCP 流套接字 API 介绍
  3. ServerSocket
  4. Socket
  5. 回显服务器
  6. 客户端
  7. 细节说明
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 第二届人工智能、虚拟现实与交互设计国际学术会议(AIVRID 2026)
  • 寻找数组中心下标与除自身以外数组的乘积 - 前缀和技巧
  • Java IO 流进阶:缓冲流、序列化与打印流
  • 医疗 AI 可信系统构建:向量索引与贝叶斯网络实现
  • Llama Factory 快速切换 Alpaca 与 Vicuna 对话模板
  • Xcode 原生集成 AI 大模型本地化配置指南
  • Dify MCP-Server 插件将工作流发布为第三方可调用服务
  • 基于量子随机源的分布式客户端密钥分发技术与后量子 RPC 架构
  • Prompt 设计原则与技巧:构建高效指令的方法论
  • Spring AI 引入 Agent Skills:Java 开发者构建智能应用新范式
  • 多模态 AI 如何重塑人机交互的未来
  • RAGFlow 搭建 AI 医疗助手实战教程
  • 基于 STM32 的智能家居环境监测系统设计
  • C++ STL 算法库核心用法与实战
  • 详解 UGC、PGC、PUGC、OGC、MGC、BGC 与 AIGC
  • 基于 Docker 与内网穿透实现 Nginx 远程访问
  • 动态规划入门:多状态 DP 问题解析(打家劫舍与股票买卖)
  • OpenClaw 公网访问实战:借助 cpolar 实现随时随地远程连接
  • 二分查找实战:山峰数组峰顶索引与寻找峰值解析
  • DeepSeek-OCR-WEBUI 高性能 OCR 文本识别部署指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online