服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-08-17 简米科技 5,993 字 14 分钟阅读

服务器转发两个客户端的源代码如何实现?

导读服务器转发两个客户端的源代码,核心就一句话:服务器端开一个监听端口,接收两个客户端连接后,把A客户端发来的字节流原样写入B客户端的输出流,同时把B发来的字节流写入A的输出流,两个方向各跑一个独立线程,这么干说可能有点绕,我把完整思路、可运行的源码片段、以及实际部署时容易被忽略的坑都拆开讲透,服务器转发两个客户端……

服务器转发两个客户端的源代码,核心就一句话:服务器端开一个监听端口,接收两个客户端连接后,把A客户端发来的字节流原样写入B客户端的输出流,同时把B发来的字节流写入A的输出流,两个方向各跑一个独立线程。

这么干说可能有点绕,我把完整思路、可运行的源码片段、以及实际部署时容易被忽略的坑都拆开讲透。


服务器转发两个客户端的源代码 用什么思路写不踩坑

先搞清楚你的需求是哪一种

写代码前得明确一点:你是要双向转发,还是单向转发

单向转发场景比如一个采集设备把数据发给服务器,服务器转给一个展示客户端,返回数据不关心,双向转发常见于两台内网机器通过公网服务器互相通信,类似简易版内网穿透。

双向转发是更完整的形态,下面的代码和思路都按双向来讲,单向的去掉一个线程就行。

整体架构就三层,不要想复杂了

  • 第一层:服务器创建 ServerSocket,绑一个端口,9000
  • 第二层:接受两个客户端的连接,分别拿到两个 Socket 对象
  • 第三层:开两个转发线程,各管一个方向,转完就循环,直到某一边断开

这里有个容易被带偏的坑:很多人一上来就想用线程池或者 Netty,其实两个客户端用不到那么重的东西,两个连接开两个线程,开销完全可以忽略,代码还直观,除非你的服务器要同时处理几十上百对客户端,那才值得上 NIO 或 Netty。


Java 版双客户端转发源码 手把手教你搭

先看核心代码,再解释每行在干嘛

import java.io.;
import java.net.;
public class TwoClientRelayServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9000);
        System.out.println("转发服务器已启动,监听端口 9000");
        Socket clientA = serverSocket.accept();
        System.out.println("客户端A已连接:" + clientA.getRemoteSocketAddress());
        Socket clientB = serverSocket.accept();
        System.out.println("客户端B已连接:" + clientB.getRemoteSocketAddress());
        StreamRelay relayA2B = new StreamRelay(clientA, clientB, "A->B");
        StreamRelay relayB2A = new StreamRelay(clientB, clientA, "B->A");
        relayA2B.start();
        relayB2A.start();
        relayA2B.join();
        relayB2A.join();
        serverSocket.close();
    }
}

再写转发线程类,核心逻辑就藏在这里:

class StreamRelay extends Thread {
    private InputStream in;
    private OutputStream out;
    private String name;
    StreamRelay(Socket src, Socket dst, String name) throws IOException {
        this.in = src.getInputStream();
        this.out = dst.getOutputStream();
     

服务器转发两个客户端的源代码如何实现?

this.name = name; } public void run() { byte[] buffer = new byte[4096]; try { int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); out.flush(); System.out.println("[" + name + "] 转发了 " + len + " 字节"); } } catch (IOException e) { System.out.println("[" + name + "] 连接断开:" + e.getMessage()); } finally { try { in.close(); out.close(); } catch (IOException ignored) { } } } }

这个版本的逻辑简单到不需要加注释就能看懂。read 阻塞地读输入流,读到就写进对方的输出流。只要 read 不返回 -1,转发就一直在跑,什么时候连接断开了,read 会抛异常或者返回 -1,线程自然退出。

记住这三件事,代码能少改一半

  • 客户端必须主动指定服务器端口去连接new Socket("1.2.3.4", 9000),服务器代码里没有去连接客户端的逻辑。
  • BufferedInputStream 不要随便套,因为 read 一次可能只读一半数据,上面的代码是直接读原始流,不用缓冲,数据反而更完整。
  • 那两个线程都要 join() 住主线程,不然主线程跑完直接退出了,转发也就结束了,这是新手最容易漏的一行代码。

怎么验证服务器确实在转发

不用写测试客户端代码,直接用现成的工具最省事。

  • 终端一跑:nc 服务器IP 9000,这是客户端A
  • 终端二跑:nc 服务器IP 9000,这是客户端B
  • 在A里随便输入一行文字,切到B的窗口,能看到同样的内容就对了

两端能互发消息,证明转发链路是通的,然后再把上面的 Java 程序部署到服务器上跑起来,用 java TwoClientRelayServer 启动,看控制台日志就一目了然。


高并发场景怎么写 双客户端转发服务器源码

你的服务器到底能扛多少路转发

别信网上那些张口就来的性能数据。 用 BIO 的多线程模型,每路转发消耗两个线程,线程本身不贵,但线程多了,上下文切换才是大头。

粗略经验是:

  • 转发 50 路以内:上面的 JAVA BIO 代码完全够用,别折腾
  • 转发 100 到 500 路:建议用线程池限制总量,避免线程数量失控
  • 转发上千路:换 Netty 或者 Go 的 goroutine 模型,单机轻松扛住

用线程池改造,代码改动很小

ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
    Socket clientA = serverSocket.accept();
    Socket clientB = serverSocket.accept();
    pool.submit(() -> {
        try {
            Thread relay1 =

服务器转发两个客户端的源代码如何实现?

new StreamRelay(clientA, clientB, "A->B"); Thread relay2 = new StreamRelay(clientB, clientA, "B->A"); relay1.start(); relay2.start(); relay1.join(); relay2.join(); } catch (IOException e) { e.printStackTrace(); } }); }

这里有个问题:accept 是阻塞的,如果客户端A连上了但B一直不来,服务器就一直卡在第二个 accept,后面的连接全堵着,行业共识是,要么设一个最大等待时间,要么干脆走 WebSocket 方案,让两边异步连接

WebSocket 是另一种值得考虑的选择

如果两个客户端是浏览器,或者要穿透某些严格限制 TCP 出站的网络环境,WebSocket 转发是更贴近实际的选择。

WebSocket 的转发原理一样,只是把传输层从 Socket 换成了 WebSocket 的帧,给一个思路参考,不贴完整代码(避免流水账):

  • 服务器用 Spring Boot 的 WebSocketHandler 维护两个会话
  • A 发来的消息通过 handler.sendMessage(sessionB, message) 推给 B
  • B 发来的消息反向同理

这样写的好处是天然支持跨网络、跨防火墙,坏处是协议开销比原生 Socket 大一些,延迟会高个几毫秒,实时性要求极高的场景别用它。


服务器端转发两个客户端消息 调试和排错技巧

最常见的故障和解决办法

现象 原因 解决办法
客户端A连接成功但B连不上 服务器防火墙只放行了一个 TCP 连接限流 检查防火墙规则,或同时连两个端口(分别为 A 和 B 分配不同的端口)
转发时偶尔丢数据 网络层有 MTU 分片问题,或者 writeflush 确认代码里每次 write 后都调了 flush()
一端断开后另一端也卡死 read 还在阻塞中,没有收到断开信号 finally 里把两个 socket 都关掉,让对端 read 立即返回
两个客户端收发顺序错乱 一个线程读入一个线程写出,没加同步控制 这种场景下加一个锁,或者在设计层面改为消息帧方式,每条消息自带序号

断线重连其实很关键

前面的代码是一次性转发,一端断了另一端就会收到异常退出,实际生产里,客户端A掉线了,服务器不应该退出,应该重新等待A连接。

改进思路也不复杂:把 accept 的流程套一个 while(true) 外层循环,客户端B的连接保存下来,A断开后重新 accept,然后把A的新 Socket 重新接上,继续转发。

while (true) {
    Socket clientA = serverSocket.accept();
    Socket clientB = serverSocket.accept();
    // 转发逻辑...
    // 等其中一个线程结束,关闭另一条连接,回到循环顶部重新开始
}

服务器转发两个客户端的源代码如何实现?

这样一来,只要客户端B不掉线,无论A重启多少次,B都不用感知到变化。

消息边界问题,戳破一个常见误区

很多人担心 TCP 是个流协议,会有粘包问题,转发场景其实不用太慌,因为你的代码是原样转发,不是解析业务消息。

什么叫原样转发?A 发来什么字节,服务器就一字不差地写给 B,字节顺序和内容不会有任何变化,粘包是因为应用层不知道一条消息从哪里开始到哪里结束,但转发中继本身不需要关心这个,那是客户端之间协议层的事。

所以上面的代码里不需要做拆包封包逻辑,真正的核心任务只有一个:把字节搬过去,别丢,别改,别停。


关于代码所有权和合作开发的思考

写到这里,这个转发逻辑已经够任何一个独立开发者用了,但如果在公司里或者开源项目里,会有代码所有权和合作模式的问题,团队协作时,核心转发模块的代码变更应该通过 pull request 流程控制,别直接往主干上推,哪怕只是改一行 buffer 大小,也要让同行看一眼,因为转发模块状态的不可见性注定了它的 Bug 极难排查。

如果你的服务器是云服务器,比如简米云或者酷番云,记得确认安全组规则是否放行了 TCP 9000 这个端口,很多情况下代码逻辑没问题,但云平台的安全组把端口封了,导致客户端永远连不上,具体路径是:控制台 → 安全组 → 入方向规则 → 添加 TCP 9000。


相关问题

服务器转发两个客户端的源代码,客户端必须用同样的语言写吗

不用,转发只处理字节流,语言无关,客户端A用 Java 写,客户端B用 Python 写,完全没影响,只要双方的业务协议能对得上,服务器只负责搬运。

两个客户端同时发消息会冲突吗

不会,两个方向各有一个独立线程,从逻辑上互相不干扰,只要操作系统层面没有把两个线程的 write 操作编到同一个 OutputStream 上(这里的代码里两个方向分别操不同的输出流),数据就不会交错。

这个转发代码能用来做内网穿透吗

可以,但有一个限制,内网穿透通常需要端口映射,即外部客户端访问服务器的某一个端口时,服务器自动把流量转发给内网那台机器上的某个端口,上面的代码是两个客户端主动连服务器,属于反向连接模式,如果场景是 A 主动连服务器、服务器再连内网机器 B 的某个端口,那就需要再改造一下,让服务器主动去 connect B,而不是被动 accept,逻辑大同小异,方向反过来而已。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱