day29:Client

来源:互联网 发布:都市星际淘宝交易商 编辑:程序博客网 时间:2024/06/05 00:49
package com.taren;


import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;


import bo.LogData;
import bo.LogRec;


import com.taren.util.IOUtil;


/**
 * 客户端应用程序
 * 运行在UNIX系统上
 * 作用是定期读取系统日志文件WTMPX文件
 * 收集每个用户的登陆登出日志,将匹配成对的日志信息
 * 发送至服务器端
 * 另:WTMPX文件中每记录一条日志信息需要用掉372个字节
 * @author Administrator
 * 
 * 总流程:
 * 第一步:
 * 1、一次从WTMPX文件中读取10*372个字节(即10条日志),记录
 * 读取的最终位置,以便程序下次继续读取
 * 2、转换这些字节为user、pid、type、time、host信息
 * 3、将这些信息写入创建的log.txt文件中,要求一行一条日志信息
 * 写完后记录写入的最终位置,以便下次继续写入
 * 第二步:
 * 1、对log.txt文件中的日志进行配对,其他3项要相同,而
 * type一个是7(表示登陆)一个是8(表示登出),time一个大一个小
 * 2、配对成功的日志信息统一写入一个文件,配对失败的日志统一
 * 写入另一个文件
 * 第三步:将保存日志配对成功的文件发送给服务器
 *
 * 需考虑执行第一步的中断问题(通常是断电引起的):
 * 正常的流程是:第一步----第二步----第三步
 * (1)假设在第一步进行中断电了,即第一步--断电
 * (2)再次运行程序时,程序会找到上次的读取位置
 * 然后:第一步----第二步----第三步
 * 仔细想想:
 * 1、从(2)中第一步的"读取游标"的开始位置a到结束位置b,
 * 这些日志信息被读取后配对(有成功有失败),然后发送
 * 2、在a位置之前的(1)中读取到的日志信息没有进行配对!
 * 永远也不会去进行第二第三步了!
 * 
 * 解决方法:
 * 程序在开始第一步前进行判断--log.txt文件是否存在?
 * 
 * 存在----说明上次程序只执行到第一步,因为某些原因
 * 之后的步骤还没执行
 * 1、直接进入第二步(读取现有的log.txt文件)
 * 2、第二步完成后删去log.txt文件
 * 3、进行第三步
 * 
 * 不存在----说明上次程序正常执行或本次是第一次读取
 * WTMPX文件
 * 1、第一步
 * 2、第二步
 * 3、删去log.txt文件
 * 4、第三步
 * 
 * 总流程:
 * 判断log.txt文件是否存在--
 * 存在--第一步--第二步--删去log.txt文件--第三步
 * 不存在--第二步--删去log.txt文件--第三步
 *
 *
 */
public class Client {
   //UNIX系统日志文件  WTMPX文件
   private File logFile;
   
   //保存每次解析后的文件
   private File textLogFile;
   
   //保存每次解析日志文件后的位置(书签)的文件
   private File lastPositionFile;
   
   //每次从WIMPX文件中读取日志的条数
   private int batch;
   
   //每次配对工作完毕后,用它保存配对日志
   //第二大步追加的属性(保存的是字符串)
   private File logRecFile;
   
   //每次配对工作完毕后,用它保存配对失败的日志
   //第二大步追加的属性(保存的是字符串)
   private File loginFile;
   
   /**
    * 构造方法中初始化
    */
   public Client(){
  try{
  this.batch=10;
  logFile=new File("wtmpx");
  lastPositionFile=new File("last-position.txt");
  textLogFile=new File("log.txt");
  
  //第二大步追加的初始化
  logRecFile=new File("logRec.txt");
  //第二大步追加的初始化
  loginFile=new File("login.txt");
  
  }catch(Exception e){
  e.printStackTrace();
  throw new RuntimeException(e);
  }
   }
   
   /**
    * 该方法为第一大步的第二小步的逻辑
    * 用于判断wtnpx文件中是否还有数据可读
    * 
    * @return -1;没有数据可读了
    *        其他数字:继续读取的位置
    */
   public long hasLogs(){
  try{
  //默认从文件开头读取
  long lastPosition=0;
  
  /*
   * 这里有两种情况:
   * 1、没有找到last-Position.txt文件,这说明
   * 从来没读过wtmpx文件
   * 2、有last-Position.txt文件,那么就从文件
   * 记录的位置开始读取
   */
  //2找到了last-Position.txt文件
  if(lastPositionFile.exists()){
  lastPosition=IOUtil.readLong(lastPositionFile);
  }
  
  /*
   * 必要判断:(wtmpx文件的总大小)-(这次准备开始
   * 读取的位置)>(一条所占字节量)(372字节)
   * 
   * 例:已经读取了20条日志,这次准备从7440位置开
   * 始读取,而wtmpx文件总大小是7500字节,最后这一
   * 点点是个不完整的日志信息!(表示服务器正在填写
   * 该用户的日志信息,正在写的过程中wtmpx文件又正好
   * 被我们调过来读取了)
   */
  if(logFile.length()-lastPosition<LogData.LOG_LENGTH){
  lastPosition=-1;
  }
  
  
  
  return lastPosition;
  
  
  
  }catch(Exception e){
  e.printStackTrace();
  return -1;
  }
   }
   
   /**
    * 判断当前RandomAccessFile读取的位置在wtmpx中
    * 是否还有日志可读
    * 
    * 不能用lastPosition来判断,它是读完整个batch条
    * 日志后的位置,而现在我们要判断的是每读完一条日志
    * 后是否还能读出下一条日志
    * @param raf
    * @return
 * @throws IOException 
    */
   public boolean hasLogsByStep
   (RandomAccessFile raf) throws IOException{
  if(logFile.length()-raf.getFilePointer()
  >=LogData.LOG_LENGTH){
  return true;
  }else{
  return false;
  }
   }
   
   /**
    * 第一大步:
    * 从wtmpx文件中一次读取batch条日志,
    * 并解析为batch行字符串,每行字符串
    * 表示一条日志,然后写入log.txt文件中
    * reture true表示解析成功
    * reture false表示解析失败
    */
   public boolean readNextLogs(){
  /*
   * 解析步骤:
   * 1、首先判断wtmpx文件是否存在
   * 2、判断是否还有数据可读
   * 3、从上一次结束的位置开始继续读取
   * 4、循环batch次,读取batch*372个字节并转换位
   * batch个日志
   * 5、将解析后的batch个日志写入log.txt文件中
   */
  //1
  if(!logFile.exists()){
  return false;
  }
  
  //2
  long lastPosition=hasLogs();
  if(lastPosition<0){//lastPosition=-1,没有日志可解析了
  return false;
  }
  
  //中断保护功能
  /* 解析wtmpx文件之前先进行判断!!!
   * 即断电后再次运行程序使得重复执行了总流程中的第一步,
   * 导致原来第一步中已经被解析的日志信息被废弃
   * 
   * 判断第一步生成的log.txt文件是否存在,存在就不执行
   * 第一步了。
   * 该文件会在第二步执行完后删除
   */
  if(textLogFile.exists()){
  /*
   * File类对象textLogFile用于保存解析后生成
   * 的log.txt文件
   * 如果对象textLogFile存在就说明解析过了
   */
  return true;
  /* 整个readNextLogs方法就是用于解析wtmpx文件
   * 的,它完成了总流程的第一步。
   * 返回ture表示解析成功,即总流程第一步完成
   * readNextLogs方法下面的代码都不会执行,
   * 程序进入总流程的第二步,完成后删去log.txt文件
   */
  }
  
  
  
  try{
  //创建RandomAccessFile来读取日志文件
  RandomAccessFile raf=
  new RandomAccessFile(logFile,"r");
  
  //移动游标到指定位置继续读取
  raf.seek(lastPosition);
  
  //定义一个集合,用于保存解析后的日志
  List<LogData> logs=new ArrayList<LogData>();
  
  //循环batch次,解析batch条日志
  for(int i=0;i<batch;i++){
  /*
   * 首先判断是否还有日志可读
   */
  if(!hasLogsByStep(raf)){
  break;
  }
  
 //用IOUTil类中的readString方法读取用户名
  String user=IOUtil.readString(raf, LogData.USER_LENGTH);
  
  /*
   * 需知:RandomAccessFile中有seek(int n)方法,
   * 该方法根据给定的n值可以从RandomAccessFile对象所读
   * 的文件的任何位置开始读取字节
   * 
   * 在用readString方法进行下一步的读取pid之前,
   * 我们要先确认“读取指针”到了相应的位置。
   * 
   * 注意:不能直接用LogData.PID_OFFSET来指示
   * readString方法的,因为该常量是每一条日志中
   * pid的起始位置,我们要读去的是众多条日志中的
   * 某一条日志的pid信息
   */
  //读取pid信息之前先将raf的"读取指针"移动到要读取的pid上
  raf.seek(lastPosition+LogData.PID_OFFSET);
  
  //在pid的起始位置读取4个字节(正好是一个pid(int))
  int pid=IOUtil.readInt(raf);
  
      //读取type
  raf.seek(lastPosition+LogData.TYPE_OFFSET);
      short type=IOUtil.readShort(raf);
      
      //读取time
  raf.seek(lastPosition+LogData.TIME_OFFSET);
      int time=IOUtil.readInt(raf);
  
      //读取host
      raf.seek(lastPosition+LogData.HOST_OFFSET);
      String host=IOUtil.readString(raf, LogData.HOST_LENGTH);
      
      //保存游标位置,方便程序下次继续读取
      /*
       * 将当前raf游标的位置传给lastPosition
       * RandomAccessFile的getFilePointer方法能
       * 返回当前游标的位置
       */
      lastPosition=raf.getFilePointer();
      
      /*
       * 将解析出来的数据存入一个LogData对象中,
       * 在将该对象存入集合中
       */
      //日志数据以对象的形式保存
      LogData log=new LogData(user,pid,type,time,host);
      //将该对象存入我们事先创建好的集合中
      logs.add(log);
  }
  System.out.println("共解析了"+logs.size()+"个日志");
  
  /*
   * 从读取到解析到保存都有可能出现问题,所以先
   * 在此输出集合中的日志看看有没有什么错误
   */
  for(LogData log:logs){
  /*
   * 在LogData类中我们已经定义了toString方法
   * 所以LogData类对象可直接输出(按toString中
   * return的形式)
   */
  System.out.println(log);
  }
  
  /*
   * 将解析后的日志写入log.txt文件中
   */
  IOUtil.saveList(logs,textLogFile);
  
  /*
   * 每次解析完后,记录RandomAccessFile
   * 游标的位置,以便于下次解析的时候继续
   * 读取
   * 
   * 1、用raf的getFilePointer方法得到当前
   * 游标的位置(long值)
   * 2、用saveLong方法将该long值写入
   * lastPositionFile文件中
   */
  IOUtil.saveLong(
  raf.getFilePointer(),lastPositionFile);
  
  /*
   * 本次readNextLogs()方法解析文件成功
   * 返回ture
   */
  return true;
  
  }catch(Exception e){
  e.printStackTrace();
  }
  return false;
   }
   
   
   /**
    * 第二大步的方法
    * 匹配日志的大体步骤:
    * 
    * 1、读取log.txt文件,将第一步解析出来的
    * 日期转换为若干个LogData对象,并存入集合
    * List中等待配对
    * 2、读取log.txt文件,将上一次没有配对成功
    * 的登陆日志转换位若干个LogData对象,也存入
    * 集合List中,等待这次的配对
    * 3、循环List,将登陆与登出日志分别存到两个
    * Map中,value就是对应的日志对象,key都是
    * [user,pid,host]这样格式的字符串
    * 4、循环登出的map,并通过key查找登陆map的登陆
    * 日志,以达到配对的目的,将配对的日志转换为
    * 一个LogRec对象存入一个List集合中
    * 5、将所有配对成功的日志写入logrec.txt文件中
    * 6、将所有配对失败的日志写入login.txt文件中
    * @return
    */
   public boolean matchLogs(){
  /*
   * 必要的判断
   */
  //1检查log.txt文件(由textLogFile指向)是否存在
  if(!textLogFile.exists()){
  return false;
  //没有log.txt还配个卵,第二大步直接失败
  }
  
  //中断保护功能
  /* 执行第二大步前必须先进行判断!!!
   * 当第二大步执行完毕后,会生成两个文件:
   * logrec.txt(保存配对日志的信息)
   * login.txt(保存配对失败的日志的信息)
   * 若第三大步在执行时出现错误,之后程序再次
   * 执行时----进行第二大步----会把上次生成
   * 的已经配好对的日志覆盖掉,造成数据丢失。
   * 
   * 所以在进行第二大步前要先进行判断:
   * logrec.txt文件是否存在?
   * 是----则说明上次的第三大步没有顺利执行(因为
   * 第三大步执行完毕后,会删除logrec.txt文件)
   * 
   * 此时程序直接进入第三大步
   */
  if(logRecFile.exists()){
  /*
   * logrec.txt文件存在
   * 表示第二大步已经完成,matchLogs方法下面的
   * 代码就都不执行了
   */return true;
  
  }
  
  
  
  
  /*
   * 业务逻辑
   */
  try {
//1
/*
 * 读取log.txt文件,将第一步解析出来的
          * 日期转换为若干个LogData对象,并存入集合
          * List中等待配对
 */
List<LogData> list=IOUtil.loadLogData(textLogFile);

//2
/* 读取log.txt文件,将上一次没有配对成功
         * 的登陆日志转换位若干个LogData对象,也存入
         * 集合List中,等待这次的配对
         * 
         * 执行改步要先进行判断,只有loginFile文件存在时(即上次
         * 有配对失败的日志)才进行该步骤
*/
if(loginFile.exists()){
//将loginFile中的字符串日志转为一个个LogData对象存入login集合
List<LogData> login=IOUtil.loadLogData(loginFile);
//将集合login中的所有信息加进集合list中
list.addAll(login);

//PS:上面两句可写成一句
}

//3
/*
* 循环List,将登陆与登出日志分别存到两个
* Map中,value就是对应的日志对象,key都是
* [user,pid,host]这样格式的字符串
*/
//创建Map用于存放登陆的日志
         Map<String,LogData> loginMap=
        new HashMap<String,LogData>();
         
         //创建Map用于存放登出的日志
         Map<String,LogData> logoutMap=
        new HashMap<String,LogData>();
 
         /*
          * 循环遍历list中的日志,登陆的放入loginMap,
          * 登出的放入logoutMap
          */
for(LogData log:list){
 
if(log.getType()==LogData.TYPE_LOGIN){
//查看LogData类知:如果type值为7即表示登陆
putLogToMap(log,loginMap);
 
}else if(log.getType()==LogData.TYPE_LOGOUT){
//查看LogData类知:如果type值为8即表示登陆
putLogToMap(log,logoutMap);
}
}

//4
/*
 * 循环登出的map,并通过key查找登陆map的登陆
          * 日志,以达到配对的目的,将配对的日志转换为
          * 一个LogRec对象存入一个List集合中
 */

/*
* Map中有个entrySet()方法:
* 1、该方法将Map中的一组key--value转为一个entry(键值对)
* 2、该方法返回一个Set集合,该集合保存了所有entry(键值对)
*/
Set<Entry<String,LogData>> set=logoutMap.entrySet();
 
 
//所有配对成功的日志都会放入logRecList集合中
List<LogRec> logRecList=new ArrayList<LogRec>();
 
//遍历set集合中的所有键值对
for(Entry<String,LogData> entry:set){
 
//从登出Map中取出key,即日志中的[user,pid,host]
String key=entry.getKey();
/*
 * Entry中的getKey()方法能返回Entry对象的key值
 */
 
/*
 * Map中的remove(key k)方法
 * 1、该方法根据给定的key找到Map中的对象,然后删去
 * 2、删去成功会返回该对象的value值(即整个日志)
 * 3、删去失败,即根据key找不到相应元素,返回null
 */
LogData login=loginMap.remove(key);
/*
 * 本句有两个作用:
 * 1、remove方法返回了与登出日志想对应的登陆日志
 * 2、remove方法删去了配成对的登陆日志,等for循环
 * 遍历的所有的登出日志后,集合loginMap中剩下的就
 * 是配对失败的登陆日志了(有登出就一定有登陆,反之
 * 则不一定)
 */
 
//以防万一,先判断配对是否成功
if(login!=null){
/*
 * 匹配成功后,将登陆日志和登出日志转为一个LogRec对象
 * 注:在LogRec类中已经定义了构造方法,我们只需要传入
 * 配对的日志就行
 */
LogRec logrec=new LogRec(login,entry.getValue());
 
//将配对日志,即LogRec对象存入集合logRecList中
logRecList.add(logrec);
    }
}
//出了for循环,相当于配对工作完毕了 
 
 
  //5
  /*
   * 将所有配对成功的日志写入logrec.txt文件中
   * 
   * 注意:
   * 在IOUtil类中我们已定义了一个叫saveList的方法:
   * 1、该方法将给定集合中每个元素的toString方法返回的
   * 字符串,作为 一行内容写入给定的文件中
   * 2、logRecList集合中存放的是logRec类对象,该类
   * 已经重写了toString方法
   * 3、本类中已定义File类对象logRecFile,该对象作为
   * 一个文件,用来保存配对工作完毕后,配对成功的配对日志
   * 的信息
   */
IOUtil.saveList(logRecList, logRecFile); 
 
  //6  
  /*
   * 将所有配对失败的日志写入login.txt文件中
   * 
   * 问题1:loginMap中剩下的都是配对失败的登陆
   * 日志,如何才能取出这些日志呢?
   * 
   * 知识点1:遍历Map的三种方式
   * 1、遍历key,用XXX.keySet()方法
   * 2、遍历key-value键值对,用XXX.entrySet()方法
   * 3、遍历value,用XXX.values()方法
   * 4、三种遍历方式里唯独values方法返回的是
   * 个Collection集合(因为value可以重复)
   * 
   * 问题2:Collection集合不能用saveList方法来写进
   * login.txt文件里了。因为该方法要求传的是个List集合
   * 
   * 解决办法:将Collection转为List
   * 知识点3:复制构造器
   * 1、所有的集合都有一个构造方法,允许你传入
   * 一个Collection
   * 2、该方法会将这个Collection种的所有元素放到你
   * 构造出的集合里
   * 
   * PS:你当然可以也可以改saveList方法
   * 将该方法接收的参数(List,File)改为(Collection,File)即可
   * 然后为了见名知意应将方法名saveList改为saveCollection
   * 
   */
Collection<LogData> c=loginMap.values();
IOUtil.saveList(new ArrayList(c),loginFile);

/*
* 第二大步执行完毕后,
* log.txt文件就可以删除了
*/
textLogFile.delete();

return true;
   } catch (IOException e) {
e.printStackTrace();

/* 如果第二大步出现异常,那么半路生成
* 的配对文件logrec.txt就是无效的,应删除
* 以便于重新进行第二大步
*/
if(logRecFile.exists()){
logRecFile.delete();
}

return false;
}
  
   }
   
   
   /**
    * 将给定的LogData日志存入给定的Map中,其中:
    * 1、value就是对应的日志对象
    * 2、key都是[user,pid,host]这样格式的字符串
    * @param log
    * @param map
    * 
    * 设置该方法的意义:
    * 1、业务逻辑//3的目的:划分登陆日志和登出日志,
    * 存入不同的map中
    * 2、登陆日志与登出日志都是按同样的格式存入Map中的
    * 3、考虑到相同的操作能用方法重用就用方法来重用,
    * 所以设置专门的方法来完成它
    */
   private void putLogToMap(
  LogData log,Map<String,LogData> map){
        map.put(
log.getUser()+","+
        log.getPid()+","+
log.getHost(),
log);
   }
   
   /**
    * 第三大步:
    * 将配对的日志发送至服务器
    * 
    * 步骤:
    * 1、创建socket用于连接服务端
    * 2、通过socket获取输出流,并逐步
    * 包装为缓冲输出流。字符集是UTF-8
    * 3、创建缓冲字符输入流,由于读取logrec.txt(配对日志)
    * 4、从logrec.txt读取每一行日志信息并发送至服务端
    * 5、通过Socket获取输入流,并逐步包装为字符输入流
    * 用于读取服务端的响应
    * 6、读取服务端发来的响应
    * 若是"OK"----则说明服务端成功接了我们发过去的配对日志,
    * 那么就将logrec.txt文件删除
    * 若不是"OK"----则表示发送失败,那么该方法应返回false,
    * 并尝试重新进行第三大步
    * @return
    */
   public boolean sendLogToServer(){
  /*
   * 必要判断
   */
  if(!logRecFile.exists()){
  return false;//logrec.txt文件不存在你还发个卵
  }
  
  /*
   * 业务逻辑
   */
 
  //1
  Socket socket=null;
  
  /*
   * 等下要在finally中关br流,所以将其创建在try之外
   */
  BufferedReader br=null;
  
  try{
  //3
  
  socket=new Socket("localhost",8088);
  
  //获取输出流
  OutputStream out=socket.getOutputStream();
 
  //指定字符集
  OutputStreamWriter osw
  =new OutputStreamWriter(out,"UTF-8");
  
  //成为字符输出流
  PrintWriter pw=new PrintWriter(osw);
  
  //2
  
  //读取log.txt文件
  FileInputStream fis
  =new FileInputStream(logRecFile);
  
  //包装用于读取文件的字节流
  InputStreamReader isr=new InputStreamReader(fis);
  /*
   * 用Writer或Reader这些字符流时我们都习惯规定字符集
   * 用以统一编码集(这是个好习惯)
   * 但是本程序中logrec.txt是用系统默认字符集写的,所以
   * 此处也应用系统默认的字符集来读取
   */
  
  //创建缓冲输入流用于按行读取logrec.txt文件
  br=new BufferedReader(isr);
  
  //4
  /*
   * 循环从logrec.txt文件中读取每一行的配对日志,
   * 并发送至服务端
   */
  String line=null;
  while((line=br.readLine())!=null){
  pw.println(line);//每读一行就写一行至服务端
  }
  
  //发送完了要跟服务端说一声,不然服务端会就那么一直等着
  pw.println("over");
  
  pw.flush();
  //至此就将logrec.txt文件中的信息都发送到服务端了
  
  //发送完后关闭用于读取logrec.txt文件的缓冲输入流br
  br.close();
  /*
   * 注意:和第一大步以及第二大步不一样,这次我们并没
   * 有在finally中关流
   * 原因:接收的服务端"OK"的响应后要删logrec.txt文件,
   * 在finally中删意味着删文件时br流还开着,一个文件的
   * 读取流还开着那么这个文件就没法删!!!
   */
  
  //5
  /*
   * 通过Socket获取输入流,并逐步包装为字符输入流
            * 用于读取服务端的响应
   */
  InputStream in=socket.getInputStream();
  
  //两句合成一句写
  BufferedReader brServer=new BufferedReader(
  new InputStreamReader(in,"UTF-8")
  );
  
  //6读取服务端发回的响应
  String response=brServer.readLine();
  
  //判断响应是不是"OK"
  /*
   * 注意一个技巧:用"OK"去点equals,这样可以
   * 防止空指针异常
   */
  if("OK".equals(response)){
 /*
  * 服务端正确接收发送的日志后
  * 就可以将第二大步生成的logrec.txt文件删了 
  */
  logRecFile.delete();
  return true;
  }
  //响应不是"OK",发送日志失败
  return false;
  }catch(Exception e){
  e.printStackTrace();
  return false;//程序进不了if{}--响应不是"OK"--发送失败
  }finally{
  //关闭Socket
  if(socket!=null){
  try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
  }
  //读取文件的输入流也可能没关
  if(br!=null){//关之前先确认非空,不然会报空指针异常
  try {//关br流系统自动提示try catch捕获异常
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
  }
  
   }
   
   
   
   
   
   
   /**
    * 客户端开始工作的方法
    */
   public void start(){
  /*
   * 在开始方法中,我们要循环以下三个步骤:
   * 1、从WTMPX文件中一次解析batch条日志
   * 2、将解析后的日志和上次没有匹配成功的日志
   * 放到一起,对其进行匹配成对
   * 3、将匹配成对的日志发送至服务器
   */
  while(true){//考虑实际情况,这三步是需要永远执行下去的
  //1
  readNextLogs();
  
  //2
  matchLogs();
  
  //3
  sendLogToServer();
       }
  }
   
   public static void main(String[]args){
  Client client=new Client();
  client.start();
   }
   
   
}
0 0
原创粉丝点击