// 本範例不適合用於對稱行網路,要解決此問題請先讓 Client 連線至 Server ,讓 Client 知道自己的 (公開/內部 IP ) 與其他人的 (公開/內部 IP ) ,再檢查自己公開 IP 是否與其他人的公開 IP 重複,如果有重複代表自己處於對稱行網路中,將使用內部 IP 進行通訊即可。
Java 伺服器程式碼:
package javaapplication1;
import java.net.*;
import java.util.ArrayList;
import java.util.HashMap;
public class JavaApplication1 {
public static void main(String[] args) throws Exception {
byte[] buffer = new byte[65507];
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
DatagramSocket ds = new DatagramSocket(5555); // Set Server Port
System.out.println("伺服器啟動於 : "
+ InetAddress.getLocalHost().getHostAddress() + ":" + ds.getLocalPort());
String msg = "No Message...";
HashMap map = new HashMap();
while (true) {
dp = new DatagramPacket(buffer, buffer.length);
ds.receive(dp);
msg = new String(dp.getData(), 0, dp.getLength());
String ipPort = dp.getAddress().getHostAddress() + ":" + dp.getPort();
// 只要一連線就會把 IP 放進 map 裡
map.put(ipPort, "");
System.out.println(ipPort + " 傳來的訊息 : " + msg);
// 回傳他們自己的 外網IP
dp = new DatagramPacket(ipPort.getBytes(), ipPort.length(), dp.getAddress(), dp.getPort());
ds.send(dp);
// 如果 2 個人上線了...
if (map.size() == 2) {
ArrayList a = new ArrayList();
for (Object ip_Port : map.keySet()) {
a.add(ip_Port.toString());
}
for (Object ip_Port : map.keySet()) {
String temp = "";
for (int i = 0; i < a.size(); i++) {
// 如果現在這個IP不等於之前存放在 map 裡的IP
// 簡單來說就是只要獲取對方的IP,並不需要用到自己的IP
if (!a.get(i).equals(ip_Port)) {
temp += a.get(i);
}
}
// 為每個連線端發送對方的 IP:Port
dp = new DatagramPacket(temp.getBytes(), temp.length(), getIP(ip_Port), getPort(ip_Port));
ds.send(dp);
}
}
}
}
static InetAddress getIP(Object ipPort) throws UnknownHostException {
return InetAddress.getByName(ipPort.toString().split(":")[0]);
}
static int getPort(Object ipPort) {
return Integer.valueOf(ipPort.toString().split(":")[1]);
}
}
C# 客戶端程式碼:
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
class MyClient
{
public static UdpClient uc = null;
public static IPEndPoint otherIP = null;
static void Main(string[] args)
{
// 伺服器的 IP 與 Port
IPEndPoint servrIP = new IPEndPoint(IPAddress.Parse("122.121.9.25"), 5555);
// 自訂要監聽的 Port
IPEndPoint myIP = new IPEndPoint(IPAddress.Any, 4444);
uc = new UdpClient(myIP.Port);
string receive;
byte[] b;
// 向伺服器傳資料:
b = System.Text.Encoding.UTF8.GetBytes("Hello Server");
uc.Send(b, b.Length, servrIP);
// 從伺服器取得目前電腦的IP:
string myPublicIP = System.Text.Encoding.UTF8.GetString(uc.Receive(ref servrIP));
Console.WriteLine("目前電腦的IP:" + myPublicIP);
Console.WriteLine("\n|-----------------------------------|\n");
// 從伺服器取得對方IP:
receive = System.Text.Encoding.UTF8.GetString(uc.Receive(ref servrIP));
otherIP = new IPEndPoint(IPAddress.Parse(receive.Split(':')[0]), int.Parse(receive.Split(':')[1]));
// 打洞:
b = System.Text.Encoding.UTF8.GetBytes("Hi");
uc.Send(b, b.Length, otherIP);
// 為監聽{uc.Receive()}建立一條執行緒:
// ( 1.接收/2.發送 兩種功能建議個別建立執行緒
// 如果沒有建立執行緒程式可能會鎖死 )
new Thread(new MyReceiveThreadClass().MyRun).Start();
// 傳送真正的資料:
int i = 0;
while (true)
{
b = System.Text.Encoding.UTF8.GetBytes("這是 " + myPublicIP + " 資料 : " + i++);
uc.Send(b, b.Length, otherIP);
//Thread.Sleep(1000);
}
}
}
class MyReceiveThreadClass
{
public void MyRun()
{
while (true)
{
string receive = System.Text.Encoding.UTF8.GetString(MyClient.uc.Receive(ref MyClient.otherIP));
Console.WriteLine(receive);
}
}
}
伺服器輸出結果:
客戶端 1 輸出結果:
客戶端 2 輸出結果:
C# Unity 版本:
using UnityEngine;
using System.Collections;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
public class MyClient : MonoBehaviour {
public static UdpClient uc = null;
public static IPEndPoint otherIP = null;
byte[] b;
string myPublicIP = "No";
string OtherSidePublicIP = "No";
string sendMsg = "No";
string receiveMsg = "No";
bool isRun = true;
// Unity Override Methods ========================================================================
void Awake () {
new Thread(P2P_Init).Start();
}
void Update(){
SendData ();
}
void OnGUI(){
GUILayout.Label ("本機電腦的IP:" + myPublicIP);
GUILayout.Label ("對方電腦的IP:" + myPublicIP);
GUILayout.Label ("送出的訊息:" + sendMsg);
GUILayout.Label ("接收的訊息:" + receiveMsg);
}
void OnDisable(){
isRun = false;
if(uc != null){
uc.Close();
}
}
// Custom Methods ===============================================================================
void P2P_Init(){
// 伺服器的 IP 與 Port
IPEndPoint servrIP = new IPEndPoint(IPAddress.Parse("219.85.221.13"), 5555);
// 自訂要監聽的 Port
IPEndPoint myIP = new IPEndPoint(IPAddress.Any, new System.Random().Next(40000, 50000));
uc = new UdpClient(myIP.Port);
// 向伺服器傳資料:
b = System.Text.Encoding.UTF8.GetBytes("Hello Server");
uc.Send(b, b.Length, servrIP);
// 從伺服器取得目前電腦的IP:
myPublicIP = System.Text.Encoding.UTF8.GetString(uc.Receive(ref servrIP));
// 從伺服器取得對方IP:
OtherSidePublicIP = System.Text.Encoding.UTF8.GetString(uc.Receive(ref servrIP));
otherIP = new IPEndPoint(IPAddress.Parse(OtherSidePublicIP.Split(':')[0]), int.Parse(OtherSidePublicIP.Split(':')[1]));
// 提高打洞成功機率,我連續發送10次,畢竟是 UDP ..
for(int i = 0; i< 10; i++){
// 打洞:
b = System.Text.Encoding.UTF8.GetBytes("Hi");
uc.Send(b, b.Length, otherIP);
}
new Thread(ReceiveData).Start();
}
int i = 0;
void SendData(){
if(otherIP!=null){
sendMsg = "這是 " + myPublicIP + " 資料 : " + i++;
b = System.Text.Encoding.UTF8.GetBytes(sendMsg);
uc.Send(b, b.Length, otherIP);
}
}
void ReceiveData(){
while (isRun){
try{
string receive = System.Text.Encoding.UTF8.GetString(MyClient.uc.Receive(ref otherIP));
receiveMsg = receive;
}catch(Exception e){
print(e.Message);
}
}
}
}

为什么我用java把你的c#进行翻译,peer无法进行通讯~
我用兩台電腦實際跑是OK的,這個至少要一個伺服器+兩個客戶端程式 伺服器使用 Java 編譯,客戶端使用C#編譯 再試試看吧~
不好意思,因為沒有學過C#語言,在建立連線的部分不知道怎麼改成JAVA版的,可不可以麻煩大大教一下>"<
伺服器這裡是Java,客戶端採C#。 基本原理很簡單:就是先讓兩個客戶端登入伺服器,伺服器取得兩邊的IP後,互相把對方的IP給客戶端,之後客戶端再互相發送對方的IP,在第一次發送的時候基本上會失敗,因為會被擋下來(安全性問題),但是第二次是由自己網對方IP發送後,對方IP送進來的資料就會被傳進來,之後兩邊就可以互相發送訊息了,不需要透過伺服器。 例如:有A、B兩個客戶端,與 S 伺服器 1. A、B 先登入 S 伺服器 2. S 伺服器把 A 的 IP 傳給 B,B 的 IP 傳給 A 3. A 對 B 發送訊息, B 對 A 發送訊息 (這一次基本上會傳失敗,不過已打洞) 4. A 對 B 發送訊息, B 對 A 發送訊息 (這一次就會成功) 上面說明的通訊協定都是使用 UDP ,客戶端可以參考 這篇伺服器的寫法,或者Google 搜尋一些UDP範例後,再套用這概念就可以。祝你成功~
那java client的部分要如何自訂監聽的port? 這部分不知道如何改>"<
如果客戶端是 "公開 IP" 那麼取得自己 IP 就會是正確的,但是大多數的客戶端都有裝 IP 分享器、交換器等等,這時寫程式寫取得自己的IP會是內部IP,例如:192.168.1.10 之類的....取得這種 IP 外面是無法連線進來的 (切記切記...這很重要,一定要清楚)。 最好的辦法就是向伺服器隨便發送資料,然後伺服器取得你的IP,之後再傳給自己 (如果你要打洞 (NAT映射) 的話,就要傳給對方)。我上面程式碼也是這樣寫,基本上不管你是 公開 IP 還是內部 IP 直接從伺服器取得自己的 IP 是大多數應用程式的做法,否則取到的的IP不一定是正確的。 再試試看吧,有問題歡迎發問,祝你成功~
我用 Unity 的 MasterServer 作 P2P 媒合,但是遇到路由器類型如果是 symmetric 似乎就無法打洞成功,請問大大有沒有別的看法呢?
想請問如果伺服器處於內網的狀態,想知道該如何寫讓客戶端有辦法。