【119】用Java实现TCP协议的IP地址和端口号的转发功能

  • 时间:
  • 来源:互联网

最近碰到了这样的需求:用户通过TCP访问服务器 A,服务器 A 再把 TCP 请求转发给服务器 B;同时服务器 A 把服务器 B 返回的数据,转发给用户。也就是服务器 A 作为中转站,在用户和服务器 B 之间转发数据。示意图如下:

1.png

为了满足这个需求,我用Java开发了程序。我为了备忘,把代码简化了一下,剔除了实际项目中的业务代码,给了一个简单的例子。

这个例子项目名字是 blog119,用 maven 管理、Java 10 编译。整个项目只有一个包:blog119。包下有三个类:CheckRunnable、Main、和 ReadWriteRunnable 。项目中还有一个 maven 项目必有的 pom.xml 文件。接下来是三个文件的内容。

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>zhangchao</groupId>
    <artifactId>blog119</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>10</java.version>
        <maven.compiler.source>10</maven.compiler.source>
        <maven.compiler.target>10</maven.compiler.target>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>blog119.Main</mainClass> <!-- 你的主类名 -->
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Main 类,包含 main 方法,调用 CheckRunnable 类和 ReadWriteRunnable 类。

package blog119;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;


/**
 * 主类。
 * @author 张超
 *
 */
public class Main {
    /**
     * 当前服务器ServerSocket的最大连接数
     */
    private static final int MAX_CONNECTION_NUM = 50;

    public static void main(String[] args) {
        // 启动一个新线程。检查是否要种植程序。
        new Thread(new CheckRunnable()).start();

        // 当前服务器的IP地址和端口号。
        String thisIp = args[0];
        int thisPort = Integer.parseInt(args[1]);

        // 转出去的目标服务器IP地址和端口号。
        String outIp = args[2];
        int outPort = Integer.parseInt(args[3]);

        ServerSocket ss = null;
        try {
            ss = new ServerSocket(thisPort, MAX_CONNECTION_NUM, InetAddress.getByName(thisIp));

            while(true){
                // 用户连接到当前服务器的socket
                Socket s = ss.accept();

                // 当前服务器连接到目的地服务器的socket。
                Socket client = new Socket(outIp, outPort);

                // 读取用户发来的流,然后转发到目的地服务器。
                new Thread(new ReadWriteRunnable(s, client)).start();

                // 读取目的地服务器的发过来的流,然后转发给用户。
                new Thread(new ReadWriteRunnable(client, s)).start();

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {         
            try {
                if (null != ss) {
                    ss.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

}

CheckRunnable 类。启动程序的时候创建 running.txt 文件,然后每隔一段时间检测 running.txt 文件是否存在。如果检测到 running.txt 不存在,就终止整个程序。我希望用这种方式来避免粗暴地杀死进程。个别情况下粗暴地杀死进程可能会出问题。

package blog119;

import java.io.File;
import java.io.IOException;

/**
 * 新启动一个线程,每隔一段时间就检查一下是否存在 running.txt文件。如果存在,程序正常运行。
 * 如果不存在,系统退出。
 * @author 张超
 *
 */
public class CheckRunnable implements Runnable {

    /**
     * 取得Java程序当前目录下的running.txt硬盘地址。如果是编译后的jar包,那么
     * running.txt 就在jar包所在的文件夹。如果是开发阶段,就在 class 文件目录里面
     * @return 取得 running.txt 路径的  File。
     */
    private File getFile() {
        String path = this.getClass().getProtectionDomain().getCodeSource().getLocation().getFile();
        File runningFile = null;
        if (path.endsWith(".jar")) {
            File tmp = new File(path);
            tmp = tmp.getParentFile();
            runningFile = new File(tmp.getAbsolutePath() + File.separator + "running.txt");
        } else {
            runningFile = new File(path + "running.txt");
        }
        return runningFile;
    }

    /**
     * 构造方法
     */
    public CheckRunnable(){
        File file = this.getFile();
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public void run() {
        try {
            while (true) {

                Thread.sleep(30L * 1000L);
                // 没有 running.txt 就退出
                File file = this.getFile();
                if (!file.exists()) {
                    System.exit(0);
                }
            }   

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

ReadWriteRunnable 类。创建对象的时候接受两个 Socket 作为成员变量。从一个 Socket 中读取数据,然后发送到另一个 Socket。

package blog119;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * 读写流的Runnable
 * @author 张超
 *
 */
public class ReadWriteRunnable implements Runnable {

    /**
     * 读入流的数据的套接字。
     */
    private Socket readSocket; 

    /**
     * 输出数据的套接字。
     */
    private Socket writeSocket;

    /**
     * 两个套接字参数分别用来读数据和写数据。这个方法仅仅保存套接字的引用,
     * 在运行线程的时候会用到。
     * @param readSocket 读取数据的套接字。
     * @param writeSocket 输出数据的套接字。
     */
    public ReadWriteRunnable(Socket readSocket, Socket writeSocket) {
        this.readSocket = readSocket;
        this.writeSocket = writeSocket;
    }

    @Override
    public void run() {
        byte[] b = new byte[1024];   
        InputStream is = null;
        OutputStream os = null;
        try {
            is = readSocket.getInputStream();
            os = writeSocket.getOutputStream();
            while(!readSocket.isClosed() && !writeSocket.isClosed()){
                int size = is.read(b); 
                if (size > -1) {
                    os.write(b, 0, size);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (null != os) {
                    os.flush();
                    os.close();
                }
            } catch (IOException e) {
                    e.printStackTrace();
            }   
        }

    }

}

在命令行执行这个程序的时候,需要输入四个参数。分别是当前服务器IP地址、当前服务器端口、目的地服务器IP地址、目的地服务器端口。

Eclipse 调试的时候,可鼠标移动到 Main.java 上,右键 → Run As → Run Configurations…
弹出的对话框如下所示:

2.png

注意左侧菜单栏选中 Java Application → Main。右侧选项卡选中 Arguments,然后在 Program arguments 中填写参数就行了。

怎么验证项目管用?

我自己建立了一个 Ubuntu 服务器,IP地址是10.30.1.106,开放 SSH 远程登录权限。SSH 默认使用 TCP 协议的 22 号端口。我就用 blog119 做TCP转发,在本地监听 65010 端口。这样,整个映射关系是: 127.0.0.1:65010 对应 10.30.1.106:22
如上图所示,参数是:127.0.0.1 65010 10.30.1.106 22
打开 putty 远程连接工具,IP地址设置成 127.0.0.1,端口是 65010,你会发现可以连接,而且所有命令都能执行,就像直接远程连接 Ubuntu 服务器一样。

为什么本地IP地址还要作为参数进行设置?默认127.0.0.1 不好吗?

我主要考虑到一个服务器可以对应多个 IP 地址的情况。有些时候,你不想在同一台服务器的所有IP地址上都监听同一个端口。所以这里把本地地址作为参数,方便灵活配置。

jar包的用法

eclipse 选中项目右键 → Run As → Maven build … → Main选项卡 → Goals 文本框中输入 clean package 点击 Run 按钮, 就可以打成jar包。直接在命令行中输入

java -jar blog119-0.0.1-SNAPSHOT.jar 127.0.0.1 65111 10.30.1.106 22

程序启动后,会在jar包的文件夹下生成一个 running.txt 文件。如果要关闭程序,删除这个文件,半分钟后程序就会自动关闭。当程序启动的时候,你可以用 putty 访问 127.0.0.1 地址的 65111 端口,就可以事实上远程控制 10.30.1.106 服务器。

本文链接http://element-ui.cn/news/show-576590.aspx