TA的每日心情 | 开心 2021-3-12 23:18 |
---|
签到天数: 2 天 [LV.1]初来乍到
|
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中只有一个对象存在,负责记录每天程序运行的信息,如错误,警行,一般信息等。
配置文件:
cfgjPortMap.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(); } }
可以比对cfgjPortMap.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 + "
- ");
- 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现在己增加了许多新东东让您欣赏!
源码下载:http://file.javaxxz.com/2014/10/28/235839203.zip |
|