P2P通信是几年前我就看过的东西,但那时候主要用TCP,还有NAT的类型会对其有限制,所以觉得并不太通用,而且,打造这种环境也实在太麻烦,也就没有去写代码实现下。其实我心里一直觉得P2P是个很神奇有用的东西,现在补坑,还是去把P2P给重新弄了一遍。理清思路之后,发现P2P其实并不像以前想象的那么复杂
首先,NAT有几种分类,这个是必须需要知道的。我是用联通4G 和 校园网宽带作为android client的测试环境的,经测试,我这属于第三类,端口限制性克隆。至于第四类对称式NAT,据说没没有办法P2P通信的,所以P2P的限制性在于第四类,但应该不多见吧。
(1)全克隆( Full Cone) : NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。任何一个外部主机均可通过该映射发送IP包到该内部主机。
(2)限制性克隆(Restricted Cone) : NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。但是,只有当内部主机先给IP地址为X的外部主机发送IP包,该外部主机才能向该内部主机发送IP包。
(3)端口限制性克隆( Port Restricted Cone) :端口限制性克隆与限制性克隆类似,只是多了端口号的限制,即只有内部主机先向IP地址为X,端口号为P的外部主机发送1个IP包,该外部主机才能够把源端口号为P的IP包发送给该内部主机。
(4)对称式NAT ( Symmetric NAT) :这种类型的NAT与上述3种类型的不同,在于当同一内部主机使用相同的端口与不同地址的外部主机进行通信时, NAT对该内部主机的映射会有所不同。对称式NAT不保证所有会话中的私有地址和公开IP之间绑定的一致性。相反,它为每个新的会话分配一个新的端口号。
理清下思路,可以将前3类作为1类来处理
C1,C2分别连接Server,那么nat就会记录对应的内网IP端口和我服务器IP端口,所以此时我Server是可以直接向recvfrom中拿到的这个IP和端口直接发送数据的,但NAT会忽略掉非Server的IP。那么我想让另外一个IP通过NAT该怎么做呢?比如C1连了我Server,Server知道对应NAT上IP和端口,那么我告诉C1,你去连接C2,此时虽然C1是无法连接到C2的,但是在NAT上会产生对应的C1/C2的IP和端口,C2发送数据就能收到了。同理,我也这样去告诉C2 C1的IP端口,那么C2连接之后,就可以收到C1发过来的数据了。如果NAT是第一类,那么C1 C2是可以直接互发数据的,也不会产生什么影响。第二三类,可能最开始会有数据丢失,当然可以避免,我在例程中当然没有去做得那么好,一端请求通信的话直接就开始互发数据了。还有个问题就是如果我Server端得到的IP是相同的,那么NAT收到连接是自己IP的话,可能会直接屏蔽掉,所以要看下最内层的环境是否在同一路由上,在同一路由上,直接通信就好,是一定能连接成功的。
先上图:
测试步骤:
1、将server端运行于公网服务器上
2、将client中serverAddress 设置server端IP
3、运行2个client在不同的nat下,可以开个虚拟机用宽带,然后真机用4G来测试
4、2个client设置不同的名称并连接主服务器
5、其中任意1个client输入对方名称并开始打洞即可
server:
// P2P.cpp: 定义控制台应用程序的入口点。 // #include "stdafx.h" #include<map> struct Info { char ip[16]; char local[16]; int port; }; int main() { WORD wVer; WSADATA wsaData; int err; wVer = MAKEWORD(2, 1); err = WSAStartup(wVer, &wsaData); if (err != 0) { return 1; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 1) { WSACleanup(); return 1; } map<string, Info*> _map; map<long, string> _map2; SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(47240); bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); while (true) { SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); char recvBuf[100]; char* context; recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len); if (recvBuf[0] == '0'&&recvBuf[1] == '0'&&recvBuf[2] == '1') { strtok_s(recvBuf,",",&context); char* name=strtok_s(0,",",&context); char* local=strtok_s(0,",",&context); char IPdotdec[20]; inet_ntop(AF_INET, &addrClient.sin_addr, IPdotdec, 20); Info* info; if(_map.find(name)==_map.end()) { info = new Info(); _map.insert(pair<string, Info*>(name, info)); _map2.insert(pair<long, string>(addrClient.sin_addr.S_un.S_addr + addrClient.sin_port, name)); } else { info = _map[name]; } info->port = ntohs(addrClient.sin_port); memcpy(info->ip, IPdotdec, strlen(IPdotdec)); memcpy(info->local, local, strlen(local)); cout << IPdotdec << ':' << local << ':'; cout << ntohs(addrClient.sin_port) << ':' << name << endl; } else if(recvBuf[0] == '0'&&recvBuf[1] == '0'&&recvBuf[2] == '2') { strtok_s(recvBuf, ",", &context); auto dist = _map.find(context); auto src = _map[_map2[addrClient.sin_addr.S_un.S_addr+addrClient.sin_port]]; if(dist==_map.end()) { continue; } char* ip1= dist->second->ip; char* ip2= src->ip; int port1= dist->second->port; int port2= src->port; if(strcmp(ip1,ip2)==0)//在同一路由下 { ip1 = dist->second->local; ip2 = src->local; port1 = 47240; port2 = 47240; } char buf[128]; sprintf_s(buf, "002,%s,%d", ip1, port1); sendto(sockSrv, buf, strlen(buf), 0, (SOCKADDR*)&addrClient, sizeof(addrClient));//告诉C1去尝试发送数据到C2,此时C1的NAT会有C1/C2相应的session,那么C2发数据C1就能收到 SOCKADDR_IN otherAddr; inet_pton(AF_INET, dist->second->ip, (void *)&otherAddr.sin_addr); otherAddr.sin_family = AF_INET; otherAddr.sin_port = htons(dist->second->port); char buf2[128]; sprintf_s(buf2, "002,%s,%d",ip2 , port2); sendto(sockSrv, buf2, strlen(buf2), 0, (SOCKADDR*)&otherAddr, sizeof(otherAddr));//去告诉C2去尝试发送数据到C1,同理 } } return 0; }
android client:
package com.ysykj.p2p; import android.content.Context; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class MainActivity extends AppCompatActivity { private BlockingQueue<String> _sendQueue=new LinkedBlockingQueue<>(); DatagramSocket socket= null; InetAddress serverAddress=null; int port=47240; String localAddr; private String GetLocalAddr(){ //获取wifi服务 WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); //判断wifi是否开启 if (wifiManager.isWifiEnabled()) { WifiInfo wifiInfo = wifiManager.getConnectionInfo(); int ipAddress = wifiInfo.getIpAddress(); return ((ipAddress & 0xff) + "." + (ipAddress >> 8 & 0xff) + "." + (ipAddress >> 16 & 0xff) + "." + (ipAddress >> 24 & 0xff)); } return "192.168.2.100"; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); localAddr=GetLocalAddr(); Button connBtn=(Button)findViewById(R.id.button); Button burrowBtn=(Button)findViewById(R.id.button2); final EditText nameEditText=(EditText)findViewById(R.id.editText); final EditText distEditText=(EditText)findViewById(R.id.editText2); connBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.i("ok",localAddr); _sendQueue.add("001,"+nameEditText.getText()+","+localAddr);//告诉server自己的name和本地ip,远程ip和端口server端是可以直接获取到的 } }); burrowBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { _sendQueue.add("002,"+distEditText.getText());//告诉server要和谁建立P2P通信 } }); try { serverAddress = InetAddress.getByName("1.1.1.1"); socket = new DatagramSocket(47240);//这个地方是指定本地发送、接收端口 } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } Thread thread1=new Thread(new Runnable() { @Override public void run() { try { while(true){ String str=_sendQueue.take(); byte[] data=(str+"\0").getBytes(); DatagramPacket packet = new DatagramPacket(data,data.length,serverAddress,port);//指定发送数据、远程IP、远程端口 socket.send(packet); } } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }); thread1.start(); Thread thread2=new Thread(new Runnable() { @Override public void run() { try { while(true) { byte[] data = new byte[1024]; DatagramPacket packet = new DatagramPacket(data, data.length); socket.receive(packet); String result = new String(packet.getData(), packet.getOffset(), packet.getLength()); if(result.charAt(0)=='0'&&result.charAt(1)=='0'&&result.charAt(2)=='2'){//收到Server发来的对方信息 String[] strs=result.split(","); serverAddress = InetAddress.getByName(strs[1]);//不再发送数据到Server,而是到C1或者C2 port=Integer.valueOf(strs[2]);//NAT上C1或C2的外网端口 Timer timer=new Timer(); timer.schedule(new TimerTask() { @Override public void run() { _sendQueue.add("003,"); } },1000,1000); } else if(result.charAt(0)=='0'&&result.charAt(1)=='0'&&result.charAt(2)=='3'){ Log.i("ok", "123");//验证P2P通信 } Log.i("ok", result+packet.getAddress()+packet.getPort()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }); thread2.start(); } }
xml布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:layout_editor_absoluteX="8dp" tools:layout_editor_absoluteY="8dp"> <EditText android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="请输入你的名称" android:inputType="textPersonName" /> <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="连接主服务器" tools:layout_editor_absoluteX="50dp" tools:layout_editor_absoluteY="97dp" /> <EditText android:id="@+id/editText2" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="请输入你要连接的名称" android:inputType="textPersonName" /> <Button android:id="@+id/button2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="开始打洞" /> <TextView android:id="@+id/textView2" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />