当前位置: 首页 > news >正文

网络编程 之网络七层模型、TCPUDP协议、JAVA IO 发展历程

网络编程 之网络七层模型、TCPUDP协议、JAVA IO 发展历程

计算机间的网络

网络硬件:电缆、光纤、交换机、路由器、卫星等等

信号数据载体:光、电、波

image-20241119125302336

网络模型

OSI网络七层模型

为使不同国家不同的通信设备能够互相通信,以便在更大的范围内建立计算机网络,有必要建立一个国际范围的网络体系结构标准

image-20241119125433924

TCP/IP模型

image-20241119125535378

IP协议及报文结构

​ IP位于网络层,对上可载送传输层各种协议的信息,对下可将IP信息包放到链路层,是整个TCP/IP协议族的核心。它只负责数据包的发送,不保证数据包能被可靠、有序、完整的交付。目前IP协议有IPV4、IPV6两个版本。

IPV4报文结构

image-20241119125811668

image-20241119125823474
  • **4位版本号:**指定IP协议的版本(IPv4/IPv6),对于IPv4来说,就是4
  • **4位首部长度:**表示IP报头的长度,以4字节为单位
  • **8位服务类型:**3位优先权字段(已弃用),4位TOS字段,和1位保留字段(必须置为0)。4位TOS分别表示:最小延时,最大吞吐量,最高可靠性,最小成本。这四者相互冲突,只能选择一个。如对于ssh/telnet这样的应用程序,最小延时比较重要,而对于ftp这样的程序,最大吞吐量较为重要
  • **16位总长度:**IP报文(IP报头+有效载荷)的总长度,用于将各个IP报文进行分离
  • **16位标识:**唯一的标识主机发送的报文,如果数据在IP层进行了分片,那么每一个分片对应的id都是相同的
  • 3位标志字段:第一位保留,表示暂时没有规定该字段的意义;第二位表示禁止分片,表示若报文长度超过MTU,IP模块就会丢弃该报文;第三位表示"更多分片",若报文没有进行分片,则该字段设置为0,若报文进行了分片,则除了最后一个分片报文设置为0以外,其余分片报文均设置为1
  • **13位片偏移:**分片相对于原始数据开始处的偏移,表示当前分片在原数据中的偏移位置,实际偏移的字节数是这个值×8得到的。因此分片的报文中除了最后一个报文,其他报文的长度必须是8的整数倍,否则报文就不连续了
  • **8位生存时间(Time To Live,TTL)****:**数据报到达目的地的最大报文跳数,一般是64,每经过一个路由,TTL -= 1,一直减到0还没到达,那么就丢弃了,这个字段主要是用来防止出现路由循环
  • **8位协议:**表示上层协议的类型
  • **16位首部检验和:**使用CRC进行校验,来鉴别数据报的首部是否损坏,但不检验数据部分
  • **32位源IP地址和32位目的IP地址:**表示发送端和接收端所对应的IP地址
  • **选项字段:**不定长,最多40字节

以太网的MTU最大传输单元是1500,IP报文小于这个数字就无需分段了,报文经过多少跳的路由,从哪个IP发往哪个IP,报文有多大,优先级高还是低

IPV6报文机构

img

IPv6报头格式中主要字段解释如下:

  • Version:版本号,长度为4bit。对于IPv6,该值为6。
  • Traffic Class:流类别,长度为8bit。等同于IPv4中的TOS字段,表示IPv6数据报的类或优先级,主要应用于QoS。
  • Flow Label:流标签,长度为20bit。IPv6中的新增字段,用于区分实时流量,不同的流标签+源地址可以唯一确定一条数据流,中间网络设备可以根据这些信息更加高效率的区分数据流。
  • Payload Length:有效载荷长度,长度为16bit。有效载荷是指紧跟IPv6报头的数据报的其它部分(即扩展报头和上层协议数据单元)。该字段只能表示最大长度为65535字节的有效载荷。如果有效载荷的长度超过这个值,该字段会置0,而有效载荷的长度用逐跳选项扩展报头中的超大有效载荷选项来表示。
  • Next Header:下一个报头,长度为8bit。该字段定义紧跟在IPv6报头后面的第一个扩展报头(如果存在)的类型,或者上层协议数据单元中的协议类型。
  • Hop Limit:跳数限制,长度为8bit。该字段类似于IPv4中的Time to Live字段,它定义了IP数据报所能经过的最大跳数。每经过一个设备,该数值减去1,当该字段的值为0时,数据报将被丢弃。
  • Source Address:源地址,长度为128bit。表示发送方的地址。
  • Destination Address:目的地址,长度为128bit。表示接收方的地址。

TCP协议及报文结构

TCP协议(传输控制协议),是 Internet一个重要的传输层协议。TCP提供面向连接、可靠、有序、字节流传输服务。应用程序在使用TCP之前,必须先建立TCP连接

image-20241119130507354

  • 16位源端口:发送方主机的应用程序的端口号
  • 16位目的端口:目的主机的应用程序的端口号
  • 32位TCP序列号:表示本报文段所发送数据的第一个字节的编号
  • 32位TCP确认序号:接收方期望收到发送方下一个报文段的第一个字节数据的编号
  • 4位TCP首部长度:数据偏移是指数据段中的“数据”部分起始处距离TCP报文段起始处的字节偏移量。确定TCP报文的报头部分长度,告诉接收端应用程序,数据(有效载荷)从何处开始
  • 6位保留字段:为TCP将来的发展预留空间,目前必须全部为0
  • 6位标志位:共有6个标志位,每个标志位占1个bit
  • 16位窗口大小:表示发送该TCP报文的接受窗口还可以接受多少字节的数据量。该字段用于TCP的流量控制
  • 16位校验和字段:用于确认传输的数据有无损坏 。发送端基于数据内容校验生成一个数值,接收端根据接受的数据校验生成一个值。两个值相同代表数据有效,反之无效,丢弃该数据包。校验和根据 伪报头 + TCP头 + TCP数据 三部分进行计算
  • 16位紧急指针字段: 仅当标志位字段的URG标志位为1时才有意义。指出有效载荷中为紧急数据的字节数。当所有紧急数据处理完后,TCP就会告诉应用程序恢复到正常操作。即使接收方窗口大小为0,也可以发送紧急数据,因为紧急数据无须缓存
  • 选项字段:长度不定,但长度必须是32bits的整数倍。内容可变,因此必须使用首部长度来区分选项的具体长度

TCP握手机制

三次握手

image-20241119130740726

image-20241119130819371

四次挥手

image-20241119130833818

UDP协议及报文结构

image-20241119131009633

UDP长度:UDP报文的整个大小,最小为8个字节(16*4位)(仅为首部)。
UDP检验和:在进行检验和计算时,会添加一个伪首部一起进行运算。伪首部(占用12个字节)为:4个字节的源IP地址、4个字节的目的IP地址、1个字节的0、一个字节的数字17、以及占用2个字节UDP长度。这个伪首部不是报文的真正首部,只是引入为了计算校验和。相对于IP协议的只计算首部,UDP检验和会把首部和数据一起进行校验。接收端进行的校验和与UDP报文中的校验和相与,如果无差错应该全为1。如果有误,则将报文丢弃或者发给应用层、并附上差错警告。

TCP和UDP

TCP类似打电话,双方建立连接后才能够说话,可以确保双方能听到各自的声音

UDP类似发短信,不是一种面向连接的服务,你随时可以发送短信,但是不能确保对方及时收到

image-20241119131128166

Socket网络编程

Internet中应用最广泛的网络应用编程接口,实现与3种底层协议的交互:

数据报类型套接字SOCK_DGRAM(面向UDP接口)

流式套接字SOCK_STREAM(面向TCP接口)

原始套接字SOCK_RAW(面向网络层协议接口IP、ICMP等)

主要socket API及其调用过程

image-20241119131329282

Socket API 函数定义

listen()、accept()函数只能用于服务器端;

connect()函数只能用于客户端;

socket()、 bind()、 send()、 recv()、 sendto() 、recvfrom()、 close()

Socket使用

ScoketServer
public class HttpServerV1 {private static ExecutorService threadPool = Executors.newCachedThreadPool();public static void main(String[] args) throws Exception {ServerSocket serverSocket = new ServerSocket(8080);Runtime.getRuntime().addShutdownHook(new Thread(()->{try {serverSocket.close();} catch (IOException e) {e.printStackTrace();}}));System.out.println("服务器启动成功");while (true) {Socket request = serverSocket.accept();// 阻塞System.out.println("收到新连接 : " + request.toString());threadPool.execute(new Runnable() {@Overridepublic void run() {try {// 接收数据、打印InputStream inputStream = request.getInputStream(); // net + i/oBufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));String msg;while ((msg = reader.readLine()) != null) { // 没有数据,阻塞if (msg.length() == 0 || "bye".equals(msg)) {break;}System.out.println(msg);}System.out.println("收到数据,来自:"+ request.toString());OutputStream outputStream = request.getOutputStream();outputStream.write("hello".getBytes());outputStream.flush();} catch (IOException e) {e.printStackTrace();} finally {try {request.close();} catch (IOException e) {e.printStackTrace();}}}});}}
}
Socket
public class HttpClientV1 {private static Charset charset = Charset.forName("UTF-8");public static void main(String[] args) throws Exception {Socket s = new Socket("localhost", 8080);OutputStream out = s.getOutputStream();Scanner scanner = new Scanner(System.in);System.out.println("请输入:");String msg = scanner.nextLine() + "\n";out.write(msg.getBytes(charset)); // 阻塞,写完成scanner.close();out.write("bye\n".getBytes());InputStream inputStream = s.getInputStream();byte[] data = new byte[1024];int len = 0;while ((len = inputStream.read(data)) > 0) {System.out.println(new String(data));}s.close();}}
注意

socket流不关闭。socket流和文件流不太一样,文件流很容易知道文件末尾,到了文件末尾,直接就把流close掉就OK了。但是socket流不一样,你无法知道它什么时候到末尾,所以连接一直保持着,流也一直保持阻塞状态。即使用了带参数的read方法,返回了有效数据,但其实流仍然没有关闭,处于阻塞状态。

因此:在发送消息结尾,再发送一个表示结尾的标识,例如在上文中结束时,输入"bye"表示结束,退出循环

例如:

public class ServerSocketDemo {public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(9090);System.out.println("服务器启动成功");while (true) {try {Socket socket = serverSocket.accept();InputStream inputStream = socket.getInputStream();BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));byte[] data = new byte[1024];int len = 0;while (true) {
//                    (len = inputStream.read(data)) != -1System.out.println("准备");System.out.println("返回值:"+inputStream.read(data));//如果没有数据,就会一致阻塞在这System.out.println("开始读取数据。。。。。。");String s = new String(data, 0, len);System.out.println(s);
//                    if(s.length() == 0 || "bye".equals(s)){
//                        break;
//                    }}
//                System.out.println("收到数据,来自:" + socket.toString());
//                OutputStream outputStream = socket.getOutputStream();
//                outputStream.write("收到数据".getBytes("utf-8"));
//                outputStream.write("bye".getBytes("utf-8"));
//
//                outputStream.flush();} catch (IOException e) {e.printStackTrace();} finally {}}}
}
//输出结果,如下:
服务器启动成功
准备
返回值:6
开始读取数据。。。。。。准备
返回值:3
开始读取数据。。。。。。准备

image-20241119141320374

Http协议

请求数据包

image-20241119163450789

相应数据包

image-20241119163500377

Http响应状态码

  • 1xx(临时响应)

    表示临时响应并需要请求者继续执行操作的状态代码

  • 2xx (成功)

    表示成功处理了请求的状态代码

  • 3xx (重定向)

    表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向

  • 4xx(请求错误)

    这些状态代码表示请求可能出错,妨碍了服务器的处理。

  • 5xx(服务器错误)

    这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

使用socket模拟

服务端

启动以下代码man方法,并按照响应的数据格式返回,如下:

public class HttpServerV2 {private static ExecutorService threadPool = Executors.newCachedThreadPool();public static void main(String[] args) throws Exception {ServerSocket serverSocket = new ServerSocket(8080);Runtime.getRuntime().addShutdownHook(new Thread(()->{try {serverSocket.close();} catch (IOException e) {e.printStackTrace();}}));System.out.println("服务器启动成功");while (true) {Socket request = serverSocket.accept();System.out.println("收到新连接 : " + request.toString());threadPool.execute(() -> {try {// 接收数据、打印InputStream inputStream = request.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));String msg;while ((msg = reader.readLine()) != null) {if (msg.length() == 0) {break;}System.out.println(msg);}System.out.println("收到数据,来自:"+ request.toString());// 响应结果 200OutputStream outputStream = request.getOutputStream();outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());outputStream.write("Content-Length: 11\r\n\r\n".getBytes());//outputStream.write("Content-Type: application/json;charset=utf-8\r\n".getBytes("utf-8"));outputStream.write("Hello World".getBytes());outputStream.flush();} catch (IOException e) {e.printStackTrace();} finally {try {request.close();} catch (IOException e) {e.printStackTrace();}}});}}
}
客户端

使用浏览器访问http://localhost:9090,获取到返回值

image-20241119163717867

参考资料

各层协议

img

【高清】网络神图.jpg

IPV4和IPV6

Internet Protocol 网际协议运行在五层协议的体系结构(Physical Layer物理层、Data Link Layer数据链路层、Network Layer网络层、Transport Layer传输层、Application Layer应用层)中的网络层,它是TCP/IP协议族中最为核心的协议之一,负责为数据包提供无连接的、不可靠的、尽最大努力交付的服务。

IPv4

Internet Protocol version 4,网际协议版本4 是1982年在SATNET和1983年1月在ARPANET上部署用于生产的第一个版本。IPv4由IETF(互联网工程任务组)在1981年9月发布的RFC 791中描述,该RFC替换了1980年1月发布的RFC 760。IPv4使用32位(4字节)地址,因此地址空间中只有4,294,967,296(2^32)个地址。然而,随着互联网的快速发展,这些地址很快被耗尽。

IPv4地址的分配和管理由多个机构负责,包括ICANN(因特网名字与号码指派公司)及其下属的区域互联网注册管理机构(RIR)。尽管IPv4地址空间有限,但通过各种技术手段(如网络地址转换NAT、无类别域间路由CIDR等),IPv4地址的枯竭速度得到了显著减缓。然而,到2019年11月26日,全球所有43亿个IPv4地址已分配完毕,标志着IPv4地址的完全耗尽。

IPv4 编址

IPv4 地址由 32 位二进制数标识,按 8位 划分成 4个字节,为了方便书写和记忆,一般使用十进制来写每个字节的值(0 ~ 255),每个字节中间以 . 间隔 (形如 192.168.3.128)。

IPv4 地址的分类
A 类地址
地址范围:从1.0.0.0到126.255.255.255。
默认子网掩码:255.0.0.0(或/8)。
第一个和最后一个地址:网络地址(如1.0.0.0)和广播地址(如126.255.255.255)不可用。
用途:A类地址分配给拥有大量主机的大型网络,但由于地址空间有限,实际上A类地址的使用已经非常少。
B 类地址
地址范围:从128.0.0.0到191.255.255.255。
默认子网掩码:255.255.0.0(或/16)。
第一个和最后一个地址:网络地址(如128.0.0.0)和广播地址(如191.255.255.255)不可用。
用途:B类地址用于中等规模的网络。然而,随着互联网的扩展,B类地址也逐渐变得稀缺。
C 类地址
地址范围:从192.0.0.0到223.255.255.255。
默认子网掩码:255.255.255.0(或/24)。
第一个和最后一个地址:网络地址(如192.0.0.0)和广播地址(如223.255.255.255)不可用。
用途:C类地址是为小型网络设计的,是目前最广泛使用的IPv4地址类型。
D 类地址
地址范围:从224.0.0.0到239.255.255.255。
用途:D类地址被保留用于多播(Multicast)地址,也称为组播地址。多播允许将数据包同时发送给网络中的一组选定设备,而不是单个设备
E 类地址
地址范围:从240.0.0.0到255.255.255.255。
用途:E类地址目前保留未用,但在某些情况下被用作实验和研究目的。
特殊地址
0.0.0.0
表示无效目标或未知目标
在IPv4网络中,0.0.0.0地址通常被用来表示一个无效的、未知的或者不可用的目标。这意味着,如果数据包的目的地址是0.0.0.0,那么这些数据包不会被发送到任何具体的网络设备上,而是可能会被丢弃或特殊处理。服务器上所有IP的监听
在服务器配置中,0.0.0.0地址具有特殊的含义。它并不指向网络中的任何具体物理设备,而是代表服务器上的所有IP地址。换句话说,当服务器上的某个服务监听在0.0.0.0上时,它实际上是在监听该服务器所有网络接口上的所有IP地址。这增加了配置的灵活性,使得无论客户端通过哪个IP地址访问服务器,该服务都能够接受连接。在服务器的防火墙规则配置中,来源 0.0.0.0/0 表示接收所有 IP 地址的来源:
在这里插入图片描述默认路由
在路由表中,0.0.0.0地址表示默认路由。当路由表中没有找到完全匹配的路由时,数据包会被发送到默认路由指定的下一跳地址。这有助于确保数据包在无法直接到达目的地址时,能够被正确地转发到下一个路由器或网络设备上。未分配IP时的表示
当一台主机还没有被分配一个IP地址时,它可以使用0.0.0.0地址来表示自己。这种情况下,0.0.0.0地址并不代表一个有效的网络地址,而是用于标识主机当前处于未分配IP地址的状态。网关地址的特殊含义
在网关配置中,0.0.0.0地址有时被用作直连规则的标识。这意味着当前记录对应的目的地与本机在同一个网段内,通信时不需要经过网关(路由器)。这种情况下,数据包可以通过二层交换机直接通过MAC地址进行通信,而无需经过三层路由。容器网络中的应用
在容器网络中,0.0.0.0地址也有特殊的应用。当命中容器的路由表直连规则时,如果目的IP是在局域网内,那么数据包可以直接通过二层网络发送,而无需走到出口网关。这有助于优化容器间的通信效率。
255.255.255.255

255.255.255.255是一个特殊的IPv4地址,被称为受限广播地址(Limited Broadcast Address)或本地广播地址(Local Broadcast Address)。它用于在同一个局域网(LAN)内发送广播数据包给所有的设备,而不会被路由器转发到其他网络。

当主机向255.255.255.255发送数据包时,该数据包会被网络上的所有设备接收,包括交换机、路由器(但仅限于与发送设备位于同一局域网内的路由器接口)和其他主机。然而,由于这是一个受限的广播地址,路由器通常不会将其转发到网络之外,从而避免了广播风暴和不必要的网络流量。

127.0.0.0 ~ 127.255.255.255

127开头的地址在IPv4地址分类中属于保留地址,具体来说是用于本地回环(Loopback)测试的。这类地址不会出现在任何网络上,也不会被路由器转发。当主机向一个127开头的地址发送数据包时,数据包不会离开主机,而是被主机上的网络接口软件捕获并处理,这通常用于测试本地网络接口或应用程序。

具体来说,127.0.0.0到127.255.255.255的地址范围都被保留用于本地回环测试。其中,一个特别常用的地址是127.0.0.1,它被称为本地回环地址或环回地址(Loopback Address)。通过这个地址,主机可以向自己发送数据包,并在不经过任何网络接口的情况下直接接收这些数据包,这在进行网络编程和测试时非常有用。localhost 通常被解析到 127.0.0.1。修改 hosts 文件可以更改本机的主机名,默认主机名就是 localhost。hosts 文件通常缓存了 域名 到 IP 地址的映射。

IPv6

IPv6 于20世纪90年代初步研究和设计,以应对 IPv4 地址资源的枯竭。在IPv6的上下文中,报文既可以被称为“数据报”,也可以被称为“数据包”,这两个术语在大多数情况下是可以互换使用的。

IPv6 的IP地址长度为 128比特,
可以有 2128 = 340282366920938463463374607431768211456 ≈ \approx ≈ 3.40282366920938×1038 个 IP 地址,号称可以为地球上每一粒沙子分配一个 IP地址。

IPv6 编址

前文已经提到 IPv6 的地址长度为 128 bit (16字节),IPv6 的表示方法为:使用十六进制表示,每隔两个字节使用 英文冒号 : 划分一个字段。连续的零字段可以使用双冒号(::)进行简化表示,但整个地址中只能出现一次双冒号。如果出现了两个连续的0字段,那么双冒号只能代替较长的那个字段。每个字段的前导0可以省略。

例如:
标准写法 2001:0db8:3902:00c2:0000:0000:0000:fe04,
可以简写为:2001:db8:3902:c2::fe04。

出现了两个连续的 0字段:2001:0000:0000:00c2:0000:0000:0000:fe04,双冒号代替较长的。
可以简写为:2001:0:0:c2::fe04。

IPv6 地址分类

IPv6地址分为多种类型,以满足不同的网络需求:

单播地址(Unicast Address):标识一个网络接口。目的地址为单播地址的报文会被送到被标识的接口。包括可聚合全球单播地址、链路本地单播地址和站点本地单播地址(已废弃,被全球单播地址取代)。
组播地址(Multicast Address):标识一组接口。目的地址为组播地址的报文会被送到被标识的所有接口。用于将报文同时传输到多个目的地,例如网络中的广播消息。
任播地址(Anycast Address):标识一组网络接口,但目标为一个任播地址的报文只会被送到最近的一个被标识接口。最近节点是由路由协议来定义的。类似于DNS中的轮询解析,但更加灵活和高效。
未指定地址(Unspecified Address):表示没有地址,用于初始化阶段。格式为0:0:0:0:0:0:0:0/128或::/128。
环回地址(Loopback Address):用于设备给自己发送报文。格式为0:0:0:0:0:0:0:1/128或::1/128。
参考

参考:https://blog.csdn.net/m0_46190471/article/details/141159880

Java IO流

参考:

https://blog.51cto.com/u_16213600/7150292

被送到被标识的所有接口。
用于将报文同时传输到多个目的地,例如网络中的广播消息。
任播地址(Anycast Address):
标识一组网络接口,但目标为一个任播地址的报文只会被送到最近的一个被标识接口。
最近节点是由路由协议来定义的。
类似于DNS中的轮询解析,但更加灵活和高效。
未指定地址(Unspecified Address):
表示没有地址,用于初始化阶段。
格式为0:0:0:0:0:0:0:0/128或::/128。
环回地址(Loopback Address):
用于设备给自己发送报文。
格式为0:0:0:0:0:0:0:1/128或::1/128。

参考

参考:https://blog.csdn.net/m0_46190471/article/details/141159880

Java IO流

参考:

https://blog.51cto.com/u_16213600/7150292

https://blog.csdn.net/qq_35207086/article/details/135463389

相关文章:

  • LeetCode 3356.零数组变换 II:二分查找 + I的差分数组
  • 数据分析师如何用OKR驱动业务增长
  • [Java][Leetcode middle] 6. Z 字形变换
  • Python可视化设计原则
  • 【工作流】Fastgpt配置豆包模型-火山引擎
  • 青少年编程与数学 02-019 Rust 编程基础 23课题、web服务器
  • React19 项目开发中antd组件库版本兼容问题解决方案。
  • React 如何封装一个可复用的 Ant Design 组件
  • Flask vs. Django:如何选择最适合你的 Web 框架?
  • 框架开发与原生开发的权衡:React案例分析(原生JavaScript)
  • JVM部分内容
  • MacBookPro上macOS安装第三方应用报错解决方案:遇到:“无法打开“XXX”,因为无法确定(验证)开发者身份?怎么解决
  • uniapp实现的简约美观的票据、车票、飞机票模板
  • EtpBot:安卓自动化脚本开发神器
  • 云原生微服务的前世今生
  • Oracle 数据文件被删除后使用rman备份恢复过程
  • VUE 文件下载,流形式的文件下载,判断返回的是流还是JSON;获取下载名称
  • 学习笔记:黑马程序员JavaWeb开发教程(2025.4.10)
  • React中常用的钩子函数:
  • go语言基础
  • 凡科网做网站如何推广/搜索引擎优化seo的英文全称是
  • 封装系统如何做自己的网站/爱站查询
  • 做网站优化选阿里巴巴还是百度/seo提高网站排名
  • 网站获取访客手机号源码/苹果被曝开发搜索引擎对标谷歌
  • 阿里云服务器责任怎么做网站/自动seo系统
  • 哔哩哔哩推广网站/企业微信营销管理软件