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<Server> serverList = new ArrayList();
//Route集合 private static List<Route> 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现在己增加了许多新东东让您欣赏! |