这算是对P2P自我理解后的成果,首次尝试多多包涵
打洞这件事可以看我上一篇博客,一对一打洞应该是可以结合到此次代码里的
是指运行于服务器上的一个程序,这个程序能够追踪到底有多少人同时在下载同一个文件。 客户端连上tracker服务器,就会获得一个下载人员的名单,根据这个,BT会自动连上别人的机器进行下载。它是提供bt的服务器。把文件用bt发布出来的人需要知道该使用哪个服务器来为要发布的文件提供tracker。
PS: 我写的tracker仅仅是实现了用来保存和提供文件及其拥有者IP的作用,没有上面的这么高大上
tracker是运行于服务器上的一个程序,这个程序能够捕获并记录各个节点信息;在共享目录更新时,提供更新资源的源节点信息给其他节点,使其他节点与源节点通信,获取该更新资源,从而使得最终每个人都能拥有一样的共享目录内容。
PS: 如果要结合UDP打洞tracker就要补充打洞时的server功能
ServerSocket serverSocket = new ServerSocket(3106);#创建绑定到特定端口的服务器Socket。
Socket socket = null;#需要接收的客户端Socket
int count = 0;#记录客户端数量
System.out.println("目录服务器启动");
#定义一个死循环,不停的接收客户端连接
while (true) {
socket = serverSocket.accept();#侦听并接受到此套接字的连接
InetAddress inetAddress=socket.getInetAddress();#获取客户端的连接
ServerThread1 thread=new ServerThread1(socket,inetAddress);#自己创建的线程类
thread.start();#启动线程
count++;#如果正确建立连接
System.out.println("查询目录的客户端数量:" + count);#打印客户端数量
}
Socket socket = null;
InetAddress inetAddress=null;#接收客户端的连接
public ServerThread1(Socket socket,InetAddress inetAddress) {
this.socket = socket;
this.inetAddress=inetAddress;
}
@Override
public void run() {
# 创建文件流用来读取文件中的数据
try {
File file = new File("/Users/huangkai/Desktop/Dir.txt");#获取了文件路径
FileInputStream fos = new FileInputStream(file);#文件字节输入流,对文件数据以字节的形式进行读取操作
# 创建网络输出流并提供数据包装器
OutputStream netOut = socket.getOutputStream();#获取输出流:客户端发来的数据。
#创建输出流对象(带缓冲)
#DataOutputStream用BufferedOutputStream修饰,是为了使这个输出流带缓冲
OutputStream doc = new DataOutputStream(
new BufferedOutputStream(netOut));
byte[] buf = new byte[20480];# 创建文件读取缓冲区(缓冲区足够包括文件的大小)
int num = fos.read(buf);# 读文件,read方法是一个一个字节逐渐读取,最后返回-1
while (num != (-1)) {# 是否读完文件
doc.write(buf, 0, num);# 从0到第num个的字符;把文件数据写出网络缓冲区
doc.flush();# 冲刷出流,将所有缓冲的数据强制发送到目的地 :刷新缓冲区把数据写往客户端
num = fos.read(buf);# 继续从文件中读取数据
}
fos.close();#冲刷并关闭输出流
doc.close();#冲刷并关闭输出流
}catch (FileNotFoundException e1) {#找不到文件或此路径为目录
JOptionPane.showMessageDialog(null, "文件目录错误或找不到此文件", "错误",
JOptionPane.ERROR_MESSAGE);
}catch (IOException e1) {
e1.printStackTrace();
}
}
Socket socket = null;
InetAddress inetAddress=null;#接收客户端的连接
public ServerThread2(Socket socket,InetAddress inetAddress) {
this.socket = socket;
this.inetAddress=inetAddress;
}
@Override
public void run() {
# 创建文件流用来读取文件中的数据
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());
# 文件名和长度
String fileName = dis.readUTF();
long fileLength = dis.readLong();
File directory = new File("/Users/huangkai/Desktop"); #保证目录存在
/*if(!directory.exists()) {
directory.mkdir();
} */
File file = new File(directory.getAbsolutePath() + File.separatorChar + fileName);
FileOutputStream fos = new FileOutputStream(file);
# 开始接收文件
byte[] bytes = new byte[1024];
int length = 0;
while((length = dis.read(bytes, 0, bytes.length)) != -1) {
fos.write(bytes, 0, length);
fos.flush();
}
File file1 = new File("/Users/huangkai/Desktop/Dir.txt");
FileOutputStream fos1 = null;
if(!file1.exists()){
file1.createNewFile();#如果文件不存在,就创建该文件
fos1 = new FileOutputStream(file1);#首次写入获取
}else{
#如果文件已存在,那么就在文件末尾追加写入
fos1 = new FileOutputStream(file1,true);#这里构造方法多了一个参数true,表示在文件末尾追加写入
}
#更新目录【文件名,IP】
OutputStreamWriter os = new OutputStreamWriter(fos1, "UTF-8");#指定以UTF-8格式写入文件
PrintWriter pw=new PrintWriter(os);
String s=fileName+","+socket.getInetAddress().getHostAddress();
pw.println(s);#每输入一个数据,自动换行,便于我们每一行每一行地进行读取
pw.close();
os.close();
fos.close();
dis.close();
socket.close();
}catch (FileNotFoundException e1) {#找不到文件或此路径为目录
JOptionPane.showMessageDialog(null, "文件目录错误或找不到此文件", "错误",
JOptionPane.ERROR_MESSAGE);
}catch (IOException e1) {
e1.printStackTrace();
}
}
Socket socket = null;
InetAddress inetAddress=null;#接收客户端的连接
public ServerThread3(Socket socket,InetAddress inetAddress) {
this.socket = socket;
this.inetAddress=inetAddress;
}
@Override
public void run() {
try {
#3.获得输入流
InputStream is=socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
#获得输出流
OutputStream os=socket.getOutputStream();
PrintWriter pw=new PrintWriter(os);
#4.读取用户输入信息
String info=null;
if(!((info=br.readLine())==null)){
System.out.println("用户请求的资源为:"+info);
}
String dirIP="无此资源";
#打开待读取的文件
BufferedReader br0 = new BufferedReader(new FileReader("/Users/huangkai/Desktop/Dir.txt"));
String line=null;
while((line=br0.readLine())!=null) {
#对字符串进一步处理
String[] str = line.split(",");#字符串转行为字符串数组
if(str[0].equals(info)) {
dirIP = str[1];
}
}
#给客户一个响应
pw.write(dirIP);
pw.flush();
br.close();
br0.close();
pw.close();
os.close();
br.close();
is.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
P2P节点即可作为对方的服务端【提供资源】,也可作为对方的客户端【请求资源】
面板窗口:
public P2p() {
setTitle("P2P文件共享系统_Client");#窗口标题
setLayout(null);#设置布局 setLayout(new BorderLayout())不同布局要求不同
setSize(800, 350);#窗口大小:宽、高
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);#关闭选项
JPanel client = new Client(); #panel类:面板容器类
JPanel server = new Server(); #panel类:面板容器类
server.setBounds(10, 10, 290, 300);#Layout需=null,组件矩形的大小
client.setBounds(300, 10, 450, 300);#= setLocation() + setSize()
client.setBorder(BorderFactory.createTitledBorder("Client"));
server.setBorder(BorderFactory.createTitledBorder("Server"));
add(client);#组件加入面板
add(server);
}
public static void main(String[] args) {
try{
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
}
#windows窗口更好看
#javax.swing.plaf.metal.MetalLookAndFeel——metal
#com.sun.java.swing.plaf.motif.MotifLookAndFeel——motif
catch (ClassNotFoundException e) {#执行try之后可能报的错误
e.printStackTrace();#报错后需执行的程序
}
catch (InstantiationException e) {
e.printStackTrace();
}
catch (IllegalAccessException e) {
e.printStackTrace();
}
catch (UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
new P2p().setVisible(true);#数据模型已构造好,允许JVM可以根据数据模型执行paint方法开始画图
}
/*
class Server ;自定义的类
extends JPanel ;继承Java的JPanel类,组件类
implements ActionListener ; 是实现 ActionListener 接口,为动作监听接口,是Java swing 监听窗体动作的一个接口
*/
JButton jb0,jb1;#按钮
JButton jb2,jb3;
JFileChooser jfc;#文件选择窗口
JTextField jtf,jtf0;#文本框,允许编辑单行文本
String st, st1;
String url="192.168.1.102";
JProgressBar jpb;#进度条
Timer timer;#线程定时
Socket s;
public Server() {
this.setLayout(null);
setSize(300, 300);#设置宽、高
# 进度条
jpb = new JProgressBar();
jpb.setOrientation(JProgressBar.HORIZONTAL);#设置进度条的方向:平行
jpb.setMinimum(0);#最小值为0
jpb.setMaximum(100);#最大值为100
jpb.setValue(0);#赋初始值
jpb.setStringPainted(true);#true:进度条呈现进度字符串
timer = new Timer(50, this); # 创建一个计时器,计时间隔为50毫秒
jfc = new JFileChooser();
/*
JFileChooser():构造一个指向用户默认目录的 JFileChooser
JFileChooser(File currentDirectory):使用给定的 File 作为路径来构造一个 JFileChooser
*/
jtf0 = new JTextField("请求文件");
jtf = new JTextField("请浏览选择待发送文件");#文本框内默认文字
jb0 = new JButton("浏览");#按钮
jb1 = new JButton("共享");
jb2 = new JButton("上传");
jb3 = new JButton("目录");
jtf0.setBounds(10, 30, 200, 30);#x、y;宽、高
jtf.setBounds(10, 80, 150, 30);
jb0.setBounds(170, 80, 100, 30);
jb1.setBounds(10, 130, 100, 30);
jb2.setBounds(10, 170, 100, 30);
jb3.setBounds(10, 210, 100, 30);
jpb.setBounds(10, 250, 200, 20);
this.add(jpb);#添加事件监听器
this.add(jtf0);
this.add(jtf);
this.add(jb0);
this.add(jb1);
this.add(jb2);
this.add(jb3);
jb0.addActionListener(this);#添加指定的动作侦听器,以接收发自此按钮的动作事件——this
jb1.addActionListener(this);
jb2.addActionListener(this);
jb3.addActionListener(this);
new s_server().start();#由线程启动
}
/*当特定于组件的动作(比如被按下)发生时,
由组件(比如 Button)生成此高级别事件。
事件被传递给每一个 ActionListener 对象,
这些对象是使用组件的 addActionListener 方法注册的,用以接收这类事件。
实现 ActionListener 接口的对象在发生事件时获取此 ActionEvent。
*/
int result;
File file1=null;
File file2=null;
#监听到有动作发生的某个控件(定时器)
if (e.getSource() == timer) {
int value = jpb.getValue();#返回进度条的当前值
jpb.setValue(0);#将进度条回复0值
if (value < 100) {#若当前进度未满
value++;#自加
jpb.setValue(value);#将进度条重新赋值
} else {
timer.stop();#进度条已满则停止Timer,使它停止向其监听器发送动作事件
jb0.setText("浏览");#浏览按钮
}
}
#浏览按钮被按下
else if ((JButton) e.getSource() == jb0) {
jfc.setApproveButtonText("确定");#文件选择窗口中的 approvebutton 内使用的文本
jfc.setDialogTitle("选择文件窗口");#文件选择窗口中的标题栏
result = jfc.showOpenDialog(this);
#null———显示在当前电脑显示器屏幕的中央
#this———显示在当前你编写的程序屏幕中央
#返回一个DialogResult,对应你按的按钮【如:单击“关闭”按钮会隐藏窗体,并将DialogResult属性设置为DialogResult.Cancel 】
if (result == JFileChooser.APPROVE_OPTION) # 当用户按下文件选择窗口中的确定
{
st = new String(jfc.getSelectedFile().getPath());#文件路径
#getSelectedFile()返回选中的文件
#getPath()将此抽象路径名转换为一个路径名字符串
jb0.setText(st);
st1 = new String(jfc.getSelectedFile().getName());#文件名
jtf.setText("文件" + st1 + "待发送!");
try {
#用来发送和接收数据报包的套接字(UDP)
#DatagramSocket(int port, InetAddress laddr) 创建数据报套接字,将其绑定到指定的本地地址。
DatagramSocket ds = new DatagramSocket();
/*数据报包(2)
1、DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。
2、DatagramPacket(byte[] buf, int length, InetAddress address, int port)
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
3、DatagramPacket(byte[] buf, int length)
构造 DatagramPacket,用来接收长度为 length 的数据包
*/
DatagramPacket dp = new DatagramPacket(jfc.getSelectedFile().getName().getBytes(),jfc.getSelectedFile().getName().getBytes().length,InetAddress.getByName("255.255.255.255"),3019);
#255.255.255.255广播地址
ds.send(dp);#发送数据包
ds.close();#关闭套接字
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
#共享按钮被按下
else if ((JButton) e.getSource() == jb1) {
jb1.setEnabled(false);#设置控件是否可用:false——不可用
new Thread(){
@Override
public void run() {
# 创建文件流用来读取文件中的数据
try {
File file = new File(jb0.getText());#获取了文件路径
FileInputStream fos = new FileInputStream(file);#文件字节输入流,对文件数据以字节的形式进行读取操作
# 创建网络服务器接受客户请求
ServerSocket ss = new ServerSocket(3108);#用于服务器端,监听客户端连接:端口——3108
Socket client = ss.accept();#accept()方法处于阻塞状态,直到有客户端连接,创建一个服务端Socket,与客户端交互
timer.start();#启动 Timer,使它开始向其监听器发送动作事件
# 创建网络输出流并提供数据包装器
OutputStream netOut = client.getOutputStream();#获取输出流:客户端发来的数据。
#创建输出流对象(带缓冲)
#DataOutputStream用BufferedOutputStream修饰,是为了使这个输出流带缓冲
OutputStream doc = new DataOutputStream(
new BufferedOutputStream(netOut));
byte[] buf = new byte[20480];# 创建文件读取缓冲区(缓冲区足够包括文件的大小)
int num = fos.read(buf);# 读文件,read方法是一个一个字节逐渐读取,最后返回-1
while (num != (-1)) {# 是否读完文件
doc.write(buf, 0, num);# 从0到第num个的字符;把文件数据写出网络缓冲区
doc.flush();# 冲刷出流,将所有缓冲的数据强制发送到目的地 :刷新缓冲区把数据写往客户端
num = fos.read(buf);# 继续从文件中读取数据
}
fos.close();#冲刷并关闭输出流
doc.close();#冲刷并关闭输出流
jb1.setEnabled(true);#按钮更新状态:true——可使用
} catch (FileNotFoundException e1) {#找不到文件或此路径为目录
JOptionPane.showMessageDialog(null, "请选择文件", "错误",
JOptionPane.ERROR_MESSAGE);
jb1.setEnabled(true);
} catch (IOException e1) {
JOptionPane.showMessageDialog(null, "IO异常", "错误",
JOptionPane.ERROR_MESSAGE);
jb1.setEnabled(true);
}
}
}.start();#运行此线程
}
#上传按钮被按下
else if ((JButton) e.getSource() == jb2){
jb2.setEnabled(false);
new Thread(){
@Override
public void run() {
try {
File file = new File(jb0.getText());#获取了文件路径
FileInputStream fis = new FileInputStream(file);#文件字节输入流,对文件数据以字节的形式进行读取操作
Socket client = new Socket(url,3120);
timer.start();#启动 Timer,使它开始向其监听器发送动作事件
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
# 文件名和长度
dos.writeUTF(file.getName());
dos.flush();
dos.writeLong(file.length());
dos.flush();
# 开始传输文件
byte[] bytes = new byte[1024];
int length = 0;
long progress = 0;
while((length = fis.read(bytes, 0, bytes.length)) != -1) {
dos.write(bytes, 0, length);
dos.flush();
}
fis.close();
dos.close();
client.close();
jb2.setEnabled(true);
} catch (FileNotFoundException e1) {#找不到文件或此路径为目录
JOptionPane.showMessageDialog(null, "请选择文件", "错误",
JOptionPane.ERROR_MESSAGE);
jb2.setEnabled(true);
} catch (IOException e1) {
JOptionPane.showMessageDialog(null, "IO异常", "错误",
JOptionPane.ERROR_MESSAGE);
jb2.setEnabled(true);
}
}
}.start();
}
#目录按钮被按下
else if ((JButton) e.getSource() == jb3){
try {
# 使用本地文件系统接受网络数据并存为新文件
file2 = new File("目录.txt");#新建一个文件并命名
if(!file2.isFile()) {
file2.createNewFile();
}
RandomAccessFile raf = new RandomAccessFile(file2, "rw");#向该文件写入数据,设置可读可写
# 通过Socket连接文件服务器
Socket server = new Socket(url, 3106);
# 创建网络接受流接受服务器文件数据
InputStream netIn = server.getInputStream();
InputStream in = new DataInputStream(new BufferedInputStream(
netIn));#封装输入流
# 创建缓冲区缓冲网络数据
byte[] buf = new byte[20480];
timer.start();
int num = in.read(buf);
while (num != (-1)) {# 是否读完所有数据
raf.write(buf, 0, num);# 将数据写往文件
raf.skipBytes(num);# 顺序写文件字节
num = in.read(buf);# 继续从网络中读取文件
}
in.close();
raf.close();
server.close();
netIn.close();
}
catch (IOException q) {
System.out.println("异常");
}
}
}
class s_server extends Thread {
@Override
public void run() {
# 创建文件流用来读取文件中的数据
try {
# 创建网络服务器接受客户请求
ServerSocket ss = new ServerSocket(3102);#用于服务器端,监听客户端连接:端口——3102
Socket client = ss.accept();#accept()方法处于阻塞状态,直到有客户端连接,创建一个服务端Socket,与客户端交互
#获得输入流
InputStream is=client.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
#读取用户输入信息
String info=null;
while(!((info=br.readLine())==null)){
jtf0.setText(info);
}
br.close();
is.close();
client.close();
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
JFileChooser jfc;
String ip;
String url="192.168.1.102";#云服务器的URL,电脑IP暂代
Timer timer;
JProgressBar jpb;
JButton jb1,jb2;
JButton jb3,jb4;
JTextField jtf1,jtf2;
JTextField jtf3;
String fileName="xxx";
File dir;
public Client() {
#进度条
timer = new Timer(50, this);
jpb = new JProgressBar();
#设置进度条的方向,SwingConstants.VERTICAL 或 SwingConstants.HORIZONTAL
jpb.setOrientation(JProgressBar.HORIZONTAL);
jpb.setMinimum(0);#设置最小进度值,最大进度值,当前进度值
jpb.setMaximum(100);
jpb.setValue(0);
jpb.setStringPainted(true);#接收时的显示标志,是否绘制百分比文本(进度条中间显示的百分数)
this.setLayout(null);
jfc = new JFileChooser();#获取打开文件对象
jb1 = new JButton("确定");
jb2 = new JButton("请选择文件存储位置");
jb3 = new JButton("接收");
jb4 = new JButton("请求");
jtf1 = new JTextField("127.0.0.1");
jtf2 = new JTextField("");
jtf3 = new JTextField("请输入请求文件名");
jtf1.setBounds(10, 30, 200, 25);
jb1.setBounds(220, 30, 80, 25);
jtf2.setBounds(10, 70, 400, 50);
jtf3.setBounds(10, 130, 200, 25);
jb2.setBounds(10, 170, 160, 25);
jb3.setBounds(150, 210, 80, 25);
jb4.setBounds(10, 210, 80, 25);
jpb.setBounds(10, 250, 250, 20);
this.add(jb1);
this.add(jb2);
this.add(jb3);
this.add(jb4);
this.add(jtf1);
this.add(jtf2);
this.add(jtf3);
this.add(jpb);
jb1.addActionListener(this);
jb2.addActionListener(this);
jb3.addActionListener(this);
jb4.addActionListener(this);
new s_server().start();#由线程启动
}
public void actionPerformed(ActionEvent e) {
int result;
File file1=null;
File file2=null;
if (e.getSource() == timer) {
int value = jpb.getValue();#进度条的现在值
jpb.setValue(0);
if (value < 100) {
value++;
jpb.setValue(value);
} else {
timer.stop();
}
}
#选择文件后显示状态实时更新为对应的选择路径
else if ((JButton) e.getSource() == jb2) {
result = jfc.showSaveDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
file1 = jfc.getSelectedFile();
jb2.setText(file1.getPath());#显示文件路径
dir = new File(file1.getPath());
}
}
若是想加入UDP穿透
UDP打洞的部分应该是加在请求按钮的部分,此处已经获取了对方的IP然后就可以提交给NATserver进行打洞操作
#请求按钮[将文件名发给服务器,接收服务器给的IP并填入jtf1]
else if ((JButton) e.getSource() == jb4){
try {
Socket server = new Socket(url, 3104);
#输出流
OutputStream os=server.getOutputStream();
PrintWriter pw=new PrintWriter(os);
#输入流
InputStream is=server.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
#3.利用流按照一定的操作,对socket进行读写操作
String info=new String(jtf3.getText());
pw.write(info);
pw.flush();
server.shutdownOutput();
#接收服务器的相应
String reply=null;
while(!((reply=br.readLine())==null)){
jtf1.setText(reply);
}
#4.关闭资源
br.close();
is.close();
pw.close();
os.close();
server.close();
} catch (UnknownHostException t) {
t.printStackTrace();
} catch (IOException t) {
t.printStackTrace();
}
ip = new String(jtf1.getText());
}
#确定按钮:[连接ip客户端,将请求文件名发送]
else if ((JButton) e.getSource() == jb1) {
ip = new String(jtf1.getText());
try {
Socket server = new Socket(ip, 37194);
OutputStream os=server.getOutputStream();
PrintWriter pw=new PrintWriter(os);
String info=new String(jtf3.getText());
pw.write(info);
pw.flush();
server.shutdownOutput();
pw.close();
os.close();
server.close();
} catch (UnknownHostException t) {
t.printStackTrace();
} catch (IOException t) {
t.printStackTrace();
}
}
#接收后触发事件
else if ((JButton) e.getSource() == jb3) {
try {
# 使用本地文件系统接受网络数据并存为新文件
file2 = new File(dir,fileName);#新建一个文件并命名
if(!file2.isFile()) {
file2.createNewFile();
}
RandomAccessFile raf = new RandomAccessFile(file2, "rw");#向该文件写入数据,设置可读可写
# 通过Socket连接文件服务器
Socket server = new Socket(ip, 3108);
# 创建网络接受流接受服务器文件数据
InputStream netIn = server.getInputStream();
InputStream in = new DataInputStream(new BufferedInputStream(
netIn));#封装输入流
# 创建缓冲区缓冲网络数据
byte[] buf = new byte[20480];
timer.start();
int num = in.read(buf);
while (num != (-1)) {# 是否读完所有数据
raf.write(buf, 0, num);# 将数据写往文件
raf.skipBytes(num);# 顺序写文件字节
num = in.read(buf);# 继续从网络中读取文件
}
in.close();
raf.close();
}
catch (IOException q) {
System.out.println("异常");
}
}
}
String message;
float a, b, c;
int i;
public void run() {
while (true) {#循环接收UDP包
try {
byte[]bytes = new byte[1024];
#使用 DatagramSocket接受数据,设置端口和接受字节的最大值
DatagramSocket ds = new DatagramSocket(3019);#端口
DatagramPacket dp = new DatagramPacket(bytes,1024);#包大小
ds.receive(dp);
message = new String(bytes,0,bytes.length);
message = message.trim();#trim()去除头尾的字符空格
fileName = message;
jtf2.setText("文件" + message + "待接收\n请设置IP:"+dp.getAddress()+"接收!");#对jtf2文本框刷新显示消息
ds.close();
} catch (IOException e) {
System.out.println(e);
}
}
}
偷摸放个资源: 仅供学习
重申:UDP打洞要自己加入