Java作为一种跨平台的开发语言,被广泛地应用,对比C++来讲,不需要关心对象的释放,缓冲区的管理,使编程的细节处理上要简单了很多,但是在高负载、多线程、多任务的协作处理时,对象的频繁new,线程的频繁创建、销毁,仍有可能导致程序的异常崩溃;每一次网络开发的调试都是一个几乎要崩溃的过程,各种细节的处理会导致开发过程缓慢、周期变长,本文通过对NIO和线程池的调度封装,使服务端的高并发、网络信息传送简化成了命令的传送,让开发的整个过程规范化,使程序员在相应开发时只需注意业务的处理,将所有的网络端、多线程设定和管理简化成了配置文件。
下面我先给出一段使用封装后的库进行网络开发的实例。
//这个参数类是创建服务所需要的一些配置参数,这些参数可以通过命令行或配件文件进行传递
//这里我们只看一下它的结构,而不用关心具体的内容
public class SceneParameter
{
public int m_iReconnectCount = Integer.MAX_VALUE;
public long m_lReconnectIntervalMSEL = 5 * 1000;
/**
* CipherKeyService监听地址
*/
public InetSocketAddress m_remoteAddressCipherKeyService = null;
/**
* CompensationService监听地址
*/
public InetSocketAddress m_remoteAddressCompensationService = null;
/**
* MulticastSourceService监听地址
*/
public InetSocketAddress m_remoteAddressMulticastSourceService = null;
/**
* PublicNetworkService监听地址
*/
public InetSocketAddress m_remoteAddressPublicNetworkService = null;
/**
* CompensationService远程连接地址的long型表示
* 高32位保存了端口号,
* 低32位保存了IP地址;
*/
public long m_lCompensationServiceAddress = 0;
public MySceneLog m_mySenceLog;
}
我们为每一个服务端所需要的配置创建一个参数类,参数类中的内容可以是由命令行或者配置文件来赋值,这个参数类做为创建服务时传递的参数。
public class AssignSceneAddressService
{
/**
* 版本号
*/
private static String s_strVersion = "V1.0.3.2020.02.23.20.11";
static class AppParameter
{
public String m_strConfigFile = "AssignSceneAddressService.config";
}
public static void main(String[] args)
{
try
{
//Tcp监听服务,用于终端与CompensationServer的连接
//CompensationServer主动与组播服务器建立连接
SceneParameter sp = new SceneParameter();
TcpServerParameter tsp = new TcpServerParameter(null, null);
tsp.m_nioTcpParameter = new NioTcpParameter();
//应该将服务端的配置变成配置文件
AppParameter ap = new AppParameter();
AssignSceneAddressServiceConfig asasc = AssignSceneAddressService.parseCmdLine( ap, args, "=");
sp.setCompensationServiceRemoteAddress( asasc.m_remoteCompensationServiceConfig.getInetSocketAddress() );
sp.m_remoteAddressCipherKeyService = asasc.m_remoteCipherKeyServiceConfig.getInetSocketAddress();
sp.m_remoteAddressMulticastSourceService = asasc.m_remoteMulticastSourceServiceConfig.getInetSocketAddress();
sp.m_remoteAddressPublicNetworkService = asasc.m_publicNetworkServiceConfig.getInetSocketAddress();
sp.m_iReconnectCount = asasc.m_nioTcpConnectorParmeterConfig.m_iReconnectCount;
sp.m_lReconnectIntervalMSEL = asasc.m_nioTcpConnectorParmeterConfig.m_lReconnectIntervalMSEL;
tsp.m_strBindAddress = asasc.m_remoteAssignSceneAddressServiceConfig.m_strBindAddress;
tsp.m_iListenPort = asasc.m_remoteAssignSceneAddressServiceConfig.m_iPort;
tsp.m_iMaxSelectorCount = asasc.m_iMaxSelectorCount;
asasc.m_nioTcpParameterConfig.copyTo( tsp.m_nioTcpParameter );
//这里可以根据参数,决定选用哪一种Log
sp.m_mySenceLog = new MySceneLog();
LogLevel.setLogOutLevel( asasc.m_logOutConfig.m_lLogOutLevel );
//LogLevel.getLogFile().setJsonLogFileConfig( asasc.m_logFileConfig );
//LogLevel.getLogFile().startThread();
LogLevel.getLogFileEx().setJsonLogFileConfig( asasc.m_logFileConfig );
LogLevel.getLogFileEx().startThread();
LogLevel.outputToLogFile( Convenient.s_dfFullDateTime.format(new Date(System.currentTimeMillis()))
+ " SMKE - AssignSceneAddressService is running! " + AssignSceneAddressService.s_strVersion
+ "; \r\n\tListen address is " + tsp.getAddress()
+ "; \r\n\tPublic network service address is " + sp.getPublicNetworkServiceRemoteAddress()
+ "; \r\n\tMulticast source service address is " + sp.getMulticastSourceServiceRemoteAddress()
+ "; \r\n\tCompensation service address is " + sp.getCompensationServiceRemoteAddress()
+ "; \r\n\tCipherKey service address is " + sp.getCipherKeyServiceRemoteAddress() );
//创建服务的处理线程
new SceneCampsite( sp, tsp, asasc ).startService();
}
catch ( Exception e )
{
e.printStackTrace();
}
}
public static AssignSceneAddressServiceConfig parseCmdLine( AppParameter ap, String[] args, String strSeparator )
{
CmdLineParameter clp = new CmdLineParameter();
clp.parseCmdLine( args, true, strSeparator );
if( args.length <= 0 )
{
//如果没有参数
AssignSceneAddressService.outHelpInfo();
}
else
{
String str = clp.getSetValue("-File");
if (str != null)
{
//设置要打开的本地的文件名
ap.m_strConfigFile = str;
}
}
AssignSceneAddressServiceConfig asasc = new AssignSceneAddressServiceConfig();
JsonConfigBase.Read( new File( ap.m_strConfigFile ), asasc,null );
return asasc;
}
private static void outHelpInfo()
{
System.out.println("可以输入以下的参数的任意组合(大小写敏感):\r\n"
+ "\t-File=n\t要加载的Config文件的名称,默认为AssignSceneAddressService.Config\r\n" );
}
}
从上面的代码可以看到,main()函数很简单,只是解晰命令行和读取配置文件,将配置文件中的内容赋值给需要的参数类,而服务端具体的创建,则由服务的入口线程根据参数完成,所有的配置文件的读取和解晰都采用Json方式进行配置,关于配置文件的标准化读取后面再介绍。
下面我们再看一下具体的服务的入口线程的代码,看一下是如何定义多线程和实现业务处理的。
/**
* 开启服务
*/
public void startService()
{
MyNioTcpLog myLog = this.m_sceneParameter.m_mySenceLog;
try
{
//创建Selector封装对象。注意:这个对象不需要关联线程启动,只要new出一个实例就可以了
NioTcpReactorRunnable ntrr = new NioTcpReactorRunnable( myLog, 1 );
//创建Selector池
SelectorPool selectorPool = new SelectorPool( this.m_tcpServerParameter.m_iMaxSelectorCount, ntrr, myLog );
//设置服务端运行的参数
this.m_tcpServerParameter.m_selectPool = selectorPool;
this.m_tcpServerParameter.m_myNioTcpLog = myLog;
this.m_tcpServerParameter.m_nioTcpProcessor = new SceneCmdReactor(
null,
ntrr,
this.m_tcpServerParameter.m_nioTcpParameter,
this );
//启动Tcp监听端口
NioTcpListenerReactorRunnable nioTcpListenerReactor = new NioTcpListenerReactorRunnable( this.m_tcpServerParameter );
new Thread( nioTcpListenerReactor ).start();
//创建定时器对象
this.m_timer = new TimerRunnable( 1000 );
new Thread( this.m_timer ).start();
//创建Nio方式下的Tcp连接的管理类,当连接建立时,会自动关联到NioTcpProcessorReadWrite类进行读写消息处理
NioTcpConnectMgr ntrm = new NioTcpConnectMgr(
selectorPool,
this.m_timer,
this.m_tcpServerParameter.m_nioTcpProcessor,
this.m_tcpServerParameter.m_nioTcpParameter,
myLog );
if( this.m_sceneParameter.m_remoteAddressCompensationService != null )
{
//创建和CompensationService之间的tcp连接,并关联到连接管理类
new MyNioTcpConnector( ntrm, this, MyNioTcpConnector.C_COMPENSATION_SERVICE ).connect(
this.m_sceneParameter.m_remoteAddressCompensationService,
this.m_sceneParameter.m_iReconnectCount,
this.m_sceneParameter.m_lReconnectIntervalMSEL );
}
if( this.m_sceneParameter.m_remoteAddressCipherKeyService != null )
{
//创建和CipherKeyService之间的tcp连接,并关联到连接管理类
new MyNioTcpConnector( ntrm, this, MyNioTcpConnector.C_CIPHER_KEY_SERVICE ).connect(
this.m_sceneParameter.m_remoteAddressCipherKeyService,
this.m_sceneParameter.m_iReconnectCount,
this.m_sceneParameter.m_lReconnectIntervalMSEL );
}
if( this.m_sceneParameter.m_remoteAddressMulticastSourceService != null )
{
//创建和MulticastSourceService之间的tcp连接,并关联到连接管理类
new MyNioTcpConnector( ntrm, this, MyNioTcpConnector.C_MULTICAST_SOURCE_SERVICE ).connect(
this.m_sceneParameter.m_remoteAddressMulticastSourceService,
this.m_sceneParameter.m_iReconnectCount,
this.m_sceneParameter.m_lReconnectIntervalMSEL );
}
if( this.m_sceneParameter.m_remoteAddressPublicNetworkService != null )
{
//创建和PublicNetworkService之间的tcp连接,并关联到连接管理类
new MyNioTcpConnector( ntrm, this, MyNioTcpConnector.C_PUBLIC_NETWORK_SERVICE ).connect(
this.m_sceneParameter.m_remoteAddressPublicNetworkService,
this.m_sceneParameter.m_iReconnectCount,
this.m_sceneParameter.m_lReconnectIntervalMSEL );
}
this.m_mysql = new MyMySql( this.m_assignSceneAddressServiceConfig.m_dbConfig );
this.loadDataFromDB();
//启动监听地址组播
MCSendDataRunnable mcsdr = new MCSendDataRunnable();
mcsdr.m_strMulticastIp = this.m_assignSceneAddressServiceConfig.m_multicastNotifyConfig.m_strBindAddress;
mcsdr.m_iMulticastPort = this.m_assignSceneAddressServiceConfig.m_multicastNotifyConfig.m_iPort;
MCAddress mcAddress = new MCAddress();
mcAddress.set( this.m_tcpServerParameter.m_strBindAddress, this.m_tcpServerParameter.m_iListenPort );
mcsdr.setSendData( mcAddress.getData() );
new Thread( mcsdr ).start();
}
catch ( IOException e )
{
MyNioTcpLog.outMinException( e );
}
}
通过上面的简单几行,就已经:
这里先简单介绍一下NIO,NIO是非阻塞式IO通讯方式,IO缓冲区中只要有数据,就会被唤醒,要求对IO进行读取,当缓冲区中的数据读取完后,则会等待接收远端的数据,在等待期间,IO会被挂起。
当有数据需要处理时,在SceneCmdReactor类中的表现形式就是命令处理方法被调用。TCP的数据是一个流,在数据处理时,必需要将流分解成一个个的数据报,才能较方便地处理,在这里,流被分解成了一个个的数据包,经过封装好的命令解析器,数据包被解析成了一个个的命令,然后才会调用SceneCmdReactor类的命令处理类。注意:在高并发的服务端设计中,单个命令处理的时间应该尽量的短,并且可控,其执行时间应该是一个常量,在处理时可以有循环,但循环的次数应该是明确的,并且次数不能太多,如果不可控,应该通过异步的方式来解决。
下面是SceneCmdReactor的具体业务处理类。
public class SceneCmdReactor extends NioCmdReactor implements ITimeoutNotifyInterface
{
private SceneCampsite m_campsite;
private Equipment m_equipment = null;
public SceneCmdReactor( SocketChannel socketChannel, NioTcpReactorRunnable ntrr, NioTcpParameter ntp, SceneCampsite campsite )
{
super( new CmdHead(), socketChannel, ntrr, ntp );
this.m_campsite = campsite;
}
@Override
public NioTcpProcessor createNew( NioTcpReactorRunnable ntrr, SocketChannel socketChannel, NioTcpParameter ntp, NioTcpConnector ntc )
{
SceneCmdReactor ccr = new SceneCmdReactor( socketChannel, ntrr, ntp, this.m_campsite );
ccr.setReconnect( true, ntc );
return ccr;
}
@Override
public void onRelease()
{
super.onRelease();
if( this.m_campsite != null )
{
if( this.m_equipment != null )
{
this.m_campsite.closeEquipment(this.m_equipment);
this.m_equipment = null;
}
this.m_campsite = null;
}
}
@Override
public void onReclaimCmd( NioCmdHead cmd, int iReason )
{
if( cmd.isHeartbeat() && iReason == NioCmdProcessor.CMD_RECLAIM_REASON_SEND_COMPLETED )
{
LogLevel.outputToLogFile( "SceneCmdReactor.onReclaimCmd() --- Heartbeat packet send complete, active!" );
this.m_campsite.activeTimeoutTracking();
}
}
@Override
public boolean onRecvOneCmd( NioCmdHead cmd )
{
if( LogLevel.isEnableLogOut( LogLevel.C_LOG_OUT_LEVEL_TCP_CMD_OUT ) )
{
cmd.println();
LogLevel.outputToLogFile( "SceneCmdReactor.onRecvOneCmd() --- CmdId = " + cmd.m_iCmd );
}
switch ( cmd.m_iCmd )
{
case CmdDef.C_CMD_NOTIFY_BUY_TICKET:
{
//买票
return this.m_campsite.processNotifyBuyTicket( cmd.m_lUserContext, (CmdTicket)cmd.m_objData, this );
}
case CmdDef.C_CMD_NOTIFY_RETURN_TICKET:
{
//退票
return this.m_campsite.processNotifyReturnTicket( cmd.m_lUserContext, (CmdTicket)cmd.m_objData, this );
}
case CmdDef.C_CMD_NOTIFY_CHANGE_SCENE:
{
//现换场次
this.m_campsite.processNotifyChangeScene( cmd.m_lUserContext, cmd.m_lFlag, (CmdTicket)cmd.m_objData, this );
return true;
}
case CmdDef.C_CMD_REQUEST_MULTICAST_ADDRESS:
{
//请求多播地址
this.m_campsite.processRequestMulticastAddress( cmd.m_lUserContext, cmd.m_lFlag, this );
return true;
}
case CmdDef.C_CMD_NOTIFY_CHECK_IN:
{
//检票入场
this.m_campsite.processNotifyCheckIn( cmd.m_lUserContext, (CmdTicket)cmd.m_objData, this );
return true;
}
case CmdDef.C_CMD_APPEND_ONE_FILM:
{
//增加一部电影
this.m_campsite.processAppendOneFilm( cmd.m_lUserContext, (CmdReturnFilmInfo)cmd.m_objData, true );
return true;
}
case CmdDef.C_CMD_OPEN_ONE_MULTICAST_SOURCE_RETURN:
{
//开启一个组播源返回,告之了开启的组播源的IP和端口
this.m_campsite.processOpenOneMulticastSourceReturn( cmd.m_lUserContext, cmd.m_lFlag, (CmdMergeSceneAddress)cmd.m_objData );
return true;
}
case CmdDef.C_CMD_NOTIFY_STATUS:
return true;
}
return false;
}
@Override
public boolean onTimeoutNotify( TimeoutTracking tt, boolean bTimeout )
{
try
{
if (bTimeout)
{
//如果是超时
LogLevel.outputToLogFile("SceneCmdReactor.onTimeoutNotify() --- Connect timeout with public networ! Close Channel!");
this.closeChannel();
return false;
}
LogLevel.outputToLogFile("SceneCmdReactor.onTimeoutNotify() --- Ready send heartbeat packet is over! wait send....");
if (this.sendHeartbeat())
{
LogLevel.outputToLogFile("SceneCmdReactor.onTimeoutNotify() --- Send heartbeat packet is over! wait send....");
return true;
}
}
catch( Exception e )
{
LogLevel.outputToLogFile("SceneCmdReactor.onTimeoutNotify() --- Send heartbeat packet is failed! Close Channel!");
this.closeChannel();
}
return false;
}
}
SceneCmdReactor类并不复杂,它派生自NioCmdReactor类,这个类是NIO的TCP命令处理的反应器,在应用中只需要重载以下几个方法即可。
createNew()方法,这个方法的内容复制一下就可以了,是标准的写法,基本不需要改变,但必需被派生类重载。
onRelease()方法,这个方法是连接被断开后都会调用的方法,可以作一些善后处理,也可以不重载。
onReclaimCmd()方法,这个方法是在一个命令被发送到远端后,如果要监控该命令是否发送成功,可以重载该方法进行处理。这里要说明一下,因为NIO是非阻塞的方式,因此在发送数据时,是首先将要发送的内容保存在缓冲区中,然后依次发送出去,从程序的流程来看,发送命令一被执行,就会立即返回,而发送的结果则是在一段时间之后才能知道。
onRecvOneCmd()方法,这个方法是标准的命令处理方法响应体,每一个应用都需要重载该方法,在该方法中处理规定的命令。
下面我们看一下命令的定义类。
public class CmdHead extends NioCmdHead
{
public CmdHead()
{
}
public CmdHead( int iCmd, long lUserContext )
{
super( iCmd, lUserContext );
}
public CmdHead( int iCmd, long lUserContext, NioCmdObjectInterface obj )
{
super( iCmd, lUserContext, obj );
}
public CmdHead( int iCmd, long lUserContext, long lFlag )
{
super( iCmd, lUserContext, lFlag );
}
public CmdHead( int iCmd, long lUserContext, long lFlag, NioCmdObjectInterface obj )
{
super( iCmd, lUserContext, lFlag );
this.m_objData = obj;
}
@Override
protected boolean createObject()
{
switch ( this.m_iCmd )
{
case CmdDef.C_CMD_NOTIFY_BUY_TICKET:
this.m_objData = new CmdTicket();
return true;
case CmdDef.C_CMD_NOTIFY_RETURN_TICKET:
this.m_objData = new CmdTicket();
return true;
case CmdDef.C_CMD_NOTIFY_CHANGE_SCENE:
this.m_objData = new CmdTicket();
return true;
case CmdDef.C_CMD_NOTIFY_CHECK_IN:
this.m_objData = new CmdTicket();
return true;
case CmdDef.C_CMD_APPEND_ONE_FILM:
this.m_objData = new CmdReturnFilmInfo();
return true;
case CmdDef.C_CMD_OPEN_ONE_MULTICAST_SOURCE_RETURN:
this.m_objData = new CmdMergeSceneAddress();
case CmdDef.C_CMD_REQUEST_MULTICAST_ADDRESS:
case CmdDef.C_CMD_NOTIFY_STATUS:
return true;
}
return false;
}
}
命令处理类实际上只需要重载createObject()方法,然后根据不同的命令ID,创建不同的命令对象就可以了。我们再看一个命令对象的定义。
public class CmdTicket implements NioCmdObjectInterface
{
public long m_lTicketId;
public long m_lSceneId;
public long m_lMac = 0;
public CmdTicket()
{
}
public CmdTicket( long lTicketId, long lSceneId )
{
this.m_lTicketId = lTicketId;
this.m_lSceneId = lSceneId;
}
public CmdTicket( long lTicketId, long lSceneId, long lMac )
{
this.m_lTicketId = lTicketId;
this.m_lSceneId = lSceneId;
this.m_lMac = lMac;
}
@Override
public boolean parse(ByteBuffer byteBuffer)
{
try
{
this.m_lTicketId = byteBuffer.getLong();
this.m_lSceneId = byteBuffer.getLong();
this.m_lMac = byteBuffer.getLong();
return true;
}
catch( Exception e )
{
e.printStackTrace();
return false;
}
}
@Override
public boolean write(ByteBuffer byteBuffer)
{
byteBuffer.putLong( this.m_lTicketId );
byteBuffer.putLong( this.m_lSceneId );
byteBuffer.putLong( this.m_lMac );
return true;
}
@Override
public void release()
{
}
@Override
public void println( NioCmdHead cmd )
{
try
{
switch (cmd.m_iCmd) {
case CmdDef.C_CMD_NOTIFY_BUY_TICKET:
{
StringBuilder sb;
if ((sb = LogLevel.getLog(LogLevel.C_LOG_OUT_LEVEL_NIO_CMD_OUT)) != null)
{
sb.append("\r\n\tBuy Ticket Cmd --- cmdId =").append(cmd.m_iCmd)
.append(", FilmId = ").append(cmd.m_lUserContext)
.append(", TicketId = ").append(this.m_lTicketId)
.append(", SceneId = ").append(this.m_lSceneId)
.append(", Opening time = ").append(Convenient.s_dfFullDateTime.format(new Date(this.m_lMac)));
}
return;
}
case CmdDef.C_CMD_NOTIFY_RETURN_TICKET:
{
StringBuilder sb;
if ((sb = LogLevel.getLog(LogLevel.C_LOG_OUT_LEVEL_NIO_CMD_OUT)) != null)
{
sb.append("\r\n\tReturn Ticket Cmd --- cmdId =").append(cmd.m_iCmd)
.append(", FilmId = ").append(cmd.m_lUserContext)
.append(", TicketId = ").append(this.m_lTicketId)
.append(", SceneId = ").append(this.m_lSceneId);
}
return;
}
case CmdDef.C_CMD_NOTIFY_CHANGE_SCENE:
{
StringBuilder sb;
if ((sb = LogLevel.getLog(LogLevel.C_LOG_OUT_LEVEL_NIO_CMD_OUT)) != null)
{
sb.append("\r\n\tChange Scene Cmd --- cmdId =").append(cmd.m_iCmd)
.append(", FilmId = ").append(cmd.m_lUserContext)
.append(", NewSceneId = ").append(cmd.m_lFlag)
.append(", TicketId = ").append(this.m_lTicketId)
.append(", SceneId = ").append(this.m_lSceneId)
.append(", New scene opening time = ").append(Convenient.s_dfFullDateTime.format(new Date(this.m_lMac)));
}
return;
}
case CmdDef.C_CMD_NOTIFY_CHECK_IN:
{
StringBuilder sb;
if ((sb = LogLevel.getLog(LogLevel.C_LOG_OUT_LEVEL_NIO_CMD_OUT)) != null)
{
sb.append("\r\n\tCheck in Cmd --- cmdId =").append(cmd.m_iCmd)
.append(", FilmId = ").append(cmd.m_lUserContext)
.append(", TicketId = ").append(this.m_lTicketId)
.append(", SceneId = ").append(this.m_lSceneId)
.append(", Mac = ").append(Convert.long2HexUpperCaseMacString(this.m_lMac));
}
return;
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
不要被这么长的代码所吓到,println方法实际上是为了日志调试方便将内容输出成文本的实现。真正必需要派生的只有2个方法,一个是parse()方法,该方法用来将传送的数据解析成对象;一个是write()方法,该方法实现命令对象的序列化,将对象封装成数据包,传送到远方。这2个方法实现很简单,在write()方法中,只需要将要传送的内容依次写入缓冲区即可,在parse()方法中,只要按照write()方法中的顺序依次从缓冲区中读出数据赋值给相应的变量即可,注意写入和读出时的数据的长度必需是一样的。
到这里,对Java中的NIO的封装的简单介绍就结束了,在后续的文章中我会从所有用到的基础类开始讲起,SelectPool线程池、Mgr管理类、Reactor反应器、纯TCP处理、基于命令行的TCP处理、配置文件的标准化、动态日志的创建和管理、消费者模型的定义、文件的NIO读写、多播等的封装模型也会依次详细介绍。