我一直在研究需要TCP打孔的P2P应用程序。
我写了大部分代码(我相信)都是为了让客户端连接起来,不幸的是,由于他们无法连接,所以我做得不对。
我正在提供我正在使用的C#代码,它是ac#console应用程序,Server是用JavaScript编写的并使用NodeJS,我不会在这里包含代码,因为我没有遇到任何问题和它仅用于向客户发送彼此的详细信息。
如果IP标记为x.x.x.x,则它取代了输出的真实公共IP。
using System;
using System.Text;
using System.Threading.Tasks;
using System.Configuration;
using System.Net;
using System.Net.Sockets;
using System.Web.Script.Serialization;
namespace ScreenViewClientTest
{
class Program
{
public static string server_ip;
public static string server_port;
public static bool im_a = false;
public static bool im_b = false;
public static bool clients_are_connected = false;
public static void Main(string[] args)
{
server_ip = ConfigurationManager.AppSettings["server_ip"];
server_port = ConfigurationManager.AppSettings["server_port"];
Console.WriteLine("Connecting to " + server_ip + ":" + server_port + "...");
TcpClient client = new TcpClient();
client.Connect(server_ip, int.Parse(server_port));
if (client.Connected)
{
Console.WriteLine("Connection to server was successful!.");
}
IPEndPoint local_endpoint = ((IPEndPoint)client.Client.LocalEndPoint);
string local_ip = local_endpoint.Address.ToString();
int local_port = local_endpoint.Port;
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
string message_to_server = "{ \"local_ip\": \"" + local_ip + "\", \"local_port\": " + local_port + "}";
// send identity message to the server
client.Client.Send(Encoding.ASCII.GetBytes(message_to_server));
Console.WriteLine("Identity message (" + message_to_server + ") sent to server...");
// listen/get a response from the server.
NetworkStream stream = new NetworkStream(client.Client);
byte[] bytes = new byte[100]; // we assume length won't be more than 100.
int length = stream.Read(bytes, 0, 100);
string initial_response_from_server = "";
for (var i = 0; i < length; i++)
initial_response_from_server += Convert.ToChar(bytes[i]);
Console.WriteLine("Response from Server: " + initial_response_from_server);
// we assume it's going to take a while for the second client to connect, that's why we don't
// have a fixed message length for the initial response. (when implemented this must be changed).
// try to read the identity response from the server specifying which client I am, (=a= or =b=).
// we know the response is going to have a fixed length (3).
bytes = new byte[3];
length = stream.Read(bytes, 0, 3);
string identity_response = "My Client Identity is: ";
for (var i = 0; i < length; i++)
identity_response += Convert.ToChar(bytes[i]);
Console.WriteLine(identity_response);
if (identity_response.IndexOf("=a=") > -1)
{
im_a = true;
Console.WriteLine("I'm A.");
}
else
{
if (identity_response.IndexOf("=b=") > -1)
{
im_b = true;
Console.WriteLine("I'm B.");
}
}
// if we've establibshed that we're a valid client.
if (im_a || im_b)
{
// start listening for second client details
// this should be changed to check first for a message length header using a fixed length.
// second client details are returnes as a json string, so we need to desearialize it as an object.
bytes = new byte[150];
length = stream.Read(bytes, 0, 150);
string second_client_details = "";
for (var i = 0; i < length; i++)
second_client_details += Convert.ToChar(bytes[i]);
Console.WriteLine("Identity of second Client is: " + second_client_details);
// try and parse the data received from the server (should be the second client's nat address info).
JavaScriptSerializer serializer = new JavaScriptSerializer();
Address cliens_2_address = serializer.Deserialize<Address>(second_client_details);
Console.WriteLine("Client 2 Local Address: " + cliens_2_address.local_ip + ":" + cliens_2_address.local_port.ToString());
Console.WriteLine("Client 2 Remote Address: " + cliens_2_address.remote_ip + ":" + cliens_2_address.remote_port.ToString());
// close the connection to the server so the local port can be used.
if (client.Connected)
client.Close();
// start the listener
Task.Factory.StartNew(() => listen_on_local_port(local_port));
// start sending requests to the second clients local endpoint.
Task.Factory.StartNew(() => connect_to_client(cliens_2_address.local_ip, cliens_2_address.local_port, local_port));
// start sending requests to the second clients remote endpoint.
Task.Factory.StartNew(() => connect_to_client(cliens_2_address.remote_ip, cliens_2_address.remote_port, local_port));
// run the tasks async
Task.WaitAll();
}
// keeps the console window open.
Console.Read();
}
public static void listen_on_local_port(int local_port)
{
Console.WriteLine("Startint Listener...");
// start listening on the local port used to connect to the server for messages from the other client.
TcpListener server = null;
try
{
// Set the TcpListener on port 13000.
Int32 port = local_port;
// TcpListener server = new TcpListener(port);
server = new TcpListener(IPAddress.Parse("127.0.0.1"), port);
// Start listening for client requests.
server.Start();
// Buffer for reading data
Byte[] incoming_bytes = new Byte[256];
String data = null;
// Enter the listening loop.
while (!clients_are_connected)
{
// Perform a blocking call to accept requests.
// You could also user server.AcceptSocket() here.
TcpClient socket = server.AcceptTcpClient();
Console.WriteLine("Listening for connections on port: " + ((IPEndPoint)server.LocalEndpoint).Port.ToString() + "... ");
// does this really mean that someone connected?
if (socket.Connected && socket.Client.Connected)
{
Console.WriteLine("Someone connected to the socket!." + ((IPEndPoint)socket.Client.RemoteEndPoint).Address + ":" + ((IPEndPoint)socket.Client.RemoteEndPoint).Port.ToString());
clients_are_connected = true;
}
data = null;
// Get a stream object for reading and writing
NetworkStream net_stream = socket.GetStream();
int i;
// never seems to come threw to the other client.
string msg1 = "hello other person!!!!!!!!!!!!";
net_stream.Write(Encoding.ASCII.GetBytes(msg1), 0, msg1.Length);
// Loop to receive all the data sent by the client.
while ((i = net_stream.Read(incoming_bytes, 0, incoming_bytes.Length)) != 0)
{
// Translate data bytes to a ASCII string.
data = System.Text.Encoding.ASCII.GetString(incoming_bytes, 0, i);
Console.WriteLine("Received: {0}", data);
// Process the data sent by the client.
data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
// Send back a response.
net_stream.Write(msg, 0, msg.Length);
Console.WriteLine("Sent: {0}", data);
}
// Shutdown and end connection
socket.Close();
}
}
catch (SocketException e)
{
Console.WriteLine("SocketException: {0}", e);
}
finally
{
// Stop listening for new clients.
server.Stop();
}
}
public static void connect_to_client(string ip, int port, int local_port)
{
Console.WriteLine("Started trying to connect to Client: " + ip + ":" + port);
// try to connect to the second client on it's public port and local ip
while (!clients_are_connected)
{
TcpClient hole_punching_client = null;
try
{
// use local port used to connect to the server to connect yo the client.
IPEndPoint local = new IPEndPoint(IPAddress.Parse("127.0.0.1"), local_port);
hole_punching_client = new TcpClient("127.0.0.1", local_port);
hole_punching_client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
// when the below line is uncommented there's an error "An attempt was made to access a socket in a way forbidden by its access permissions"
//hole_punching_client.Client.Bind(local);
//Console.WriteLine("Trying to connect to the second client at address: " + ip + ":" + port);
Console.WriteLine("Real Hole Punching Client Port is: " + ((IPEndPoint)hole_punching_client.Client.LocalEndPoint).Port.ToString());
// connect to the second client using the address provided as parameters.
hole_punching_client.Connect(ip, port);
if (hole_punching_client.Connected)
{
Console.WriteLine("Connection to the other client was successful!.");
clients_are_connected = true;
}
}
catch (SocketException se)
{
Console.WriteLine("Socket Exception from Sender: " + se.Message);
System.Threading.Thread.Sleep(250);
}
catch (Exception ex)
{
Console.WriteLine("Exception from Sender: " + ex.Message);
}
finally
{
// program crashes when enabled.
//if (hole_punching_client.Connected)
//{
// hole_punching_client.Close();
//}
}
}
}
}
}
目前,当我测试代码时,一台客户端在我的PC上运行,而服务器和第二台客户端在VM上运行,因此一台客户机与服务器位于同一主机上,但客户端仍然连接到服务器使用服务器的公共地址。
这应该没有任何区别,因为我与服务器的通信没有任何问题,问题随着客户试图相互连接而到达。
以下是2个客户的输出: 客户A(首先开始):
Connecting to x.x.x.x:1994...
Connection to server was successful!.
Identity message ({ "local_ip": "192.168.1.137", "local_port": 52974}) sent to server...
Response from Server: Welcome new client!, your remote address is: ::ffff:x.x.x.x:52974.
My Client Identity is: =a=
I'm A.
Identity of second Client is: {"local_ip":"10.0.0.4","local_port":49754,"remote_ip":"::ffff:x.x.x.x","remote_port":1024}
Client 2 Local Address: 10.0.0.4:49754
Client 2 Remote Address: ::ffff:x.x.x.x:1024
Started trying to connect to Client: ::ffff:x.x.x.x:1024
Starting Listener...
Started trying to connect to Client: 10.0.0.4:49754
Real Hole Punching Client Port is: 53025
Listening for connections on port: 52974...
Someone connected to the socket!.127.0.0.1:53025
Socket Exception from Sender: A connect request was made on an already connected socket
Real Hole Punching Client Port is: 53024
Socket Exception from Sender: A connect request was made on an already connected socket
客户B:
Connecting to x.x.x.x:1994...
Connection to server was successful!.
Identity message ({ "local_ip": "10.0.0.4", "local_port": 49754}) sent to server...
Response from Server: Welcome new client!, your remote address is: ::ffff:x.x.x.x:1024.
My Client Identity is: =b=
I'm B.
Identity of second Client is {"local_ip":"192.168.1.137","local_port":52974,"remote_ip":"::ffff:x.x.x.x","remote_port":52974}
Client 2 Local Address: 192.168.1.137:52974
Client 2 Remote Address: ::ffff:x.x.x.x:52974
Starting Listener...
Started trying to connect to Client: 192.168.1.137:52974
Listening for connections on port: 49754...
Someone connected to the socket!.127.0.0.1:49755
Real Hole Punching Client Port is: 49755
Socket Exception from Sender: A connect request was made on an already connected socket
Started trying to connect to Client: ::ffff:x.x.x.x:52974
我无法弄清楚的第一件事是“在已连接的套接字上发出了连接请求”。
我认为可能的另一个问题是将侦听器和客户端绑定到同一本地端口。
需要发生的事情是我需要在关闭与服务器的连接之后监听与服务器连接中使用的本地端口(因为这是其他客户端将要获得的),同时我还需要尝试使用来自同一本地端口的请求连接到其他客户端公共和专用端点(我正在监听)。
我不认为我这样做是正确的,当我尝试绑定时会出现另一个错误,如代码中所示(注释)。
经过进一步检查后,我注意到虽然我应该发出来自本地端口的请求,但请求实际上来自不同的端口。
客户端A用于连接服务器的本地端口是52974,并且侦听器实际使用该端口(客户端A输出第14行),但是用于尝试连接到另一个客户端的端口在一个案例中为53025(线13)和一个案例53024(线17)。 (记住我们正在尝试连接到第二个客户端本地“和”远程端点,因此2次尝试)。
您可以在客户B的输出中看到相同内容。
令我困惑的另一件事是客户A输出的第15行和客户B输出的第13行,它表明有人成功连接到两个听众(在两个客户端上!),这是没有任何意义的,因为只要有人连接到监听器建立了P2P连接,并且其他客户端不需要继续监听。 (我使用Boolean clients_are_connected来验证)。
如果连接远程ip的客户端是本地IP地址是没有意义的,连接是否来自同一个客户端?我尝试注释掉试图连接到其他客户端本地的代码端点,所以它只尝试连接到其他客户端远程端点,但输出输出是相同的。
我很感激任何见解。
由于