〖 作者:javafound 〗〖 发布日期:2007-05-18 〗

图1

      因此,A机需实现端口转发功能,在Liunx上,可以通过配置IPTable由OS实现,在本例中,我们将开发一个java实现的端口转发程序jPortMap,此程序将运行在A机器上,以实现转发C与B之间通信的转发。


2.源码下载及测试说明:


从www.NetJava.cn上下载源代码解压后,可看到如下目录结构:




     现在,你可以修改一下jPortMap.cfg中的配置,比如,想通过本机的127.0.0.1地址上的8899端口转发到10.10.3.156,则这样配置:

##本地IP

LocalIP.1 = 127.0.0.1

##本地端口

LocalPort.1 = 8899

##目标IP

DestHost.1 = 10.10.3.156

##目标端口

DestPort.1 = 80

##客户端IP过滤表,*表示许可模糊匹配

AllowClient.1 = *.*.*.*


,双击jPortMap.bat启动程序后,在你的IE里输入http://127.0.0.1:8899试试看:)

3.jPortMap程序类的构成说明:
jPortMap由Main.java、Server.java、Transfer.java、Route.java、SysLog.java五个类构成。

类文件功能概要:
Main.java:程序启动主类,负责从配置文件读取转发的配置参数,启动转发服务器;

Server.java:其实是一个ServerSocket服务器,接受C机器进入的Socket连结请求,生成Transfer.对象,由Transfer负责在本机(A上)转发B和C之间的通信。

Route.java:转发对象的数据模板类,用来将转发配置映射为java对象,以由Server,ransfer对象使用。

Transfer.java:按字面意思,可理解为“传送者”,如在图示中,当C要通过A连结B时,是先连结到A机上,这里在C和A间生成一个socket对象,Transfer对象则使用这个生成的socket对象和这个传输任务的Route对象执行具体的转发任务。

SysLog.java:jPortMap是一个服务器端程序,在运行中可能会出现错误,因此需要一个日志工具,日志工具在jPortMap中只有一个对象存在,负责记录每天程序运行的信息,如错误,警行,一般信息等。

配置文件:
    cfg/jPortMap.cfg:这是一个文本文件,其中存放jPortMap的配置数据,当程序启动时,主类会从中读取数据配置程序,以生成多个Route对象在内存中保持数据。

4.Route.java解析:
我们己经说明,Route类是转发对象配置数据的模板类,当jPortMap启运时,它需要知道如下配置:
1. 有多少处转发任务(意味着要监听哪几个ServerSocket);
2. jPortMap程序对每个转发任务要启动的监听ServerSocket端口及所绑定的IP地址;
3. 每个转发任务的目标IP地址和端口;

    因此,jPortMap一但启动,可能会创建多个Route对象,而每个具体的Route对象则保布着一个转发任务的以上配置数据。
另外,从安全方面着想,我们的jPortMap程序还需要对请求进入的连结进行安全管理,这里我们简单的用IP过滤的方法,即jPortMap中ServerSocekt监听到的进入连结请求会认证IP地址,如发现IP地址没有在许可的列表中,则断开这个请求;所以Route类还要保存每个任务对应的许可IP表;
我们的Route.java源文件如下:

/* * Route.java * * Created on 2006年12月28日, 下午12:36 * * To change this template, choose Tools | Template Manager * and open the template in the editor. */package org.netjava.jportmap;/** *转发任务的配置数据对象模板 * 
Company: www.NetJava.org
 * @author javafound */public class Route {    public Route() {}    //jPortMap绑定的IP    String  LocalIP="";    //监听的端口    int     LocalPort=0;    //转发数据的目标机器IP    String  DestHost="";    //转发的目标端口    int     DestPort=0;    //这个转发上许可进入的IP列表    String  AllowClient="";
    //重写的toString方法,输出具体Route对象的信息以便debug        public String toString() {        StringBuffer stb = new StringBuffer();        stb.append(" LocalADD  " + LocalIP);        stb.append(" :" + LocalPort);        stb.append(" --->DestHost " + DestHost);        stb.append(" :" + DestPort);        stb.append("   (AllowClient) " + AllowClient);        return stb.toString();    }}



      可以比对cfg/jPortMap.cfg(可用notepad打开)中的内容,Route类只需要据文本件中的配配生成多个Route对象或者说转发任务,再由其它对象来使用,因此,Route类的功能和结构很简单,就像映射表结构的javaBean一样,只是负责保存数据在内存中。


5. SysLog.java解析:

SysLog保存每天的日志信息到指定的目录下,简单的说就是提供方法供别的对象来调用,写内容到文件中:

package org.netjava.jportmap;
import java.io.*;
import java.util.Calendar;

/**
 * Title: 端口转发器 
 * Description:日志工具类 
 * Copyright: Copyright (c) 2005
 * Company: www.NetJava.org
 * @author javafound
 * @version 1.0
 */

public class SysLog {
    //记录输出一般信息
  public  static void info(String s) {
        writeToTodayLog("INFO  :", s);
    }
   记录警告信息
 public   static void warning(String s) {
        writeToTodayLog("WARN:", s);
    }
   //记录错误信息
public   static void severe(String s) {
        writeToTodayLog("ERROR:", s);
    }
//输出到当天日志文件的具体实现
    private static void writeToTodayLog(String flag, String msg) {
        RandomAccessFile raf = null;
        try {
            Calendar now = Calendar.getInstance();
            String yyyy = String.valueOf(now.get(java.util.Calendar.YEAR));
            String mm = String.valueOf(now.get(Calendar.MONTH) + 1);
            String dd = String.valueOf(now.get(Calendar.DAY_OF_MONTH));
            String hh = String.valueOf(now.get(Calendar.HOUR_OF_DAY));
            String ff = String.valueOf(now.get(Calendar.MINUTE));
            String ss = String.valueOf(now.get(Calendar.SECOND));
            mm = (1 == mm.length()) ? ("0" + mm) : mm;
            dd = (1 == dd.length()) ? ("0" + dd) : dd;
            hh = (1 == hh.length()) ? ("0" + hh) : hh;
            ff = (1 == ff.length()) ? ("0" + ff) : ff;
            ss = (1 == ss.length()) ? ("0" + ss) : ss;
            String yyyymmdd = yyyy + mm + dd;
            String hhffss=hh+ff+ss;
            String path = System.getProperties().getProperty("user.dir")
                          + File.separator + "log";
            File p = new File(path);
            if (!p.exists()) {
                p.mkdirs();
            }
            path += File.separator + "jPortMap_" + yyyymmdd + ".log";
            File f = new File(path);
            if (f.isDirectory()) {
                f.delete();
            }
            raf = new RandomAccessFile(f, "rw");
            raf.seek(raf.length());
            raf.writeBytes(hhffss+"  "+flag + " : " + msg + "/r/n");
            raf.close();
        } catch (Exception ex) {
            System.out.println("write file has error=" + ex);
        }
    }
     /** Creates a new instance of SysLog 
      *做为一个工具类,一般不需要实例化,所以此处private
      */
    private SysLog() {}
}

说明:
首先我们看到提供的三个公用静态方法:
//记录一般信息
public static void info(String s)

记录警告信息
public static void warning(String s)

//记录错误信息
public static void severe(String s)

     SysLog做为系统中的工具类,一般是不需要实例化的,所以只提供调用功能即可,这三个调用方法为其它对象提供了调用接口,分别输出不同类型的信息到目志中,而调用对象并不需要去关心具体日志的格式,日志文件命令,文件读写等问题----只需传入要记录的消息即可。

     System.getProperties()返回一个Properties对象,其实是一个Map接口的实现,其中存入格式为 名字:值 一一对应的表,系统的许多环境变量,如程序运行的当前目录user.dir,操作系统类型,java当前版本等都在其中存放。

     RandomAccessFile:在写日志时使用了这个类向日志文件中写入内容,其中seek(int length)可以指定跳过文件中内容的长度后再开始写入;这样我们的日志就不会丢失。

6.Server.java解析:
      如其名,Server是一个转发服务器的实现类,我们的jPortMap可同时执行多个转发服务,所以每个Server对象都将做为一个独立的线程运行,在jPortMap.cfg中配置了几个转发任务,系统就会实例几个Route对象,并生成对应个数的的Server对象,每个Server对象使用自己的一个Route对象的数据在指定的端口启动监听服务,等待客户端(如前面图示则是C机器)发起的连结,接收到连结请求并通过IP验证后,这个Server对象则将具体的转发任务交给自己的一个Transfer对象去独立处理,而Server对象则继续运行,等待到来的连结请求。

      我们可以将这个Server理解为一个看门人的角色---使用ServerSocket监听指定端口,等待到来的连结,它只负责接待来客,并核查来客的身份,如核查通过,至于来客进的门怎么办,它不管-----由它所持有的另外一个对象Transfer类的一个实例去处理。解析代码如下:

package org.netjava.jportmap;
import java.net.*;
import java.util.*;
/**
 * Title: 端口转发器
 * Description:启动监听服务 
 * Copyright: Copyright (c) 2005
 * Company: www.NetJava.org
 * @author javafound
 * @version 1.0
 */

public class Server extends Thread {
    //创建一个转发服务器
    public Server(Route route, int id) {
        this.route = route;
        connectionQueue = new Vector();
        myID = id;
        start();
    }
    //关闭这个服务器:
    public void closeServer() {
        isStop = true;
        if (null != myServer) {
            closeServerSocket();
        } while (this.connectionQueue.size() > 0) {
            Transfer tc = (Transfer) connectionQueue.remove(0);
            tc.closeSocket(tc.socket);
            tc = null;
        }
    }
//启动转发服务器的执行线程
    public void run() {
        SysLog.info(" start Transfer......:" + route.toString());
        ServerSocket myServer = null;
        try {
            InetAddress myAD = Inet4Address.getByName(route.LocalIP);
            myServer = new ServerSocket(route.LocalPort, 4, myAD);
        } catch (Exception ef) {
            SysLog.severe("Create Server " + route.toString() + " error:" + ef);
            closeServerSocket();
            return;
        }
        SysLog.info("Transfer Server : " + route.toString() + " created OK");
        while (!isStop) {
            String clientIP = "";
            try {
                Socket sock = myServer.accept();
                clientIP = sock.getInetAddress().getHostAddress();
                if (checkIP(route, clientIP)) {
                    SysLog.warning(" ransfer Server : " + route.toString() +
                                   "  Incoming:" + sock.getInetAddress());
                    sock.setSoTimeout(0);
                    connCounter++;
                    Transfer myt = new Transfer(sock, route);
                    connectionQueue.add(myt);
                } else {
                    SysLog.warning(" ransfer Server : " + route.toString() +
                                   "  Refuse :" + sock.getInetAddress());
                    closeSocket(sock);
                }
            } catch (Exception ef) {
                SysLog.severe(" Transfer Server : " + route.toString() +
                              " accept error" + ef);
            }
        }
    }
    //检测进入的IP是否己许可
    private static boolean checkIP(Route route, String inIP) {
        String[] inI = string2StringArray(inIP, ".");
        String[] list = string2StringArray(route.AllowClient, ".");
        if (inI.length != list.length) {
            SysLog.severe(" Transfer Server Error Cfg AllowClient : " +
                          route.toString());
            return false;
        }
        for (int i = 0; i < inI.length; i++) {
            if ((!inI[i].equals(list[i])) && !(list[i].equals("*"))) {
                System.out.println(": " + inI[i] + " :" + list[i]);
                return false;
            }
        }
        return true;
    }
    /*
     * @param srcString 原字符串
     * @param separator 分隔符
     * @return 目的数组
     */
    private static final String[] string2StringArray(String srcString,
            String separator) {
        int index = 0;
        String[] temp;
        StringTokenizer st = new StringTokenizer(srcString, separator);
        temp = new String[st.countTokens()];
        while (st.hasMoreTokens()) {
            temp[index] = st.nextToken().trim();
            index++;
        }
        return temp;
    }
 
  //关闭ServerSocket
    private void closeServerSocket() {
        try {
            this.myServer.close();
          } catch (Exception ef) {
        }
    }
 private void closeSocket(Socket s) {
        try {
            s.close();
        } catch (Exception ef) {
        }
    }
    
    //服务器
  private ServerSocket myServer = null;
    //连结队列控制
    private boolean isStop = false;
            //
    private Vector connectionQueue = null;
    private int connCounter = 0;
    // 路由对象
    private Route route = null;
    //连结的ID号,暂未用
    private static int  myID = 0;
}

   Server类关键功能是在一个独立的线程中执行监听任务,当我们实例化一个ServerSocket时,即绑定了本机的一个IP和端口,这个ServerSocket对象就在这个地址(由IP和端口组成)上通过调用accept()方法等待客户端连结,默认情况下,这个等待会一直持续,直到有一个连结进入----生成一个socket对象;

而我们的ServerSocket.accept()是在一个wilhe循环中,这保证了监听服务器不会中途退出。


7. Transfer.java解析

      在分析Server.java中我们看到,Server做为一个服务器,在与客户端建立连结后使用生成的Socket对象和自己的Routc对象来实例化一个Transfer,具体的传输工作就交给了Transfer对象完成。


    Server生成的Socket对象是机器C与A之间连结的一个代码,通过这个Socekt对象上的Input/OutPut Stream,可以让C与A之间通信----工作还只完成了一半,这里我们还需要建立A与B之间的Socket连结,这里就出现了两个Socket连结,分别是C与A间,我们叫SocketCA;A与B间我们假设叫做SocketAB; Transfer对象的任务就是行建立SocketAB,然后,将SocketCA的输入写入到SocketAB的输出流,将SocketAB的输出流写到SocketCA的输出流中,这样,就完成了C,B机器之间的数据转发。

package org.netjava.jportmap;
import java.net.*;
import java.io.*;

/**
 * Title: 端口转发器
 * Description: 对连结进行转发处理
 * Copyright: Copyright (c) 2005
 * Company: www.NetJava.org
 * @author javafound
 * @version 1.0
 */

public class Transfer extends Thread {
     /**
     * 创建传输对象
     * @param s Socket   :进入的socket
     * @param route Route:转发配置
     */
    public Transfer(Socket s, Route route) {
        this.route = route;
        this.socket = s;
        this.start();
    }
    // 执行操作的线程
    public void run() {
        Socket outbound = null;
        try {
            outbound = new Socket(route.DestHost, route.DestPort);
            socket.setSoTimeout(TIMEOUT);
            InputStream is = socket.getInputStream();
            outbound.setSoTimeout(TIMEOUT);
            OutputStream os = outbound.getOutputStream();
            pipe(is, outbound.getInputStream(), os, socket.getOutputStream());
        } catch (Exception e) {
            SysLog.severe(" transfer error:" +route.toString()+ " :" + e);
        } finally {
            SysLog.warning("Disconnect :"+ route.toString());
            closeSocket(outbound);
            closeSocket(socket);
        }
    }
    
/**
 *传输的实现方法
 */
 private   void pipe(InputStream is0, InputStream is1,
              OutputStream os0, OutputStream os1) {
        try {
            int ir;
            byte bytes[] = new byte[BUFSIZ];
            while (true) {
                try {
                    if ((ir = is0.read(bytes)) > 0) {
                        os0.write(bytes, 0, ir);
                    } else if (ir < 0) {
                        break;
                    }
                } catch (InterruptedIOException e) {}
                try {
                    if ((ir = is1.read(bytes)) > 0) {
                        os1.write(bytes, 0, ir);
                        // if (logging) writeLog(bytes,0,ir,false);
                    } else if (ir < 0) {
                        break;
                    }
                } catch (InterruptedIOException e) {}
            }
        } catch (Exception e0) {
            SysLog.warning(" Method pipe" + this.route.toString() + " error:" +
                           e0);
        }
    }
    //关闭socket
     void closeSocket(Socket s) {
        try {
            s.close();
        } catch (Exception ef) {
        }
    }
   //传输任务的Route对象
Route route = null;
     // 传入数据用的Socket
Socket socket;
   //超时
   static private int TIMEOUT = 1000;
   //缓存
   static private int BUFSIZ = 1024;
}

8.Main.java解析

OK,至此己万事具备!我们需要一个启动主类,根据读入的配置文件数据来启动转发服务器,执行转发工作:

package org.netjava.jportmap;
import java.io.*;
import java.util.*;
import java.net.*;
/**
 * Title: 端口转发器
 * Description:启动主类:读取配置,启动监听服务 
 * Copyright: Copyright (c) 2005
 * Company: www.NetJava.org
 * @author javafound
 * @version 1.0
 */

public class Main {
  //start......
    public static void main(String args[]) {
        startService();
    }
//start
    public static void startService() {
        if (!loadCfgFile()) {
            System.exit(1);
        } while (serverList.size() > 0) {
            Server ts =   serverList.remove(0);
            ts.closeServer();
        }
        for (int i = 0; i < routeList.size(); i++) {
            Route r = routeList.get(i);
            Server server = new Server(r, i);
            serverList.add(server);
        }
    }
// 停止服务接口,备用其它模块调用
    public static void stop() {
        while (serverList.size() > 0) {
            Server ts = serverList.remove(0);
            ts.closeServer();
        }
    }
    /**
     *从配置文件读取数据,生成Route对象
     * read cfg parameter
     * @return boolean
     */
    private static boolean loadCfgFile() {
        try {
            String userHome = System.getProperties().getProperty("user.dir");
            if (userHome == null) {
                userHome = "";
            } else {
                userHome = userHome + File.separator;
            }
            userHome += "cfg" + File.separator + "jPortMap.cfg";
            InputStream is = new FileInputStream(userHome);
            Properties pt = new Properties();
            pt.load(is);
            //共有几个业务模块
            int ServiceCount = Integer.parseInt(pt.getProperty("TransferCount"));
            for (; ServiceCount > 0; ServiceCount--) {
                Route r = new Route();
                r.LocalIP = pt.getProperty("LocalIP." + ServiceCount).trim();
                r.LocalPort = Integer.parseInt(pt.getProperty("LocalPort." +
                        ServiceCount).trim());
                r.DestHost = pt.getProperty("DestHost." + ServiceCount).trim();
                r.DestPort = Integer.parseInt(pt.getProperty("DestPort." +
                        ServiceCount).trim());
                r.AllowClient = pt.getProperty("AllowClient." + ServiceCount).
                                trim();
                routeList.add(r);
            }
            is.close();
            SysLog.info("ystem Read cfg file OK");
        } catch (Exception e) {
            System.out.println("找不到配置文件:"+e);
            SysLog.severe("loadCfgFile false :" + e);
            return false;
        }
        return true;
    }
    //Server服务器集合
    private static List< Server> serverList = new ArrayList();
    //Route集合
    private static List< Route> routeList = new ArrayList();
}

Main类中需要注意的是loadCfgFile()方法,它用来读取当前目录下面cfg/jPortMap.cfg文件中的配置数据,如读取成功,返加ture值,如读取失败,程序测会退出。
另外:

//Server服务器集合
private static List<Server> serverList = new ArrayList();

//Route集合
private static List<Route> routeList = new ArrayList();



这两行代码,生成两个列表,来保存己启动的Server对象和Route对象。

     现在,我们只要启动Main类,jPortMap就开始运行了,同时会在log目录下行成每天的运行日志;当然,千万不要忘了cfg/目录下面jPortMap.cfg中配置转发的参数,配置的具体说明在该文件中有注解。
源码目录结构图(NetBean中):

9.改进设想:

java实现的端口映射器


下载源代码