`
san_yun
  • 浏览: 2595098 次
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

Java网络编程--Socket编程

 
阅读更多

原文:http://blog.sina.com.cn/s/blog_616e189f0100s3px.html

 

 

Socket 缓冲区探讨

       本文主要探讨 java 网络套接字传输模型,并对如何将 NIO 应用于服务端,提高服务端的运行能力和降低服务负载。

       1.1 socket 套接字缓冲区

       Java 提供了便捷的网络编程模式,尤其在套接字中,直接提供了与网络进行沟通的输入和输出流,用户对网络的操作就如同对文件操作一样简便。在客户端与服务端建立 Socket 连接后,客户端与服务端间的写入和写出流也同时被建立,此时即可向流中写入数据,也可以从流中读取数据。在对数据流进行操作时,很多人都会误以为,客户端和服务端的 read write 应当是对应的,即:客户端调用一次写入,服务端必然调用了一次写出,而且写入和写出的字节数应当是对应的。为了解释上面的误解,我们提供了 Demo-1 的示例。

       Demo-1 中服务端先向客户端输出了两次,之后刷新了输出缓冲区。客户端先向服务端输出了一次,然后刷新输出缓冲,之后调用了一次接收操作。从 Demo-1 源码以及后面提供的可能出现的结果可以看出,服务端和客户端的输入和输出并不是对应的,有时一次接收操作可以接收对方几次发过来的信息,并且不是每次输出操作对方都需要接收处理。当然了 Demo-1 的代码是一种错误的编写方式,没有任何一个程序员希望编写这样的代码。

Demo-1

package com.upc.upcgrid.guan.chapter02;

 

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

import java.net.UnknownHostException;

 

import org.junit.Test;

 

public class SocketWriteTest {

    public static final int PORT = 12123;

    public static final int BUFFER_SIZE = 1024;

    // 服务端代码

    @Test

    public void server() throws IOException, InterruptedException{

       ServerSocket ss = new ServerSocket( PORT );

       while ( true )

       {

           Socket s = ss.accept();

           // 这里向网络进行两次写入

           s.getOutputStream().write( "hello " .getBytes());

           s.getOutputStream().write( "guanxinquan " .getBytes());

           s.getOutputStream().flush();

           s.close();

       }

    }

   

    // 客户端代码

    @Test

    public void client() throws UnknownHostException, IOException{

       byte [] buffer;

       Socket s = new Socket( "localhost" , PORT ); // 创建 socket 连接

       s.getOutputStream().write( new byte [ BUFFER_SIZE ]);

       s.getOutputStream().flush();

       int i = s.getInputStream().read(buffer = new byte [ BUFFER_SIZE ]);

       System. out .println( new String(buffer,0,i));

      

    }

}

Demo-1 可能输出的结果:

结果 1

hello

结果 2

hello guanxinquan

       为了深入理解网络发送数据的流程,我们需要对 Socket 的数据缓冲区有所了解。在创建 Socket 后,系统会为新创建的套接字分配缓冲区空间。这时套接字已经具有了输入缓冲区和输出缓冲区。可以通过 Demo-2 中的方式来获取和设置缓冲区的大小。缓冲区大小需要根据具体情况进行设置,一般要低于 64K TCP 能够指定的最大负重载数据量, TCP 的窗口大小是由 16bit 来确定的),增大缓冲区可以增大网络 I/O 的性能,而减少缓冲区有助于减少传入数据的 backlog (就是缓冲长度,因此提高响应速度)。对于 Socket SeverSocket 如果需要指定缓冲区大小,必须在连接之前完成缓冲区的设定。

Demo-2

package com.upc.upcgrid.guan.chapter02;

 

import java.net.Socket;

import java.net.SocketException;

 

public class SocketBufferTest {

    public static void main(String[] args) throws SocketException {

       // 创建一个 socket

       Socket socket = new Socket();

       // 输出缓冲区大小

       System. out .println(socket.getSendBufferSize());

       System. out .println(socket.getReceiveBufferSize());

       // 重置缓冲区大小

       socket.setSendBufferSize(1024*32);

       socket.setReceiveBufferSize(1024*32);

       // 再次输出缓冲区大小

       System. out .println(socket.getSendBufferSize());

       System. out .println(socket.getReceiveBufferSize());     

    }

}

Demo-2 的输出:

8192

8192

32768

32768

       了解了 Socket 缓冲区的概念后,需要探讨一下 Socket 的可写状态和可读状态。当输出缓冲区未满时, Socket 是可写的(注意,不是对方启用接收操作后,本地才能可写,这是错误的理解),因此,当套接字被建立时,即处于可写如的状态。对于可读,则是指缓冲区中有接收到的数据,并且这些数据未完成处理。在 socket 创建时,并不处于可读状态,仅当连接的另一方向本套接字的通道写入数据后,本套接字方能处于可读状态(注意,如果对方套接字已经关闭,那么本地套接字将处于可读状态,并且每次调用 read 后,返回的都是 -1 )。

       现在应用前面的讨论,重新分析一下 Demo-1 的 执行流程,服务端与客户端建立连接后,服务器端先向缓冲区写入两条信息,在第一条信息写入时,缓冲区并未写满,因此在第二条信息输入时,第一条信息很可能 还未发送,因此两条信息可能同时被传送到客户端。另一方面,如果在第二条信息写入时,第一条已经发送出去,那么客户端的接收操作仅会获得第一条信息,因为 客户端没有继续接收的操作,因此第二条信息在缓冲区中,将不会被读取,当 socket 关闭时,缓冲区将被释放,未被读取的数据也就变的无效了。如果对方的 socket 已经关闭,本地再次调用读取方法,则读取方法直接返回 -1 ,表示读到了文件的尾部。

       对于缓冲区空间的设定,要根据具体情况来定,如果存在大量的长信息(比如文件传输),将缓冲区定义的大些,可能更好的利用网络资源,如果更多的是短信息(比如聊天消息),使用小的缓冲区可能更好些,这样刷新的速度会更快。一般系统默认的缓冲大小是 8*1024 。除非对自己处理的情况很清晰,否则请不要随意更改这个设置。

       由于可读状态是在对方写入数据后或 socket 关闭时才能出现,因此如果客户端和服务端都停留在 read 时,如果没有任何一方,向对方写入数据,这将会产生一个死锁。

       此外,在本地接收操作发起之前,很可能接收缓冲区中已经有数据了,这是一种异步。不要误以为,本地调用接收操作后,对方才会发送数据,实际数据何时到达,本地不能做出任何假设。

       如果想要将多条输入的信息区分开,可以使用一些技巧,在文件操作中使用 -1 表示 EOF ,就是文件的结束,在网络传输中,也可以使用 -1 表示一条传输语句的结束。 Demo-3 中给出了一个读取和写入操作,在客户端和服务端对称的使用这两个类,可以将每一条信息分析出来。 Demo-3 中并不是将网络的传输同步,而是分析出缓冲中的数据,将以 -1 为结尾进行数据划分。如果写聊天程序可以使用类似的模式。

Demo-3

package com.upc.upcgrid.guan.chapter02;

 

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.ServerSocket;

import java.net.Socket;

import java.net.UnknownHostException;

import java.nio.ByteBuffer;

import java.util.ArrayList;

import java.util.List;

 

import org.junit.Test;

 

public class SocketWriteTest {

    public static final int PORT = 12123;

    public static final int BUFFER_SIZE = 1024;

   

    // 读取一条传入的,以 -1 为结尾的数据

    public class ReadDatas{

       // 数据临时缓冲用

       private List<ByteBuffer> buffers = new ArrayList<ByteBuffer>();

       private Socket socket ; // 数据的来源

       public ReadDatas(Socket socket) throws IOException {

           this . socket = socket;

       }

      

       public void read() throws IOException

       {

           buffers .clear(); // 清空上次的读取状态

           InputStream in = socket .getInputStream(); // 获取输入流

           int k = 0;

           byte r = 0;

           while ( true )

           {

              ByteBuffer buffer = ByteBuffer.allocate ( BUFFER_SIZE ); // 新分配一段数据区

              // 如果新数据区未满,并且没有读到 -1 ,则继续读取

              for (k = 0 ; k < BUFFER_SIZE ; k++)

              {

                  r = ( byte ) in.read(); // 读取一个数据

                  if (r != -1) // 数据不为 -1 ,简单放入缓冲区

                     buffer.put(r);

                  else { // 读取了一个 -1 ,表示这条信息结束

                     buffer.flip(); // 翻转缓冲,以备读取操作

                     buffers .add(buffer); // 将当前的 buffer 添加到缓冲列表

                     return ;

                  }

              }

              buffers .add(buffer); // 由于缓冲不足,直接将填满的缓冲放入缓冲列表

 

           }

          

       }

      

      

       public String getAsString()

       {

           StringBuffer str = new StringBuffer();

           for (ByteBuffer buffer: buffers ) // 遍历缓冲列表

           {

              str.append( new String(buffer.array(),0,buffer.limit())); // 组织字符串

           }

           return str.toString(); // 返回生成的字符串

       }

    }

   

    // 将一条信息写出给接收端

    public class WriteDatas{

       public Socket socket ; // 数据接收端

       public WriteDatas(Socket socket,ByteBuffer[] buffers) throws IOException {

           this . socket = socket;

           write(buffers);

       }

      

       public WriteDatas(Socket socket) {

           this . socket = socket;

       }

      

       public   void write(ByteBuffer[] buffers) throws IOException

       {

           OutputStream out = socket .getOutputStream(); // 获取输出流

           for (ByteBuffer buffer:buffers)

           {

              out.write(buffer.array()); // 将数据输出到缓冲区

           }

           out.write( new byte []{-1}); // 输出终结符

           out.flush(); // 刷新缓冲区

          

       }

      

    }

   

    // 服务端代码

    @Test

    public void server() throws IOException, InterruptedException{

       ServerSocket ss = new ServerSocket( PORT );

       while ( true )

       {

           Socket s = ss.accept();

          

           // 从网络连续读取两条信息

           ReadDatas read = new ReadDatas(s);

           read.read();

           System. out .println(read.getAsString());

           read.read();

           System. out .println(read.getAsString());

           // 向网络中输出一条信息

           WriteDatas write = new WriteDatas(s);

           write.write( new ByteBuffer[]{ByteBuffer.wrap ( "welcome to us ! " .getBytes())});

           // 关闭套接字

           s.close();

          

       }

    }

   

   

    // 客户端代码

    @Test

    public void client() throws UnknownHostException, IOException{

       Socket s = new Socket( "localhost" , PORT ); // 创建 socket 连接

       // 连续向服务端写入两条信息

       WriteDatas write = new WriteDatas(s, new ByteBuffer[]{ByteBuffer.wrap ( "ni hao guan xin quan ! " .getBytes())} );

       write.write( new ByteBuffer[]{ByteBuffer.wrap ( "let's study java network !" .getBytes())});      

       // 从服务端读取一条信息

       ReadDatas read = new ReadDatas(s);

       read.read();

       System. out .println(read.getAsString());

       // 关闭套接字

       s.close();

    }

}

       Demo-3 中的这种消息处理方式过于复杂,需要理解 java 底层的缓冲区的知识,还需要编程人员完成消息的组合(在消息末尾添加 -1 ),在 Java 中可以使用一种简单的方式完成上述的操作,就是使用 java DataInputStream DataOutputStream 提供的方法。 Demo-4 给出了使用 java 相关流类完成同步的消息的方法(估计他们与我们 Demo-3 使用的方式是相似的)。你可以查阅 java 其它 API ,可以找到其他的方式。

Demo-4

package com.upc.upcgrid.guan.chapter02;

 

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

import java.net.UnknownHostException;

 

 

import org.junit.Test ;

 

public class SocketDataStream {

    public static final int PORT = 12123;

    @Test

    public void server() throws IOException

    {

       ServerSocket ss = new ServerSocket( PORT );

       while ( true )

       {

           Socket s = ss.accept();

           DataInputStream in = new DataInputStream(s.getInputStream());

           DataOutputStream out = new DataOutputStream(s.getOutputStream());

          

           out.writeUTF( "hello guan xin quan ! " );

           out.writeUTF( "let's study java togethor! " );

          

           System. out .println(in.readUTF());

           s.close();

       }

    }

   

    @Test

    public void client() throws UnknownHostException, IOException

    {

       Socket s = new Socket( "localhost" , PORT );

       DataInputStream in = new DataInputStream(s.getInputStream());

       DataOutputStream out = new DataOutputStream(s.getOutputStream());

      

       System. out .println(in.readUTF());

       System. out .println(in.readUTF());

       out.writeUTF( "welcome to java net world ! " );

       s.close();

    }

}

 

简单总结:

       上面主要介绍了 java Socket 通信的缓冲区机制,并通过几个示例让您对 java Socket 的工作原理有了简单了解。这里需要注意的是可读状态和可写状态,因为这两个概念将对下一节的内容理解至关重要。下一节将描述 java NIO 提高服务端的并发性。

分享到:
评论

相关推荐

    高校学生选课系统项目源码资源

    项目名称: 高校学生选课系统 内容概要: 高校学生选课系统是为了方便高校学生进行选课管理而设计的系统。该系统提供了学生选课、查看课程信息、管理个人课程表等功能,同时也为教师提供了课程发布和管理功能,以及管理员对整个选课系统的管理功能。 适用人群: 学生: 高校本科生和研究生,用于选课、查看课程信息、管理个人课程表等。 教师: 高校教师,用于发布课程、管理课程信息和学生选课情况等。 管理员: 系统管理员,用于管理整个选课系统,包括用户管理、课程管理、权限管理等。 使用场景及目标: 学生选课场景: 学生登录系统后可以浏览课程列表,根据自己的专业和兴趣选择适合自己的课程,并进行选课操作。系统会实时更新学生的选课信息,并生成个人课程表。 教师发布课程场景: 教师登录系统后可以发布新的课程信息,包括课程名称、课程描述、上课时间、上课地点等。发布后的课程将出现在课程列表中供学生选择。 管理员管理场景: 管理员可以管理系统的用户信息,包括学生、教师和管理员账号的添加、删除和修改;管理课程信息,包括课程的添加、删除和修改;管理系统的权限控制,包括用户权限的分配和管理。 目标: 为高校学生提

    TC-125 230V 50HZ 圆锯

    TC-125 230V 50HZ 圆锯

    影音娱乐北雨影音系统 v1.0.1-bymov101.rar

    北雨影音系统 v1.0.1_bymov101.rar 是一个计算机专业的 JSP 源码资料包,它为用户提供了一个强大而灵活的在线影音娱乐平台。该系统集成了多种功能,包括视频上传、播放、分享和评论等,旨在为用户提供一个全面而便捷的在线视频观看体验。首先,北雨影音系统具有强大的视频上传功能。用户可以轻松地将本地的视频文件上传到系统中,并与其他人分享。系统支持多种视频格式,包括常见的 MP4、AVI、FLV 等,确保用户能够方便地上传和观看各种类型的视频。其次,该系统提供了丰富的视频播放功能。用户可以选择不同的视频进行观看,并且可以调整视频的清晰度、音量等参数,以适应不同的观看需求。系统还支持自动播放下一个视频的功能,让用户可以连续观看多个视频,无需手动切换。此外,北雨影音系统还提供了一个社交互动的平台。用户可以在视频下方发表评论,与其他观众进行交流和讨论。这为用户之间的互动提供了便利,增加了观看视频的乐趣和参与感。最后,该系统还具备良好的用户体验和界面设计。界面简洁明了,操作直观易用,让用户可以快速上手并使用各项功能。同时,系统还提供了个性化的推荐功能,根据用户的观看历史和兴趣,为用户推荐

    Tripp Trapp 儿童椅用户指南 STOKKE

    Tripp Trapp 儿童椅用户指南

    node-v8.13.0-linux-armv6l.tar.gz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    谷歌浏览器 64位-89.0.4389.128.exe

    Windows版本64位谷歌浏览器,是由Google谷歌公司开发的一款电脑版网络浏览器,可以运行在Windows 10/8.1/8/7 64位的操作系统上。该浏览器是基于其它开放原始码软件所撰写,包括WebKit和Mozilla,目标是提升稳定性、速度和安全性,并创造出简单且有效率的使用者界面。软件的特点是简洁、快速。并且支持多标签浏览,每个标签页面都在独立的“沙箱”内运行,在提高安全性的同时,一个标签页面的崩溃也不会导致其他标签页面被关闭。此外,谷歌浏览器(Google Chrome)基于更强大的JavaScript V8引擎,这是当前Web浏览器所无法实现的。

    适用于鲲鹏麒麟的OpenJDK1.8

    适用于鲲鹏麒麟的OpenJDK1.8

    毕业设计-基于SSH的任务调度系统的设计与实现

    任务调度试系统,基本功能包括:用户的注册、用户的登录、发起项目、项目详细及搜索等。本系统结构如下: (1)用户的注册登录: 注册模块:完成用户注册功能; 登录模块:完成用户登录功能; (2)发起项目: 发起项目模块:完成了项目及项目下一个或者多个任务的添加; 项目详细:点击项目名称,可以看到项目及任务详细信息; 搜索项目:完成对项目名称的模糊搜索功能 任务调度试系统,基本功能包括:用户的注册、用户的登录、发起项目、项目详细及搜索等。本系统结构如下: (1)用户的注册登录: 注册模块:完成用户注册功能; 登录模块:完成用户登录功能; (2)发起项目: 发起项目模块:完成了项目及项目下一个或者多个任务的添加; 项目详细:点击项目名称,可以看到项目及任务详细信息; 搜索项目:完成对项目名称的模糊搜索功能

    30个炫酷的数据可视化大屏(含源码)

    大屏数据可视化是以大屏为主要展示载体的数据可视化设计,30个可视化大屏包含源码,直接运行文件夹中的index.html,即可看到大屏。 内含:数据可视化页面设计;数据可视化演示系统;大数据可视化监管平台;智能看板;翼兴消防监控;南方软件视频平台;全国图书零售监测数据;晋城高速综合管控大数据;无线网络大数据平台;设备大数据;游戏数据大屏;厅店营业效能分析;车辆综合管控平台;政务大数据共享交换平台;智慧社区;物流云数据看板平台;风机可视化大屏等。

    基于yolov5识别算法实现的DNF自动脚本源码.zip

    优秀源码设计,详情请查看资源源码内容

    毕业设计:基于SSM的mysql-在线网上书店(源码 + 数据库 + 说明文档)

    毕业设计:基于SSM的mysql_在线网上书店(源码 + 数据库 + 说明文档) 2.系统分析与设计 3 2.1系统分析 3 2.1.1需求分析 3 2.1.2必要性分析 3 2.2系统概要设计 3 2.2.1 项目规划 3 2.2.2系统功能结构图 4 2.3开发及运行环境 4 2.4逻辑结构设计 5 2.4.1 数据库概要说明 5 2.4.2 主要数据表结构 6 2.5文件夹架构 9 2.6编写JAVA BEAN 9 3.网站前台主要功能模块设计 10 3.1前台首页架构设计 10 3.2网站前台首页设计 11 3.3新书上市模块设计 12 3.4特价书籍模块设计 13 3.5书籍分类模块设计 14 3.6会员管理模块设计 15 3.7购物车模块设计 17 3.8收银台设计模块 19 3.9畅销书籍模块设计 20 4.网站后台主要功能模块设计 21 4.1网站后台文件夹架构设计 21 4.2后台主页面设计 21 4.3书籍管理模块设计 22 4.4会员管理模块设计 25 4.5订单管理模块设计 26 4.6公告管理模块设计 28 4.7退出系统页面设计 29 5.网站制作中遇到的问

    python 开发 python爬虫数据可视化分析项目源码加课题报告,源码注解清晰一看就懂,适合新手.zip

    python 开发 python爬虫数据可视化分析项目源码加课题报告,源码注解清晰一看就懂,适合新手

    node-v8.0.0-linux-armv7l.tar.gz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    使用FPGA发送一个经过曼彻斯特编码的伪随机序列

    rtl中存放的是设计文件 sim中存放的是仿真文件

    基于Java的班级管理系统课程设计源码

    附件是基于 Java的班级管理系统课程设计源码,包含程序说明和运行环境要求,文件绿色安全,仅供学习交流使用,欢迎大家下载学习交流!

    最新获取QQ微信头像橘头像阁PHP源码下载.rar

    最新获取QQ微信头像橘头像阁PHP源码下载.rar最新获取QQ微信头像橘头像阁PHP源码下载.rar

    K-750 管道疏通机手册

    K-750 管道疏通机手册 Drain Cleaner Manual K-750 Drain Cleaning Machine

    基于哈希链表的简单人员信息管理系统

    实现基于哈希表的员工信息管理系统,该系统主要用于处理员工信息,主要包括员工个人信息的录入、删除、查找、修改等,同时支持数据的导入导出

    node-v6.16.0.tar.xz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    3D模型007,可用于建模、GIS、BIM、CIM学习

    3D模型007,可用于建模、GIS、BIM、CIM学习

Global site tag (gtag.js) - Google Analytics