内网之间实现TCP通讯需要用到内网穿透技术,具体原理网上都有,参考:
https://blog.csdn.net/leisure512/article/details/4900191
https://blog.csdn.net/aaron133/article/details/79206257
TCP穿透成功的条件需要两边网络都是锥形NAT(或者至少一端网络是锥形NAT),具体可以参考
https://blog.csdn.net/h_armony/article/details/45167975
里面有给出各种NAT说明:
有公网IP的宽带:比如联通的ADSL,这类宽带会给每个用户分配一个公网IP,所以其NAT类型取决于用户所选用的路由器,大部分家用路由器都是端口限制锥型NAT;
无公网IP的宽带:比如宽带通,这类宽带给用户分配的是局域网IP,连接公网的NAT是运营商的,一般都是对称型NAT;
移动互联网:跟“无公网IP的宽带”类似,分配给手机的是局域网IP,出口基本都是对称型NAT;
大公司路由器:大部分都把路由器配置成对称型NAT。
这边使用VS2010 C#实现:
服务端代码:
static void Main(string[] args)
{
int port = 555;
IPEndPoint ipe = new IPEndPoint(IPAddress.Any, port);
Socket sSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sSocket.Bind(ipe);
sSocket.Listen(100);
Console.WriteLine("监听已经打开,请等待");
while (true)
{
Socket serverSocket1 = sSocket.Accept();
Console.WriteLine("连接已经建立");
string recStr = "";
byte[] recByte = new byte[4096];
int bytes = serverSocket1.Receive(recByte);
IPEndPoint ep1 = (IPEndPoint)serverSocket1.RemoteEndPoint;
Console.WriteLine(" from {0}", ep1.ToString());
recStr = Encoding.ASCII.GetString(recByte, 0, bytes);
Console.WriteLine("客户端1:{0}", recStr);
Socket serverSocket2 = sSocket.Accept();
bytes = serverSocket2.Receive(recByte);
IPEndPoint ep2 = (IPEndPoint)serverSocket2.RemoteEndPoint;
Console.WriteLine(" from {0}", ep2.ToString());
recStr = Encoding.ASCII.GetString(recByte, 0, bytes);
Console.WriteLine("客户端2:{0}", recStr);
byte[] sendByte =Encoding.ASCII.GetBytes(ep1.ToString() + ":" + ep2.ToString());
serverSocket1.Send(sendByte, sendByte.Length, 0);
sendByte = Encoding.ASCII.GetBytes(ep2.ToString() + ":" + ep1.ToString());
serverSocket2.Send(sendByte, sendByte.Length, 0);
serverSocket1.Close();
serverSocket2.Close();
}
}
功能:两边客户端连接服务器后将映射的外网IP和端口号传给双方。
客户端代码
static void Main(string[] args)
{
string host = "115.21.X.X";//服务端IP地址
int port = 555;
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//设置端口可复用
clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
//连接服务端
clientSocket.Connect(host, port);
Console.WriteLine("Connect:" + host + " " + port);
string data = "hello,Server!";
clientSocket.Send(Encoding.ASCII.GetBytes(data));
Console.WriteLine("Send:" + data);
byte[] recBytes = new byte[100];
//获取到双方的ip及端口号
int bytes = clientSocket.Receive(recBytes, recBytes.Length, 0);
string result = Encoding.ASCII.GetString(recBytes, 0, bytes);
Console.WriteLine("Recv:" +result);
clientSocket.Close();
string[] ips = result.Split(':');
int myPort = Convert.ToInt32(ips[1]);
string otherIp = ips[2];
int otherPort = Convert.ToInt32(ips[3]);
Socket mySocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
mySocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
//绑定到之前连通过的端口号
IPEndPoint ipe = new IPEndPoint(IPAddress.Any, Convert.ToInt32(myPort));
mySocket.Bind(ipe);
//尝试5次连接
for (int j = 0; j < 5; j++)
{
try
{
mySocket.Connect(otherIp, otherPort);
Console.WriteLine("Connect:成功{0},{1}", otherIp,otherPort);
break;
}
catch (Exception)
{
Console.WriteLine("Connect:失败");
// otherPort++;//如果是对称NAT,则有可能客户端的端口号已经改变,正常有规律的应该是顺序加1,可以尝试+1再试(我使用手机热点连接的时候端口号就变成+1的了)除非是碰到随机端口,那就不行了。
}
}
while (true)
{
mySocket.Send(Encoding.ASCII.GetBytes("hello,the other client!"));
byte[] recv = new byte[4096];
int len = mySocket.Receive(recv, recv.Length, 0);
result = Encoding.ASCII.GetString(recv, 0, len);
Console.WriteLine("recv :" + result);
Thread.Sleep(1000);
}
}
另一边客户端也一样。连接服务器后,可以绑定之前的端口号复用,但如果碰到一端是对称NAT时每次使用端口号会不一样时,这样就得通过预测下次可能使用的端口号来连通。如:使用手机热点网络连接服务器时,获取到它使用的端口号是56324,等到下一次客户端互相连接使用56325才连上。