Linux网络编程1——socket通信
一.网络准备
1.套接字
在TCP/IP
协议中,“ip 地址+TCP 或UDP 端口号”唯一标识网络通讯中的一个进程。“IP 地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个 socket 来标识,那么这两个 socket 组成的 socket pair 就唯一标识一个连接。因此可以用 socket 来描述网络连接的一对一关系。
简单来说,用数学里面的坐标系类比一下,(IP地址,端口号)表示一个进程即一个套接字,两个套接字连线表示通信。
- 一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。
- 在通信过程中, 套接字一定是成对出现的。
2.网络字节序列
计算机(主机)对于多字节序列采用的是小端法存储,而网络通信的多字节序列是大端法存储的,因此在进行网络通信时,主机把数据送到客户端的发送缓冲区前需要把数据进行网络字节转换,同样的接受主机在服务器的接收端取出数据后,又要把数据转化成主机字节序列。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong); //本地---》网络(IP地址)
uint16_t htons(uint16_t hostshort); //本地---》网络(端口)
uint32_t ntohl(uint32_t netlong); //网络---》本地(IP地址)
uint16_t ntohs(uint16_t netshort); //网络---》本地(端口)
解释:
h
表示本地主机(host)to
表示变换n
表示网络(net)l
表示IP
地址(IP
地址用32位表示)s
表示端口
注意:我们平时用点分十进制表示IP地址,所以要想进行网络字节转换,先要使用
atoi
把string
转化成int
3.IP地址转换函数
为了直接从点分十进制进行转化成网络字节序,所以有了IP地址转换函数。
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst); //IP地址---》网络
解释:
af
:表示版本协议号,只能取AF_INET
表示IPv4或AF_INET6
表示IPv6src
:传入的IP地址(点分十进制,string
类型)dst
:传出的转换后的IP地址(网络字节序)- 返回值:1表示成功;0表示异常,说明传入的src不是一个IP地址;-1表示失败
#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); //网络---》IP地址
解释:
src
:传入的转换后的IP地址(网络字节序)dst
:传出的IP地址(点分十进制,string
类型)size
:dst缓冲区的大小- 返回值:成功返回dst,失败返回NULL
4.sockaddr数据结构
直接说结论:后面的bind()
函数的参数要用到strcut sockaddr
这种结构体的指针,但是现在IPv4使用的结构体普遍是strcut sockaddr_in
,所以现在的使用方式是我们一般先定义strcut sockaddr_in
的结构体,使用的时候进行强转。比如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
其余不要管,重点看bind()
的第二个参数。
所以,现在我们重点学习一下strcut sockaddr_in
结构体:
struct sockaddr_in {__kernel_sa_family_t sin_family; //地址结构类型__be16 sin_port; //端口号(网络字节序)struct in_addr sin_addr; //IP地址(网络字节序)
};struct in_addr { __be32 s_addr; //IP地址(网络字节序)
};
注意:这里的sockaddr_in
的第三个参数是一个结构体的形式,所以赋值时需要注意,例如:
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET,"192.157.22.45",(void *)&dst);
addr.sin_addr.s_addr = dst;
这里使用强转把dst
变成void *
类型,赋值是不要忘了.s_addr
进入对应结构体内部赋值。
一般的时候,我们用的是本地主机写代码,所以可以将:
int dst; inet_pton(AF_INET,"192.157.22.45",(void *)&dst); addr.sin_addr.s_addr = dst;
写成
addr.sin_addr.s_addr = htonl(INADDR_ANY);
这里的
INADDR_ANY
是一种宏,表示取出系统中有效的任意IP地址,但是是二进制类型。
二.网络套接字函数
1.socket模型
socket()
:创建套接字bind()
:绑定服务器的IP和端口listen()
:设置同时监听上限accept()
:阻塞监听客户端连接connect()
:客户端绑定IP和端口,进行连接
注意,在一个模型中其实有三个socket套接字,其中服务器刚开始的套接字在accept()函数里作为参数传入,返回另一个套接字来与客户端进行真正的连接。
2.socket函数
函数原型:
#include <sys/socket.h>int socket(int domain, int type, int protocol);
domain
:用来指定传输协议,AF_INET
、AF_INET6
、AF_UNIX
(表示本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用)type
:用来指定协议类型,可以取SOCK_STREAM
表示流式协议或SOCK_DGRAM
表示报式协议protocol
:传0表示默认协议
type为
SOCK_STREAM
且protocol=0表示使用TCP传输,type为SOCK_DGRAM
且protocol=0表示使用UDP传输。
3.bind函数
作用:给socket绑定一个地址结构(IP地址+端口)
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:socket
文件描述符,一般取上面socket()
函数的返回值*addr
:上面介绍过,用来指定地址结构信息addrlen
:地址结构大小,总是取sizeof(addr)
4.listen函数
作用:设置同时与服务器建立连接的客户端数量
#include <sys/socket.h>int listen(int sockfd, int backlog);
sockfd
:socket文件描述符,一般取上面socket()
函数的返回值backlog
:连接上限,最大为128
5.accept函数
作用:阻塞等待客户端连接,成功的话,返回一个成功与客户端连接的socket文件描述符
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf
: socket文件描述符,一般取上面socket()
函数的返回值,相当于把开始的socket传入addr
:传出参数,返回成功链接客户端地址信息,含IP地址和端口号addrlen
:传入传出参数(值-结果),传入sizeof(addr)
大小,函数返回时返回真正接收到地址结构体的大小- 返回值: 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置
errno
6.connect函数
作用:使用现有的socket
与服务器建立连接
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:socket
文件描述符,一般取上面socket()
函数的返回值*addr
:服务器的地址结构,用来连接addrlen
:地址结构大小,总是取sizeof(addr)
7.read函数
read函数用于从socket中读取数据。它的原型如下:
ssize_t read(int fd, void *buffer, size_t count);
fd
:是socket的文件描述符,用于标识一个打开的socket。buffer
:是一个指针,指向一个缓冲区,该缓冲区用于存储从socket读取的数据。count
:指定了buffer的大小,即希望读取的最大字节数。
read
函数返回实际读取的字节数。如果read返回0,表示连接已经关闭。如果返回-1,表示发生了错误,此时可以通过errno
变量查看错误类型。
8.write函数
write函数用于向socket写入数据。它的原型如下:
ssize_t write(int fd, const void *buffer, size_t count);
fd
:同样是socket的文件描述符。buffer
:是一个指针,指向包含要发送数据的缓冲区。count
:指定了要发送的字节数。write
函数返回实际写入的字节数。如果返回-1,表示发生了错误,同样可以通过errno
变量查看错误类型。
8.文件描述符
fd_set 是一个数据类型,用于在 select 系统调用中表示一组文件描述符。以下是与 fd_set 相关的一些常用宏函数,它们用于操作 fd_set:
- FD_ZERO:
作用:将 fd_set 清零,即初始化 fd_set,使其不包含任何文件描述符。
使用方法:FD_ZERO(&fdset); - FD_SET:
作用:将一个文件描述符添加到 fd_set 中。
使用方法:FD_SET(fd, &fdset); 其中 fd 是要添加的文件描述符,fdset 是 fd_set 的实例。 - FD_CLR:
作用:从 fd_set 中移除一个文件描述符。
使用方法:FD_CLR(fd, &fdset); 其中 fd 是要移除的文件描述符,fdset 是 fd_set 的实例。 - FD_ISSET:
作用:测试一个文件描述符是否在 fd_set 中。
使用方法:FD_ISSET(fd, &fdset); 如果 fd 在 fdset 中,则返回非零值;否则返回0。
三.实现一个简单的通信
1.通信流程分析
TCP通信流程分析:
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close(); client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据。
5. 显示读取结果
6. close()
2.实现服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>#define ser_port 9527
int main()
{//1.socket partint sfd=0,cfd=0;sfd = socket(AF_INET, SOCK_STREAM, 0);if(sfd==-1){printf("socket error\n");}//2.bind partstruct sockaddr_in ser_addr,cet_addr;ser_addr.sin_family=AF_INET;ser_addr.sin_port=htons(ser_port);ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));//3.listen partlisten(sfd,20);//4.accept partchar client_ip[BUFSIZ];socklen_t cet_addr_len = sizeof(cet_addr);cfd = accept(sfd,(struct sockaddr *)&cet_addr,&cet_addr_len);if(cfd==-1){printf("accept socket error\n");}//5.read partchar buf[BUFSIZ];while(true){int ret = read(cfd,buf,sizeof(buf));write(STDOUT_FILENO,buf,ret);for(int i=0;i<ret;i++){buf[i]=toupper(buf[i]);}write(cfd,buf,ret);}//6.close partclose(sfd);close(cfd);return 0;
}
3.实现客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define ser_port 9527
int main() {//1.socket partint hfd=0;hfd=socket(AF_INET, SOCK_STREAM, 0);if(hfd==-1){printf("socket error\n");}//2.connect partstruct sockaddr_in cil_addr;cil_addr.sin_family=AF_INET;cil_addr.sin_port=htons(ser_port);inet_pton(AF_INET,"192.168.242.128",&cil_addr.sin_addr.s_addr);connect(hfd,(struct sockaddr *)&cil_addr,sizeof(cil_addr));// 3. Write and Read partchar buf[BUFSIZ];int count = 10;while (count-- > 0) {if (write(hfd, "hello", 5) == -1) {perror("write");close(hfd);exit(EXIT_FAILURE);}int ret = read(hfd, buf, sizeof(buf));if (ret == -1) {perror("read");close(hfd);exit(EXIT_FAILURE);} else if (ret == 0) {printf("Server disconnected\n");break;}write(STDOUT_FILENO, buf, ret);}close(hfd);return 0;
}
四.出错封装函数思想
1.封装思想
上面简单的写了一个实现大小写转换的通信服务器和客户端,但是并不完善,里面的很多函数调用都没有做错误处理,在实际编写过程中并不规范。
我们以accept()
函数为例,在上面的例子中,我们是这样写的:
char client_ip[BUFSIZ];socklen_t cet_addr_len = sizeof(cet_addr);cfd = accept(sfd,(struct sockaddr *)&cet_addr,&cet_addr_len);if(cfd==-1){printf("accept socket error\n");}
但是实际过程中的main.cpp
代码量其实是很少的,我们可以把accept()
函数进行封装成类似自定义函数,存储在另一个源文件中,这样就可以在main.cpp调用自定义的Accept()
函数啦。
一般这种错误函数的封装,我们把重新自定义的函数名取为原函数名基础上首字母大写,然后在自定义函数内部实现错误封装,所以上述代码可以改成:
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{int n;again:if ( (n = accept(fd, sa, salenptr)) < 0) {if ((errno == ECONNABORTED) || (errno == EINTR))goto again;elseperr_exit("accept error");}return n;
}
注意:自定义函数封装在wrcp.cpp
文件中,不要忘了对应的wrcp.h
头文件。
2.读写函数
我们在C语言文件操作时,学习了一些读写函数,但是强调过只有read()
和write()
是满足系统调用的(即在socket通信过程中使用),所以平时的读写我们要自定义一些读(写)n字节函数或读(写)n行函数。下面代码仅供参考:
ssize_t Readn(int fd, void *vptr, size_t n)
{size_t nleft;ssize_t nread;char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR)nread = 0;elsereturn -1;} else if (nread == 0)break;nleft -= nread;ptr += nread;}return n - nleft;
}ssize_t Writen(int fd, const void *vptr, size_t n)
{size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n;
}static ssize_t my_read(int fd, char *ptr)
{static int read_cnt;static char *read_ptr;static char read_buf[100];if (read_cnt <= 0) {
again:if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {if (errno == EINTR)goto again;return -1; } else if (read_cnt == 0)return 0;read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;return 1;
}ssize_t Readline(int fd, void *vptr, size_t maxlen)
{ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = my_read(fd, &c)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {*ptr = 0;return n - 1;} elsereturn -1;}*ptr = 0;return n;
}
相关文章:

Linux网络编程1——socket通信
一.网络准备 1.套接字 在TCP/IP 协议中,“ip 地址TCP 或UDP 端口号”唯一标识网络通讯中的一个进程。“IP 地址端口号”就对应一个socket。欲建立连接的两个进程各自有一个 socket 来标识,那么这两个 socket 组成的 socket pair 就唯一标识一个连接。因…...
【每日一题】LeetCode 1052.爱生气的书店老板(数组、滑动窗口)
【每日一题】LeetCode 1052.爱生气的书店老板(数组、滑动窗口) 题目描述 书店老板的商店每天有不同数量的顾客进入。每分钟,老板可能或可能不会生气。如果老板生气,那一分钟的顾客就会不满意。老板知道一个秘密技巧,…...

IDEA中无法使用 Subversion 命令行客户端 svn Subversion 可执行文件的路径可能是错误的
IDEA中无法使用 Subversion 命令行客户端 svn 我在新电脑上安装好IDEA和SVN后使用IDEA拉取和提交项目时提示无法使用。 解决方案 我这边的问题是在安装TortoiseSVN的时候少启用了一个功能,需要重新安装并把这个功能启用。 在这一步需要把command line client to…...
ThreadLocal 在线程池中的内存泄漏问题
ThreadLocal 是一种非常方便的工具,它为每个线程创建独立的变量副本,避免了线程之间的共享数据问题。然而,在线程池环境中,ThreadLocal 的使用必须非常谨慎,否则可能会引发内存泄漏问题。 为什么 ThreadLocal 可能导致…...

如何编写Prompt,利用AI高效生成图表——图表狐(FoxChart)指南
在数据可视化领域,图表是数据的重要表达方式。为了让更多人能够轻松高校地生成美观、专业的图表,图表狐(FoxChart)应用而生。然而,要想充分发挥AI的潜力,编写合适的Prompt至关重要。本文介绍一些编写Prompt的原则,帮助…...

Redis主从数据同步过程:命令传播、部分重同步、复制偏移量等
请记住胡广一句话,所有的中间件所有的框架都是建立在基础之上,数据结构,计算机网络,计算机原理大伙一定得看透!!~ 1. Redis数据同步 1.1 数据同步过程 大家有没想过为什么Redis多机要进行数据同步&#…...

《JavaEE进阶》----13.<Spring Boot【配置文件】>
本篇博客讲解 1.SpringBoot配置文件的格式以及对应的语法 2.了解两个配置文件格式的差异、优缺点。 我们这里只做简单的介绍。看会,了解,学会读取就行了。 因为配置文件实在太多了,这里只做基础的介绍。 一、配置文件的作用 前言 计算机中有许…...

【练习8】
链接:https://www.nowcoder.com/questionTerminal/e671c6a913d448318a49be87850adbcc 分析: 创建一个二维数组来实现杨辉三角,因为当前元素的值是上一行的当前列与前一列的和,所以创建数组的时候要实现n1,相当于罩子一…...

vivado 时间汇总报告
步骤7:时间汇总报告 定时路径在时钟元素处开始和结束。输入和输出端口不是顺序的 元素,默认情况下,Vivado时序分析不会对进出I/O端口的路径进行计时 设计,除非指定了输入/输出延迟约束。 在此步骤中,您将在Vivado中生成…...

【软考】设计模式之代理模式
目录 1. 说明2. 应用场景3. 结构图4. 构成5. 适用性6. 优点7. 缺点8. java示例 1. 说明 1.代理模式(Proxy Pattern)。2.意图:为其他对象提供一种代理以控制对这个对象的访问。3.通过提供与对象相同的接口来控制对这个对象的访问。4.是设计模…...
3.创建型设计模式详解:生成器模式与原型模式的深度解析
设计模式(Design Patterns)是软件开发中常用的解决方案,帮助开发者处理常见的设计问题。创建型设计模式专注于对象的实例化,旨在提高系统的灵活性和可维护性。在这篇文章中,我们将深入探讨创建型设计模式中的生成器模式…...
goframe结构体标签和命令行标签
元数据gmeta 基础标签 更多了解:https://swagger.io/specification/ g.Meta path:"/profile" method:"get" summary:"展示个人资料页面" tags:"个人" g.Meta mime:"text/html" type:"string" example…...

pytest压力测试:不断发送数据,直到发现数据丢失
示例场景 假设有一个 send_data 函数接受数据并返回成功或失败的状态。 创建一个测试用例,通过逐步增加数据量来测试这个函数,直到返回失败为止。 步骤 定义压力测试函数 定义一个函数。不断发送数据,直到发现数据丢失。 创建 pytest 测试…...

自选择问题和处理效应模型
自选择问题和处理效应模型 DGP 注意: 这里的概率密度超过了1,这是正常的。概率密度的三原则,1是大于等于0;2是积分等于1;对于连续型随机变量,给定一个具体的x值,f(x)并不是该事件发生的概率。而…...

[数据集][目标检测]水面垃圾检测数据集VOC+YOLO格式2027张1类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):2027 标注数量(xml文件个数):2027 标注数量(txt文件个数):2027 标注…...

OpenCV 之 模版匹配多个对象、图片旋转 综合应用
引言 在图像处理和计算机视觉中,模板匹配是一种常用的技术,用于在一幅较大的图像中查找与给定模板图像相似的部分。然而,在实际应用中,目标物体可能会出现在不同的角度,这就需要我们在匹配之前对模板进行旋转处理。本…...
ZooKeeper 中的 Curator 框架解析
Apache ZooKeeper 是一个为分布式应用提供一致性服务的软件。它提供了诸如配置管理、分布式同步、组服务等功能。在使用 ZooKeeper 时,Curator 是一个非常流行的客户端库,它简化了 ZooKeeper 的使用,提供了高级的抽象和丰富的工具。本文将详细…...

机械学习—零基础学习日志(Python做数据分析02)
现在开始使用Python尝试做数据分析。具体参考的网址链接放在了文章末尾。 引言 我通过学习《利用Python进行数据分析》这本书来尝试使用Python做数据分析。书里让下载,anaconda,使用Jupyter来写代码,只是下载一个anaconda的确有点费时间&am…...

BRAM IP Native模式使用
简介 BRAM(Block RAM)是FPGA(Field-Programmable Gate Array)中的一种专用RAM资源,固定分布在FPGA内部的特定位置。该内容主要对BRAM(Block RAM”的缩写)Native模式下IP界面做详细描述和使用…...
react的useRef用什么作用
useRef 是 React 提供的一个钩子,用于在函数组件中创建和管理对 DOM 元素或组件实例的引用。它返回一个包含 current 属性的对象,可以用来存储对某个值的引用,而这个引用在组件的整个生命周期内保持不变。 useRef 的主要用途 1.访问 DOM 元素…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...