趣谈网络协议 (geekbang.org)

目录

一、UDP协议

1、TCP和UDP的区别

2、UDP包头

3、UDP的特点

4、UDP使用场景

5、基于UDP的例子

二、TCP协议

1、TCP包头

2、三次握手、四次挥手

3、 顺序问题与丢包问题

确认与重发机制

流量控制问题

拥塞控制问题

三、Socket

1、基于 TCP 协议的 Socket 程序函数调用过程

2、基于 UDP 协议的 Socket 程序函数调用过程

3、服务器如何连接更多的端口

方式一:多进程

方式二:多线程

方式三、IO 多路复用,一个线程维护多个 Socket

方式四、IO多路复用,事件通知



一、UDP协议

传输层有两个比较重要的协议,一个是TCP,一个是UDP。

1、TCP和UDP的区别

  1. TCP是面向连接的协议,在互通之前会先建立连接,属于有状态服务。所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。而UDP是面向无连接的,是无状态服务。
  2. TCP提供可靠交付。通过TCP连接传输的数据,无差错、不丢失、不重复且按序到达。而UDP继承了IP包的特性,不保证不丢失,不保证按序到达。
  3. TCP是面向字节流的,发送的时候发的是一个流。UDP继承IP包的特性,是基于数据包,一个一个地发,一个一个地收。
  4. TCP可以有堵塞控制,它意识到包被丢弃了或网络环境不好了,就会根据情况调整自己的行为(降低发送速度之类)。UDP不会。

2、UDP包头

3、UDP的特点

  • 首部开销小,只有8个字节,比 TCP 的20个字节的首部要短
  • 面向报文,发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。
  • 无拥塞控制,网络出现的拥塞不会使源主机的发送速率降低。
  • 支持一对一、一对多、多对一和多对多的交互通信

4、UDP使用场景

(1)需要资源少,网络情况比较好的内网,或者对于丢包不敏感的应用。DHCP就是基于UDP协议的。一般的获取IP地址都是内网请求,而且,一次获取失败还可以继续获取。PXE启动时自动安装操作系统,操作系统镜像下载所使用的TFTP也是基于UDP协议的。

(2)无需一对一沟通,无需建立连接,可以广播的应用。UDP 的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议。对于多播,32位 IP 地址中的 D 类地址(组播地址),可以将包组播给一批机器。当一台机器上的某个进程想监听某个组播地址的时候,需要发送 IGMP 包,所在网络的路由器就能收到这个包,知道有个机器上有个进程在监听这个组播地址。当路由器收到这个组播地址的时候,会将包转发给这台机器,这样就实现了跨路由器的组播。云中网络部分的 VXLAN协议,也是需要用到组播,也是基于 UDP 协议的。

(3)需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的应用。如果应用本身实现了自己的连接策略、可靠保证和时延要求,也可以使用UDP代替TCP。

5、基于UDP的例子

(1)网页或APP的访问

起初访问网页和手机 APP 都是基于 HTTP 协议的。HTTP 协议是基于 TCP 的,建立连接都需要多次交互,对于时延比较大的目前主流的移动互联网来讲,建立一次连接需要的时间会比较长,然而既然是移动中,TCP 可能还会断了重连,也是很耗时的。而且目前的 HTTP 协议,往往采取多个数据通道共享一个连接的情况,这样本来为了加快传输速度,但是 TCP 的严格顺序策略使得哪怕共享通道,前一个不来,后一个和前一个即便没关系,也要等着,时延也会加大。而 QUIC( Quick UDP Internet Connections,快速 UDP 互联网连接)是 Google 提出的一种基于 UDP 改进的通信协议,在应用层上,会自己实现快速建立连接,减少重传时延、自适应拥塞控制。提供了更好的用户互动体验。

(2)流媒体协议

直播协议多采用RTMP,RTMP协议也是基于TCP的。TCP 的严格顺序传输要保证前一个收到了,下一个才能确认,如果前一个收不到,下一个包就算已经收到了,在缓存里面,也需要等着。对于直播来讲,这显然是不合适的,因为老的视频帧丢了其实也就丢了,就算再传过来用户也不在意了,他们要看新的了,如果老是没来就等着,卡顿了,新的也看不了,那就会丢失客户,所以直播,实时性比较比较重要,宁可丢包,也不要卡顿的。另外,对于丢包,其实对于视频播放来讲,有的包可以丢,有的包不能丢,因为视频的连续帧里面,有的帧重要,有的不重要,如果必须要丢包,隔几个帧丢一个,其实看视频的人不会感知,但是如果连续丢帧,就会感知了,因而在网络不好的情况下,应用希望选择性的丢帧。还有就是当网络不好的时候,TCP 协议会主动降低发送速度,这对本来当时就卡的看视频来讲是要命的,应该应用层马上重传,而不是主动让步。因而,很多直播应用,都基于 UDP 实现了自己的视频传输协议。

(3)实时游戏

游戏有一个特点,就是实时性比较高,因而,实时游戏中客户端和服务端要建立长连接,来保证实时传输。但是游戏玩家很多,服务器却不多。由于维护 TCP 连接需要在内核维护一些数据结构,因而一台机器能够支撑的 TCP 连接数目是有限的,然后 UDP 由于是没有连接的,在异步 IO 机制引入之前,常常是应对海量客户端连接的策略。另外还是 TCP 的强顺序问题,对战的游戏,对网络的要求很简单,玩家通过客户端发送给服务器鼠标和键盘行走的位置,服务器会处理每个用户发送过来的所有场景,处理完再返回给客户端,客户端解析响应,渲染最新的场景展示给玩家。如果出现一个数据包丢失,所有事情都需要停下来等待这个数据包重发。客户端会出现等待接收数据,然而玩家并不关心过期的数据,激战中卡 1 秒,等能动了都已经死了。游戏对实时要求较为严格的情况下,采用自定义的可靠 UDP 协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响。

(4)IOT物联网

一方面,物联网领域终端资源少,很可能只是个内存非常小的嵌入式系统,而维护 TCP 协议代价太大;另一方面,物联网对实时性要求也很高,而 TCP 还是因为上面的那些原因导致时延大。Google 旗下的 Nest 建立 Thread Group,推出了物联网通信协议 Thread,就是基于 UDP 协议的。

(5)移动通信领域

在 4G 网络里,移动流量上网的数据面对的协议 GTP-U 是基于 UDP 的。因为移动网络协议比较复杂,而 GTP 协议本身就包含复杂的手机上线下线的通信协议。如果基于 TCP,TCP 的机制就显得非常多余。

二、TCP协议

1、TCP包头

包的序号是为了解决乱序问题,确认序号是为了解决丢包问题。

状态位中,SYN是发起一个连接,ACK是回复,RDT是重新连接,FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些带状态位的包发送,会引起双方的状态变更。

窗口大小:TCP要做流量控制,通信双方各声明一个窗口,标识自己的当前的处理能力。

除了流量控制,TCP还会做拥塞控制,控制发送的速度。

2、三次握手、四次挥手

三次握手也常称为“请求——应答——应答之应答”,是为了确认通信双方都可发可收,以及沟通包的序号问题。每次连接都要有不同的序号,这个序号的起始序号是随时间变化的,可以看成一个32位的计数器,每4微秒加一,到重复需要4个多小时。

通信双方断开连接是在第四次挥手后再等2MSL。MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理它的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

断开状态时序图

TCP状态机

3、 顺序问题与丢包问题

TCP 协议为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。

为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。第一部分:发送了并且已经确认的。第二部分:发送了并且尚未确认的。第三部分:没有发送,但是已经等待发送的。第四部分:没有发送,并且暂时还不会发送的。

之所以区分三、四部分,是为了做流量控制。在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,。超过这个窗口的,接收端做不过来,就不能发送了。

确认与重发机制

一种方法是超时重试,即对每一个发送了但没有 ACK 的包都设一个定时器,超过一定时间,就重新尝试。超时时间的选择需要评估往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。超时触发重传存在的问题是,超时周期可能相对较长。有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。还有一种方式称为 SACK (Selective Acknowledgment ),这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 SACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制问题

如果接收方处理消息太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,发送方将暂时停止发送。这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。这就是我们常说的流量控制。

拥塞控制问题

TCP 的拥塞控制主要来避免两种现象,包丢失超时重传。一旦出现了这些现象。就说明发送速度太快了,要慢一点。

拥塞控制也是通过窗口的大小来控制的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。这里有一个公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} ,是拥塞窗口和滑动窗口共同控制发送的速度。那发送方怎么判断网络是不是慢呢?网络上,通道的容量 = 带宽 × 往返延迟。如果我们设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。

TCP BBR 拥塞算法企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

三、Socket

Socket 可以作插口或者插槽讲,在通信之前,双方都要建立一个 Socket。

Socket 编程进行的是端到端的通信,往往意识不到中间经过多少局域网,多少路由器,因而能够设置的参数,也只能是端到端协议之上网络层和传输层的。

在网络层,Socket 函数需要指定是 IPv4 还是 IPv6,分别对应设置为 AF_INETAF_INET6。另外,还要指定是 TCP 还是 UDP。TCP 协议是基于数据流的,所以设置为 SOCK_STREAM,而 UDP 是基于数据报的,因而设置为 SOCK_DGRAM

1、基于 TCP 协议的 Socket 程序函数调用过程

TCP 的服务端要先监听一个端口,一般是先调用 bind 函数,给这个 Socket 赋予一个 IP 地址和端口。当一个网络包来的时候,内核要通过 TCP 头里面的这个端口,来找到应用程序。

当服务端有了 IP 地址和端口号,就可以调用 listen 函数进行监听。在 TCP 的状态图里面,有一个 listen 状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了。

在内核中,为每个 Socket 维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态。

接下来,服务端调用 accept 函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。

在服务端等待的时候,客户端可以通过 connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的 accept 就会返回另一个 Socket。

监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket。连接建立成功之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

说 TCP 的 Socket 就是一个文件流,是非常准确的。因为,Socket 在 Linux 中就是以文件的形式存在的。除此之外,还存在文件描述符。写入和读出,也是通过文件描述符。

在内核中,Socket 是一个文件,那对应就有文件描述符。每一个进程都有一个数据结构 task_struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。这个数组中的内容是一个指针,指向内核中所有打开的文件的列表。

既然是一个文件,就会有一个 inode,只不过 Socket 对应的 inode 不像真正的文件系统一样,保存在硬盘上的,而是在内存中的。在这个 inode 中,指向了 Socket 在内核中的 Socket 结构。

在这个结构里面,主要的是两个队列,一个是发送队列,一个是接收队列。在这两个队列里面保存的是一个缓存 sk_buff。缓存里面能够看到完整的包的结构。

数据结构图:

2、基于 UDP 协议的 Socket 程序函数调用过程

UDP 是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是,UDP 的交互仍然需要 IP 和端口号,因而也需要 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom,都可以传入 IP 地址和端口。

3、服务器如何连接更多的端口

系统会用一个四元组来标识一个 TCP 连接


{本机IP, 本机端口, 对端IP, 对端端口}

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端TCP 连接四元组中只有对端 IP和对端的端口,也即客户端IP端口是可变的,因此,最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。

当然,服务端最大并发 TCP 连接数远不能达到理论上限。首先主要是文件描述符限制,按照上面的原理,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;另一个限制是内存,按上面的数据结构,每个 TCP 连接都要占用一定内存,而操作系统是有限的。

在资源有限的情况下,要想接更多的项目,就需要降低每个项目消耗的资源数目。

方式一:多进程

这就相当于你是一个代理,在那里监听来的请求。一旦建立了一个连接,就会有一个已连接 Socket,这时候你可以创建一个子进程,然后将基于已连接 Socket 的交互交给这个新的子进程来做。

在 Linux 下,创建子进程使用 fork 函数,在父进程的基础上完全拷贝一个子进程。在 Linux 内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程。复制的时候再调用 fork,复制完毕之后,父进程和子进程都会记录当前刚刚执行完 fork。这两个进程刚复制完的时候,几乎一模一样,只是根据 fork 的返回值来区分到底是父进程,还是子进程。如果返回值是 0,则是子进程;如果返回值是其他的整数,就是父进程。

方式二:多线程

在 Linux 下,通过 pthread_create 创建一个线程,也是调用 do_fork。不同的是,虽然新的线程在 task 列表会新创建一项,但是很多资源,例如文件描述符列表、进程空间,还是共享的,只不过多了一个引用而已。

新的线程也可以通过已连接 Socket 处理请求,从而达到并发处理的目的。

上面基于进程或者线程模型的,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个 C10K,它的意思是一台机器要维护 1 万个连接,就要创建 1 万个进程或者线程,那么操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器,成本也太高了。

方式三、IO 多路复用,一个线程维护多个 Socket

由于 Socket 是文件描述符,因而某个线程盯的所有的 Socket,都放在一个文件描述符集合 fd_set 中,这就是项目进度墙,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1,表示 Socket 可读或者可写,从而可以进行读写操作,然后再调用 select,接着盯着下一轮的变化。

select 函数还是有问题的,因为每次 Socket 所在的文件描述符集合中有 Socket 发生变化的时候,都需要通过轮询的方式,也就是需要将全部项目都过一遍的方式来查看进度,这大大影响了一个项目组能够支撑的最大的项目数量。因而使用 select,能够同时盯的项目数量由 FD_SETSIZE 限制。

方式四、IO多路复用,事件通知

epoll函数在内核中的实现不是通过轮询的方式,而是通过注册 callback 函数的方式,当某个文件描述符发送变化的时候,就会主动通知。

如图所示,假设进程打开了 Socket m, n, x 等多个文件描述符,现在需要通过 epoll 来监听是否这些 Socket 都有事件发生。其中 epoll_create 创建一个 epoll 对象,也是一个文件,也对应一个文件描述符,同样也对应着打开文件列表中的一项。在这项里面有一个红黑树,在红黑树里,要保存这个 epoll 要监听的所有 Socket。

当 epoll_ctl 添加一个 Socket 的时候,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的 Socket 的事件列表中。当一个 Socket 来了一个事件的时候,可以从这个列表中得到 epoll 对象,并调用 call  back 通知它。这种通知方式使得监听的 Socket 数据增加的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器。

趣谈网络协议(二)传输层相关推荐

  1. 趣谈网络协议-第二模块-底层网络知识详解:2最重要的传输层

    趣谈网络协议-第二模块-底层网络知识详解:2最重要的传输层 1:第10讲 | UDP协议:因性善而简单,难免碰到"城会玩" TCP 和 UDP 有哪些区别? UDP 包头是什么样的 ...

  2. 趣谈网络协议笔记-二(第十九讲)

    趣谈网络协议笔记-二(第十九讲) HttpDNS:网络世界的地址簿也会指错路 自勉 勿谓言之不预也 -- 向为祖国牺牲的先烈致敬! 引用 dns缓存刷新时间是多久?dns本地缓存时间介绍 - 东大网管 ...

  3. 趣谈网络协议笔记-二(第十七讲)

    趣谈网络协议笔记-二(第十七讲) P2P协议:我下小电影,99%急死你 自勉 逃离舒适区! 正文 一. P2P协议 整个篇章讲的就是这两个协议之间的区别.P2P协议就是迅雷下载数据时所用的协议, 众所 ...

  4. 趣谈网络协议笔记-二(第十三讲)

    趣谈网络协议笔记-二(第十三讲) 套接字Socket:Talk is cheap, show me the code 前言 这只是笔记,是为了整理刘超大神的极客时间专栏的只是而存在的! 经常会在网络上 ...

  5. 趣谈网络协议笔记-二(第五讲)

    趣谈网络协议笔记-二(第五讲) 目录 第二模块 底层网络知识讲解:第二层到第三层 第5讲 | 从物理层到MAC层:如何在宿舍离自己组网完联机游戏 第6讲 | 交换机与VLAN:办公室太复杂,我要回学校 ...

  6. 趣谈网络协议笔记-二(第十六讲上)

    趣谈网络协议笔记-二(第十六讲上) 流媒体协议:如何在直播里看到美女帅哥? 自勉 给岁月以文明,而不是给文明以岁月!--<三体> 在触不到的獠牙上点火--就像不必仰望那星星就能够解决--就 ...

  7. 趣谈网络协议笔记-二(第十二讲)

    趣谈网络协议笔记-二(第十二讲) TCP协议(下):西行必定多妖孽,恒心智慧消磨难 前言 哈哈哈,越当我看刘超的通俗讲解,我就越感觉自己的无能.每次当我看了讲解之后,每次当我感觉到这个东西原来是这么简 ...

  8. 趣谈网络协议笔记-二(第十一讲)

    趣谈网络协议笔记-二(第十一讲) TCP协议(上):因性恶而复杂,先恶后善反轻松 自勉 我似乎天性不擅长争斗,但是有些时候,我也必须砥砺前行. 强大是和平的前提,而善良不是. 前言 今天回到家里已经是 ...

  9. 趣谈网络协议笔记-二(第十讲)

    趣谈网络协议笔记-二(第十讲) UDP协议:因性善而简单,难免碰到"城会玩" 自勉 如果手上没有剑,我就无法保护你.如果我一直握着剑,我就无法抱紧你.--<Bleach> ...

  10. 趣谈网络协议笔记-二(第七,八,九讲)

    趣谈网络协议笔记-二(第七,八,九讲) 勉励 人生而自由,却无往不在枷锁(牢笼)之中. ------卢梭@<社会契约论> 为那些有形的牢笼感到庆幸吧. 为自己出生于这个时代感到庆幸吧,因为 ...

最新文章

  1. GStreamer 1.18.4稳定的错误修复版本
  2. 50 调度器事件监听
  3. 中国火锅对话农民丰收节交易会·万祥军:谋定餐调产业链
  4. 【干货】极简体验+免费真的万能嘛?北森Saas第一坑:免费
  5. 在ASP.NET中面向对象的编程思想
  6. 删除数据oracle,oracle删除数据
  7. BFS Codeforces Beta Round #94 (Div. 2 Only) C. Statues
  8. 脚本小子福利:安卓远控木马直接生成
  9. JAVA中数字转大写字母_java实现数字转大写
  10. 中望CAD的引线标注格式怎么改_大神说这样学CAD命令,超容易掌握
  11. php 在文本域中添加qq表情 createelement,仿微信在对话框文字中插入Emoji表情包
  12. exadata的infiniband交换机的ilom
  13. vue 页面文字转播放声音 (SpeechSynthesisUtterance)
  14. Android下载管理问题分析
  15. 马丁富勒微服务论文学习
  16. C Primer Plus 第3章 数据和C-编程练习
  17. 《剑与电——角色扮演游戏设计艺术》读书笔记(二)
  18. 一个很好用的网站:大英百科全书
  19. 网站关键词优化技术:如何限制关键词挖掘的范围
  20. q币充值php,腾讯Q币会员充值

热门文章

  1. XLSTransformer生成excel文件简单演示样例
  2. 如何安装JCreator
  3. flightgear基于udp用c++传输信息
  4. POI导出Excel换行
  5. pytorch打包exe出现WARNING: file already exists but should not: C:\Users\workAI\AppData\Local\Temp\_MEI13
  6. 在计算机操作系统中操作系统是处于应用软件,计算机操作系统考试复习题
  7. HarmoneyOS鸿蒙系统零代码编程入门
  8. 开源免费强大的按键精灵软件----AutoHotKey
  9. 适合android手机 pdf阅读器,手机版pdf阅读器有哪些 这五款软件一定有适合你的
  10. 最具有中国特色的脑筋急转弯