java实现的端口映射器

java实现的端口映射器

下载源代码


〖 作者:javafound 〗〖 大小:152k 〗〖 发布日期:2007-05-18 〗〖 浏览:0 〗

1.介绍:
      本文手把手的详解了jPortMap端口映射程序开发中的每一步,做为己运行在实际的企业项目中的应用, jPortMap程序较全面的展示了Thread、List、Vector、Socket、ServerSocket、Input/OutpuStream、File Read/Write、Properties等核心API的用法,是初学者快速进阶的一个优秀案例。

        在涉及内外网数据交换的网络应用系统开发中,我们经常需要做端口映射,比如放在外部网络主机上的程序要与内部网络上的某台机器建主TCP/IP连结,如下图(1)示:

    C机器可以与A机连通,但要与B机连通,由与不在同一网络,就无能为力了;这时,就需在A机器上做交换或是转发,来接通C与B之间的TCP/IP连结,即C机先与A机器建立Socket连结,A再与B机建立连结,然后由A在中间转发C与B通信的数据;B机器上可能运行着数据库,WebService等在Tcp/IP上通信的程序,而C机器必须访问这些服务。这里A机器就充当像现实生活中介绍人的角色,负责将C、B之间的通信数据流在Socket上转发;

图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 serverList = new ArrayList();

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

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

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

9.改进设想:
       无论如何,这还是个比较简陋的程序!假如我们把配置改成XML格式、假如我们使用Thread Pool来执行任务、假如我们使用NIO、假如我们再做一套PL的UI界面….,您的任何建议,都会是对jPortMap走向完美的支持,请登陆www.NetJava.cn发表您的看法,发布您的创新!当然,www.NetJava.cn现在己增加了许多新东东让您欣赏!

你可能感兴趣的:(java技术,unix&linux)