BIO、NIO、AIO 模型工作方式

一、简介在计算机中,io 传输数据有三种工作方式,分别是: bio、nio、aio 。
在讲解 bio、nio、aio 之前,我们先来回顾一下这几个概念: 同步与异步,阻塞与非阻塞 。
同步与异步的区别
同步就是发起一个请求后,接受者未处理完请求之前,不返回结果。异步就是发起一个请求后,立刻得到接受者的回应表示已接收到请求,但是接受者并没有处理完,接受者通常依靠事件回调等机制来通知请求者其处理结果。阻塞和非阻塞的区别
阻塞就是请求者发起一个请求,一直等待其请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。非阻塞就是请求者发起一个请求,不用一直等着结果返回,可以先去干其他事情,当条件就绪的时候,就自动回来。而我们要讲的 bio、nio、aio 就是同步与异步、阻塞与非阻塞的组合。
bio:同步阻塞 io;nio:同步非阻塞 io;aio:异步非阻塞 io;不同的工作方式,带来的传输效率是不一样的,下面我们以网络 io 为例,一起看看不同的工作方式下,彼此之间有何不同 。
二、biobio 俗称同步阻塞 io,是一种非常传统的 io 模型,也是最常用的网络数据传输处理方式,优点就是编程简单,但是缺点也很明显,i/o 传输性能一般比较差,cpu 大部分处于空闲状态。
采用 bio 通信模型的服务端,通常由一个独立的 acceptor 线程负责监听所有客户端的连接,当服务端接受到多个客户端的请求时,所有的客户端只能排队等待服务端一个一个的处理。
bio 通信模型图如下!
一般在服务端通过while(true)循环中会调用accept()方法监听客户端的连接,一旦接收到一个连接请求,就可以建立通信套接字进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。
服务端操作,样例程序如下 :
public class bioservertest { public static void main(string[] args) throws ioexception { //初始化服务端socket并且绑定 8080 端口 serversocket serversocket = new serversocket(8080); //循环监听客户端请求 while (true){ try { //监听客户端请求 socket socket = serversocket.accept(); //将字节流转化成字符流,读取客户端输入的内容 bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(socket.getinputstream())); //读取一行数据 string str = bufferedreader.readline(); //打印客户端发送的信息 system.out.println(服务端收到客户端发送的信息: + str); //向客户端返回信息,将字符转化成字节流,并输出 printwriter printwriter = new printwriter(new outputstreamwriter(socket.getoutputstream()),true); printwriter.println(hello,我是服务端,已收到消息); // 关闭流 bufferedreader.close(); printwriter.close(); } catch (ioexception e) { e.printstacktrace(); } } }}客户端操作,样例程序如下 :
public class bioclienttest { public static void main(string[] args) { //创建10个线程,模拟10个客户端,同时向服务端发送请求 for (int i = 0; i 收到服务端返回的内容: + result); // 关闭流 bufferedreader.close(); printwriter.close(); // 关闭socket socket.close(); } catch (ioexception e) { e.printstacktrace(); } } }).start(); } }}最后,依次启动服务端、客户端,看看控制台输出情况如何。
服务端控制台结果如下:
服务端收到客户端发送的信息:hello,我是8个,客户端!服务端收到客户端发送的信息:hello,我是9个,客户端!服务端收到客户端发送的信息:hello,我是7个,客户端!服务端收到客户端发送的信息:hello,我是5个,客户端!服务端收到客户端发送的信息:hello,我是4个,客户端!服务端收到客户端发送的信息:hello,我是3个,客户端!服务端收到客户端发送的信息:hello,我是6个,客户端!服务端收到客户端发送的信息:hello,我是2个,客户端!服务端收到客户端发送的信息:hello,我是1个,客户端!服务端收到客户端发送的信息:hello,我是0个,客户端!客户端控制台结果如下:
客户端发送请求内容:hello,我是8个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是9个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是7个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是5个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是4个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是3个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是6个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是2个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是1个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息客户端发送请求内容:hello,我是0个,客户端! - > 收到服务端返回的内容:hello,我是服务端,已收到消息随着客户端的请求次数越来越多,可能需要排队的时间会越来越长,因此是否可以在服务端,采用多线程编程进行处理呢?
答案是,可以的!
下面我们对服务端的代码进行改造,服务端多线程操作,样例程序如下:
public class bioservertest { public static void main(string[] args) throws ioexception { //初始化服务端socket并且绑定 8080 端口 serversocket serversocket = new serversocket(8080); //循环监听客户端请求 while (true){ //监听客户端请求 socket socket = serversocket.accept(); new thread(new runnable() { @override public void run() { try { string threadname = thread.currentthread().tostring(); //将字节流转化成字符流,读取客户端输入的内容 bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(socket.getinputstream())); //读取一行数据 string str = bufferedreader.readline(); //打印客户端发送的信息 system.out.println(线程名称 + threadname + ,服务端收到客户端发送的信息: + str); //向客户端返回信息,将字符转化成字节流,并输出 printwriter printwriter = new printwriter(new outputstreamwriter(socket.getoutputstream()),true); printwriter.println(hello,我是服务端,已收到消息); // 关闭流 bufferedreader.close(); printwriter.close(); } catch (ioexception e) { e.printstacktrace(); } } }).start(); } }}依次启动服务端、客户端,服务端控制台输出结果如下:
线程名称thread[thread-8,5,main],服务端收到客户端发送的信息:hello,我是4个,客户端!线程名称thread[thread-4,5,main],服务端收到客户端发送的信息:hello,我是8个,客户端!线程名称thread[thread-0,5,main],服务端收到客户端发送的信息:hello,我是1个,客户端!线程名称thread[thread-7,5,main],服务端收到客户端发送的信息:hello,我是5个,客户端!线程名称thread[thread-5,5,main],服务端收到客户端发送的信息:hello,我是2个,客户端!线程名称thread[thread-9,5,main],服务端收到客户端发送的信息:hello,我是3个,客户端!线程名称thread[thread-1,5,main],服务端收到客户端发送的信息:hello,我是0个,客户端!线程名称thread[thread-3,5,main],服务端收到客户端发送的信息:hello,我是7个,客户端!线程名称thread[thread-2,5,main],服务端收到客户端发送的信息:hello,我是9个,客户端!线程名称thread[thread-6,5,main],服务端收到客户端发送的信息:hello,我是6个,客户端!当服务端接收到客户端的请求时,会给每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,最后线程会销毁。
但是这样的编程模型也有很大的弊端,如果出现 100、1000、甚至 10000 个客户端同时请求服务端,采用这种编程模型,服务端也会创建与之相同的线程数量, 线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终可能导致服务端宕机或者僵死,不能对外提供服务 。
三、伪异步 bio为了解决上面提到的同步阻塞 i/o 面临的一个链路需要一个线程处理的问题,后来有人对它的编程模型进行了优化。
在服务端通过使用 java 中threadpoolexecutor线程池机制来处理多个客户端的请求接入,防止由于海量并发接入导致资源耗尽,让线程的创建和回收成本相对较低,保证了系统有限的资源得以控制,实现了 n (客户端请求数量)大于 m (服务端处理客户端请求的线程数量)的伪异步 i/o 模型。
伪异步 io 模型图,如下图:
采用线程池和任务队列可以实现一种叫做伪异步的 i/o 通信框架,当有新的客户端接入时,将客户端的 socket 封装成一个 task 投递到线程池中进行处理。
服务端采用线程池处理客户端请求,样例程序如下:
public class bioservertest { public static void main(string[] args) throws ioexception { //在线程池中创建5个固定大小线程,来处理客户端的请求 executorservice executorservice = executors.newfixedthreadpool(5); //初始化服务端socket并且绑定 8080 端口 serversocket serversocket = new serversocket(8080); //循环监听客户端请求 while (true){ //监听客户端请求 socket socket = serversocket.accept(); //使用线程池执行任务 executorservice.execute(new runnable() { @override public void run() { try { string threadname = thread.currentthread().tostring(); //将字节流转化成字符流,读取客户端输入的内容 bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(socket.getinputstream())); //读取一行数据 string str = bufferedreader.readline(); //打印客户端发送的信息 system.out.println(线程名称 + threadname + ,服务端收到客户端发送的信息: + str); //向客户端返回信息,将字符转化成字节流,并输出 printwriter printwriter = new printwriter(new outputstreamwriter(socket.getoutputstream()),true); printwriter.println(hello,我是服务端,已收到消息); // 关闭流 bufferedreader.close(); printwriter.close(); } catch (ioexception e) { e.printstacktrace(); } } }); } }}依次启动服务端、客户端,服务端控制台输出结果如下:
线程名称thread[pool-1-thread-4,5,main],服务端收到客户端发送的信息:hello,我是6个,客户端!线程名称thread[pool-1-thread-2,5,main],服务端收到客户端发送的信息:hello,我是8个,客户端!线程名称thread[pool-1-thread-3,5,main],服务端收到客户端发送的信息:hello,我是9个,客户端!线程名称thread[pool-1-thread-5,5,main],服务端收到客户端发送的信息:hello,我是5个,客户端!线程名称thread[pool-1-thread-1,5,main],服务端收到客户端发送的信息:hello,我是7个,客户端!线程名称thread[pool-1-thread-5,5,main],服务端收到客户端发送的信息:hello,我是2个,客户端!线程名称thread[pool-1-thread-5,5,main],服务端收到客户端发送的信息:hello,我是0个,客户端!线程名称thread[pool-1-thread-1,5,main],服务端收到客户端发送的信息:hello,我是1个,客户端!线程名称thread[pool-1-thread-5,5,main],服务端收到客户端发送的信息:hello,我是3个,客户端!线程名称thread[pool-1-thread-1,5,main],服务端收到客户端发送的信息:hello,我是4个,客户端!本例中测试的客户端数量是 10,服务端使用 java 线程池来处理任务,线程数量为 5 个,服务端不用为每个客户端都创建一个线程,由于线程池可以设置消息队列的大小和最大线程数,因此它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
在活动连接数不是特别高的情况下,这种模型还是不错的,可以让每一个连接专注于自己的 i/o 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。
但是,它的底层仍然是同步阻塞的 bio 模型,当面对十万甚至百万级请求接入的时候,传统的 bio 模型无能为力,因此我们需要一种更高效的 i/o 处理模型来应对更高的并发量。
四、nionio,英文全称: non-blocking-io ,一种同步非阻塞的 i/o 模型。
在 java 1.4 中引入,对应的代码在java.nio包下。
与传统的 io 不同,nio 新增了 channel、selector、buffer 等抽象概念, 支持面向缓冲、基于通道的 i/o 数据传输方法 。
nio 模型图,如下图:
与此同时,nio 还提供了与传统 bio 模型中的 socket 和 serversocket 相对应的 socketchannel 和 serversocketchannel 两种不同的套接字通道实现。
nio 这两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的 bio 一样,比较简单,但是性能和可靠性都不好; 非阻塞模式正好与之相反 。
对于低负载、低并发的应用程序,可以使用同步阻塞 i/o 来提升开发效率和更好的维护性;对于高负载、高并发的( 网络 )应用,使用 nio 的非阻塞模式来开发可以显著的提升数据传输效率。
在介绍样例之前,我们先看一下 nio 涉及到的核心关联类图,如下:
上图中有三个关键类: channel 、selector 和 buffer ,它们是 nio 中的核心概念。
channel:可以理解为通道;selector:可以理解为选择器;buffer:可以理解为数据缓冲区;从名词上看感觉很抽象,我们还是用之前介绍的城市交通工具来继续形容 nio 的工作方式,这里的 channel 要比 socket 更加具体,它可以比作为某种具体的交通工具,如汽车或是高铁、飞机等,而 selector 可以比作为一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态,是已经出站还是在路上等等,也就是说它可以轮询每个 channel 的状态。
还有一个 buffer 类,你可以将它看作为 io 中 stream ,但是它比 io 中的 stream 更加具体化,我们可以将它比作为车上的座位,channel 如果是汽车的话,那么 buffer 就是汽车上的座位,channel 如果是高铁上,那么 buffer 就是高铁上的座位,它始终是一个具体的概念,这一点与 stream 不同。
socket 中的 stream 只能代表是一个座位,至于是什么座位由你自己去想象,也就是说你在上车之前并不知道这个车上是否还有座位,也不知道上的是什么车,因为你并不能选择,这些信息都已经被封装在了运输工具( socket )里面了。
nio 引入了 channel、buffer 和 selector 就是想把 io 传输过程中涉及到的 信息具体化 ,让程序员有机会去控制它们。
当我们进行传统的网络 io 操作时,比如调用write()往 socket 中的sendq队列写数据时,当一次写的数据超过sendq长度时,操作系统会按照sendq 的长度进行分割的,这个过程中需要将用户空间数据和内核地址空间进行切换,而这个切换不是程序员可以控制的,由底层操作系统来帮我们处理。
而在buffer中,我们可以控制buffer的capacity(容量),并且是否扩容以及如何扩容都可以控制。
理解了这些概念后我们看一下,实际上它们是如何工作的呢?
我们一起来看看代码实例!
服务端操作,样例程序如下 :
/** * nio 服务端 */public class nioservertest { public static void main(string[] args) throws ioexception { // 打开服务器套接字通道 serversocketchannel ssc = serversocketchannel.open(); // 服务器配置为非阻塞 ssc.configureblocking(false); // 进行服务的绑定,监听8080端口 ssc.socket().bind(new inetsocketaddress(8080)); // 构建一个selector选择器,并且将channel注册上去 selector selector = selector.open(); // 将serversocketchannel注册到selector,并对accept事件感兴趣(serversocketchannel只能支持accept操作) ssc.register(selector, selectionkey.op_accept); while (true){ // 查询指定事件已经就绪的通道数量,select方法有阻塞效果,直到有事件通知才会有返回,如果为0就跳过 int readychannels = selector.select(); if(readychannels == 0) { continue; }; //通过选择器取得所有key集合 set selectedkeys = selector.selectedkeys(); iterator iterator = selectedkeys.iterator(); while (iterator.hasnext()){ selectionkey key = iterator.next(); //判断状态是否有效 if (!key.isvalid()) { continue; } if (key.isacceptable()) { // 处理通道中的连接事件 serversocketchannel server = (serversocketchannel) key.channel(); socketchannel sc = server.accept(); sc.configureblocking(false); system.out.println(接收到新的客户端连接,地址: + sc.getremoteaddress()); // 将通道注册到选择器并处理通道中可读事件 sc.register(selector, selectionkey.op_read); } else if (key.isreadable()) { // 处理通道中的可读事件 socketchannel channel = (socketchannel) key.channel(); bytebuffer bytebuffer = bytebuffer.allocate(1024); while (channel.isopen() && channel.read(bytebuffer) != -1) { // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了) if (bytebuffer.position() > 0) { break; }; } bytebuffer.flip(); //获取缓冲中的数据 string result = new string(bytebuffer.array(), 0, bytebuffer.limit()); system.out.println(收到客户端发送的信息,内容: + result); // 将通道注册到选择器并处理通道中可写事件 channel.register(selector, selectionkey.op_write); } else if (key.iswritable()) { // 处理通道中的可写事件 socketchannel channel = (socketchannel) key.channel(); bytebuffer bytebuffer = bytebuffer.allocate(1024); bytebuffer.put(server send.getbytes()); bytebuffer.flip(); channel.write(bytebuffer); // 将通道注册到选择器并处理通道中可读事件 channel.register(selector, selectionkey.op_read); //写完之后关闭通道 channel.close(); } //当前事件已经处理完毕,可以丢弃 iterator.remove(); } } }}客户端操作,样例程序如下 :
/** * nio 客户端 */public class nioclienttest { public static void main(string[] args) throws ioexception { // 打开socket通道 socketchannel sc = socketchannel.open(); //设置为非阻塞 sc.configureblocking(false); //连接服务器地址和端口 sc.connect(new inetsocketaddress(127.0.0.1, 8080)); while (!sc.finishconnect()) { // 没连接上,则一直等待 system.out.println(客户端正在连接中,请耐心等待); } // 发送内容 bytebuffer writebuffer = bytebuffer.allocate(1024); writebuffer.put(hello,我是客户端.getbytes()); writebuffer.flip(); sc.write(writebuffer); // 读取响应 bytebuffer readbuffer = bytebuffer.allocate(1024); while (sc.isopen() && sc.read(readbuffer) != -1) { // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了) if (readbuffer.position() > 0) { break; }; } readbuffer.flip(); string result = new string(readbuffer.array(), 0, readbuffer.limit()); system.out.println(客户端收到服务端: + sc.socket().getremotesocketaddress() + ,返回的信息: + result); // 关闭通道 sc.close(); }}最后,依次启动服务端、客户端,看看控制台输出情况如何。
服务端控制台结果如下:
接收到新的客户端连接,地址:/127.0.0.1:57644收到客户端发送的信息,内容:hello,我是客户端客户端控制台结果如下:
客户端收到服务端:/127.0.0.1:8080,返回的信息:server send从编程上可以看到,nio 的操作比传统的 io 操作要复杂的多 !
selector 被称为选择器 ,当然你也可以翻译为多路复用器 。它是java nio 核心组件中的一个,用于检查一个或多个 channel (通道)的状态是否处于 连接就绪 、 接受就绪 、 可读就绪 、 可写就绪 。
如此可以实现单线程管理多个 channels 的目的,也就是可以管理多个网络连接。
使用 selector 的好处在于 :相比传统方式使用多个线程来管理 io,selector 使用了更少的线程就可以处理通道了,并且实现网络高效传输!
虽然 java 中的 nio 传输比较快,为什么大家都不愿意用 jdk 原生 nio 进行开发呢?
从上面的代码中大家都可以看出来,除了编程复杂之外,还有几个让人诟病的问题:
jdk 的 nio 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%!项目庞大之后,自行实现的 nio 很容易出现各类 bug,维护成本较高!但是,netty 框架的出现,很大程度上改善了 jdk 原生 nio 所存在的一些让人难以忍受的问题 ,关于 netty 框架应用,会在后期的文章里进行介绍。
五、aio最后就是 aio 了,全称 asynchronous i/o,可以理解为异步 io,也被称为 nio 2,在 java 7 中引入,它是异步非阻塞的 io 模型。
异步 io 是基于事件回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
具体的实例如下!
服务端操作,样例程序如下 :
/** * aio 服务端 */public class aioserver { public asynchronousserversocketchannel serverchannel; /** * 监听客户端请求 * @throws exception */ public void listen() throws exception { //打开一个服务端通道 serverchannel = asynchronousserversocketchannel.open(); serverchannel.bind(new inetsocketaddress(8080));//监听8080端口 //服务监听 serverchannel.accept(this, new completionhandler(){ @override public void completed(asynchronoussocketchannel client, aioserver attachment) { try { if (client.isopen()) { system.out.println(接收到新的客户端连接,地址: + client.getremoteaddress()); final bytebuffer buffer = bytebuffer.allocate(1024); //读取客户端发送的信息 client.read(buffer, client, new completionhandler(){ @override public void completed(integer result, asynchronoussocketchannel attachment) { try { //读取请求,处理客户端发送的数据 buffer.flip(); string content = new string(buffer.array(), 0, buffer.limit()); system.out.println(服务端收到客户端发送的信息: + content); //向客户端发送数据 bytebuffer writebuffer = bytebuffer.allocate(1024); writebuffer.put(server send.getbytes()); writebuffer.flip(); attachment.write(writebuffer).get(); } catch (exception e) { e.printstacktrace(); } } @override public void failed(throwable exc, asynchronoussocketchannel attachment) { try { exc.printstacktrace(); attachment.close(); } catch (ioexception e) { e.printstacktrace(); } } }); } } catch (exception e) { e.printstacktrace(); } finally { //当有新客户端接入的时候,直接调用accept方法,递归执行下去,保证多个客户端都可以阻塞 attachment.serverchannel.accept(attachment, this); } } @override public void failed(throwable exc, aioserver attachment) { exc.printstacktrace(); } }); } public static void main(string[] args) throws exception { //启动服务器,并监听客户端 new aioserver().listen(); //因为是异步io执行,让主线程睡眠但不关闭 thread.sleep(integer.max_value); }}客户端操作,样例程序如下 :
/** * aio 客户端 */public class aioclient { public static void main(string[] args) throws ioexception, interruptedexception { //打开一个客户端通道 asynchronoussocketchannel channel = asynchronoussocketchannel.open(); //与服务器建立连接 channel.connect(new inetsocketaddress(127.0.0.1, 8080)); //睡眠1s,等待与服务器建立连接 thread.sleep(1000); try { //向服务器发送数据 channel.write(bytebuffer.wrap(hello,我是客户端.getbytes())).get(); } catch (exception e) { e.printstacktrace(); } try { //从服务器读取数据 bytebuffer bytebuffer = bytebuffer.allocate(1024); channel.read(bytebuffer).get();//将通道中的数据写入缓冲buffer bytebuffer.flip(); string result = new string(bytebuffer.array(), 0, bytebuffer.limit()); system.out.println(客户端收到服务器返回的内容: + result);//输出返回结果 } catch (exception e) { e.printstacktrace(); } }}同样的,依次启动服务端程序,再启动客户端程序,看看运行结果!
服务端控制台结果如下:
接收到新的客户端连接,地址:/127.0.0.1:56606服务端收到客户端发送的信息:hello,我是客户端客户端控制台结果如下:
客户端收到服务器返回的内容:server send这种组合方式用起来十分复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 i/o 组合方式。如 cassandra 的 gossip 通信机制就是采用异步非阻塞的方式,可以实现非常高的网络传输性能。
netty 之前也尝试使用过 aio,不过又放弃了!
六、小结本文主要围绕 bio、nio、aio 等模型,结合一些样例代码,做了一次简单的内容知识总结,希望对大家有所帮助。

中国十大mcu公司有哪些
TLC优质颗粒+SSD协议 aigo高速固态U盘U393试用
PCBA打样过程的注意事项
华为matex2折叠新机多少钱 华为matex2折叠新机价格17999元起 全新双螺旋水滴铰链设计
Ovum最近对全球企业的物联网部署进行了调查涉及14个国家的1343家企业
BIO、NIO、AIO 模型工作方式
意大利议会安全委员会呼吁政府禁用华为中兴设备,但没有任何约束力
如何在振动环境下使涡街流量计能够实现准确计量
多方力量协同努力加快车载显示“上路”步伐
电容器开关跳闸的检查顺序
Oppo正式面向全球LOL粉丝推出了Oppo Find X2英雄联盟特别版
总线接口的类型
如何推进5G网络建设与规模商用
防静电实时监控系统可以实现的功能
三星智能电视来袭,有限的视野来探索无限的科技
智能电话销售、电话机器人的对话原理是什么?你知道吗?-悟空话务积机器人
台积电2nm和3nm制程工艺
如何加快5G室内覆盖建设步伐?
控温认准莱克空气调节扇,你的贴心空调伴侣
阿尔泰科技—创新解决农业环境监测难题!