Client
package com.company;
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 com.company.bo.LogData;
import com.company.bo.LogRec;
import com.company.util.IOUtil;
/**
* 客户端应用程序:
* 运行在unix系统上,作用是定期读取系统日志文件wtmpx文件,
* 收集每个用户的登入登出日志,将匹配成对的日志信息发送至服务器
*/
public class Client {
//UNIX系统日志文件wtmpx文件
private File logFile;
//保存解析后的日孩子文件
private File textLogFile;
//保存每次解析日志文件后的位置(书签)的文件
private File lastPositionFile;
//每次从wtmpx文件中解析日志的条数
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);
}
}
/**
* 该方法为第一大步第二小步的逻辑
* 用于检查wtmpx文件是否还有数据可读
* @return -1:没有数据可读了
* 其他数字:继续读取的位置
*/
public long hasLogs(){
try {
//默认从文件开始读取
long lastposition = 0;
/*
* 这里有两种情况
* 1:没有找到last-position.txt
* 文件,这说明从来没读过wtmpx
* 2:有last-position.txt文件,
* 那么,那么就从文件记录的位置开始读取
*/
if(lastPositionFile.exists()){
lastposition = IOUtil.readLong(lastPositionFile);
}
/*
* 必要判断,wtmpx文件的总大小
* 减去这次准备开始读取的位置,应当
* 大于一条日志所占用的字节量(372)
*/
if(logFile.length()-lastposition < LogData.LOG_LENGTH){
lastposition = -1;
}
return lastposition;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* 当前RandomAccessFile读取的位置
* 在logfile中是否还有内容可读
*/
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文件中
*
* return true:解析成功
* return false:解析失败
*/
public boolean readNextLogs(){
/*
* 解析步骤:
* 1:先判断wtmpx文件是否存在
* 2:判断是否有新数据可读
* 3:从上次读取的位置继续开始读取
* 4:循环batch次,读取batch个372字节,并转换为batch个日志
* 5:将解析后的batch个日志写入log.txt文件中
*/
//1:先判断wtmpx文件是否存在
if(!logFile.exists()){
return false;
}
//2:判断是否有新数据可读
long lastposition = hasLogs();
if(lastposition<0){
return false;
}
/*
* 为了避免重复执行第一步,而导致原来第一步中已经解析的日志文件被废弃,
* 我们可以先判断:若第一步执行完毕后生成的log.txt文件存在,就不再执行第一步了。
* 该文件会在第二步执行完毕后删除。
*/
if(textLogFile.exists()){
return true;
}
try{
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;
}
//读取用户名
String user = IOUtil.readString(raf,LogData.USER_LENGTH);
//读取PID
raf.seek(lastposition+LogData.PID_OFFSET);
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);
//将RandomAccessFile的游标位置定位到该条数据的末尾
raf.seek(lastposition+LogData.LOG_LENGTH);
//将lastposition设置为raf的游标位置,以便下次循环使用
lastposition = raf.getFilePointer();
//System.out.println("游标位置:"+lastposition);
/*
* 将解析出来的数据存入一个LogData对象中,
* 再将该对象存入集合中
*/
LogData log = new LogData(user,pid,type,time,host);
logs.add(log);
}
// System.out.println("解析日志数:"+logs.size());
// for(LogData log : logs){
// System.out.println(log);
// }
/*
* 将解析后的日志,写入log.txt文件中
*/
IOUtil.saveList(logs, textLogFile);
/*
* 将这次解析后RandomAccessFile的游标位置记录,
* 以便于下次解析的时候继续读取。
*/
IOUtil.saveLong(lastposition, lastPositionFile);
}catch(Exception e){
}
return false;
}
/**
* 第二大步的:
* 匹配日志
* 大体步骤:
* 1:读取log.txt文件,将第一步解析出的日期读取出来
* 并转为若干个LogData对象存入list集合中等待配对
* 2:读取login.txt文件,将上次没有配对成功的登入日志读取出来
* 并转换为若干个LogData对象,也存入List集合中,等待这次配对
* 3:循环list,将登入登出日志分别存入到2个map中,value就是对应的日志对象,
* key都是【user,pid,ip】这样格式的字符串
* 4:循环登出的map,并通过key寻找登入map中的登入日志,
* 以达到配对的目的,将配对的日志转换为一个LogRec对象存入一个list集合中
* 5:将所有配对成功的日志写入文件logrec.txt
* 6:将所有没配对成功的日志写入文件login.txt
* @return
*/
public boolean matchLogs(){
/*
* 必要的判断
*/
if(!textLogFile.exists()){
return false;
}
/*
* 当第二步执行完毕后,会生成两个文件:logrec.txt, login.txt
* 若第三步在执行时出现错误,我们若重新执行第二步,
* 会将上次第二步已经配对的日志覆盖,从而导致数据丢失。
* 为此我们要做一个必要的判断,就是
* logrec.txt文件若存在,则说明第二步
* 已经完成,但是第三部没有顺利执行。
* 因为第三步执行完毕后,会将该文件删除。
* 所以,若存在,则第二步不再执行。
*/
if(logRecFile.exists()){
return true;
}
/*
* 业务逻辑
*/
try{
/*
* 1读取log.txt文件,将第一步解析出的日期读取出来
* 并转为若干个LogData对象存入list集合中等待配对
*/
List<LogData> list = IOUtil.loadLogData(textLogFile);
/*2读取login.txt文件,将上次没有配对成功的登入日志读取出来,
并转换为若干个LogData对象,也存入List集合中,等待这次配对*/
if(loginFile.exists()){
list.addAll(IOUtil.loadLogData(logRecFile));
}
/*3循环list,将登入登出日志分别存入到2个map中,value就是对应的日志对象,
key都是【user,pid,ip】这样格式的字符串*/
Map<String,LogData> loginMap = new HashMap<String,LogData>();
Map<String,LogData> logoutMap = new HashMap<String,LogData>();
for(LogData log : list){
if(log.getType()==LogData.TYPE_LOGIN){
putLogToMap(log, loginMap);
}else if(log.getType()==LogData.TYPE_LOGOUT){
putLogToMap(log, logoutMap);
}
}
/*4:循环登出的map,并通过key寻找登入map中的登入日志,
以达到配对的目的,将配对的日志转换为一个LogRec对象存入一个list集合中*/
Set<Entry<String,LogData>> set =logoutMap.entrySet();
//用于存放所有配对成功的日志的集合
List<LogRec> logRecList = new ArrayList<LogRec>();
for(Entry<String,LogData> entry : set){
/*
* 从登出map中,取出key
*/
String key = entry.getKey();
/*
* 根据登出的key,从登入map中
* 以相同的key删除元素,删除的
* 就是对应的登入日志
*/
LogData login = loginMap.remove(key);
if(login!=null){
//匹配后,转为一个LogRec对象
LogRec logrec = new LogRec(login,entry.getValue());
//将配对日志存入集合
logRecList.add(logrec);
}
}
//出了for循环,相当于配对工作就完毕了
//5:将所有配对成功的日志写入文件logrec.txt
IOUtil.saveList(logRecList, logRecFile);
//6:将所有没配对成功的日志写入文件login.txt
Collection<LogData> c = loginMap.values();
IOUtil.saveList(new ArrayList<LogData>(c), loginFile);
/*
* 当第二步执行完毕后,
* log.txt文件就可以删除了
*/
textLogFile.delete();
return true;
}catch(Exception e){
e.printStackTrace();
/*
* 若第二步出现异常,那么第二步生成的
* 配对文件logrec.txt文件就是无效的。
* 应当删除,以便于重新执行第二步
*/
if(logRecFile.exists()){
logRecFile.delete();
}
return false;
}
}
/**
* 将给定的日志存入给定的map中
* @param log
* @param map
*/
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;
}
/*
* 业务逻辑
*/
Socket socket = null;
BufferedReader br = null;
try{
socket = new Socket("localhost",8088);
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
PrintWriter pw = new PrintWriter(osw);
//读取logrec.txt
FileInputStream fis = new FileInputStream(logRecFile);
InputStreamReader isr = new InputStreamReader(fis);
br = new BufferedReader(isr);
String line = null;
/*
* 循环从logrec.txt文件中读取每一行
* 配对日志,并发送至服务端
*/
while((line=br.readLine())!=null){
pw.println(line);
}
//最后发送一个over,表示发送完毕了
pw.println("over");
pw.flush();
//已经将logrec.txt文件中的内容发送了
//发送完,将读取文件的流关掉
br.close();
/*
* 通过socket创建输入流,用于读取服务端的响应
*/
InputStream in = socket.getInputStream();
BufferedReader brServer = new BufferedReader(new InputStreamReader(in,"UTF-8"));
//读取服务端发送回来的响应
String response = brServer.readLine();
if("OK".equals(response)){
/*
* 服务端正确接收发送的日之后
* 就可以将第二步生成的logrec.txt
* 文件删除了。
*/
logRecFile.delete();
return true;
}
return false;
}catch(Exception e){
e.printStackTrace();
return false;
}finally{
//将socket关闭
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
}
}
//读取文件的输入流也可能没关闭
if(br!=null){
try {
br.close();
} catch (IOException e) {
}
}
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
/*
* 开始方法中,我们要循环以下3个步骤
* 1:从wtmpx文件中一次解析batch跳日志
* 2:将解析后的日志,和上次没有匹配的日志一起配成对
* 3:将匹配成对的日志发送至服务端
*/
while(true){
//1:从wtmpx文件中一次解析batch跳日志
readNextLogs();
//2将解析后的日志,和上次没有匹配的日志一起配成对
matchLogs();
//3
sendLogToServer();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
Server
package com.company;
import java.io.BufferedReader;
import java.io.File;
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.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 服务端应用程序
*/
public class Server {
//运行在服务端的socket
private ServerSocket server;
//线程池,用于管理客户端连接的交互线程
private ExecutorService threadPool;
//保存所有客户端发送过来的配对日志的文件
private File serverLogFile;
//创建一个双缓冲队列,用于存储配对日志
private BlockingQueue<String> messageQueue;
/**
* 构造方法,用于初始化服务器
*/
public Server() throws IOException{
try{
/*
* 创建ServerSocket时需要指定服务器端口
*/
System.out.println("初始化服务端");
server = new ServerSocket(8088);
//初始化线程池
threadPool = Executors.newFixedThreadPool(50);
//初始化保存的日志
serverLogFile = new File("server-log.txt");
//初始化缓冲队列
messageQueue = new LinkedBlockingQueue<String>();
System.out.println("服务器初始化完毕");
}catch(IOException e){
e.printStackTrace();
throw e;
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try{
/*
* 将写日志的线程启动起来
*/
WriteLogThread thread = new WriteLogThread();
thread.start();
/*
* ServerSocket的accept方法
* 用于监听8088端口,等待客户端的连接
* 该方法是一个阻塞方法,直到一个
* 客户端连接,否则该方法一直阻塞。
* 若一个客户端连接了,会返回该客户端的
* Socket
*/
while(true){
System.out.println("等待客户端连接");
Socket socket = server.accept();
/*
* 当一个客户端连接后,启动一个线程
* ClientHandler,将该客户端的
* socket传入,使得该线程处理与该
* 客户端交互。
* 这样,我们能再次进入循环,接收
* 下一个客户端的连接了。
*/
Runnable handler = new ClientHandler(socket);
//Thread t = new Thread(handler);
//t.start();
/*
* 使用线程池分配空闲线程来处理
* 当前连接的客户端
*/
threadPool.execute(handler);
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server;
try{
server = new Server();
server.start();
}catch(IOException e){
e.printStackTrace();
System.out.println("服务器初始化失败");
}
}
/**
* 服务端中的一个线程,用于与某个客户端交互。
* 使用线程的目的是使得服务端可以处理多客户端了。
*/
class ClientHandler implements Runnable{
//当前线程处理的客户端socket
private Socket socket;
/**
* 根据给定的客户端的socket,创建
* 线程体
* @param socket
*/
public ClientHandler(Socket socket){
this.socket = socket;
}
/**
* 该线程会将当前socket中的输入流获取
* 用来循环读取客户端发送过来的消息
*/
@Override
public void run() {
/*
* 定义在try语句外的目的是,为了在
* finally中也可以引用到
*/
PrintWriter pw = null;
try{
/*
* 为了让服务端与客户端发送信息,
* 我们需要通过socket获取输出流。
*/
OutputStream out = socket.getOutputStream();
pw = new PrintWriter(
new OutputStreamWriter(out,"UTF-8"), true);
//获取输入流
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(in,"UTF-8"));
String message = null;
/*
* 循环读取客户端发送过来的每一组
* 配对日志
* 读取到一组,就将该日志存入
* 消息队列,等待被写入文件。
*/
while((message=br.readLine())!=null){
/*
* 若读取到客户端发送的内容是over
* 表示客户端发送完毕所有的日志了
* 应当停止再接收客户端发送的内容了
*/
if("over".equals(message)){
break;
}
messageQueue.offer(message);
}
/*
* 当退出循环,说明所有客户端发送的日志
* 均接收成功,并存入了消息队列中。
* 那么我们回复客户端OK
*/
pw.println("OK");
}catch(Exception e){
//在windows中的客户端,
//报错通常是因为客户端断开了连接
pw.println("ERROR");
}finally{
/*
* 无论是linux用户还是windows
* 用户,当与服务端断开连接后
* 我们都应该在服务端也与客户端
* 断开连接
*/
try {
socket.close();
} catch (IOException e) {
}
}
}
}
/**
* 该线程在server中仅有一个实例
* 作用是:
* 循环从消息队列中取出一个配对日志,
* 并写入sever-log.txt文件中
* 当队列没有日志后,就休眠一段时间
* 等待客户端发送新的日志过来
*/
class WriteLogThread extends Thread{
@Override
public void run() {
try{
PrintWriter pw = new PrintWriter(serverLogFile);
while(true){
if(messageQueue.size()>0){
String log = messageQueue.poll();
pw.println(log);
}else{
pw.flush();
Thread.sleep(500);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
LogData
package com.company.bo;
/**
* LogData的每一个实例用于表示wtmpx文件中的每一条日志信息
*/
public class LogData {
/**
* 日志wtmpx文件中的长度
* 每条日志的长度都是372个字节
*/
public static final int LOG_LENGTH=372;
/**
* user在单挑日志中的起始字节
*/
public static final int USER_OFFSET=0;
/**
* user在日志中占用的字节量
*/
public static final int USER_LENGTH=32;
/**
* pid的起始位置
*/
public static final int PID_OFFSET=68;
/**
* type在日志中的起始位置
*/
public static final int TYPE_OFFSET=72;
/**
* time在日志中的起始位置
*/
public static final int TIME_OFFSET=80;
/**
* host在日志中的起始位置
*/
public static final int HOST_OFFSET=114;
/**
* host在日志中的长度
*/
public static final int HOST_LENGTH=258;
/**
* 日志类型:登入为7
*/
public static final short TYPE_LOGIN=7;
/**
* 日志类型:登出为8
*/
public static final short TYPE_LOGOUT=8;
//登录用户名
private String user;
//进程id
private int pid;
//日志类型(登入/登出)
private short type;
//生成日志的时间(登入登出的时间),以秒为单位
private int time;
//登录用户的ip地址
private String host;
public LogData(){}
public LogData(String user, int pid, short type, int time, String host) {
super();
this.user = user;
this.pid = pid;
this.type = type;
this.time = time;
this.host = host;
}
/**
* 给定一个字符串
* (格式应该是当前类toString方法生成)
* 将该字符串转换为一个LogData对象
*/
public LogData(String line){
//1:按照“,”拆分字符串
String[] array = line.split(",");
//2:将数组中的每一项设置到属性上即可
this.user = array[0];
this.pid = Integer.parseInt(array[1]);
this.type = Short.parseShort(array[2]);
this.time = Integer.parseInt(array[3]);
this.host = array[4];
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public short getType() {
return type;
}
public void setType(short type) {
this.type = type;
}
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
@Override
public String toString() {
return user + "," + pid + "," + type + "," + time + "," + host;
}
}
LogRec
package com.company.bo;
/**
* 该类用于描述一组匹配成对的日志
*/
public class LogRec {
private LogData login;
private LogData logout;
public LogRec(LogData login, LogData logout) {
super();
this.login = login;
this.logout = logout;
}
public LogData getLogin() {
return login;
}
public void setLogin(LogData login) {
this.login = login;
}
public LogData getLogout() {
return logout;
}
public void setLogout(LogData logout) {
this.logout = logout;
}
/**
* toString()
* 格式:
* login.toString()|logout.toString()
*/
@Override
public String toString() {
return login + "|" +logout.toString();
}
}
IOUtil
package com.company.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import com.company.bo.LogData;
/**
* 该类是一个工具类,负责读写数据,
* 把读写逻辑单独定义在该类的目的是为了重用这些逻辑。
*/
public class IOUtil {
/**
* 从给定的文件中读取第一行字符串,
* 并将其转为一个long值返回
*/
public static long readLong(File file){
BufferedReader br = null;
try{
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
br = new BufferedReader(isr);
String line = br.readLine();
long l = Long.parseLong(line);
return l;
}catch(Exception e){
e.printStackTrace();
throw new RuntimeException(e);
}finally{
try {
if(br != null){
br.close();
}
} catch (IOException e) {
}
}
}
/**
* 从给定的RandomAccessFile的当前位置处
* 连续读取len个字节,并转为字符串
*/
public static String readString(RandomAccessFile raf, int len) throws IOException{
byte[] buf = new byte[LogData.USER_LENGTH];
raf.read(buf);
String str = new String(buf,"ISO8859-1");
return str.trim();
}
/**
* 从给定的RandomAccessFile当前位置处,
* 读取一个int值并返回
*/
public static int readInt(RandomAccessFile raf) throws IOException{
return raf.readInt();
}
/**
* 从给定的RandomAccessFile当前位置处,
* 读取一个short值并返回
*/
public static short readShort(RandomAccessFile raf) throws IOException{
return raf.readShort();
}
/**
* 将给定的集合中的每个元素的toString方法返回的字符串
* 作为一行内容写入给定的文件中
*/
public static void saveList(List list,File file) throws IOException{
PrintWriter pw = null;
try {
pw = new PrintWriter(file);
for(Object o : list){
pw.println(o);
}
}finally{//异常抛出,故不catch,但流要关闭
if(pw != null){
pw.close();
}
}
}
/**
* 将给定的long值作为一行字符串写入给定的文件中
*/
public static void saveLong(long l,File file) throws IOException{
PrintWriter pw =null;
try{
pw = new PrintWriter(file);
pw.println(l);
}finally{//异常抛出,故不catch,但流要关闭
if(pw!=null){
pw.close();
}
}
}
/**
* 从指定的文件中按行读取每一条日志,并
* 转换为一个LogData对象,最终将所有日志
* 对象存入一个List集合中并返回
* @param file
* @return
*/
public static List<LogData> loadLogData(File file) throws IOException{
BufferedReader br = null;
try{
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
br = new BufferedReader(isr);
List<LogData> list = new ArrayList<LogData>();
String line = null;
while((line=br.readLine()) != null){
/*
* 解析过程应当交给LogData
* 原因在于该字符串的格式是由LogData自身的toString决定的
* 所以解析自然也应该交给它
*/
LogData log = new LogData(line);
list.add(log);
}
return list;
}finally{
if(br != null){
br.close();
}
}
}
}