文章目录
- 前言
- 一、局域网通信
- 1.1 基本原理和方法
- 1.1.1 获取本地ip
- 1.1.2 实现局域网内的广播
- 1.1.3 进行局域网通信
- 1.2 实现多客户端连接
- 1.3 Python源码
- 1.4 C#源码
- 1.5 可能存在的问题
- 二、互联网通信
- 2.1 实现原理
- 2.1.1 内网穿透软件
- 2.1.2 实现互联网通信
- 2.2 Python源码
- 2.3 C#源码
- 结语
前言
本文整合了在局域网和互联网两种情况下的通信都应该怎么实现。网上的资料大多在教学socket
使用时只会教学怎么实现局域网通信,导致还需要搜很多额外的资料才能接触到互联网通信。
这里需要注意,个人电脑上可以自己和自己通信成功,不代表代码放到其他计算机上就可以成功实现局域网或互联网通信,有条件一定要尝试两个不同计算机的通信。互联网通信如果不确定是不是依旧是使用的局域网通信方法,可以将一台计算机连路由器,一台计算机连手机热点(总之不是一个局域网内即可)。
一、局域网通信
1.1 基本原理和方法
1.1.1 获取本地ip
获取本地IP的方法有很多,这里介绍三种方法。分别是cmd中查看、python和C#调用函数查看。
1、cmd查看本地IP
此种方法有较大的局限性,因为本地ip在每次的分配过程中有可能会改变,想要每次都连接上对方的计算机需要每次都修改成当前的本地IP地址,添加了不必要的工作量。当然,如果只是初学做一个实验测试还是可以使用的。
1)首先win + R打开“运行”,在搜索框输入cmd。
2)点击确定后进入cmd命令窗口,输入ipconfig
并回车执行命令,就可以得到结果。
可以看到图中标注红框的部分,192.168.0.103就是本机的本地IP。
2、python查看本地IP
由于本地IP每次分配是有很大可能是会变化的,所以大部分应用场景需要程序中直接获取,而不是在程序中写一个既定的IP地址。
import socket
ip = socket.gethostbyname(socket.gethostname())
socket.gethostname()
将返回本机名称,socket.gethostbyname()
参数放入本机名称后就会返回本地IP。
3、C#查看本地IP
using System.Net;
// 主机名
string hostName = Dns.GetHostName();
// 获取本机本地ip
IPAddress address = Dns.GetHostAddresses(hostName)[1];
步骤与python是类似的,就不多解释了。
注:其实也可以用python和C#的系统调用,调用cmd里输入的命令获取返回值。
1.1.2 实现局域网内的广播
在知道如何获取本机的本地IP之后,需要一种方法将服务端的IP地址告诉客户端,这个时候就需要用到广播技术(因为不通过广播告诉客户端服务端的IP地址,客户端将无从得知应该连接哪一个IP)。首先服务端要不断的广播,将自己的IP地址广播出去,随后客户端要接听广播。当客户端收到广播之后,再给服务端广播,告知服务端已经收到了服务端的广播。这时服务端进入监听阶段,服务端进入连接阶段。连接成功后就可以开始通信了。
整个流程如图所示:
当然,上述这种方式是线性的流程,只适用于有一个客户端时候的通信。之后再讲一种可以连接多个客户端的方法。
1、python进行广播的方法:
1)先用subprocess
库获取子网掩码
import subprocess
subnet_mask = subprocess.getoutput("ipconfig | findstr 子网掩码")
subnet_mask = subnet_mask.split(":")[-1].strip()
2)根据本地IP和子网掩码获取广播地址
import ipaddress
network = ipaddress.ip_network(f"{self.ip}/{self.subnet_mask}", strict=False)
broadcast_address = network.broadcast_address
3)进行广播
# 创建一个socket对象, 参数表示使用IPV4和TCP协议
broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置广播选项
# socket.SOL_SOCKET表示选项的级别是socket级别, 这意味着这个选项将应用于socket本身, 而不是特定的协议
# socket.SO_BROADCAST表示要设置的选项是广播选项, 决定了socket是否可以发送广播消息
# 1表示启用广播选项
broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 发送广播信息
broadcast_socket.sendto(info.encode("utf-8"), (self.broadcast_ip, self.port))
接收广播
broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 绑定到一个特定的端口
broadcast_socket.bind(("", broadcast_port))
# 接收广播信息
data, addr = broadcast_socket.recvfrom(1024)
2、C#进行广播的方法
1)获取子网掩码
using System.Net.NetworkInformation;// 获取所有网络接口
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
// 遍历所有网络接口
foreach (var networkInterface in networkInterfaces) {// 获取IP属性var ipProperties = networkInterface.GetIPProperties();// 获取单播地址var unicastAddresses = ipProperties.UnicastAddresses;// 获取IPv4单播地址var ipv4UnicastAddresses = unicastAddresses.Where(x => x.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork);foreach (var unicastAddress in ipv4UnicastAddresses) {// 如果这个地址是本机的本地IP地址if (unicastAddress.Address.ToString() == ip) {// 获取子网掩码var subnetMask = unicastAddress.IPv4Mask;return subnetMask.ToString();}}
}
还有一种使用System.Management包的方法,代码量会低一些,但是这种方法只支持windows操作系统,所以这里给出的是另一种方法。
2)计算广播地址
string broadcastAddress = "";
// 分割IP地址和子网掩码
string[] ipArray = ip.Split('.');
string[] subnetMaskArray = subnetMask.Split('.');
// 计算广播地址
for (int i = 0; i < 4; i++) {int ipInt = Convert.ToInt32(ipArray[i]);int subnetMaskInt = Convert.ToInt32(subnetMaskArray[i]);// 广播地址 = IP地址 | (~子网掩码 & 0xff)int broadcastInt = ipInt | (~subnetMaskInt & 0xff);// 拼接广播地址broadcastAddress += broadcastInt.ToString() + ".";
}
broadcastAddress = broadcastAddress.Substring(0, broadcastAddress.Length - 1);
3)进行广播
// 创建UDP客户端
UdpClient udpClient = new UdpClient();
// 允许发送广播
udpClient.EnableBroadcast = true;
// 广播地址
IPEndPoint broadcastPoint= new IPEndPoint(broadcastAddress , 8080);// 要发送的数据
byte[] bytes = Encoding.ASCII.GetBytes(info);
while (toBroadcast) {// 发送数据broadcastClient.Send(bytes, bytes.Length, broadcastPoint);// 等待1秒System.Threading.Thread.Sleep(delay);
}
接收广播
UdpClient udpClient = new();
IPEndPoint endPoint = new(IPAddress.Any, 8080);
// 绑定本地端口
udpClient.Client.Bind(endPoint);while (true) {// 接收广播byte[] bytes = udpClient.Receive(ref endPoint);string message = Encoding.ASCII.GetString(bytes);Console.WriteLine($"接收到广播: {message} 来自: {endPoint.Address}:{endPoint.Port}");
}
注:端口号是需要提前设定好的,是用来区别应用程序的标志。
1.1.3 进行局域网通信
1、python实现局域网通信
服务端需要一个socket
对象,再让其绑定到本地IP和一个端口上,之后监听就可以了。如果需要发送消息就对连接上的客户端发消息,需要接收就接收。
import socket
# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到一个特定的端口
server_socket.bind((ip, post))
# 开始监听连接, 参数表示最大连接数
server_socket.listen(max_connections)
print("服务器已启动,等待连接...")while True:# 接受一个连接, 返回一个客户的socket对象和客户端的IP地址client_socket, client_address = server_socket.accept()print(f"客户端{client_address}已连接")# 接收数据, 参数表示最大接收字节数data = client_socket.recv(1024)print(f"{client_address}接收到数据:{data.decode('utf-8')}")# 发送反馈client_socket.send("数据已接收".encode('utf-8'))
客户端需要连接服务端,连接上之后就可以发送和接收消息。
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 尝试连接服务器
client_socket.connect((server_ip, server_post))
2、C#实现局域网通信
服务端
// 创建服务端, TcpListener是采用TCP协议的监听器
// UDP协议的监听器是UDPListener
TcpListener server = new(IPAddress.Parse(serverIp), serverPost);
server.Start();
Console.WriteLine("服务器已启动,等待连接...");while (true) {// 接收客户端连接TcpClient client = server.AcceptTcpClient();Console.WriteLine("客户端已连接");// 获取客户端的网络流NetworkStream stream = client.GetStream();byte[] buffer = new byte[1024];// 读取客户端发送的数据int bytesRead = stream.Read(buffer, 0, buffer.Length);string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);Console.WriteLine($"接收到数据:{data}");// 反馈信息byte[] response = Encoding.UTF8.GetBytes("数据已接收");stream.Write(response, 0, response.Length);// 结束连接client.Close();
}
客户端
// 创建客户端, TCPClient是采用TCP协议的客户端
// UDP协议的客户端是UDPClient
TcpClient client = new("192.168.0.103", 8888);
// 发送数据给服务器
NetworkStream stream = client.GetStream();
byte[] data = Encoding.UTF8.GetBytes("你好, 服务器");
stream.Write(data, 0, data.Length);
// 接收服务器的响应
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"接收到服务器的响应:{response}");
// 关闭连接
client.Close();
1.2 实现多客户端连接
在1.1中,讲述了如何实现最简单的单客户端和服务端的连接,而想要实现多客户端连接,使用多线程技术比较容易一些。这里将广播、监听、接收和发送信息这三个功能分别创建了一个线程。如果有其他需求,比如想要接受和发送分开或者要修改什么内容之类的,可以自行设置。在设计某些功能时候可能会用到更多的多线程相关知识,这里就不做解释了。如果想要详细学习多线程,请移步到【Python】Python多线程详解和C#高级–多线程详解等教程。
分为三个线程后,就可以一边广播,一边监听,一边接收发送信息。这样就互不影响了,也就可以让多个客户端连接上服务端了。具体的代码实现见1.3 Python源码和1.4 C#源码
1.3 Python源码
源码分为服务端和客户端代码。下面是服务端代码
import time
import socket
import threading
import ipaddress
import subprocessclass Server:def __init__(self) -> None:# 创建一个socket对象, 参数表示使用IPV4和TCP协议# IPV6使用AF_INET6, UDP使用SOCK_DGRAMself.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 广播用socket对象self.broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 设置广播选项self.broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)# 获取本机的IP地址self.ip = socket.gethostbyname(socket.gethostname())# 子网掩码self.subnet_mask = self.get_subnet_mask()# 广播地址self.broadcast_ip = self.get_broadcast_ip()# 端口号self.broadcast_port = 8080self.server_port = 8081# 客户端列表self.clients = {}# 最大连接数self.max_connections = 1# 是否要广播self.to_broadcast = True# 是否要接收客户连接self.to_accept = Truedef get_subnet_mask(self):"""获取子网掩码"""subnet_mask = subprocess.getoutput("ipconfig | findstr 子网掩码")return subnet_mask.split(":")[-1].strip()def get_broadcast_ip(self):"""获取广播地址"""# 计算广播地址network = ipaddress.ip_network(f"{self.ip}/{self.subnet_mask}", strict=False)return str(network.broadcast_address)def broadcast(self, info: str, delay=1):"""进行广播"""while self.to_broadcast:# 发送广播消息self.broadcast_socket.sendto(info.encode("utf-8"), (self.broadcast_ip, self.broadcast_port))# 等一段时间time.sleep(delay)def accept_client(self):"""启动服务器"""# 绑定到一个特定的端口self.server_socket.bind((self.ip, self.server_port))# 开始监听连接, 参数表示最大连接数self.server_socket.listen(self.max_connections)print("服务器已启动,等待连接...")while self.to_accept:# 接受一个连接, 返回一个客户的socket对象和客户端的IP地址client_socket, client_address = self.server_socket.accept()self.clients[client_address] = client_socketprint(f"客户端{client_address}已连接")# 如果连接数达到最大连接数, 则不再接受连接if len(self.clients) == self.max_connections:self.to_accept = Falseprint("已达到最大连接数")time.sleep(0.01)def receive_data(self):"""接收数据并发送反馈"""while True:for client_address, client_socket in self.clients.items():# 接收数据, 参数表示最大接收字节数data = client_socket.recv(1024)print(f"{client_address}接收到数据:{data.decode('utf-8')}")# 发送反馈client_socket.send("数据已接收".encode('utf-8'))time.sleep(0.01)def run(self):"""运行服务器"""# 广播线程broadcast_thread = threading.Thread(target=self.broadcast, args=(self.ip, 1))# 客户端连接线程accept_thread = threading.Thread(target=self.accept_client)# 接收数据线程receive_thread = threading.Thread(target=self.receive_data)# 启动线程broadcast_thread.start()accept_thread.start()receive_thread.start()if __name__ == "__main__":s = Server()s.run()
下面是客户端代码
import time
import socket
import threadingclass Client:def __init__(self):# 创建一个UDP socketself.broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 设置socket的广播选项self.broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)# 创建一个TCP socketself.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 端口号self.broadcast_port = 8080self.server_post = 8081# 服务端地址self.server_ip = Nonedef receive_broadcast(self):"""接收广播消息"""# 绑定到一个特定的端口self.broadcast_socket.bind(("", self.broadcast_port))while self.server_ip is None:# 接收广播消息data, addr = self.broadcast_socket.recvfrom(1024)print(f"接收到消息: {data} 来自: {addr}")# 设置服务端地址if data.decode("utf-8") == addr[0]:self.server_ip = addr[0]# 暂停一段时间time.sleep(0.01)def connect_server(self):"""连接服务器"""# 等待接收到广播消息while self.server_ip is None:time.sleep(1)# 连接服务器self.client_socket.connect((self.server_ip, self.server_post))print("连接服务器成功")while True:data = self.client_socket.recv(1024)if data:print(f"接收到数据: {data.decode('utf-8')}")else:print("服务器断开连接")breakdef run(self):"""运行客户端"""receive_broadcast_thread = threading.Thread(target=self.receive_broadcast)connect_server_thread = threading.Thread(target=self.connect_server)receive_broadcast_thread.start()connect_server_thread.start()if __name__ == "__main__":client = Client()client.run()
代码当中服务端的监听和广播都是在不符合要求后就会直接退出函数的,也就是说假如有客户端退出后也不会再次运行这两个函数。如果想要持续监听和广播,可以改为如下的形式:
def broadcast(self, info: str, delay=1):"""进行广播"""while True:# 不需要广播就暂停1秒后再查看需不需要广播if not self.to_broadcast:time.sleep(1)# 发送广播消息self.broadcast_socket.sendto(info.encode("utf-8"), (self.broadcast_ip, self.broadcast_port))# 等一段时间time.sleep(delay)
监听也是如此,将外部条件改为无条件循环,内层再进行判别。
1.4 C#源码
服务端代码
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Net.NetworkInformation;class Server {// 服务端public TcpListener server;// 广播套接字public UdpClient broadcastClient;// 本地IP地址public string ip;// 广播地址public string broadcastAddress;public IPEndPoint broadcastPoint;// 连接了的客户端public TcpClient[] clients;// 最大连接数量public int maxClient;// 是否要广播public bool toBroadcast;// 是否要接收客户连接public bool toAccept;public Server() {ip = Convert.ToString(Dns.GetHostAddresses(Dns.GetHostName())[1]);// 创建服务端, TcpListener是采用TCP协议的监听器// UDP协议的监听器是UDPListenerserver = new(IPAddress.Parse(ip), 8081);// 获取广播地址broadcastAddress = GetBroadcastAddress(ip, GetSubnetMask());// 创建广播套接字broadcastPoint = new IPEndPoint(IPAddress.Parse(broadcastAddress), 8080);broadcastClient = new UdpClient();// 启用广播broadcastClient.EnableBroadcast = true;// 最大连接数量maxClient = 1;clients = new TcpClient[maxClient];toBroadcast = true;toAccept = true;}// 获取本地IP子网掩码public string GetSubnetMask() {// 获取所有网络接口var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();// 遍历所有网络接口foreach (var networkInterface in networkInterfaces) {// 获取IP属性var ipProperties = networkInterface.GetIPProperties();// 获取单播地址var unicastAddresses = ipProperties.UnicastAddresses;// 获取IPv4单播地址var ipv4UnicastAddresses = unicastAddresses.Where(x => x.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork);foreach (var unicastAddress in ipv4UnicastAddresses) {// 如果这个地址是本机的本地IP地址if (unicastAddress.Address.ToString() == ip) {// 获取子网掩码var subnetMask = unicastAddress.IPv4Mask;return subnetMask.ToString();}}}return "";}// 根据子网掩码和IP地址获取广播地址public string GetBroadcastAddress(string ip, string subnetMask) {string broadcastAddress = "";// 分割IP地址和子网掩码string[] ipArray = ip.Split('.');string[] subnetMaskArray = subnetMask.Split('.');// 计算广播地址for (int i = 0; i < 4; i++) {int ipInt = Convert.ToInt32(ipArray[i]);int subnetMaskInt = Convert.ToInt32(subnetMaskArray[i]);// 广播地址 = IP地址 | (~子网掩码 & 0xff)int broadcastInt = ipInt | (~subnetMaskInt & 0xff);// 拼接广播地址broadcastAddress += broadcastInt.ToString() + ".";}return broadcastAddress.Substring(0, broadcastAddress.Length - 1);}// 进行广播public void Broadcast(string info, int delay=1000) {// 要发送的数据byte[] bytes = Encoding.ASCII.GetBytes(info);while (toBroadcast) {// 发送数据broadcastClient.Send(bytes, bytes.Length, broadcastPoint);// 等待1秒System.Threading.Thread.Sleep(delay);}}// 接收客户端连接public void AcceptClient() {// 开始监听server.Start();Console.WriteLine("服务器已启动,等待连接...");// 接收客户端连接while (toAccept) {// 接收客户端连接TcpClient client = server.AcceptTcpClient();// 添加到客户端列表for (int i = 0; i < clients.Length; i++) {if (clients[i] == null) {clients[i] = client;break;}}Console.WriteLine($"客户端{client.Client.RemoteEndPoint}已连接");// 如果连接数超过最大连接数int count = 0;for (int i = 0; i < maxClient; i++) {if (clients[i] != null) {count++;}}if (count > maxClient) {Console.WriteLine("连接数超过最大连接数");toAccept = false;break;}// 暂停0.01秒System.Threading.Thread.Sleep(10);}}// 接收数据并反馈public void ReceiveData() {// 接收数据while (true) {foreach (TcpClient client in clients) {if (client == null) {continue;}// 获取网络流NetworkStream stream = client.GetStream();// 接收数据byte[] bytes = new byte[1024];int length = stream.Read(bytes, 0, bytes.Length);string data = Encoding.ASCII.GetString(bytes, 0, length);Console.WriteLine($"接收到来自{client.Client.RemoteEndPoint}的数据:{data}");// 发送数据byte[] msg = Encoding.ASCII.GetBytes("已收到数据");stream.Write(msg, 0, msg.Length);}}}// 运行服务器public void Run() {// 广播线程Thread broadcastThread = new(() => Broadcast(ip, 1000));// 接收客户端连接线程Thread acceptThread = new(AcceptClient);// 接收数据线程Thread receiveThread = new(ReceiveData);// 启动线程broadcastThread.Start();acceptThread.Start();receiveThread.Start();}
}class MainClass {static void Main() {Server server = new();server.Run();}
}
客户端代码
using System.Net;
using System.Text;
using System.Net.Sockets;class Client {// 客户端public TcpClient client;// 广播套接字public UdpClient broadcastClient;// 广播地址public IPEndPoint broadcastPoint;// 服务端IP地址public string serverIp;public Client() {client = new TcpClient();// 创建广播套接字并启用广播broadcastClient = new UdpClient();broadcastClient.EnableBroadcast = true;// 设置广播地址broadcastPoint = new(IPAddress.Any, 8080);// 设置服务端IP地址serverIp = "";}// 接收广播消息public void ReceiveBroadcast() {// 绑定广播地址broadcastClient.Client.Bind(broadcastPoint);while (serverIp == "") {// 接收广播消息byte[] data = broadcastClient.Receive(ref broadcastPoint);// 设置服务端IP地址serverIp = Encoding.UTF8.GetString(data);// 输出服务端IP地址Console.WriteLine("获取到服务端ip地址: " + serverIp);}}// 连接服务端public void ConnectServer() {// 等待接收广播消息while (serverIp == "") {Thread.Sleep(1000);}// 连接服务端client.Connect(serverIp, 8081);// 输出连接成功Console.WriteLine("连接成功");// 接收数据while (true) {byte[] bytes = new byte[1024];int length = client.GetStream().Read(bytes, 0, bytes.Length);string data = Encoding.UTF8.GetString(bytes, 0, length);Console.WriteLine("接收到来自服务端的数据:" + data);}}// 运行客户端public void Run() {// 接收广播消息线程Thread receiveBroadcastThread = new(() => ReceiveBroadcast());Thread connectServerThread = new(() => ConnectServer());receiveBroadcastThread.Start();connectServerThread.Start();}
}class Program {static void Main(string[] args) {Client client = new();client.Run();}
}
1.5 可能存在的问题
局域网通信存在可能无法通信的情况,即服务端开始监听后,客户端却连接不到服务端。这种情况首先需要检查是否在同一个路由器的公网IP下,并要确认是否处于同一个子网。随后再查看防火墙的设置,有时候防火墙会阻止通信,可以关掉防火墙试试。
检查公网IP的方法。这里依旧是三种方法。
1、cmd中查看公网IP
输入curl 4.ipw.cn
,执行后便会返回一个IP地址的结果,该结果便是公网ip。
图中马赛克部分就是我的公网IP了。
2、python查看公网IP
import subprocess
ip = subprocess.getoutput("curl 4.ipw.cn").split("\n")[-1].strip()
3、C#查看公网IP
// 获取公网IP所需的URL
string url = "http://checkip.dyndns.org";
// 请求URL
WebRequest request = WebRequest.Create(url);
// 获取响应
WebResponse response = request.GetResponse();
// 读取响应流
StreamReader stream = new StreamReader(response.GetResponseStream());
// 读取返回的HTML
string publicIP = stream.ReadToEnd();// 清理返回的HTML标签
int first = publicIP.IndexOf("Address: ") + 9;
int last = publicIP.LastIndexOf("</body>");
// 获取公网IP
publicIP = publicIP.Substring(first, last - first);
确认一致后再确认是否处于一个子网中,依旧是输入ipconfig
来检查。
这里可以看到,本地IP为192.168.0.103,子网掩码为255.255.255.0,那么根据小学三年级学过的数学知识将其转换为二进制(如果嫌麻烦可以使用windows自带的程序员计算器):
1100 0000.1010 1000.0000 0000.0110 0111
1111 1111.1111 1111.1111 1111.0000 0000
上下对应着做与运算,就可以得到结果了:
1100 0000.1010 1000.0000 0000.0000 0000
也就是说我当前处于的子网是192.168.0.0。相同步骤去检查另一台计算机,如果一致,那大概率是防火墙阻止了,这时就应该试试关闭防火墙。关闭防火墙就不详细讲了,直接上链接。
Win10怎么关闭防火墙,Win10系统防火墙关闭的方法
Win11关闭防火墙,5种不同路径,总有一款适合你
二、互联网通信
2.1 实现原理
互联网上的通信稍微复杂些,因为内网是无法直接在互联网上通信的,想要互联网通信就需要暴露在公网上才行,例如可以和一个有公网IP的服务器通信。个人计算机想做到这一点也并不是很困难,本文介绍一种方法,可以实现互联网通信,该技术被称作内网穿透技术。该技术有一个形象的解释视频,可以去看看→学会突破那一层,收获更多快乐←
据说还可以更改路由器的转发表来达到效果,不过本文就不采用这种方式了。
2.1.1 内网穿透软件
内网穿透有很多网站提供了软件来方便我们打通隧道,本文将以cpolar为例进行解释如何使用。
cpolar官网地址:cpolar - secure introspectable tunnels to localhost
1、首先注册一个账号,这一步就不多说了。
2、下载软件,并打开软件。
3、根据网站给的authtoken执行代码
执行指令根据自己的系统目录确定用./cpolar还是直接cpolar。
4、最后就可以打通隧道了,根据自己想要用的协议和端口来设置。
例如用TCP协议和端口号为808,那么就执行cpolar tcp 808
,执行后便会给出一个URL,之后就可以根据URL来通信了。
有些网站给出的将会是一个公网IP,那么就将这里给出的URL替换为公网IP即可,这两个是一个效果。
2.1.2 实现互联网通信
其实程序实现互联网通信和实现局域网通信的过程最大不同就是内网穿透的实现,其余的便是服务器地址的确定方式不同。互联网应用服务器公网IP一般是放入配置文件或采用DNS服务,不像局域网应用一般是随便一个主机都可能当服务端,需要广播实现。当然,这里为了方便实现,就直接写入程序了。
由此可见,只是简单实现互联网通信,其实程序还要比局域网通信少一个广播的过程。所以这里就不再详细解释代码了,直接放上源码。
要注意的是,在互联网通信中,服务端的IP可以是127.0.0.1
和0.0.0.0
,端口号是打通隧道时候用的端口号,例打通隧道时用的cpolar tcp 8080
,那么端口号也要用8080。客户端的IP应当是返回的URL或公网IP,假如是URL,例tcp://16.tcp.cpolar.top:11858
,那么IP位置应该写16.tcp.cpolar.top
,端口号应该写11858。
2.2 Python源码
服务端代码
import time
import socket
import threadingclass Server:def __init__(self) -> None:# 创建一个socket对象self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 端口号self.port = 8080# 客户端列表self.clients = {}# 最大连接数self.max_connections = 1# 是否要接收客户连接self.to_accept = Truedef accept_client(self):"""启动服务器"""# 绑定到一个特定的端口self.server_socket.bind(("0.0.0.0", self.port))# 开始监听连接, 参数表示最大连接数self.server_socket.listen(self.max_connections)print("服务器已启动,等待连接...")while self.to_accept:# 接受一个连接, 返回一个客户的socket对象和客户端的IP地址client_socket, client_address = self.server_socket.accept()self.clients[client_address] = client_socketprint(f"客户端{client_address}已连接")# 如果连接数达到最大连接数, 则不再接受连接if len(self.clients) == self.max_connections:self.to_accept = Falseprint("已达到最大连接数")time.sleep(0.01)def receive_data(self):"""接收数据并发送反馈"""while True:for client_address, client_socket in self.clients.items():# 接收数据, 参数表示最大接收字节数data = client_socket.recv(1024)print(f"{client_address}接收到数据:{data.decode('utf-8')}")# 发送反馈client_socket.send("数据已接收".encode('utf-8'))time.sleep(0.01)def run(self):"""运行服务器"""# 客户端连接线程accept_thread = threading.Thread(target=self.accept_client)# 接收数据线程receive_thread = threading.Thread(target=self.receive_data)# 启动线程accept_thread.start()receive_thread.start()if __name__ == "__main__":s = Server()s.run()
客户端代码
import socket
import threadingclass Client:def __init__(self):# 创建一个TCP socketself.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 服务端地址self.server_ip = "18.tcp.cpolar.top"# 服务端端口self.server_post = 11925def connect_server(self):"""连接服务器"""# 连接服务器self.client_socket.connect((self.server_ip, self.server_post))print("连接服务器成功")while True:data = self.client_socket.recv(1024)if data:print(f"接收到数据: {data.decode('utf-8')}")else:print("服务器断开连接")breakdef run(self):"""运行客户端"""# 启动接收广播消息的线程connect_server_thread = threading.Thread(target=self.connect_server)connect_server_thread.start()if __name__ == "__main__":client = Client()client.run()
2.3 C#源码
服务端代码
using System.Net;
using System.Text;
using System.Net.Sockets;
using System.Net.NetworkInformation;class Server {// 服务端public TcpListener server;// 最大连接数量public int maxClient;// 客户端列表public TcpClient[] clients;// 是否要接收客户连接public bool toAccept;public Server() {// 创建服务端server = new TcpListener(IPAddress.Parse("0.0.0.0"), 8080);// 最大连接数量maxClient = 1;// 客户端列表clients = new TcpClient[maxClient];// 是否要接收客户连接toAccept = true;}// 接收客户端连接public void AcceptClient() {// 开始监听server.Start();Console.WriteLine("服务器已启动,等待连接...");// 接收客户端连接while (toAccept) {// 接收客户端连接TcpClient client = server.AcceptTcpClient();// 添加到客户端列表for (int i = 0; i < clients.Length; i++) {if (clients[i] == null) {clients[i] = client;break;}}Console.WriteLine($"客户端{client.Client.RemoteEndPoint}已连接");// 如果连接数超过最大连接数int count = 0;for (int i = 0; i < maxClient; i++) {if (clients[i] != null) {count++;}}if (count > maxClient) {Console.WriteLine("连接数超过最大连接数");toAccept = false;break;}// 暂停0.01秒System.Threading.Thread.Sleep(10);}}// 接收数据并反馈public void ReceiveData() {// 接收数据while (true) {foreach (TcpClient client in clients) {if (client == null) {continue;}// 获取网络流NetworkStream stream = client.GetStream();// 接收数据byte[] bytes = new byte[1024];int length = stream.Read(bytes, 0, bytes.Length);string data = Encoding.ASCII.GetString(bytes, 0, length);Console.WriteLine($"接收到来自{client.Client.RemoteEndPoint}的数据:{data}");// 发送数据byte[] msg = Encoding.ASCII.GetBytes("已收到数据");stream.Write(msg, 0, msg.Length);}}}// 运行服务器public void Run() {// 接收客户端连接线程Thread acceptThread = new(AcceptClient);// 接收数据线程Thread receiveThread = new(ReceiveData);// 启动线程acceptThread.Start();receiveThread.Start();}
}class MainClass {static void Main() {Server server = new();server.Run();}
}
客户端代码
using System.Net;
using System.Text;
using System.Net.Sockets;class Client {// 客户端public TcpClient client;// 服务端IP地址public string serverIp;// 服务端端口public int serverPort;public Client() {client = new TcpClient();// 设置服务端IP地址serverIp = "18.tcp.cpolar.top";// 设置服务端端口serverPort = 11925;}// 连接服务端public void ConnectServer() {// 连接服务端client.Connect(serverIp, serverPort);// 输出连接成功Console.WriteLine("连接成功");// 接收数据while (true) {byte[] bytes = new byte[1024];int length = client.GetStream().Read(bytes, 0, bytes.Length);string data = Encoding.UTF8.GetString(bytes, 0, length);if (data == "") {continue;}Console.WriteLine("接收到来自服务端的数据:" + data);}}// 运行客户端public void Run() {// 接收广播消息线程Thread connectServerThread = new(() => ConnectServer());connectServerThread.Start();}
}class Program {static void Main(string[] args) {Client client = new();client.Run();}
}
结语
程序在不同环境可能会有些许问题,本文的编程环境是python 3.9.10和.net8。