用java实现一个简单的p2p连接以及stun NAT探测
网上有很多相关的理论都大同小异,但java实现的不多,我就实现了一下,其实是没有什么意义的,只是因为提高一下编程水平,而且我还没有条件去做实验,因为这里要用到两个外网ip,所以代码到底行不行不一定,但是我觉得应该能行!至少理论实现上是对的。。。好吧,我还是不确定。
具体实现全程都是用UDP来进行的,因为tcp需要三次握手连接,发送完随时都会关闭,关闭就不能发送了,而p2p需要长期的发送与接收的。stun的所用是探测NAT的类型,根据本内网的NAT和对方内网的NAT来进行相应的p2p连接
p2p连接的基本步骤是:
1.客户端A登录服务器S(stun)
2.客户端B登录服务器S(stun)
3.S记录下A,B的NAT信息,下次登录不用stun,另外也要把所有登录上S的客户端记录上,发送给其他每个客户端,这样每个客户端就知道可以连接的对象了
3.A想连接B,给S发送个信息,这个信息一定要包含B的外网的ip,端口
4.S发送信息给B,这个信息一定包含A的外网的ip,端口。B向A发送信息(打洞),A肯定接受不到,但不所谓,目的是打洞而已(在B的NAT中建立session)。
5,.这样A就可以向B发送信息了
下面的代码:
服务器的代码---
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
public class P2PServer {
protected static int serverPort;
protected static int proxyPort;
protected static String proxyIp;
//内存缓存
protected static HashMap<String,HashMap<Integer,Integer>> clientList=new HashMap<String,HashMap<Integer,Integer>>();
private static StringBuffer lists=null;
private static int udp_size=500;
//服务器主程序
public static void main(){
try{
Properties p=Stupid.getPZ();
int p1=Integer.parseInt(p.getProperty("serverPort"));
int p2=Integer.parseInt(p.getProperty("port"));
String ip=p.getProperty("ip");
serverPort=p1;
proxyPort=p2;
proxyIp=ip;
DatagramSocket socket=new DatagramSocket(serverPort);
while(true){//下面是并发代码
DatagramPacket request=new DatagramPacket(new byte[1024],1024);
socket.receive(request);
DatagramPacket response=null;
String msg=null;
//判断是不是已有数据
int port=request.getPort();
if(clientExit(request.getAddress().getHostAddress(),request.getPort())!=-1){
msg=""+clientList.get(request.getAddress().getHostAddress()).get(request.getPort());
response=new DatagramPacket(msg.getBytes(),msg.getBytes().length,request.getAddress(),request.getPort());
socket.send(response);
continue;
}
String bin=new String(request.getData(),0,request.getLength(),"ASCII");
switch(bin){
case "1":
InetAddress i=request.getAddress();
msg=i.getHostAddress()+":"+port;
response=new DatagramPacket(msg.getBytes(), msg.getBytes().length,request.getAddress(),request.getPort());
socket.send(response);
break;
case "2":
//切换ip或者用udp代理,这里用代理
i=InetAddress.getByName(proxyIp);
response=new DatagramPacket(msg.getBytes(),msg.getBytes().length,i,proxyPort);
socket.send(response);
break;
case "3":
getLists();
int j=0;
while(j!=-1){
byte[] bn=new byte[500];
j=readString(bn,j,j+500);
response=new DatagramPacket(bn,bn.length,request.getAddress(),request.getPort());
socket.send(response);
DatagramPacket dp=new DatagramPacket(new byte[1],1);
socket.receive(dp);
}
byte[] mop="-1".getBytes();
response=new DatagramPacket(mop,mop.length,request.getAddress(),request.getPort());
socket.send(response);
default:
if(bin.startsWith(">>>")){
bin=bin.substring(3, bin.length());
String[] bs=bin.split(":");
bin=">>>"+request.getAddress().getHostAddress()+":"+request.getPort();
response=new DatagramPacket(bin.getBytes(),
bin.getBytes().length,InetAddress.getByName(bs[0]),Integer.parseInt(bs[1]));
socket.send(response);
}else if(bin.startsWith("<<<")){//存储客户端的NAT类型
String ipx=request.getAddress().getHostAddress();
HashMap<Integer,Integer> h1=new HashMap<Integer,Integer>();
if(!clientList.containsKey(ipx)){
h1.put(request.getPort(), Integer.parseInt(bin.substring(3,bin.length())));
clientList.put(ipx, h1);
}else{
clientList.get(ipx).put(request.getPort(), Integer.parseInt(bin.substring(3,bin.length())));
}
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
public static int clientExit(String ip,int port){
if(!clientList.containsKey(ip))
return -1;
HashMap<Integer,Integer> l=clientList.get(ip);
if(l.containsKey(port))
return l.get(port);
return -1;
}
public static synchronized void getLists(){
if(lists!=null)
return;
Set<String> ss=clientList.keySet();
Iterator<String> it=ss.iterator();
while(it.hasNext()){
String k=it.next();
HashMap<Integer,Integer> h=clientList.get(k);
Iterator<Integer> it1=h.keySet().iterator();
while(it1.hasNext()){
Integer i=it1.next();
Integer i1=h.get(i);
lists.append(k+":"+i+":"+i1+" ");
}
}
}
public static int readString(byte[] r,int si,int oi){
String s=lists.toString();
byte[] b=s.getBytes();
int l=b.length;
for(int i=si;i<=oi;i++){
b[i]=r[i];
}
if(oi>=l){
return -1;
}
return oi+1;
}
}
客户端的代码---
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Properties;
public class P2PClient {
private static String serverIp;
private static int serverPort;
private static int proxyPort;
private static String proxyIp;
private static int clientPort;
private static boolean full_cone_nat=false;
private static boolean cone_nat=true;
private static boolean full_Port_Cone_nat=true;
private static boolean own_ip=false;
protected static DatagramSocket socket=null;
protected static DatagramPacket request;
protected static DatagramPacket response;
protected static HashMap<String,Integer> hm;
protected P2PClient() throws Exception{
Properties p=Stupid.getPZ();
int port=Integer.parseInt(p.getProperty("serverPort"));
String ip=p.getProperty("serverIp");
serverIp=ip;
serverPort=port;
int _port=Integer.parseInt(p.getProperty("port"));
String _ip=p.getProperty("ip");
proxyIp=_ip;
proxyPort=_port;
clientPort=Integer.parseInt(p.getProperty("clientPort"));
socket=new DatagramSocket(clientPort);
}
//首先用stun探测NAT类型
public void stun() throws Exception{
try{
socket.setSoTimeout(5000);
InetAddress host=InetAddress.getByName(serverIp);
request=new DatagramPacket("1".getBytes(),"1".getBytes().length,host,serverPort);
response=new DatagramPacket(new byte[1024],1024);
String result=null;
try{
socket.send(request);
socket.receive(response);
result=new String(response.getData(),0,response.getLength(),"ASCII");
}catch(Exception e){
//超时抛异常
System.out.println("服务器炸了或者你的路由器坏了");
throw new Exception("nat error");
}
System.out.println("stun step 1 response from server "+result);
String[] rss=result.split(":");
if(rss.length==0){
int r=Integer.parseInt(result);
if(r==0)
own_ip=true;
else if(r==11)
full_cone_nat=true;
else if(r==666)
cone_nat=false;
return;
}
String ip=rss[0];
String local=InetAddress.getLocalHost().getHostAddress();
int NATport=Integer.parseInt(rss[1]);
if(!ip.equals(local)){
//客户端在内网里面
request=new DatagramPacket("2".getBytes(),"2".getBytes().length,host,serverPort);
response=new DatagramPacket(new byte[1024],1024);
try{
socket.send(request);
socket.receive(response);//这里接收的是来自代理服务器
full_cone_nat=true;
System.out.println("全锥形NAT");
}catch(Exception e){
//一般肯定会到这里
System.out.println("受限锥形NAT");
}
InetAddress i=InetAddress.getByName(proxyIp);
request=new DatagramPacket(new byte[1],1,i,proxyPort);
if(!full_cone_nat){
response=new DatagramPacket(new byte[1024],1024);
socket.send(request);
socket.receive(response);
int _port=Integer.parseInt(new String(response.getData(),"ASCII"));
if(_port!=NATport){
System.out.println("对称NAT");
cone_nat=false;//对称NAT 不是圆锥NAT
return;
}
}
socket.send(request);//在发送 让代理换个端口发回来
try{
response=new DatagramPacket(new byte[1024],1024);
socket.receive(response);
}catch(Exception e){
full_Port_Cone_nat=false;
System.out.println("端口受限锥形NAT");
}
}else{
System.out.println("客户端拥有自己的外网ip,并不具有NAT");
own_ip=true;
}
}catch(Exception e){
throw e;
}finally{//记录下来,下次不再探测
DatagramPacket dp;
String m="";
if(!own_ip&&cone_nat){
if(full_cone_nat){
m="<<<"+11;
}else{
m="<<<"+10;
}
}else if(own_ip){
m="<<<"+0;
}else if(!cone_nat){
m="<<<"+666;
}
if(m!=""){
dp=new DatagramPacket(m.getBytes(),m.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(dp);
}
System.out.println("探测NAT完成,与服务器成功建立连接");
//if(socket!=null)
//socket.close();
}
}
public void bin(String ip,int port) throws Exception{
InetAddress in=InetAddress.getByName(serverIp);
String bin=">>>"+ip+":"+port;
request=new DatagramPacket(bin.getBytes(),bin.getBytes().length,in,serverPort);
socket.send(request);
response=new DatagramPacket(new byte[1024],1024);
try{
socket.receive(response);
}catch(Exception e){//来自目的主机的访问,肯定到不了
System.out.println("连接成功");
}
}
//获取其他客户端以及他们的NAT属性
public void getClients() throws Exception{
request=new DatagramPacket("3".getBytes(),"3".getBytes().length,
InetAddress.getByName(serverIp),serverPort);
}
public void connect(String ip,int port) throws Exception{
//首先探测NAT 看看能不能p2p
synchronized(socket){
stun();
getClients();
socket.notify();//唤醒其他进程,本线程进入等待状态
socket.wait();
String key=ip+":"+port;
int bz=hm.get(key);
//获得客户端B的NAT状态
if(!cone_nat || bz==666){
System.out.println("连接失败");
return;
}else if(bz==11 && !full_cone_nat){//目标客户端是全圆锥的,直联
String bin="---";
request=new DatagramPacket(bin.getBytes(),bin.getBytes().length,InetAddress.getByName(ip),port);
socket.send(request);
response=new DatagramPacket(new byte[1024],1024);
try{
socket.receive(response);
}catch(Exception e){//来自目的主机的访问,肯定到不了
System.out.println("连接成功");
}
}else if(bz==11 && full_cone_nat){//两个都是全圆锥,这种情况就不需要进行连接了
System.out.println("连接成功");
}else{
bin(ip,port);
}
//连接结束,之后要把锁释放掉,交给p2pclientserver,让其他主机来访问本主机
socket.notify();
}
}
public static void main(String[] args){
try{
P2PClient p =new P2PClient();
Thread t=new thread(new P2PClientServer());
t.start();
p.connect("0.0.0.0", -1);
}catch(Exception e){
e.printStackTrace();
}
}
}
同样是客户端的,但这是客户端监听程序的代码---
import java.net.DatagramPacket;
import java.net.InetAddress;
public class P2PClientServer extends P2PClient implements Runnable{
protected P2PClientServer() throws Exception {
super();
}
private StringBuffer sb=null;
@Override
public void run() {
synchronized(socket){
try{
socket.wait();//上来就堵住
DatagramPacket request=new DatagramPacket(new byte[500],500);
while(true){
socket.receive(request);
String bin=new String(request.getData(),"ASCII");
if(bin.startsWith(">>>")){//请求客户端向某客户端打洞,来建立点对点连接
bin=bin.substring(3, bin.length());
String[] bs=bin.split(":");
bin=">>>"+request.getAddress().getHostAddress()+":"+request.getPort();
DatagramPacket response=new DatagramPacket("---".getBytes(),
"---".getBytes().length,InetAddress.getByName(bs[0]),Integer.parseInt(bs[1]));
socket.send(response);
}else{
//接收server 3的发送过来的所有客户端的相关数据,NAT类型,ip,port等
if(!"-1".equals(bin)){
sb.append(bin.trim());
}
String[] sm=sb.toString().split(" ");
for(int i=0;i<sm.length;i++){
String[] op=sm[i].split(":");
hm.put(op[0]+":"+op[1], Integer.parseInt(op[2]));
//这里hm是变化中的所以就不做null的判断了
}
socket.notify();//唤醒父类的线程
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
代理服务器的代码,这个代理服务器用于stun探测---
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Properties;
public class P2PProxyServer {
private P2PProxyServer(){
}
//下面是代理服务器的代理,非本服务器代码
public static void main(String[] args) throws Exception{
Properties p=Stupid.getPZ();
int port=Integer.parseInt(p.getProperty("port"));
int port_=Integer.parseInt(p.getProperty("_port"));
DatagramSocket socket=new DatagramSocket(port);
DatagramSocket _socket=null;
while(true){
DatagramPacket request=new DatagramPacket(new byte[1024],1024);
socket.receive(request);//The following is the shared code
InetAddress clientIp=null;
int clientPort=0;
int _port=0;//session port from server
String result=new String(request.getData(),"ASCII");
String[] s=result.split(":");
if(s.length==2){
clientIp=InetAddress.getByName(s[0]);
clientPort=Integer.parseInt(s[1]);
DatagramPacket response=new DatagramPacket(new byte[1],1,clientIp,clientPort);
socket.send(response);
}else{
if(_port==0){
_port=request.getPort();//session port from client
String msg=_port+"";
DatagramPacket response=new DatagramPacket(msg.getBytes(),msg.getBytes().length,clientIp,clientPort);
socket.send(response);
}else{
//换个端口
_socket=new DatagramSocket(port_);
DatagramPacket response=new DatagramPacket(new byte[1],1,clientIp,clientPort);
_socket.send(response);
}
}
}
}
}
一个工具类,没啥东西,就读取个properties配置文件
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.Properties;
import org.Yeah.FHttp;
import org.Yeah.FHttpSocket;
public class Stupid {
public static Properties getPZ() throws Exception{
Properties p=new Properties();
BufferedReader in=new BufferedReader(new InputStreamReader(new FileInputStream(new File("ipAndPort.properties"))));
p.load(in);
return p;
}
}
最后是配置文件---
ip=1.1.1.1
port=9998
_port=9997
sererIp=1.1.1.1
serverPort=1314
clientPort=1314
欢迎大家指出我代码中的错误,相互提高
上一篇: ElasticSearch-常用的几种Java客户端
下一篇: oracle全文检索