问题
I've been working on a P2P application which requires TCP Hole Punching.
I've written most of the code (I believe) up to getting the clients to connect, unfortunately there's something I'm not doing right since they won't connect.
I'm providing the C# code I'm using, it's a c# console application, the Server is written in JavaScript and uses NodeJS, I won't include the code here since I'm not having any issues with it and it's only being used to send the Clients each others details.
Where an IP is marked x.x.x.x it's a replacement to the real Public IP that was outputted.
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();
//}
}
}
}
}
}
Currently when I'm testing the code, one client is run on my PC and the Server and second Client are run on a VM, so one client is on the same host as the server, but the client is still connecting to the server using the server's public address.
This shouldn't make any difference since I'm not having any issues with the communication with the server, the issues arrive with clients trying to connect to each other.
Here's the output of the 2 clients: Client A (started first):
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
Client 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
The first thing I can't figure out is the Exception "A connect request was made on an already connected socket".
One other thing I presume might be the issue is binding the listener and client(s) to the same local port.
What needs to happen is I need to listen on the local port used in the connection to the server (since that's what the other client is going to get) after closing the connection to the server, at the same time I also need to try to connect to the other clients public and private endpoints with requests originating from the same local port (I'm listening on).
I don't think I'm doing that correctly, when I try to bind there's another error as seen in the code (commented).
After further inspection I noticed that while I'm supposed to make requests originating from the local port, the requests actually originate from a different port.
Client A's local port used to connect to the server is 52974, and the listener actually uses that port (Client A output Line 14), but the one being used to try to connect to the other client is in one case 53025 (Line 13) and in one case 53024 (Line 17). (remember we're trying to connect to the second clients local "and" remote endpoints, therefore the 2 tries).
You can see the same in the Client B's output.
Another thing that's keeping me puzzled is Line 15 of Client A's output and Line 13 of Client B's output, it states that someone connected successfully to both listeners (on both clients!) that doesn't make any sense since as soon as someone connects to the listener a P2P connection is established and there's no need for the other client to keep listening. (I use the Boolean clients_are_connected to verify that).
What's more if the client that's connecting remote ip is a local ip address which makes no sense, is it possible the connection is coming from the same client?, I tries commenting out the code which tries to connect to the other client's local endpoint so it only tries to connect to the other clients remote endpoint, but the output the output was the same.
I'd appreciate any insight.
Thanks
来源:https://stackoverflow.com/questions/37930824/c-sharp-tcp-hole-punching-clients-not-connecting