当前位置:网站首页>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;
}
边栏推荐
- ICer知识点杂烩(后附大量题目,持续更新中)
- 元宇宙带来的创意性改变
- The report of the state of world food security and nutrition was released: the number of hungry people in the world increased to 828million in 2021
- What are the financial products in 2022? What are suitable for beginners?
- Win11C盘满了怎么清理?Win11清理C盘的方法
- 2021年全国平均工资出炉,你达标了吗?
- 带动画的列表选中js特效
- How to open an account for wealth securities? Is it safe to open a stock account through the link
- 线上比赛相关规则补充说明
- Explain it in simple terms. CNN convolutional neural network
猜你喜欢
三仙归洞js小游戏源码
More than 10000 units were offline within ten days of listing, and the strength of Auchan Z6 products was highly praised
[distributed theory] (II) distributed storage
USB通信协议深入理解
回归测试的分类
Deep learning machine learning various data sets summary address
3分钟学会制作动态折线图!
Robot engineering lifelong learning and work plan-2022-
YARN Capacity Scheduler容量调度器(超详细解读)
[deep learning] 3 minutes introduction
随机推荐
Mobile pixel bird game JS play code
Deep learning machine learning various data sets summary address
[trusted computing] Lesson 10: TPM password resource management (II)
Debian10 compile and install MySQL
Chapter 3 business function development (user login)
手机版像素小鸟游js戏代码
AI 击败了人类,设计了更好的经济机制
SD_DATA_RECEIVE_SHIFT_REGISTER
Simple loading animation
Dragging the custom style of Baidu map to the right makes the global map longitude 0 unable to be displayed normally
Machine vision (1) - Overview
How to open an account for wealth securities? Is it safe to open a stock account through the link
DatePickerDialog and trimepickerdialog
手撕Nacos源码(先撕客户端源码)
漫画 | 宇宙第一 IDE 到底是谁?
Live broadcast software construction, canvas Text Bold
机器视觉(1)——概述
线上比赛相关规则补充说明
SD_DATA_SEND_SHIFT_REGISTER
More than 10000 units were offline within ten days of listing, and the strength of Auchan Z6 products was highly praised