java实现P2P通信(含安卓实现的基于IPV6的p2p通信代码)
- 什么是P2P网络
- 用udp打洞的三种方式
- IPV6实现P2P通信
什么是P2P网络
p2p网络又叫对等网络,顾名思义就是在该网络中所有节点都是平等的,都可以共享自己的硬件资源和数据资源。每个节点都能被其它对等节点直接访问而无需经过中间实体。换句话来说,目前绝大多数应用都是基于C/S或者B/S架构的,就拿微信来说,当A要通过微信给B发一条消息时,消息首先会从A发送到服务器,然后服务器在转发给B。这样服务器就会知道AB之间的聊天记录。而P2P网络来说,A可以直接发送消息给B不需要中转服务器。这样的好处就不言而喻了。
用udp打洞的三种方式
本来按照互联网之前的设计p2p通信不是什么大问题。A想给B发消息只需要知道B的IP地址和端口号就可以直接发送消息给B了。但是由于IPV4地址数量只有42亿多个,已经远远不能满足人们的上网需求,因此出现了NAT技术,通过绑定端口让多台计算机可以共用一个公网IP从而缓解公网IP地址空间的枯竭。但这样就带来了新的问题。A不知道B的公网IP和映射端口号就找不到B了。那么我就有了个大胆的想法,通过中间服务器存储A和B的公网IP以及映射的端口号,然后分别发送给A和B,这样A和B就知道对方的公网IP和端口号,不就可以访问了吗。这个也叫做udp打洞。如图所示:
基于上图我用java的udp协议包实现了程序代码如下:
服务器:
// An highlighted block
public class p2pService {
//map存储每个注册用户的名字和公网地址
Map<String,String>map =new HashMap<String,String>();
byte[] inBuff=new byte[2048];
DatagramPacket inpacket=new DatagramPacket(inBuff,2048);
public void init() {
try {
System.out.println("服务器启动");
DatagramSocket socket=new DatagramSocket(30000);
while(true) {
socket.receive(inpacket);
System.out.println("地址:"+inpacket.getSocketAddress()+" 端口:"+inpacket.getPort()+" 消息:"+new String(inBuff,0,inpacket.getLength()));
JSONObject json=JSONObject.fromObject(new String(inBuff,0,inpacket.getLength()));
//message封装通信消息格式
message mess=(message)JSONObject.toBean(json, message.class);
//link类消息为注册消息
if("link".equals(mess.type))
{
map.put(mess.name, inpacket.getSocketAddress()+"");
List<Map<String,String>>list=new ArrayList<>();
for(String key:map.keySet())
{
Map<String,String> map1=new HashMap<>();
map1.put("name", key);
map1.put("IP", map.get(key));
list.add(map1);
}
String messs=JSONArray.fromObject(list)+"";
message mes=new message();
mes.type="list";
mes.value=messs;
messs=JSONObject.fromObject(mes)+"";
System.out.println(messs);
byte[] outmess=messs.getBytes("utf-8");
DatagramPacket out=new DatagramPacket(outmess,outmess.length,inpacket.getSocketAddress());
//返回所有以注册用户的姓名
socket.send(out);
}
//comm是需要获取通信对方的ip
else if("comm".equals(mess.type)) {
//B的IP
String IPB=map.get(mess.name);
//A的IP
String IPA=inpacket.getSocketAddress()+"";
message result=new message();
result.type="link";
result.IP=IPB.substring(1,IPB.lastIndexOf(":"));
System.out.println("comm:"+result.IP);
result.port=IPB.split(":")[8];
System.out.println("link:"+result.IP+" port"+result.port);
byte[] buff=JSONObject.fromObject(result).toString().getBytes();
DatagramPacket out=new DatagramPacket(buff,buff.length,inpacket.getSocketAddress());
//返回B的IP给A
socket.send(out);
result.IP=IPA.substring(1,IPB.lastIndexOf(":"));
result.port=IPA.split(":")[8];
buff=JSONObject.fromObject(result).toString().getBytes();
out=new DatagramPacket(buff,buff.length,InetAddress.getByName(IPB.substring(2,IPB.lastIndexOf(":")-1)),Integer.parseInt(IPB.split(":")[8]));
//返回A的IP给B
socket.send(out);
}
}
}
catch(Exception ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
p2pService service=new p2pService();
service.init();
}
}
客户端:
public class p2pClient {
byte[] inBuff=new byte[2048];
DatagramPacket inpacket=new DatagramPacket(inBuff,2048);
String name;//用户名字
DatagramSocket socket;
String IP=null;//通信对方的IP
int port=0;//通信对方的端口
String ServiceIP="32.33.133.23";//服务器IP
int ServicePort=30000;//服务器端口
public void init(int part) {
try {
socket=new DatagramSocket(part);
message mes=new message();
mes.setName(name);
mes.setType("link");
byte[] outbyte=JSONObject.fromObject(mes).toString().getBytes("utf-8");
DatagramPacket out=new DatagramPacket(outbyte,outbyte.length,InetAddress.getByName(ServiceIP),ServicePort);
socket.send(out);
socket.receive(inpacket);
System.out.println("地址1:"+inpacket.getSocketAddress()+" 端口:"+inpacket.getPort()+" 消息:"+new String(inBuff,0,inpacket.getLength()));
new Thread(new receive(this)).start();
}
catch(Exception ex) {
ex.printStackTrace();
}
}
public void p2pcomm()throws Exception {
Scanner sc=new Scanner(System.in);
while(sc.hasNext())
{
String message=sc.next();
//A获得B的IPcomm:B
if(message.startsWith("comm:")) {
DatagramPacket out=new DatagramPacket(message.getBytes(),message.getBytes().length,InetAddress.getByName(ServiceIP),ServicePort);
socket.send(out);
}
//A改变通信IP为B chang:IP。以下是基于IPV6,可自己改成IPV4
else if(message.startsWith("change:")) {
IP=message.substring(message.indexOf(":")+1, message.lastIndexOf(":"));
System.out.println("IP:"+IP);
port=Integer.parseInt(message.split(":")[9]);
System.out.println("转换成功");
}
//直接发送消息给B
else {
DatagramPacket out=new DatagramPacket(message.getBytes(),message.getBytes().length,InetAddress.getByName(IP),port);
socket.send(out);
}
}
}
public static void main(String[] args) {
p2pClient client=new p2pClient();
try {
Scanner sc=new Scanner(System.in);
if(sc.hasNext()) {
client.name=sc.next();
}
System.out.println("客户端"+client.name+"启动");
if(sc.hasNext()) {
client.init(Integer.parseInt(sc.next().substring(0, 5)));
client.p2pcomm();
}
}
catch(Exception ex) {
ex.printStackTrace();
}
}
}
class receive implements Runnable{
p2pClient client;
//String IP=null;
//int port=0;
//byte[] inBuff=new byte[2048];
//DatagramPacket inpacket=new DatagramPacket(inBuff,2048);
String name;
public receive(p2pClient client) {
this.client=client;
name=client.name;
}
//监听接收的消息
public void run() {
try {
while(true) {
client.socket.receive(client.inpacket);
if(new String(client.inBuff,0,client.inpacket.getLength()).startsWith("commIP:"))
{
String message=new String(client.inBuff,0,client.inpacket.getLength());
System.out.println();
client.IP=message.substring(message.indexOf(":")+3, message.lastIndexOf(":")-1);
//client.IP=new String(client.inBuff,0,client.inpacket.getLength()).split(":")[1].substring(1);
System.out.println("IP:"+client.IP);
client.port=Integer.parseInt(new String(client.inBuff,0,client.inpacket.getLength()).split(":")[2]);
DatagramPacket out=new DatagramPacket((name+":1").getBytes(),(name+":1").getBytes().length,InetAddress.getByName(client.IP),client.port);
client.socket.send(out);
}
else if(new String(client.inBuff,0,client.inpacket.getLength()).startsWith("link:")) {
String message=new String(client.inBuff,0,client.inpacket.getLength());
System.out.println("message:"+message);
client.IP=message.substring(message.indexOf(":")+3, message.lastIndexOf(":")-1);
System.out.println("IP:"+client.IP);
client.port=Integer.parseInt(new String(client.inBuff,0,client.inpacket.getLength()).split(":")[9]);
DatagramPacket out=new DatagramPacket((name+" link you").getBytes(),(name+" link you").getBytes().length,InetAddress.getByName(client.IP),client.port);
client.socket.send(out);
}
System.out.println(name+"收到消息:"+new String(client.inBuff,0,client.inpacket.getLength())+" 来自"+client.inpacket.getAddress()+":"+client.inpacket.getPort());
}
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
按照这个思路代码应该是可以实现不同内网下的计算机直接通信。但当我测试的时候却无法实现。而如果我让B直接处于公网状态,则可以通信。这个问题让我听困惑。难道是哪出问题了。我仔细把我的方案屡了一遍发现,是在NAT的时候出问题了。我设想的NAT是固定的,即从A的30001端口映射的公网端口就是41200,但实际却不是这样的。这个就要好好补一下NAT了。NAT被分为四类:
1、full cone 全椎
2、Restricted Cone ip受限
3、port Restricted Cone 端口受限
4、Symmetric 对称。
1.full cone:完全圆锥型的NAT,将从同一内部IP地址和端口来的所有请求,都映射到相同的外部IP地址和端口。而且,任何外部主机通过向映射的外部地址发送报文,可以实现和内部主机进行通信。
2.Restricted Cone ip: 受限圆锥型NAT也是将从相同的内部IP地址和端口来的所有请求,映射到相同的公网IP地址和端口。但是与完全圆锥型NAT不同,外部主机B不能直接通过映射到相同的公网IP地址和端口访问A。而是需要内部主机A主动向外部主机B发送了消息后,外部主机B才能使用公网IP地址和端口访问A。
3.port Restricted Cone 端口受限: 类似于受限圆锥型NAT,但更严格。端口受限圆锥型NAT增加了端口号的限制,要求B访问A时B的端口号必须和A访问B时B的端口号相同才能通信。
4.Symmetric:对称型NAT把从同一内网地址和端口到相同的地址和端口的所有请求,都映射到同一个公网地址和端口。
即A访问服务器时留下的的端口,B是不能用的。B无法知道要访问什么端口才能和A通信,同时如果B也处在对称型NAT下,那么A和B都无法找到通讯端口。可见,对称性NAT是所有NAT类型中限制最为严格的。
从上可以知道我的方案设计时默认了移动通信运营商是采用端口受限NAT来设计的。但事实上目前基本上用的都是对称型NAT。A向服务器端口port0注册时留下的映射端口port1只能服务器端口port0才能访问,B是访问不了的。同理B向服务器注册时留下的映射端口port2,A也无法访问。就无法打洞。后来我看了一些文章,又说用猜端口的方法打洞。怎么说了,理论上是没问题的,但是实际操作的话十分困难,首先A不知道自己映射的端口号porta,同时也不知道B留给A的端口号portb。同理B也不知道porta和portb。在A向B打洞时A去猜portb,那么porta就会一直变化。同理b向a打洞时也要猜porta,那么portb也要变化。两个都在变,就很难猜了。因此在双方都处在对称的NAT下的话是无法打洞的。
到这感觉P2P通讯就此破产了?,不,此路不通还有他路。
IPV6实现P2P通信
终于在我快要放弃的时候,我突然想到了IPV6,既然IPV4地址不够使得我们都处在NAT下。那IPV6总没有问题把。于是我又动手用IPV6实现了P2P通信。方法也很简单 ,只需要A和B双方都有IPV6地址,同时双方都知道对方的IPV6地址和端口号就可以通信了,为了实现双方都知道对方的IPV6地址和端口号,我同样设计了服务器用来获取A和B的IPV6和端口号并通知对方。如下图:
效果图如下:
1.向服务器注册填写名字
2.服务器返回通讯列表
3.点击zzz和他通讯
4.给zzz发送信息并接受zzz发来的信息
5.zzz的屏幕显示
因为时间比较仓促而且以及好几年没有碰过安卓开发了,所以界面很丑哈哈哈哈。并且通讯的手机都必须处于wifi模式下才能通讯,如果是4G模式下会出现问题。具体问题我还没有研究出来,可能和运营商有关系。总之暂时先这样吧。