当前位置:网站首页>socket編程之常用api介紹與socket、select、poll、epoll高並發服務器模型代碼實現
socket編程之常用api介紹與socket、select、poll、epoll高並發服務器模型代碼實現
2022-07-07 18:11:00 【cheems~】
socket編程之常用api介紹與socket、select、poll、epoll高並發服務器模型代碼實現
前言
本文旨在學習socket網絡編程這一塊的內容,epoll是重中之重,後續文章寫reactor模型是建立在epoll之上的。
本專欄知識點是通過零聲教育的線上課學習,進行梳理總結寫下文章,對c/c++linux課程感興趣的讀者,可以點擊鏈接 C/C++後臺高級服務器課程介紹 詳細查看課程的服務。
socket編程
socket介紹
傳統的進程間通信借助內核提供的IPC機制進行, 但是只能限於本機通信, 若要跨機通信, 就必須使用網絡通信( 本質上借助內核-內核提供了socket偽文件的機制實現通信----實際上是使用文件描述符), 這就需要用到內核提供給用戶的socket API函數庫。
使用socket會建立一個socket pair,如下圖, 一個文件描述符操作兩個緩沖區。
使用socket的API函數編寫服務端和客戶端程序的步驟
預備知識
網絡字節序
網絡字節序:大端和小端的概念
- 大端: 低比特地址存放高比特數據, 高比特地址存放低比特數據
- 小端: 低比特地址存放低比特數據, 高比特地址存放高比特數據
大端和小端的使用使用場合:在網絡中經常需要考慮大端和小端的是IP和端口。網絡傳輸用的是大端,計算機用的是小端, 所以需要進行大小端的轉換
下面4個函數就是進行大小端轉換的函數,函數名的h錶示主機host, n錶示網絡network, s錶示short, l錶示long。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
上述的幾個函數, 如果本來不需要轉換函數內部就不會做轉換。
IP地址轉換函數
IP地址轉換函數
int inet_pton(int af, const char *src, void *dst);
- p->錶示點分十進制的字符串形式
- to->到
- n->錶示network網絡
函數說明: 將字符串形式的點分十進制IP轉換為大端模式的網絡IP(整形4字節數)
參數說明:
- af: AF_INET
- src: 字符串形式的點分十進制的IP地址
- dst: 存放轉換後的變量的地址
- 例如
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
手工也可以計算: 如192.168.232.145, 先將4個正數分別轉換為16進制數,
192—>0xC0 168—>0xA8 232—>0xE8 145—>0x91
最後按照大端字節序存放: 0x91E8A8C0, 這個就是4字節的整形值。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
函數說明: 網絡IP轉換為字符串形式的點分十進制的IP
參數說明:
- af: AF_INET
- src: 網絡的整形的IP地址
- dst: 轉換後的IP地址,一般為字符串數組
- size: dst的長度
返回值:
- 成功–返回執行dst的指針
- 失敗–返回NULL, 並設置errno
例如: IP地址為010aa8c0, 轉換為點分十進制的格式:
01---->1 0a---->10 a8---->168 c0---->192
由於從網絡中的IP地址是高端模式, 所以轉換為點分十進制後應該為: 192.168.10.1
struct sockaddr
socket編程用到的重要的結構體:struct sockaddr
//struct sockaddr結構說明:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
//struct sockaddr_in結構:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
}; //網絡字節序IP--大端模式
通過man 7 ip可以查看相關說明
主要API函數介紹
socket
int socket(int domain, int type, int protocol);
函數描述: 創建socket
參數說明:
- domain: 協議版本
- - AF_INET IPV4
- - AF_INET6 IPV6
- - AF_UNIX AF_LOCAL本地套接字使用
- type:協議類型
- - SOCK_STREAM 流式, 默認使用的協議是TCP協議
- - SOCK_DGRAM 報式, 默認使用的是UDP協議
- protocal:
- - 一般填0, 錶示使用對應類型的默認協議.
- 返回值:
- - 成功: 返回一個大於0的文件描述符
- - 失敗: 返回-1, 並設置errno
當調用socket函數以後, 返回一個文件描述符, 內核會提供與該文件描述符相對應的讀和寫緩沖區, 同時還有兩個隊列, 分別是請求連接隊列和已連接隊列(監聽文件描述符才有,listenFd)
bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數描述: 將socket文件描述符和IP,PORT綁定
參數說明:
- socket: 調用socket函數返回的文件描述符
- addr: 本地服務器的IP地址和PORT,
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY: 錶示使用本機任意有效的可用IP
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
- addrlen: addr變量的占用的內存大小
返回值:
- 成功: 返回0
- 失敗: 返回-1, 並設置errno
listen
int listen(int sockfd, int backlog);
函數描述: 將套接字由主動態變為被動態
參數說明:
- sockfd: 調用socket函數返回的文件描述符
- backlog: 在linux系統中,這裏代錶全連接隊列(已連接隊列)的數量。在unix系統種,這裏代錶全連接隊列(已連接隊列)+ 半連接隊列(請求連接隊列)的總數
返回值:
- 成功: 返回0
- 失敗: 返回-1, 並設置errno
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函數說明:獲得一個連接, 若當前沒有連接則會阻塞等待.
函數參數:
- sockfd: 調用socket函數返回的文件描述符
- addr: 傳出參數, 保存客戶端的地址信息
- addrlen: 傳入傳出參數, addr變量所占內存空間大小
返回值:
- 成功: 返回一個新的文件描述符,用於和客戶端通信
- 失敗: 返回-1, 並設置errno值.
accept函數是一個阻塞函數, 若沒有新的連接請求, 則一直阻塞.
從已連接隊列中獲取一個新的連接, 並獲得一個新的文件描述符, 該文件描述符用於和客戶端通信. (內核會負責將請求隊列中的連接拿到已連接隊列中)
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數說明: 連接服務器
函數參數:
- sockfd: 調用socket函數返回的文件描述符
- addr: 服務端的地址信息
- addrlen: addr變量的內存大小
返回值:
- 成功: 返回0
- 失敗: 返回-1, 並設置errno值
讀取和發送數據
接下來就可以使用write和read函數進行讀寫操作了。除了使用read/write函數以外, 還可以使用recv和send函數。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//對應recv和send這兩個函數flags直接填0就可以了
注意: 如果寫緩沖區已滿, write也會阻塞, read讀操作的時候, 若讀緩沖區沒有數據會引起阻塞。
高並發服務器模型-select
select介紹
多路IO技術: select, 同時監聽多個文件描述符, 將監控的操作交給內核去處理
int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
數據類型fd_set::文件描述符集合——本質是比特圖
函數介紹: 委托內核監控該文件描述符對應的讀,寫或者錯誤事件的發生
參數說明:
- nfds: 最大的文件描述符+1
- readfds: 讀集合, 是一個傳入傳出參數
傳入: 指的是告訴內核哪些文件描述符需要監控
傳出: 指的是內核告訴應用程序哪些文件描述符發生了變化
- writefds: 寫文件描述符集合(傳入傳出參數,同上)
- execptfds: 异常文件描述符集合(傳入傳出參數,同上)
- timeout:
NULL--錶示永久阻塞, 直到有事件發生
0 --錶示不阻塞, 立刻返回, 不管是否有監控的事件發生
>0 --到指定事件或者有事件發生了就返回
- 返回值: 成功返回發生變化的文件描述符的個數。失敗返回-1, 並設置errno值。
select-api
將fd從set集合中清除
void FD_CLR(int fd, fd_set *set);
功能描述: 判斷fd是否在集合中
返回值: 如果fd在set集合中, 返回1, 否則返回0
int FD_ISSET(int fd, fd_set *set);
將fd設置到set集合中
void FD_SET(int fd, fd_set *set);
初始化set集合
void FD_ZERO(fd_set *set);
用select函數其實就是委托內核幫我們去檢測哪些文件描述符有可讀數據,可寫,錯誤發生
int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select優缺點
select優點:
- select支持跨平臺
select缺點:
- 代碼編寫困難
- 會涉及到用戶區到內核區的來回拷貝
- 當客戶端多個連接, 但少數活躍的情况, select效率較低(例如: 作為極端的一種情况, 3-1023文件描述符全部打開, 但是只有1023有發送數據, select就顯得效率低下)
- 最大支持1024個客戶端連接(select最大支持1024個客戶端連接不是有文件描述符錶最多可以支持1024個文件描述符限制的, 而是由FD_SETSIZE=1024限制的)
FD_SETSIZE=1024 fd_set使用了該宏, 當然可以修改內核, 然後再重新編譯內核, 一般不建議這麼做
select代碼實現
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_LEN 4096
int main(int argc, char **argv) {
int listenfd, connfd, n;
struct sockaddr_in svr_addr;
char buff[MAX_LEN];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(8081);
if (bind(listenfd, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//select
fd_set rfds, rset, wfds, wset;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(listenfd, &rfds);
int max_fd = listenfd;
while (1) {
rset = rfds;
wset = wfds;
int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) {
//
struct sockaddr_in clt_addr;
socklen_t len = sizeof(clt_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &clt_addr, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rfds);
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++) {
if (FD_ISSET(i, &rset)) {
//
n = recv(i, buff, MAX_LEN, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
FD_SET(i, &wfds);
}
else if (n == 0) {
//
FD_CLR(i, &rfds);
close(i);
}
if (--nready == 0) break;
}
else if (FD_ISSET(i, &wset)) {
send(i, buff, n, 0);
FD_SET(i, &rfds);
FD_CLR(i, &wfds);
}
}
}
close(listenfd);
return 0;
}
高並發服務器模型-poll
poll介紹
poll跟select類似, 監控多路IO, 但poll不能跨平臺。其實poll就是把select三個文件描述符集合變成一個集合了。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數說明:
- fds: 傳入傳出參數, 實際上是一個結構體數組
fds.fd: 要監控的文件描述符
fds.events:
POLLIN---->讀事件
POLLOUT---->寫事件
fds.revents: 返回的事件
- nfds: 數組實際有效內容的個數
- timeout: 超時時間, 單比特是毫秒.
-1:永久阻塞, 直到監控的事件發生
0: 不管是否有事件發生, 立刻返回
>0: 直到監控的事件發生或者超時
返回值:
- 成功:返回就緒事件的個數
- 失敗: 返回-1。若timeout=0, poll函數不阻塞,且沒有事件發生, 此時返回-1, 並且errno=EAGAIN, 這種情况不應視為錯誤。
struct pollfd {
int fd; /* file descriptor */ 監控的文件描述符
short events; /* requested events */ 要監控的事件---不會被修改
short revents; /* returned events */ 返回發生變化的事件 ---由內核返回
};
說明:
- 當poll函數返回的時候, 結構體當中的fd和events沒有發生變化, 究竟有沒有事件發生由revents來判斷, 所以poll是請求和返回分離
- struct pollfd結構體中的fd成員若賦值為-1, 則poll不會監控
- 相對於select, poll沒有本質上的改變; 但是poll可以突破1024的限制.在/proc/sys/fs/file-max查看一個進程可以打開的socket描述符上限,如果需要可以修改配置文件: /etc/security/limits.conf,加入如下配置信息, 然後重啟終端即可生效
* soft nofile 1024
* hard nofile 100000
soft和hard分別錶示ulimit命令可以修改的最小限制和最大限制
poll代碼實現
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_LEN 4096
#define POLL_SIZE 1024
int main(int argc, char **argv) {
int listenfd, connfd, n;
struct sockaddr_in svr_addr;
char buff[MAX_LEN];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(8081);
if (bind(listenfd, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//poll
struct pollfd fds[POLL_SIZE] = {
0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
int i = 0;
for (i = 1; i < POLL_SIZE; i++) {
fds[i].fd = -1;
}
while (1) {
int nready = poll(fds, max_fd + 1, -1);
if (fds[0].revents & POLLIN) {
struct sockaddr_in client = {
};
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *) &client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept \n");
fds[connfd].fd = connfd;
fds[connfd].events = POLLIN;
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
//int i = 0;
for (i = listenfd + 1; i <= max_fd; i++) {
if (fds[i].revents & POLLIN) {
n = recv(i, buff, MAX_LEN, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
}
else if (n == 0) {
//
fds[i].fd = -1;
close(i);
}
if (--nready == 0) break;
}
}
}
}
高並發服務器模型-epoll (重點)
epoll介紹
將檢測文件描述符的變化委托給內核去處理, 然後內核將發生變化的文件描述符對應的事件返回給應用程序。
記住,epoll是事件驅動的,其底層數據結構是紅黑樹,紅黑樹的key是fd,val是事件,返回的是事件。
epoll有兩種工作模式,ET和LT模式。
水平觸發LT:
- 高電平代錶1
- 只要緩沖區中有數據, 就一直通知
邊緣觸發ET:
- 電平有變化就代錶1
- 緩沖區中有數據只會通知一次, 之後再有新的數據到來才會通知(若是讀數據的時候沒有讀完, 則剩餘的數據不會再通知, 直到有新的數據到來)
epoll默認是水平觸發LT,在需要高性能的場景下,可以改成邊緣ET非阻塞方式來提高效率。
一般使用LT是一次性讀數據讀不完,數據較多的情况。而一次性能够讀完,小數據量則用邊緣ET。
ET模式由於只通知一次, 所以在讀的時候要循環讀, 直到讀完, 但是當讀完之後read就會阻塞, 所以應該將該文件描述符設置為非阻塞模式(fcntl函數)
read函數在非阻塞模式下讀的時候, 若返回-1, 且errno為EAGAIN, 則錶示當前資源不可用, 也就是說緩沖區無數據(緩沖區的數據已經讀完了); 或者當read返回的讀到的數據長度小於請求的數據長度時,就可以確定此時緩沖區中已沒有數據可讀了,也就可以認為此時讀事件已處理完成。
epoll反應堆
反應堆: 一個小事件觸發一系列反應
epoll反應堆的思想: c++的封裝思想(把數據和操作封裝到一起)
- 將描述符,事件,對應的處理方法封裝在一起
- 當描述符對應的事件發生了, 自動調用處理方法(其實原理就是回調函數)
epoll反應堆的核心思想是: 在調用epoll_ctl函數的時候, 將events上樹的時候,利用epoll_data_t的ptr成員, 將一個文件描述符,事件和回調函數封裝成一個結構體, 然後讓ptr指向這個結構體。然後調用epoll_wait函數返回的時候, 可以得到具體的events, 然後獲得events結構體中的events.data.ptr指針, ptr指針指向的結構體中有回調函數, 最終可以調用這個回調函數。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll-api
int epoll_create(int size);
函數說明: 創建一個樹根
參數說明:
- size: 最大節點數, 此參數在linux 2.6.8已被忽略, 但必須傳遞一個大於0的數,曆史意義,用epoll_create1也行。
- 返回值:
成功: 返回一個大於0的文件描述符, 代錶整個樹的樹根.
失敗: 返回-1, 並設置errno值.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函數說明: 將要監聽的節點在epoll樹上添加, 删除和修改
參數說明:
epfd: epoll樹根
op:
EPOLL_CTL_ADD: 添加事件節點到樹上
EPOLL_CTL_DEL: 從樹上删除事件節點
EPOLL_CTL_MOD: 修改樹上對應的事件節點
- fd: 事件節點對應的文件描述符
- event: 要操作的事件節點
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- event.events常用的有:
EPOLLIN: 讀事件
EPOLLOUT: 寫事件
EPOLLERR: 錯誤事件
EPOLLET: 邊緣觸發模式
- event.fd: 要監控的事件對應的文件描述符
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函數說明:等待內核返回事件發生
參數說明:
- epfd: epoll樹根
- events: 傳出參數, 其實是一個事件結構體數組
- maxevents: 數組大小
- timeout:
-1: 錶示永久阻塞
0: 立即返回
>0: 錶示超時等待事件
返回值:
- 成功: 返回發生事件的個數
- 失敗: 若timeout=0, 沒有事件發生則返回; 返回-1, 設置errno值
epoll_wait的events是一個傳出參數, 調用epoll_ctl傳遞給內核什麼值, 當epoll_wait返回的時候, 內核就傳回什麼值,不會對struct event的結構體變量的值做任何修改。
epoll優缺點
epoll優點:
- 性能高,百萬並發不在話下,而select就不行
epoll缺點:
- 不能跨平臺,linux下的
epoll代碼實現
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define POLL_SIZE 1024
#define MAX_LEN 4096
int main(int argc, char **argv) {
int listenfd, connfd, n;
char buff[MAX_LEN];
struct sockaddr_in svr_addr;
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(8081);
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (bind(listenfd, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
int epfd = epoll_create(1); //int size
struct epoll_event events[POLL_SIZE] = {
0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
if (nready == -1) {
continue;
}
int i = 0;
for (i = 0; i < nready; i++) {
int actFd = events[i].data.fd;
if (actFd == listenfd) {
struct sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &cli_addr, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
else if (events[i].events & EPOLLIN) {
n = recv(actFd, buff, MAX_LEN, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(actFd, buff, n, 0);
}
else if (n == 0) {
//
epoll_ctl(epfd, EPOLL_CTL_DEL, actFd, NULL);
close(actFd);
}
}
}
}
return 0;
}
边栏推荐
- MRS离线数据分析:通过Flink作业处理OBS数据
- Mobile pixel bird game JS play code
- 2021-06-28
- Performance test process and plan
- 机器人工程终身学习和工作计划-2022-
- Personal best practice demo sharing of enum + validation
- 开发一个小程序商城需要多少钱?
- [4500 word summary] a complete set of skills that a software testing engineer needs to master
- Target detection 1 -- actual operation of Yolo data annotation and script for converting XML to TXT file
- Easy to understand [linear regression of machine learning]
猜你喜欢
Summary of debian10 system problems
[answer] if the app is in the foreground, the activity will not be recycled?
Supplementary instructions to relevant rules of online competition
Taffydb open source JS database
socket编程之常用api介绍与socket、select、poll、epoll高并发服务器模型代码实现
Mobile app takeout ordering personal center page
Click on the top of today's headline app to navigate in the middle
Deep learning - make your own dataset
[OKR target management] case analysis
Yarn capacity scheduler (ultra detailed interpretation)
随机推荐
Slider plug-in for swiper left and right switching
Mobile pixel bird game JS play code
ICer知识点杂烩(后附大量题目,持续更新中)
Mui side navigation anchor positioning JS special effect
Vscode three configuration files about C language
[network attack and defense principle and technology] Chapter 4: network scanning technology
回归测试的分类
Sanxian Guidong JS game source code
Robot engineering lifelong learning and work plan-2022-
原生js验证码
手机版像素小鸟游js戏代码
js拉下帷幕js特效显示层
做软件测试 掌握哪些技术才能算作 “ 测试高手 ”?
[re understand the communication model] the application of reactor mode in redis and Kafka
Backup Alibaba cloud instance OSS browser
SD_DATA_SEND_SHIFT_REGISTER
目标检测1——YOLO数据标注以及xml转为txt文件脚本实战
Unlike the relatively short-lived industrial chain of consumer Internet, the industrial chain of industrial Internet is quite long
三仙归洞js小游戏源码
机器人工程终身学习和工作计划-2022-