服务器转发两个客户端的源代码,核心就一句话:服务器端开一个监听端口,接收两个客户端连接后,把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 分片问题,或者 write 没 flush |
确认代码里每次 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,逻辑大同小异,方向反过来而已。