1 2 3 4 5 6 7 8 9 10 | # include <stdio.h> # include <stdlib.h> # include <limits.h> int main() { unsigned char requestIdB1 = UCHAR_MAX; unsigned char requestIdB0 = UCHAR_MAX; printf( "%d\n" , (requestIdB1 << 8) + requestIdB0); // 65535 } |
FastCGI
在讨论 FastCGI 之前,不得不说传统的 CGI 的工作原理,同时应该大概了解 CGI 1.1 协议
CGI 简介
CGI全称是“通用网关接口”(Common Gateway Interface),它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据。 CGI描述了客户端和这个程序之间传输数据的一种标准。 CGI的一个目的是要独立于任何语言的,所以CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。 如php,perl,tcl等。
CGI 的运行原理
- 客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求。
- 服务器端的 HTTP Daemon(守护进程)启动一个子进程。然后在子进程中,将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果通过标准输出 stdout 返回给 HTTP Daemon 子进程。
- 再由 HTTP Daemon 子进程通过 HTTP 协议返回给客户端。
上面的这段话理解可能还是比较抽象,下面我们就通过一次 GET 请求为例进行详细说明。

如图所示,本次请求的流程如下:
- 客户端访问 http://127.0.0.1:9003/cgi-bin/user?id=1
- 127.0.0.1 上监听 9003 端口的守护进程接受到该请求
- 通过解析 HTTP 头信息,得知是 GET 请求,并且请求的是
/cgi-bin/
目录下的user
文件。 - 将 uri 里的
id=1
通过存入QUERY_STRING
环境变量。 - Web 守护进程 fork 一个子进程,然后在子进程中执行 user 程序,通过环境变量获取到
id
。 - 执行完毕之后,将结果通过标准输出返回到子进程。
- 子进程将结果返回给客户端。
下面是演示代码:
Web 服务器程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | # include <stdio.h> # include <stdlib.h> # include <unistd.h> # include <sys/types.h> # include <sys/socket.h> # include <arpa/inet.h> # include <netinet/in.h> # include <string.h> #define SERV_PORT 9003 char *str_join(char *str1, char *str2); char *html_response(char *res, char *buf); int main(void) { int lfd, cfd; struct sockaddr_in serv_addr, clin_addr; socklen_t clin_len; char buf[1024], web_result[1024]; int len; FILE *cin; if ((lfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror( "create socket failed" ); exit (1); } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); if (bind(lfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1) { perror( "bind error" ); exit (1); } if (listen(lfd, 128) == -1) { perror( "listen error" ); exit (1); } signal(SIGCLD, SIG_IGN); while (1) { clin_len = sizeof(clin_addr); if ((cfd = accept(lfd, (struct sockaddr *) &clin_addr, &clin_len)) == -1) { perror( "接收错误\n" ); continue ; } cin = fdopen(cfd, "r" ); setbuf(cin, (char *) 0); fgets (buf, 1024, cin); //读取第一行 printf( "\n%s" , buf); //============================ cgi 环境变量设置演示 ============================ // 例如 "GET /cgi-bin/user?id=1 HTTP/1.1"; char *delim = " " ; char *p; char *method, *filename, *query_string; char *query_string_pre = "QUERY_STRING=" ; method = strtok (buf, delim); // GET p = strtok (NULL, delim); // /cgi-bin/user?id=1 filename = strtok (p, "?" ); // /cgi-bin/user if ( strcmp (filename, "/favicon.ico" ) == 0) { continue ; } query_string = strtok (NULL, "?" ); // id=1 putenv(str_join(query_string_pre, query_string)); //============================ cgi 环境变量设置演示 ============================ int pid = fork(); if (pid > 0) { close(cfd); } else if (pid == 0) { close(lfd); FILE *stream = popen(str_join( "." , filename), "r" ); fread (buf, sizeof(char), sizeof(buf), stream); html_response(web_result, buf); write(cfd, web_result, sizeof(web_result)); pclose(stream); close(cfd); exit (0); } else { perror( "fork error" ); exit (1); } } close(lfd); return 0; } char *str_join(char *str1, char *str2) { char *result = malloc( strlen (str1) + strlen (str2) + 1); if (result == NULL) exit (1); strcpy(result, str1); strcat(result, str2); return result; } char *html_response(char *res, char *buf) { char *html_response_template = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: %d\r\nServer: mengkang\r\n\r\n%s" ; sprintf(res, html_response_template, strlen (buf), buf); return res; } |
如上代码中的重点:
66~81行找到CGI程序的相对路径(我们为了简单,直接将其根目录定义为Web程序的当前目录),这样就可以在子进程中执行 CGI 程序了;同时设置环境变量,方便CGI程序运行时读取; 94~95行将 CGI 程序的标准输出结果写入 Web 服务器守护进程的缓存中; 97行则将包装后的 html 结果写入客户端 socket 描述符,返回给连接Web服务器的客户端。
CGI 程序(user.c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | # include <stdio.h> # include <stdlib.h> // 通过获取的 id 查询用户的信息 int main(void) { //============================ 模拟数据库 ============================ typedef struct { int id; char *username; int age; } user; user users[] = { {}, { 1, "mengkang.zhou" , 18 } }; //============================ 模拟数据库 ============================ char *query_string; int id; query_string = getenv ( "QUERY_STRING" ); if (query_string == NULL) { printf( "没有输入数据" ); } else if (sscanf(query_string, "id=%d" , &id) != 1) { printf( "没有输入id" ); } else { printf( "用户信息查询<br>学号: %d<br>姓名: %s<br>年龄: %d" , id, users[id].username, users[id].age); } return 0; } |
将上面的 CGI 程序编译成gcc user.c -o user,放在上面web程序的 ./cgi-bin/
目录下。 代码中的第28行,从环境变量中读取前面在Web服务器守护进程中设置的环境变量,是我们演示的重点。
FastCGI 简介
FastCGI是Web服务器和处理程序之间通信的一种协议, 是CGI的一种改进方案,FastCGI像是一个常驻(long-lived)型的CGI, 它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI最为人诟病的fork-and-execute模式)。 正是因为他只是一个通信协议,它还支持分布式的运算,所以 FastCGI 程序可以在网站服务器以外的主机上执行,并且可以接受来自其它网站服务器的请求。
FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此获得较高的性能。 CGI 程序反复加载是 CGI 性能低下的主要原因,如果 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则可以提供良好的性能、伸缩性、Fail-Over 特性等。
FastCGI 工作流程如下:
- FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程,并等待来自 Web Server 的连接。
- Web 服务器与 FastCGI 进程管理器进行 Socket 通信,通过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 解释器进程。
- CGI 解释器进程完成处理后将标准输出和错误信息从同一连接返回 Web Server。
- CGI 解释器进程接着等待并处理来自 Web Server 的下一个连接。

FastCGI 与传统 CGI 模式的区别之一则是 Web 服务器不是直接执行 CGI 程序了,而是通过 Socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,也正是由于 FastCGI 进程管理器是基于 Socket 通信的,所以也是分布式的,Web 服务器可以和 CGI 响应器服务器分开部署。Web 服务器需要将数据 CGI/1.1 的规范封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序。
FastCGI 协议
可能上面的内容理解起来还是很抽象,这是由于第一对FastCGI协议还没有一个大概的认识,第二没有实际代码的学习。所以需要预先学习下 FastCGI 协议,不一定需要完全看懂,可大致了解之后,看完本篇再结合着学习理解消化。
下面结合 PHP 的 FastCGI 的代码进行分析,不作特殊说明以下代码均来自于 PHP 源码。
FastCGI 消息类型
FastCGI 将传输的消息做了很多类型的划分,其结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 | typedef enum _fcgi_request_type { FCGI_BEGIN_REQUEST = 1, /* [in] */ FCGI_ABORT_REQUEST = 2, /* [in] (not supported) */ FCGI_END_REQUEST = 3, /* [out] */ FCGI_PARAMS = 4, /* [in] environment variables */ FCGI_STDIN = 5, /* [in] post data */ FCGI_STDOUT = 6, /* [out] response */ FCGI_STDERR = 7, /* [out] errors */ FCGI_DATA = 8, /* [in] filter data (not supported) */ FCGI_GET_VALUES = 9, /* [in] */ FCGI_GET_VALUES_RESULT = 10 /* [out] */ } fcgi_request_type; |
消息的发送顺序
下图是一个比较常见消息传递流程

最先发送的是FCGI_BEGIN_REQUEST
,然后是FCGI_PARAMS
和FCGI_STDIN
,由于每个消息头(下面将详细说明)里面能够承载的最大长度是65535,所以这两种类型的消息不一定只发送一次,有可能连续发送多次。
FastCGI 响应体处理完毕之后,将发送FCGI_STDOUT
、FCGI_STDERR
,同理也可能多次连续发送。最后以FCGI_END_REQUEST
表示请求的结束。 需要注意的一点,FCGI_BEGIN_REQUEST
和FCGI_END_REQUEST
分别标识着请求的开始和结束,与整个协议息息相关,所以他们的消息体的内容也是协议的一部分,因此也会有相应的结构体与之对应(后面会详细说明)。而环境变量、标准输入、标准输出、错误输出,这些都是业务相关,与协议无关,所以他们的消息体的内容则无结构体对应。
由于整个消息是二进制连续传递的,所以必须定义一个统一的结构的消息头,这样以便读取每个消息的消息体,方便消息的切割。这在网络通讯中是非常常见的一种手段。
FastCGI 消息头
如上,FastCGI 消息分10种消息类型,有的是输入有的是输出。而所有的消息都以一个消息头开始。其结构体定义如下:
1 2 3 4 5 6 7 8 9 10 | typedef struct _fcgi_header { unsigned char version; unsigned char type; unsigned char requestIdB1; unsigned char requestIdB0; unsigned char contentLengthB1; unsigned char contentLengthB0; unsigned char paddingLength; unsigned char reserved; } fcgi_header; |
字段解释下:
version
标识FastCGI协议版本。 type
标识FastCGI记录类型,也就是记录执行的一般职能。 requestId
标识记录所属的FastCGI请求。 contentLength
记录的contentData组件的字节数。
关于上面的xxB1
和xxB0
的协议说明:当两个相邻的结构组件除了后缀“B1”和“B0”之外命名相同时,它表示这两个组件可视为估值为B1<<8 + B0
的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定归纳了一个由超过两个字节表示的数字的处理方式。
比如协议头中requestId
和contentLength
表示的最大值就是 65535。
你可能会想到如果一个消息体长度超过65535怎么办,则分割为多个相同类型的消息发送即可。
FCGI_BEGIN_REQUEST 的定义
1 2 3 4 5 6 | typedef struct _fcgi_begin_request { unsigned char roleB1; unsigned char roleB0; unsigned char flags; unsigned char reserved[5]; } fcgi_begin_request; |
字段解释: role
表示Web服务器期望应用扮演的角色。分为三个角色(而我们这里讨论的情况一般都是响应器角色)
1 2 3 4 5 | typedef enum _fcgi_role { FCGI_RESPONDER = 1, FCGI_AUTHORIZER = 2, FCGI_FILTER = 3 } fcgi_role; |
而FCGI_BEGIN_REQUEST
中的flags
组件包含一个控制线路关闭的位:flags & FCGI_KEEP_CONN
:如果为0,则应用在对本次请求响应后关闭线路。如果非0,应用在对本次请求响应后不会关闭线路;Web服务器为线路保持响应性。
FCGI_END_REQUEST 的定义
1 2 3 4 5 6 7 8 | typedef struct _fcgi_end_request { unsigned char appStatusB3; unsigned char appStatusB2; unsigned char appStatusB1; unsigned char appStatusB0; unsigned char protocolStatus; unsigned char reserved[3]; } fcgi_end_request; |
字段解释:
appStatus
组件是应用级别的状态码。 protocolStatus
组件是协议级别的状态码;protocolStatus
的值可能是:
FCGI_REQUEST_COMPLETE:请求的正常结束。 FCGI_CANT_MPX_CONN:拒绝新请求。这发生在Web服务器通过一条线路向应用发送并发的请求时,后者被设计为每条线路每次处理一个请求。 FCGI_OVERLOADED:拒绝新请求。这发生在应用用完某些资源时,例如数据库连接。 FCGI_UNKNOWN_ROLE:拒绝新请求。这发生在Web服务器指定了一个应用不能识别的角色时。
protocolStatus
在 PHP 中的定义如下
1 2 3 4 5 6 | typedef enum _fcgi_protocol_status { FCGI_REQUEST_COMPLETE = 0, FCGI_CANT_MPX_CONN = 1, FCGI_OVERLOADED = 2, FCGI_UNKNOWN_ROLE = 3 } dcgi_protocol_status; |
需要注意dcgi_protocol_status
和fcgi_role
各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。
消息通讯样例
为了简单的表示,消息头只显示消息的类型和消息的 id,其他字段都不予以显示。而一行表示一个数据包。下面的例子来自于官网
1 2 3 4 5 | {FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}} {FCGI_PARAMS, 1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... " } {FCGI_STDIN, 1, "quantity=100&item=3047936" } {FCGI_STDOUT, 1, "Content-type: text/html\r\n\r\n<html>\n<head> ... " } {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}} |
配合上面各个结构体,则可以大致想到 FastCGI 响应器的解析和响应流程: 首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST
,然后解析其消息体,得知其需要的角色就是FCGI_RESPONDER
,flag
为0,表示请求结束后关闭线路。然后解析第二段消息,得知其消息类型为FCGI_PARAMS
,然后直接将消息体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT
消息体和FCGI_END_REQUEST
消息体供 Web 服务器解析。
PHP中的CGI实现
PHP的CGI实现了FastCGI协议,是一个TCP或UDP协议的服务器接受来自Web服务器的请求, 当启动时创建TCP/UDP协议的服务器的socket监听,并接收相关请求进行处理。随后就进入了PHP的生命周期: 模块初始化,sapi初始化,处理PHP请求,模块关闭,sapi关闭等就构成了整个CGI的生命周期。
以TCP为例,在TCP的服务端,一般会执行这样几个操作步骤:
- 调用socket函数创建一个TCP用的流式套接字;
- 调用bind函数将服务器的本地地址与前面创建的套接字绑定;
- 调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;
- 服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;
- 当与客户端创建连接后,服务器调用read_stream函数读取客户的请求;
- 处理完数据后,服务器调用write函数向客户端发送应答。
TCP上客户-服务器事务的时序如图2.6所示:

PHP的CGI实现从cgi_main.c文件的main函数开始,在main函数中调用了定义在fastcgi.c文件中的初始化,监听等函数。 对比TCP的流程,我们查看PHP对TCP协议的实现,虽然PHP本身也实现了这些流程,但是在main函数中一些过程被封装成一个函数实现。 对应TCP的操作流程,PHP首先会执行创建socket,绑定套接字,创建监听:
if (bindpath) {
fcgi_fd = fcgi_listen(bindpath, 128); // 实现socket监听,调用fcgi_init初始化
...
}
在fastcgi.c文件中,fcgi_listen函数主要用于创建、绑定socket并开始监听,它走完了前面所列TCP流程的前三个阶段,
1 2 3 4 5 6 | if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 || ... bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 || listen(listen_socket, backlog) < 0) { ... } |
当服务端初始化完成后,进程调用accept函数进入阻塞状态,在main函数中我们看到如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | while (parent) { do { pid = fork(); // 生成新的子进程 switch (pid) { case 0: // 子进程 parent = 0; /* don't catch our signals */ sigaction(SIGTERM, &old_term, 0); // 终止信号 sigaction(SIGQUIT, &old_quit, 0); // 终端退出符 sigaction(SIGINT, &old_int, 0); // 终端中断符 break ; ... default : /* Fine */ running++; break ; } while (parent && (running < children)); ... while (!fastcgi || fcgi_accept_request(&request) >= 0) { SG(server_context) = (void *) &request; init_request_info(TSRMLS_C); CG(interactive) = 0; ... } |
如上的代码是一个生成子进程,并等待用户请求。在fcgi_accept_request函数中,程序会调用accept函数阻塞新创建的进程。 当用户的请求到达时,fcgi_accept_request函数会判断是否处理用户的请求,其中会过滤某些连接请求,忽略受限制客户的请求, 如果程序受理用户的请求,它将分析请求的信息,将相关的变量写到对应的变量中。 其中在读取请求内容时调用了safe_read方法。如下所示: [main() -> fcgi_accept_request() -> fcgi_read_request() -> safe_read()]
1 2 3 4 5 6 7 8 9 10 | static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count ) { size_t n = 0; do { ... // 省略 对win32的处理 ret = read(req->fd, ((char*)buf)+n, count -n); // 非win版本的读操作 ... // 省略 } while (n != count ); } |
如上对应服务器端读取用户的请求数据。
在请求初始化完成,读取请求完毕后,就该处理请求的PHP文件了。 假设此次请求为PHP_MODE_STANDARD则会调用php_execute_script执行PHP文件。 在此函数中它先初始化此文件相关的一些内容,然后再调用zend_execute_scripts函数,对PHP文件进行词法分析和语法分析,生成中间代码, 并执行zend_execute函数,从而执行这些中间代码。关于整个脚本的执行请参见第三节 脚本的执行。
在处理完用户的请求后,服务器端将返回信息给客户端,此时在main函数中调用的是fcgi_finish_request(&request, 1); fcgi_finish_request函数定义在fastcgi.c文件中,其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | int fcgi_finish_request(fcgi_request *req, int force_close) { int ret = 1; if (req->fd >= 0) { if (!req->closed) { ret = fcgi_flush(req, 1); req->closed = 1; } fcgi_close(req, force_close, 1); } return ret; } |
如上,当socket处于打开状态,并且请求未关闭,则会将执行后的结果刷到客户端,并将请求的关闭设置为真。 将数据刷到客户端的程序调用的是fcgi_flush函数。在此函数中,关键是在于答应头的构造和写操作。 程序的写操作是调用的safe_write函数,而safe_write函数中对于最终的写操作针对win和linux环境做了区分, 在Win32下,如果是TCP连接则用send函数,如果是非TCP则和非win环境一样使用write函数。如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 | #ifdef _WIN32 if (!req->tcp) { ret = write(req->fd, ((char*)buf)+n, count -n); } else { ret = send(req->fd, ((char*)buf)+n, count -n, 0); if (ret <= 0) { errno = WSAGetLastError(); } } # else ret = write(req->fd, ((char*)buf)+n, count -n); # endif |
在发送了请求的应答后,服务器端将会执行关闭操作,仅限于CGI本身的关闭,程序执行的是fcgi_close函数。 fcgi_close函数在前面提的fcgi_finish_request函数中,在请求应答完后执行。同样,对于win平台和非win平台有不同的处理。 其中对于非win平台调用的是write函数。
以上是一个TCP服务器端实现的简单说明。这只是我们PHP的CGI模式的基础,在这个基础上PHP增加了更多的功能。 在前面的章节中我们提到了每个SAPI都有一个专属于它们自己的sapi_module_struct结构:cgi_sapi_module,其代码定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /* {{{ sapi_module_struct cgi_sapi_module */ static sapi_module_struct cgi_sapi_module = { "cgi-fcgi" , /* name */ "CGI/FastCGI" , /* pretty name */ php_cgi_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ sapi_cgi_activate, /* activate */ sapi_cgi_deactivate, /* deactivate */ sapi_cgibin_ub_write, /* unbuffered write */ sapi_cgibin_flush, /* flush */ NULL, /* get uid */ sapi_cgibin_getenv, /* getenv */ php_error, /* error handler */ NULL, /* header handler */ sapi_cgi_send_headers, /* send headers handler */ NULL, /* send header handler */ sapi_cgi_read_post, /* read POST data */ sapi_cgi_read_cookies, /* read Cookies */ sapi_cgi_register_variables, /* register server variables */ sapi_cgi_log_message, /* Log message */ NULL, /* Get request time */ NULL, /* Child terminate */ STANDARD_SAPI_MODULE_PROPERTIES }; /* }}} */ |
同样,以读取cookie为例,当我们在CGI环境下,在PHP中调用读取Cookie时, 最终获取的数据的位置是在激活SAPI时。它所调用的方法是read_cookies。 由SAPI实现来实现获取cookie,这样各个不同的SAPI就能根据自己的需要来实现一些依赖环境的方法。
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);
所有使用PHP的场合都需要定义自己的SAPI,例如在第一小节的Apache模块方式中, sapi_module是apache2_sapi_module,其对应read_cookies方法的是php_apache_sapi_read_cookies函数, 而在我们这里,读取cookie的函数是sapi_cgi_read_cookies。 从sapi_module结构可以看出flush对应的是sapi_cli_flush,在win或非win下,flush对应的操作不同, 在win下,如果输出缓存失败,则会和嵌入式的处理一样,调用php_handle_aborted_connection进入中断处理程序, 而其它情况则是没有任何处理程序。这个区别通过cli_win.c中的PHP_CLI_WIN32_NO_CONSOLE控制。
转载自:http://www.php-internals.com/book/?p=chapt02/02-02-03-fastcgi
本文来自博客园,作者:孙龙-程序员,转载请注明原文链接:https://www.cnblogs.com/sunlong88/p/9001262.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能