1. 程式人生 > >【119】用Java實現TCP協議的IP地址和埠號的轉發功能

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