当前位置:首页 > 技术 > 正文内容

理解并实现 Socket 通信基础

访客 技术 2026年9月22日 10

网络应用程序通常包含客户端和服务端。客户端向服务器发送请求并接收响应,服务器则提供数据支持。在网络编程中,选择 TCP 还是 UDP 取决于连接建立的需求。TCP 需要先建立连接,而 UDP 直接发送数据。服务器端需在特定端口监听,等待客户端连接,并为其分配资源。客户端则主动发起连接请求,并通过建立的通路与服务器通信。

Socket(套接字)是操作系统提供的一种进程间通信机制,可用于本地或远程进程通信。应用程序通过 Socket API 使用套接字进行数据交换。TCP 和 UDP 是传输层协议,而 Socket 是实现连接和数据传输的途径。TCP 的三次握手是基于 TCP 协议创建网络通路的过程,最终由 Socket 完成。

Socket 连接实现

建立网络连接至少需要一对套接字:服务器端套接字和客户端套接字。

服务器端准备过程

  • 创建套接字:这里我们使用 TCP 实现。
  • 绑定监听地址:将套接字与特定的 IP 地址和端口号关联。为了适应不同的部署环境,IP 地址通常使用通配符或指定具体网卡的 IP。如果不指定端口号,操作系统会自动分配一个空闲端口。
  • 开启监听模式:调用 listen 函数使套接字进入被动监听状态,等待客户端连接请求。
  • 接受连接:调用 accept 函数(阻塞式)等待客户端连接。当客户端成功连接后,accept 返回一个新的套接字用于与该客户端通信。

以下是一个简化的服务器端实现:


package com.example.network;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleServerSocket {

    private static final int PORT = 8088;
    private static final ExecutorService threadPool = Executors.newFixedThreadPool(10);
    private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SimpleServerSocket.class.getName());

    public static void main(String[] args) {
        try {
            new SimpleServerSocket().startServer();
        } catch (IOException e) {
            logger.severe("Server startup failed: " + e.getMessage());
        }
    }

    private void startServer() throws IOException {
        // 创建 ServerSocket,绑定端口并开始监听
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            logger.info("Server started, listening on port " + PORT);
            while (true) {
                // 阻塞等待客户端连接
                Socket clientSocket = serverSocket.accept();
                logger.info("Client connected: " + clientSocket.getInetAddress());
                // 为每个客户端连接创建一个新线程处理
                threadPool.execute(new ClientHandler(clientSocket));
            }
        }
    }

    private static class ClientHandler implements Runnable {
        private final Socket clientSocket;

        public ClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        @Override
        public void run() {
            try (var in = clientSocket.getInputStream();
                 var out = clientSocket.getOutputStream();
                 var reader = new BufferedReader(new InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8));
                 var writer = new BufferedWriter(new OutputStreamWriter(out, java.nio.charset.StandardCharsets.UTF_8))) {

                String message;
                // 循环读取客户端发送的消息,直到客户端断开连接
                while ((message = reader.readLine()) != null) {
                    logger.info("Received from client: " + message);
                    // 将收到的消息回显给客户端
                    writer.write("Echo: " + message);
                    writer.newLine();
                    writer.flush();
                }
            } catch (IOException e) {
                logger.warning("Client handling error: " + e.getMessage());
            } finally {
                try {
                    clientSocket.close();
                    logger.info("Client disconnected: " + clientSocket.getInetAddress());
                } catch (IOException e) {
                    logger.warning("Error closing client socket: " + e.getMessage());
                }
            }
        }
    }
}
    

客户端发起连接过程

  • 创建套接字:客户端创建一个 Socket 对象。
  • 发起连接:使用 connect 方法(或直接在 Socket 构造函数中指定服务器地址和端口)向服务器发起连接请求。此过程会触发 TCP 的三次握手。客户端在发起连接前不必显式调用 bind,操作系统会自动分配一个临时端口。

注意:读取操作(read)通常也是阻塞的,直到接收到数据或对方明确表示数据发送完毕。在实际应用中,常通过约定数据长度或使用特定分隔符(如换行符 \n)来标识数据结束。

以下是一个简化的客户端实现:


package com.example.network;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleSocketClient {

    private static final String SERVER_HOST = "127.0.0.1";
    private static final int SERVER_PORT = 8088;
    private static final ExecutorService threadPool = Executors.newSingleThreadExecutor();
    private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SimpleSocketClient.class.getName());

    public static void main(String[] args) {
        try {
            // 创建 Socket 并连接到服务器
            Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
            logger.info("Connected to server: " + SERVER_HOST + ":" + SERVER_PORT);

            // 启动一个线程用于接收服务器的响应
            threadPool.execute(new ServerResponseHandler(socket));

            // 从标准输入读取用户输入并发送给服务器
            try (var reader = new BufferedReader(new InputStreamReader(System.in, java.nio.charset.StandardCharsets.UTF_8));
                 var writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8))) {

                String messageToSend;
                while ((messageToSend = reader.readLine()) != null) {
                    writer.write(messageToSend);
                    writer.newLine(); // 发送换行符作为消息结束标识
                    writer.flush();
                }
            } finally {
                socket.close();
                logger.info("Connection closed.");
            }

        } catch (IOException e) {
            logger.severe("Client connection failed: " + e.getMessage());
        } finally {
            threadPool.shutdown();
        }
    }

    private static class ServerResponseHandler implements Runnable {
        private final Socket clientSocket;

        public ServerResponseHandler(Socket socket) {
            this.clientSocket = socket;
        }

        @Override
        public void run() {
            try (var in = clientSocket.getInputStream();
                 var reader = new BufferedReader(new InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8))) {

                String response;
                // 循环读取服务器的响应
                while ((response = reader.readLine()) != null) {
                    logger.info("Received from server: " + response);
                }
            } catch (IOException e) {
                // 忽略连接中断的异常,这通常意味着服务器关闭了连接
                if (!clientSocket.isClosed()) {
                    logger.warning("Error receiving from server: " + e.getMessage());
                }
            }
        }
    }
}
    
标签: Socket

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。