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();
}
}
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
- day29:Client
- Day29
- day29
- Summary Day29
- Day29:Today
- day29 Linux02
- day29,page50,total300
- 【Day29】关于Redis
- day29 Linux基本命令
- javascript实现掉落弹出层------Day29
- iOS开发-Day29-UI UIScrollView&多视图
- 达内学习日志Day29:基础查询
- DAY29:leetcode #32 Longest Valid Parentheses
- client
- client
- Client
- Client
- Client
- Swiperefreshlayout与Recyclerview下拉刷新和上拉加载
- dijkstra模板
- Mybatis学习总结(五).动态SQL与Mybatis缓存
- 博为峰Java技术文章 ——JavaSE Swing JRootPane面板I
- Java并发编程之volatile关键字解析
- day29:Client
- 4种输出模式
- 解压文件夹下所有压缩包文件,并将压缩包下多层文件夹下文件拷贝至压缩包名文件夹下
- 读写cookie
- 树-堆结构练习——合并果子之哈夫曼树
- WC2017总结
- 如何使用 docker 配置 PHP LEMP 开发环境
- Pascal GPU 架构详解
- c++==继承(6)