【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 服務器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章