序列化
主机在互相传送数据时,是无法将一个对象直接给到另一台主机上的,需要在以某种形式把对象的内容封装进包中然后通过网络发送过去。而最主要的一种手法就是把对象编程一个字符串的形式,而这个字符串的书写规则是双方暗中约定好的。这样,两台主机就可以获取对方发送的对象信息了。使用序列化也可以让通信脱离使用相同语言的限制。
案例
案例准备
该案例在单机模式下运行。
案例描述:统计每个电话号的上行流量(upFlow)、下行流量(downFlow)和总流量(sumFlow)。从输入文件中读所有数据,留下有用的数据并加和。最后输出到指定路径。
输入文件如下,其中有一个字段是可选的,每个字段以“\t”(制表符)为分割符分开。各字段意义分别是【电话号、IP、域名、上行流量、下行流量、状态码】
13012345678 192.168.200.1 www.xunxun.games 1345 14567 200
13012345679 192.168.200.1 2345 94567 200
13012345672 192.168.200.1 www.xunxun.games 2345 34567 200
13012345678 192.168.200.1 www.xunxun.games 4345 64567 200
13012345674 192.168.200.1 www.xunxun.games 7345 84567 200
13012345678 192.168.200.1 www.xunxun.games 4345 24567 200
13012345674 192.168.200.1 www.xunxun.games 5345 24567 200
输出文件如下,各字段意义分别是【电话号、上行流量、下行流量、总流量】
13012345672 2345 34567 36912
13012345674 12690 109134 121824
13012345678 10035 103701 113736
13012345679 2345 94567 96912
代码编写
FlowBean
因为要在主机之间传递上下行流量,所以需要准备一个对象,装入需要的字段内容。
那么在java文件夹下创建一个包com.xunn.mapreduce.writable。在包中创建一个类,叫做FlowBean。具体内容如下,注意导包时不要导错了。
编写的时候有以下注意事项
- 实现writeable接口,从而使得该类可以序列化
- 因为要利用这个类传数据,所以要重写序列化、反序列化方法(序列化:write,反序列化:readFields,序列化和反序列化的顺序要一致)
- 重写一下空参构造函数
- 重写toString方法用于打印输出,这里要按双方约定好的格式去写
- 生成好私有变量的set和get方法,然后再加一个setSumFlow方法,让其可以直接相加upFlow和downFlow
package com.xunn.mapreduce.writable;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class FlowBean implements Writable {
// 上下行和总流量
private long upFlow;
private long downFlow;
private long sumFlow;
// 空参构造
public FlowBean() {
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
// 序列化的顺序必须和反序列化的顺序一致
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow ;
}
}
Mapper
相比之前文章的案例,这次主要是多了一个封装的过程,还有再拆分的时候需要想办法处理或绕过可选的数据。其他的在此不再赘述。
package com.xunn.mapreduce.writable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
private Text outK = new Text();
private FlowBean outV = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
//获取一行 e:13012345678 192.168.200.1 www.xunxun.games 2345 34567 200
String line = value.toString();
//切割行 e:[13012345678,192.168.200.1,www.xunxun.games,2345,34567,200]
String[] split = line.split("\t");
//抓取想要的数据 e:13012345678 [2345,34567]
String phone = split[0];
String up = split[split.length - 3]; //用length减是为了绕开其中的一个可能为空的数据
String down = split[split.length - 2];
// 封装
outK.set(phone);
outV.setUpFlow(Long.parseLong(up));
outV.setDownFlow(Long.parseLong(down));
outV.setSumFlow();
// 写
context.write(outK, outV);
}
}
Reducer
reducer主要负责数据加和,而整理相同手机号(key)的工作是由框架利用map自身的数据结构自己完成的不需要自己操作,这个在之前的案例中已经了解了,不过这次的values是FlowBean类型的集合,用迭代器获取每个value,并提取upFlow和downFlow,然后做加和。
封装后写出。
package com.xunn.mapreduce.writable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
private FlowBean outV = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
// 遍历集合values
long totalUp = 0;
long totalDown = 0;
for (FlowBean value : values) {
totalUp += value.getUpFlow();
totalDown += value.getDownFlow();
}
// 封装
outV.setUpFlow(totalUp);
outV.setDownFlow(totalDown);
outV.setSumFlow();
// 写
context.write(key, outV);
}
}
Driver
Driver的流程与之前案例一致,指定好参数中的类型就可以,如果有疑问可以看之前mapreduce的前两篇文章。Driver部分代码如下。
package com.xunn.mapreduce.writable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 获取job对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 设置jar包
job.setJarByClass(FlowDriver.class);
// 关联mapper和reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
// 设置map的输出kv
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 设置最终的输出kv
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("D:\\DARoom\\Hadoop\\somefiles\\inputD2"));
FileOutputFormat.setOutputPath(job, new Path("D:\\DARoom\\Hadoop\\somefiles\\output3"));
// 提交job ,true会打印更多的日志信息
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
准备好输入数据和输出路径,就可以在单机模式下运行了。