• 基于reactor设计http服务器


    目录

    1. HTTP 简介

    2. HTTP 工作原理

    3. 基于reactor设计http服务器

    3.1 解析http头

     3.2 封装http的响应

    3.3 sendfile函数

    4. 完整代码

    5.  运行结果


    1. HTTP 简介

            HTTP 协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的 WWW 文件都必须遵守这个标准。 HTTP 是一个基于 TCP/IP 通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

    2. HTTP 工作原理

            HTTP 协议工作于客户端-服务端架构上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即 WEB 服务器发送所有请求。

    1. Web 服务器有: Apache 服务器, IIS 服务器(Internet Information Services)等。
    2. Web 服务器根据接收到的请求后,向客户端发送响应信息。
    3. HTTP 默认端口号为 80,但是你也可以改为 8080 或者其他端口。

     

    3. 基于reactor设计http服务器

            reactor的原理与实现可以参考这里!!!

            主要实现俩部分,一部分是对http头的解析。另一部分是封装http的响应。

      

    3.1 解析http头

    1. //读行
    2. int readline(char* allbuf, int idx, char* linebuf)
    3. {
    4. int len = strlen(allbuf);
    5. for(; idx < len; idx++){
    6. if(allbuf[idx] == '\r' && allbuf[idx+1] == '\n')
    7. return idx+2;
    8. else
    9. *(linebuf++) = allbuf[idx];
    10. }
    11. return -1;
    12. }
    13. //解析http头
    14. int nty_http_request(struct ntyevent * ev)
    15. {
    16. char linebuff[1024] = {0};
    17. int idx = readline(ev->buffer, 0, linebuff);
    18. if(strstr(linebuff, "GET")){
    19. ev->method = HTTP_METHOD_GET;
    20. int i = 0;
    21. while(linebuff[sizeof("GET") + i] != ' ')i++;
    22. linebuff[sizeof("GET") + i] = '\0';
    23. sprintf(ev->resource, "%s/%s", HTTP_WEB_ROOT, linebuff+sizeof("GET "));
    24. printf("resource: %s\n", ev->resource);
    25. }
    26. else if(strstr(linebuff, "POST"))
    27. {
    28. ev->method = HTTP_METHOD_POST;
    29. }
    30. return 0;
    31. }

     3.2 封装http的响应

    1. //打包http头
    2. int nty_http_response_get_method(struct ntyevent *ev)
    3. {
    4. //int filed = open()
    5. #if 0
    6. int len = sprintf(ev->wbuffer,
    7. "HTTP/1.1 200 OK\r\n"
    8. "Accept-Ranges: bytes\r\n"
    9. "Content-Length: 78\r\n"
    10. "Content-Type: text/html\r\n"
    11. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n"
    12. "0voice.king

      King

      "
      );
    13. ev->wlength = len;
    14. #else
    15. int len;
    16. int filefd = open(ev->resource, O_RDONLY);
    17. if (filefd == -1) {
    18. len = sprintf(ev->wbuffer,
    19. "HTTP/1.1 200 OK\r\n"
    20. "Accept-Ranges: bytes\r\n"
    21. "Content-Length: 78\r\n"
    22. "Content-Type: text/html\r\n"
    23. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n"
    24. "0voice.king

      King

      "
      );
    25. ev->wlength = len;
    26. } else {
    27. //获取文件大小
    28. struct stat stat_buf;
    29. fstat(filefd, &stat_buf);
    30. close(filefd);
    31. #if 1
    32. len = sprintf(ev->wbuffer,
    33. "HTTP/1.1 200 OK\r\n"
    34. "Accept-Ranges: bytes\r\n"
    35. "Content-Length: %ld\r\n"
    36. "Content-Type: text/html\r\n"
    37. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n", stat_buf.st_size);
    38. #else
    39. len = sprintf(ev->wbuffer,
    40. "HTTP/1.1 200 OK\r\n"
    41. "Accept-Ranges: bytes\r\n"
    42. "Content-Length: %ld\r\n"
    43. "Content-Type: image/png\r\n"
    44. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n", stat_buf.st_size);
    45. #endif
    46. ev->wlength = len;
    47. }
    48. #endif
    49. return len;
    50. }
    51. //数据打包
    52. int nty_http_response(struct ntyevent* ev)
    53. {
    54. // ev->method, ev->resouces
    55. if(ev->method == HTTP_METHOD_GET){
    56. nty_http_response_get_method(ev);
    57. }else if(ev->method == HTTP_METHOD_POST){
    58. }
    59. return 0;
    60. }

    3.3 sendfile函数

    sendfile函数:

            sendfile函数是在两个文件描述符中直接传递数据(完全在内核中操作),从而避免了用户和内核之间的数据拷贝,所以效率很高,也被称之为零拷贝

    sendfile函数用法

    头文件:#include

    用法:ssize_t sendfile(int out_fd,int in_fd,off_t * offset,size_t count)

    1. out_fd:待写入内容的文件描述符,一般为accept的返回值
    2. in_fd:待读出内容的文件描述符,一般为open的返回值
    3. offset:指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流的默认位置,一般设置为NULL
    4. count:两文件描述符之间的字节数,一般给struct stat结构体的一个变量,在struct stat中可以设置文件描述符属性

    注意:in_fd规定指向真实的文件,不能是socket等管道文件描述符,一般使open返回值,而out_fd则是socket描述符

    sendfile的优点:是专门为网络上传输文件而设计的函数,效率高

    4. 完整代码

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include
    14. #define BUFFER_LENGTH 1024
    15. #define MAX_EPOLL_EVENTS 1024 //epoll事件数量
    16. #define RESOURCE_LENGTH 1024
    17. #define SERVER_PORT 8888
    18. #define PORT_COUNT 100
    19. typedef int NCALLBACK(int ,int ,void*);
    20. #define HTTP_METHOD_GET 0
    21. #define HTTP_METHOD_POST 1
    22. #define HTTP_WEB_ROOT "/home/kaka/share/html"
    23. //管理每一个io fd的结构体
    24. struct ntyevent{
    25. int fd; //io fd
    26. int events;
    27. void *arg;
    28. int (*callback)(int fd, int events, void* arg); //执行回调函数
    29. int status; //判断是否已有事件
    30. char buffer[BUFFER_LENGTH]; //用户缓冲区
    31. char wbuffer[BUFFER_LENGTH]; //发送的数据
    32. int length; //用户缓冲区长度
    33. int wlength; //发送长度
    34. // http reqeust
    35. int method;
    36. char resource[RESOURCE_LENGTH];
    37. };
    38. //管理ntyevent fd的块
    39. struct eventblock{
    40. struct eventblock* next; //指向ntyevent fd集合
    41. struct ntyevent* events; //指向下一个ntyevent fd的块
    42. };
    43. //reacotr结点
    44. struct ntyreactor{
    45. int epfd; //epoll fd
    46. int blkcnt; //ntyevent fd的块 计数
    47. struct eventblock* evblks; //指向ntyevent fd的块头结点
    48. };
    49. int recv_cb(int fd, int events, void *arg);
    50. int send_cb(int fd, int events, void *arg);
    51. int accept_cb(int fd, int events, void* arg);
    52. struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd);
    53. //io fd结构体设置
    54. void nty_event_set(struct ntyevent* ev, int fd, NCALLBACK callback, void* arg)
    55. {
    56. ev->fd = fd;
    57. ev->callback = callback;
    58. ev->events = 0;
    59. ev->arg = arg;
    60. return ;
    61. }
    62. //io fd add
    63. int nty_event_add(int epfd, int events, struct ntyevent *ev)
    64. {
    65. struct epoll_event ep_ev = {0, {0}};
    66. ep_ev.data.ptr = ev; //io fd结构体
    67. ep_ev.events = ev->events = events; //需要检测的fd事件
    68. int op; //操作类型
    69. if(ev->status == 1){
    70. op = EPOLL_CTL_MOD; //修改
    71. }else{
    72. op = EPOLL_CTL_ADD; //添加
    73. ev->status = 1; //标志已经添加
    74. }
    75. if(epoll_ctl(epfd, op, ev->fd, &ep_ev) <0 ){ //绑定
    76. printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
    77. return -1;
    78. }
    79. return 0;
    80. }
    81. //io fd del
    82. int nty_event_del(int epfd, struct ntyevent* ev)
    83. {
    84. struct epoll_event ep_ev = {0, {0}};
    85. if(ev->status != 1){ //没有添加过检测的fd事件
    86. return -1;
    87. }
    88. ep_ev.data.ptr = ev;
    89. ev->status = 0; //标志未添加
    90. epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);
    91. return 0;
    92. }
    93. // request
    94. // location /0voice/king/index.html HTTP/1.1
    95. //读行
    96. int readline(char* allbuf, int idx, char* linebuf)
    97. {
    98. int len = strlen(allbuf);
    99. for(; idx < len; idx++){
    100. if(allbuf[idx] == '\r' && allbuf[idx+1] == '\n')
    101. return idx+2;
    102. else
    103. *(linebuf++) = allbuf[idx];
    104. }
    105. return -1;
    106. }
    107. //解析http头
    108. int nty_http_request(struct ntyevent * ev)
    109. {
    110. char linebuff[1024] = {0};
    111. int idx = readline(ev->buffer, 0, linebuff);
    112. if(strstr(linebuff, "GET")){
    113. ev->method = HTTP_METHOD_GET;
    114. int i = 0;
    115. while(linebuff[sizeof("GET") + i] != ' ')i++;
    116. linebuff[sizeof("GET") + i] = '\0';
    117. sprintf(ev->resource, "%s/%s", HTTP_WEB_ROOT, linebuff+sizeof("GET "));
    118. printf("resource: %s\n", ev->resource);
    119. }
    120. else if(strstr(linebuff, "POST"))
    121. {
    122. ev->method = HTTP_METHOD_POST;
    123. }
    124. return 0;
    125. }
    126. //打包http头
    127. int nty_http_response_get_method(struct ntyevent *ev)
    128. {
    129. //int filed = open()
    130. #if 0
    131. int len = sprintf(ev->wbuffer,
    132. "HTTP/1.1 200 OK\r\n"
    133. "Accept-Ranges: bytes\r\n"
    134. "Content-Length: 78\r\n"
    135. "Content-Type: text/html\r\n"
    136. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n"
    137. "0voice.king

      King

      "
      );
    138. ev->wlength = len;
    139. #else
    140. int len;
    141. int filefd = open(ev->resource, O_RDONLY);
    142. if (filefd == -1) {
    143. len = sprintf(ev->wbuffer,
    144. "HTTP/1.1 200 OK\r\n"
    145. "Accept-Ranges: bytes\r\n"
    146. "Content-Length: 78\r\n"
    147. "Content-Type: text/html\r\n"
    148. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n"
    149. "0voice.king

      King

      "
      );
    150. ev->wlength = len;
    151. } else {
    152. //获取文件大小
    153. struct stat stat_buf;
    154. fstat(filefd, &stat_buf);
    155. close(filefd);
    156. #if 1
    157. len = sprintf(ev->wbuffer,
    158. "HTTP/1.1 200 OK\r\n"
    159. "Accept-Ranges: bytes\r\n"
    160. "Content-Length: %ld\r\n"
    161. "Content-Type: text/html\r\n"
    162. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n", stat_buf.st_size);
    163. #else
    164. len = sprintf(ev->wbuffer,
    165. "HTTP/1.1 200 OK\r\n"
    166. "Accept-Ranges: bytes\r\n"
    167. "Content-Length: %ld\r\n"
    168. "Content-Type: image/png\r\n"
    169. "Date: Sat, 06 Aug 2022 13:16:46 GMT\r\n\r\n", stat_buf.st_size);
    170. #endif
    171. ev->wlength = len;
    172. }
    173. #endif
    174. return len;
    175. }
    176. //数据打包
    177. int nty_http_response(struct ntyevent* ev)
    178. {
    179. // ev->method, ev->resouces
    180. if(ev->method == HTTP_METHOD_GET){
    181. nty_http_response_get_method(ev);
    182. }else if(ev->method == HTTP_METHOD_POST){
    183. }
    184. return 0;
    185. }
    186. //recv回调
    187. int recv_cb(int fd, int events, void* arg)
    188. {
    189. struct ntyreactor* reactor = (struct ntyreactor*)arg;
    190. struct ntyevent* ev = ntyreactor_idx(reactor, fd);
    191. if(ev == NULL)return -1;
    192. int len = recv(fd, ev->buffer, BUFFER_LENGTH, 0);
    193. nty_event_del(reactor->epfd, ev);
    194. if (len > 0) {
    195. ev->length = len;
    196. ev->buffer[len] = '\0';
    197. //printf("recv [%d]:%s\n", fd, ev->buffer);
    198. nty_http_request(ev); //http解析
    199. //将fd 设置为发送事件
    200. nty_event_set(ev, fd, send_cb, reactor);
    201. nty_event_add(reactor->epfd, EPOLLOUT, ev);
    202. } else if (len == 0) { //客户端断开连接
    203. nty_event_del(reactor->epfd, ev);
    204. printf("recv_cb --> disconnect\n");
    205. close(ev->fd);
    206. } else { //返回错误
    207. if (errno == EAGAIN && errno == EWOULDBLOCK) { //
    208. } else if (errno == ECONNRESET){
    209. nty_event_del(reactor->epfd, ev);
    210. close(ev->fd);
    211. }
    212. printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
    213. }
    214. return len;
    215. }
    216. //send回调
    217. int send_cb(int fd, int events, void* arg)
    218. {
    219. struct ntyreactor* reactor = (struct ntyreactor*)arg;
    220. struct ntyevent* ev = ntyreactor_idx(reactor, fd);
    221. if (ev == NULL) return -1;
    222. nty_http_response(ev); //encode
    223. int len = send(fd, ev->wbuffer, ev->wlength, 0);
    224. if (len > 0) {
    225. //printf("send[fd=%d], [%d]%s\n", fd, len, ev->wbuffer);
    226. int filefd = open(ev->resource, O_RDONLY);
    227. if(filefd < 0) return 0;
    228. struct stat stat_buf;
    229. fstat(filefd, &stat_buf);
    230. int flag = fcntl(fd, F_GETFL, 0);
    231. flag &= ~O_NDELAY;
    232. fcntl(fd, F_SETFL, flag);
    233. //sendfile 需要做成阻塞io
    234. int ret = sendfile(fd, filefd, NULL, stat_buf.st_size);
    235. if (ret == -1) {
    236. printf("sendfile: errno: %d\n", errno);
    237. }
    238. flag |= O_NONBLOCK;
    239. fcntl(fd, F_SETFL, flag);
    240. close(filefd);
    241. send(fd, "\r\n", 2, 0);
    242. //发送后,将fd设置为接收事件
    243. nty_event_del(reactor->epfd, ev);
    244. nty_event_set(ev, fd, recv_cb, reactor);
    245. nty_event_add(reactor->epfd, EPOLLIN, ev);
    246. } else { //发送失败
    247. nty_event_del(reactor->epfd, ev);
    248. close(ev->fd);
    249. printf("send[fd=%d] error %s\n", fd, strerror(errno));
    250. }
    251. return len;
    252. }
    253. //客户端接入回调
    254. int accept_cb(int fd, int events, void* arg)
    255. {
    256. struct ntyreactor *reactor = (struct ntyreactor*)arg;
    257. if (reactor == NULL) return -1;
    258. struct sockaddr_in client_addr;
    259. socklen_t len = sizeof(client_addr);
    260. int clientfd;
    261. //客户端接入
    262. if ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) {
    263. if (errno != EAGAIN && errno != EINTR) {
    264. }
    265. printf("accept: %s\n", strerror(errno));
    266. return -1;
    267. }
    268. //设置非阻塞fd
    269. int flag = 0;
    270. if ((flag = fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {
    271. printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
    272. return -1;
    273. }
    274. struct ntyevent *event = ntyreactor_idx(reactor, clientfd);
    275. if (event == NULL) return -1;
    276. //将该fd设置为recv
    277. nty_event_set(event, clientfd, recv_cb, reactor);
    278. nty_event_add(reactor->epfd, EPOLLIN, event);
    279. printf("new connect [%s:%d], pos[%d]\n",
    280. inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd);
    281. return 0;
    282. }
    283. //创建socket监听
    284. int init_sock(short port)
    285. {
    286. int fd = socket(AF_INET, SOCK_STREAM, 0);
    287. fcntl(fd, F_SETFL, O_NONBLOCK);
    288. struct sockaddr_in server_addr;
    289. memset(&server_addr, 0, sizeof(server_addr));
    290. server_addr.sin_family = AF_INET;
    291. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    292. server_addr.sin_port = htons(port);
    293. bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    294. if (listen(fd, 20) < 0) {
    295. printf("listen failed : %s\n", strerror(errno));
    296. return -1;
    297. }
    298. printf("listen server port : %d\n", port);
    299. return fd;
    300. }
    301. //reactor扩展大小
    302. int ntyreactor_alloc(struct ntyreactor* reactor)
    303. {
    304. if(reactor == NULL) return -1;
    305. if(reactor->evblks == NULL) return -1;
    306. struct eventblock* blk = reactor->evblks; //块的头结点
    307. //找尾节点
    308. while(blk->next != NULL){ //找到尾节点
    309. blk = blk->next;
    310. }
    311. struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
    312. if (evs == NULL) {
    313. printf("ntyreactor_alloc ntyevent failed\n");
    314. return -2;
    315. }
    316. memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
    317. struct eventblock *block = malloc(sizeof(struct eventblock));
    318. if (block == NULL) {
    319. printf("ntyreactor_alloc eventblock failed\n");
    320. return -3;
    321. }
    322. //io fd集合连接成块
    323. block->events = evs;
    324. block->next = NULL;
    325. //指向新块
    326. blk->next = block;
    327. reactor->blkcnt ++;
    328. return 0;
    329. }
    330. //根据io fd来找fd结构体
    331. struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd) {
    332. if (reactor == NULL) return NULL;
    333. if (reactor->evblks == NULL) return NULL;
    334. int blkidx = sockfd / MAX_EPOLL_EVENTS; //在哪一个块
    335. while (blkidx >= reactor->blkcnt) { //大小不够扩容
    336. ntyreactor_alloc(reactor);
    337. }
    338. int i = 0;
    339. struct eventblock *blk = reactor->evblks; //头结点块
    340. while (i++ != blkidx && blk != NULL) { //找到所在的块
    341. blk = blk->next;
    342. }
    343. return &blk->events[sockfd % MAX_EPOLL_EVENTS]; //返回fd结构体
    344. }
    345. //reactor初始化
    346. int ntyreactor_init(struct ntyreactor* reactor)
    347. {
    348. if(reactor == NULL) return -1;
    349. memset(reactor, 0, sizeof(struct ntyreactor));
    350. reactor->epfd = epoll_create(1);
    351. if (reactor->epfd <= 0) {
    352. printf("create epfd in %s err %s\n", __func__, strerror(errno));
    353. return -2;
    354. }
    355. //创建第一个块
    356. struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
    357. if (evs == NULL) {
    358. printf("create epfd in %s err %s\n", __func__, strerror(errno));
    359. close(reactor->epfd);
    360. return -3;
    361. }
    362. memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
    363. struct eventblock *block = malloc(sizeof(struct eventblock));
    364. if (block == NULL) {
    365. free(evs);
    366. close(reactor->epfd);
    367. return -3;
    368. }
    369. block->events = evs;
    370. block->next = NULL;
    371. reactor->evblks = block;
    372. reactor->blkcnt = 1;
    373. return 0;
    374. }
    375. //销毁reactor
    376. int ntyreactor_destory(struct ntyreactor* reactor)
    377. {
    378. close(reactor->epfd);
    379. struct eventblock *blk = reactor->evblks;
    380. struct eventblock *blk_next;
    381. while (blk != NULL) {
    382. blk_next = blk->next;
    383. free(blk->events);
    384. free(blk);
    385. blk = blk_next;
    386. }
    387. return 0;
    388. }
    389. //初始化接收连接socket
    390. int ntyreactor_addlistener(struct ntyreactor* reactor, int sockfd, NCALLBACK *acceptor){
    391. if (reactor == NULL) return -1;
    392. if (reactor->evblks == NULL) return -1;
    393. struct ntyevent* event = ntyreactor_idx(reactor, sockfd);
    394. if (event == NULL) return -1;
    395. nty_event_set(event, sockfd, acceptor, reactor);
    396. nty_event_add(reactor->epfd, EPOLLIN, event);
    397. return 0;
    398. }
    399. //reactor事件循环
    400. int ntyreactor_run(struct ntyreactor* reactor)
    401. {
    402. if (reactor == NULL) return -1;
    403. if (reactor->epfd < 0) return -1;
    404. if (reactor->evblks == NULL) return -1;
    405. struct epoll_event events[MAX_EPOLL_EVENTS+1];
    406. int checkpos = 0, i;
    407. while(1){
    408. int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
    409. if (nready < 0) {
    410. printf("epoll_wait error, exit\n");
    411. continue;
    412. }
    413. for(i = 0;i < nready; i++){
    414. struct ntyevent* ev = (struct ntyevent*)events[i].data.ptr; //发生事件的io fd结构体
    415. if((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)){
    416. ev->callback(ev->fd, events[i].events, ev->arg);
    417. }
    418. if((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)){
    419. ev->callback(ev->fd, events[i].events, ev->arg);
    420. }
    421. }
    422. }
    423. }
    424. int main(int argc, char *argv[]) {
    425. struct ntyreactor *reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
    426. ntyreactor_init(reactor);
    427. //起始的端口号
    428. unsigned short port = SERVER_PORT;
    429. if (argc == 2) {
    430. port = atoi(argv[1]);
    431. }
    432. int i = 0;
    433. int sockfds[PORT_COUNT] = {0};
    434. for (i = 0;i < PORT_COUNT;i ++) {
    435. sockfds[i] = init_sock(port+i);
    436. ntyreactor_addlistener(reactor, sockfds[i], accept_cb);
    437. }
    438. ntyreactor_run(reactor);
    439. ntyreactor_destory(reactor);
    440. for (i = 0;i < PORT_COUNT;i ++) {
    441. close(sockfds[i]);
    442. }
    443. free(reactor);
    444. return 0;
    445. }

    5. 运行结果

     

  • 相关阅读:
    详解设计模式:组合模式
    动规(19)-并查集基础题——城镇道路
    超精准!AI 结合邮件内容与附件的意图理解与分类!
    0号进程,1号进程,2号进程
    wxPython 4.2.0 发布
    通用BIOS自动化修改脚本
    [GitLab CI/CD] 实践操作片段记录
    如何快速优化几千万数据量的订单表
    SpringSecurity系列一:04 SpringSecurity 的默认用户是如何生成的?
    Unity 切换场景后场景变暗
  • 原文地址:https://blog.csdn.net/kakaka666/article/details/126237738