Linux内核net/socket.c定义了一套socket的操作api。图1展示了socket层所处与TCP/IP协议栈之上和应用层之下。

socket()函数

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而**socket()**用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

int socket(int domain, int type, int protocol);

参数:

  • domain: 即协议域,又称为协议族(family)。

    常用的协议组有:

    • AF_INET(ipv4地址(32位的)与端口号(16位的)的组合)
    • AF_INET6( IPv6 的地址族)
    • AF_LOCAL(或称AF_UNIX,Unix域socket)(用一个绝对路径名作为地址)
    • AF_ROUTE
  • type: 指定socket类型。

    常用的socket类型有:

    • SOCK_STREAM
    • SOCK_DGRAM
    • SOCK_RAW
    • SOCK_PACKET
    • SOCK_SEQPACKET等等
  • protocol:指定协议。

    常用的协议有:

    • IPPROTO_TCP(TCP传输协议)
    • IPPTOTO_UDP(UDP传输协议)
    • IPPROTO_SCTP(STCP传输协议)
    • IPPROTO_TIPC等(TIPC传输协议)

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

bind()函数

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd: 即socket描述字,它是通过socket()函数创建了,唯一标识一个socketbind()函数就是将给这个描述字绑定一个名字。

  • addr: 一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:

    struct sockaddr_in {short int sin_family; /* 通信类型 */unsigned short int sin_port; /* 端口 */struct in_addr sin_addr; /* Internet 地址 */unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/};
    /* Internet address. */
    struct in_addr {uint32_t       s_addr;     /* address in network byte order */
    };
    

    注意,原来的sockaddr的格式是(16个字节):

    struct sockaddr {unsigned short sa_family; /* 地址家族, AF_xxx */char sa_data[14]; /*14字节协议地址*/
    };
    

    sockaddr_in这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero()memset() 来全部置零。 同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也可以被指向结构体sockaddr并且代替它。这样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,并且在最后转换。

    ipv6对应的是:

    struct sockaddr_in6 { sa_family_t     sin6_family;   /* AF_INET6 */ in_port_t       sin6_port;     /* port number */ uint32_t        sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr;     /* IPv6 address */ uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */
    };struct in6_addr { unsigned char   s6_addr[16];   /* IPv6 address */
    };
    

    Unix域对应的是:

    #define UNIX_PATH_MAX    108struct sockaddr_un { sa_family_t sun_family;               /* AF_UNIX */ char        sun_path[UNIX_PATH_MAX];  /* pathname */
    };
    
  • addrlen:地址的长度。

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-EndianLittle-Endian的定义如下:

a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket

在网络编程中,能够转换两种类型: short (两个字节)和 long (四个字节)。这个函 数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 “h” 表示 “本机 (host)”,接着是 “to”,然后用 “n” 表 示 “网络 (network)”,最后用 “s” 表示 “short”: h-to-n-s, 或者 htons() (“Host to Network Short”)。

这里有:

  • htons()–“Host to Network Short”
  • htonl()–“Host to Network Long”
  • ntohs()–“Network to Host Short”
  • ntohl()–“Network to Host Long”

为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?

答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。

处理IP地址

    listenAddr.sin_addr.s_addr = INADDR_ANY;// listenAddr.sin_addr.s_addr = inet_addr(IP);// inet_pton(AF_INET,IP,&listenAddr.sin_addr);

listen()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket

int listen(int sockfd, int backlog);

参数:

  • sockfd::即socket描述字,唯一的id。
  • backlog:相应socket可以排队的最大连接个数。

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect()函数

客户端调用connect()发出连接请求,服务器端就会接收到这个请求。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:socket描述符。
  • addr:服务器的socket地址。
  • addrlen:socket地址的长度

再一次,你应该检查 connect() 的返回值–它在错误的时候返回-1,并 设置全局错误变量 errno。 同时,你可能看到,我没有调用 bind()。因为我不在乎本地的端口号。 我只关心我要去那。内核将为我选择一个合适的端口号,而我们所连接的 地方也自动地获得这些信息。一切都不用担心。

客户端通过调用connect函数来建立与TCP服务器的连接。

accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd:服务器的socket描述字。
  • addr:指向struct sockaddr *的指针,用于返回客户端的协议地址。
  • addrlen:协议地址的长度。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

send() and recv()函数

两个函数用于流式套接字或者数据报套接字的通讯

send()函数

int send(int sockfd, const void *msg, int len, int flags);

参数:

  • sockfd:想要发送数据的套接字描述符。
  • msg:指向发送的消息数据的指针。
  • flags:一般设置为0.

send() 返回实际发送的数据的字节数–它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是 发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送其它的数据。但是这里也 有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。

recv() 函数

int recv(int sockfd, void *buf, int len, unsigned int flags);

参数:

  • sockfd:要读的套接字描述符。
  • buf:读取数据的缓冲区。
  • len:缓冲区的最大长度。
  • flags:一般设置为0.

recv() 返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。

sendto() 和 recvfrom()函数

既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之 前需要什么信息呢? 不错,是目标地址!

sendto()函数

int sendto(int sockfd, const void *msg, int len, unsigned int flags,const struct sockaddr *to, int tolen);

除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。

recvfrom() 函数

int recvfrom(int sockfd, void *buf, int len, unsigned int flags,  struct sockaddr *from, int *fromlen);

from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。

recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。

close()函数

关闭文件描述符。

close(sockfd);

shutdown()函数

它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企 图都将返回错误信息。如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭,你可以使用:

int shutdown(int sockfd, int how);

参数:

  • sockfd:是你想要关闭的套接字文件描述符。
  • how:值是下面的其中之 一:
    • 0 – 不允许接受
    • 1 – 不允许发送
    • 2 – 不允许发送和接受(和 close() 一样)

shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可以使用它们的)。

getpeername()函数

函数 getpeername() 告诉你在连接的流式套接字上谁在另外一边。函数是这样的:

#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

参数:

  • sockfd:连接的流式套接字的描述符。
  • addr:是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的信息。
  • addrlen:是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)

函数在错误的时候返回 -1,设置相应的 errno。

一旦你获得它们的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 来打印或者获得更多的信息。但是你不能得到它的帐号

gethostname()函数

它返回你程序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你的机器的 IP 地址。

#include <unistd.h>
int gethostname(char *hostname, size_t size);

参数:

  • hostname:字符串指针,保存返回的主机名。
  • size:hostname 数组的字节长度。

函数调用成功时返回 0,失败时返回 -1,并设置 errno。

简单的实例

服务器demo代码server.c:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>#define SERVER_PORT 10086     /*定义用户连接端口*/
#define MAX_CONNECT_NUM 10    /*多少等待连接控制*/
const char *IP = "127.0.0.1"; /*设定本地地址*/void ServerDemo()
{int sockFd;                               //服务器的文件描述符struct sockaddr_in serverAddr;            // 服务器端的ip地址和端口即协议族sockFd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字if (sockFd == -1){printf("create Server Socket error!\n");exit(-1);}// 设置ip地址和协议族及端口serverAddr.sin_family = AF_INET;// serverAddr.sin_addr.s_addr = inet_addr(IP);inet_pton(AF_INET, IP, &serverAddr.sin_addr);serverAddr.sin_port = htons(SERVER_PORT);bzero(&serverAddr.sin_zero, sizeof(serverAddr.sin_zero));// 绑定,服务器需要绑定int bind_ret = bind(sockFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));if (bind_ret == -1){printf("bind error!\n");if (errno == EACCES){exit(1); // 没有权限}else if (errno == EADDRINUSE){exit(2); // 端口被占用}else{exit(errno);}}// 监听int ret = listen(sockFd, MAX_CONNECT_NUM);if (ret == -1){printf("listen error!\n");exit(-1);}// do once// 接收客户端的连接struct sockaddr_in clientAddr;socklen_t clientAddrLen = sizeof(clientAddr);int connectFd = accept(sockFd, (struct sockaddr *)&clientAddr, &clientAddrLen);if (connectFd == -1){printf("connect error!\n");printf("accept failed, errno:%d\n", errno);}else{//  接收一次消息char clientIp[INET_ADDRSTRLEN];printf("connected with %s:%d\n", inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN), ntohs(clientAddr.sin_port));char data[1024];recv(connectFd, data, sizeof(data), 0);printf("receive data: %s\n", data);}close(sockFd);  // 关闭套接字
}int main()
{ServerDemo();return 0;
}

客户端demo代码client.c:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/wait.h>
#include <arpa/inet.h>#define PORT 10086            /* 客户机连接远程主机的端口 */
#define MAXDATASIZE 100       /* 每次可以接收的最大字节 */
const char *IP = "127.0.0.1"; /* 设定连接IP地址为本地IP地址 */void ClientDemo()
{int sockFd;// 创建客户端套接字sockFd = socket(AF_INET, SOCK_STREAM, 0);if (sockFd == -1){printf("create client socket error!\n");exit(-1);}// 设定需要连接的服务器的IP地址、地址族以及端口struct sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;// serverAddr.sin_addr.s_addr = inet_addr(IP);inet_pton(AF_INET, IP, &serverAddr.sin_addr);serverAddr.sin_port = htons(PORT);// 连接服务器int ret = connect(sockFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));if (ret == -1){printf("connect failed, errno:%d\n", errno);}else{char data[] = "hello,world!";        // 定义缓冲区send(sockFd, data, sizeof(data), 0); // 发送数据}close(sockFd); // 关闭客户端套接字
}
int main()
{ClientDemo();return 0;
}

分别执行下面的命令:

gcc server.c -o server
gcc client.c -o client

执行

下面是UDP通信的例子(UDP不存在服务器客户端的概念)

udpListen.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>#define MYPORT 4950 /* the port users will be sending to */
#define MAXBUFLEN 100const char *IP = "127.0.0.1";
void UDP_ListenDemo()
{int udpSockFd;struct sockaddr_in listenAddr; // 接收方地址struct sockaddr_in talkAddr;   // 发送方的地址udpSockFd = socket(AF_INET, SOCK_DGRAM, 0);if (udpSockFd == -1){printf("create udp sock error!\n");exit(-1);}listenAddr.sin_family = AF_INET;listenAddr.sin_addr.s_addr = INADDR_ANY;// listenAddr.sin_addr.s_addr = inet_addr(IP);// inet_pton(AF_INET,IP,&listenAddr.sin_addr);listenAddr.sin_port = htons(MYPORT);bzero(&listenAddr.sin_zero, sizeof(listenAddr.sin_zero));if (bind(udpSockFd, (struct sockaddr *)&listenAddr, sizeof(listenAddr)) == -1){printf("bind error!\n");exit(-1);}socklen_t talkAddrLen = sizeof(talkAddr);char buf[MAXBUFLEN];int numbytes;numbytes = recvfrom(udpSockFd, buf, MAXBUFLEN, 0, (struct sockaddr *)(&talkAddr), &talkAddrLen);if (numbytes == -1{printf("revefrom error!\n");exit(-1);}printf("got packet from %s\n", inet_ntoa(talkAddr.sin_addr)); // 得到对方的ip地址printf("packet is %d bytes long\n", numbytes);buf[numbytes] = '\0';printf("packet contains \"%s\"\n", buf); //输出接收的内容close(udpSockFd);                        // 关闭UDP套接字
}
int main()
{UDP_ListenDemo();return 0;
}

udpTalk.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>#define MYPORT 4950
const char *IP = "127.0.0.1";void UDP_TalkDemo()
{int udpTalkFd;struct sockaddr_in listenAddr;struct hostent *he;int numbytes;udpTalkFd = socket(AF_INET, SOCK_DGRAM, 0);if (udpTalkFd == -1){fprintf(stdout, "create udp sock error!\n");exit(-1);}listenAddr.sin_family = AF_INET;listenAddr.sin_addr.s_addr = inet_addr(IP);// inet_pton(AF_INET,IP,&listenAddr.sin_addr);listenAddr.sin_port = htons(MYPORT);bzero(&listenAddr.sin_zero, sizeof(listenAddr.sin_zero));char buf[] = "Hello!";numbytes = sendto(udpTalkFd, buf, sizeof(buf), 0, (struct sockaddr *)&listenAddr, sizeof(listenAddr));if (numbytes == -1){printf("udp send error!\n");exit(-1);}printf("sent %d bytes to %s\n", numbytes, inet_ntoa(listenAddr.sin_addr));close(udpTalkFd);
}int main()
{UDP_TalkDemo();return 0;
}

编写Makefile进行编译

test: udpListen.cgcc udpListen.c -o udpListengcc udpTalk.c -o udpTalk

分别执行:

make test
./udpListen
./udpTalk

结果:


参考:

  1. c++中Socket编程(入门)
  2. Linux socket api
  3. socket接口api的深度探究

Linux Socket接口使用方法相关推荐

  1. linux点对点聊天室的实现与设计心得,基于Socket接口的Linux与Windows网络聊天室设计与实现...

    陈洁 孟晓景 摘要:为了实现Linux与Windows跨平台通信,及时共享信息,构建了一个适用于跨平台的网络聊天室通信程序.先搭建跨平台通信环境,然后使用Socket套接字网络编程接口实现通信.整个系 ...

  2. pythondockerapi_docker-py 用Python调用Docker接口的方法

    众所周知,Docker向外界提供了一个API来管理其中的资源.这个API可以是socket文件形式的(一般也是默认的,在/var/run/docker.sock中),也可以是TCP形式的.以前想要通过 ...

  3. linux socket bind 内核详解,Socket与系统调用深度分析(示例代码)

    1. 什么是系统调用 操作系统通过系统调用为运行于其上的进程提供服务.当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 . 内核函数负责响应应用程序的要求,例如操作文 ...

  4. 对于linux socket与epoll配合相关的一些心得记录

    对于linux socket与epoll配合相关的一些心得记录 没有多少高深的东西,全当记录,虽然简单,但是没有做过测试还是挺容易让人糊涂的 int nRecvBuf=32*1024;//设置为32K ...

  5. linux socket高性能服务器处理框架

    这个博客很多东西 http://blog.csdn.net/luozhonghua2014/article/details/37041765 思考一种高性能的服务器处理框架 1.首先需要一个内存池,目 ...

  6. Linux Socket详解 大全 基础知识

    1. Socket基础概念: 1.1:形象类比: Socket和电话网络的概念可以做一个很好的类比: Linux 编程中所说的socket就如同一个端点,类比到电话网中,它就如同一个电话机. 而Soc ...

  7. linux socket 编程

    socket  <script type="text/javascript"> </script> <script type="text/j ...

  8. Linux Socket编程

    IP socket 是在其上建立高级Internet 协议的最低级的层:从HTTP到SSL到POP3到Kerberos再到UDP-Time,每种Internet协议都建立在它的基础上.为了实现自定义的 ...

  9. api有哪些 javasocket_Java Socket编程以及与Linux Socket API关系

    Socket 编程(基于Linux) Socket独立于具体协议的网络编程接口,在ISO模型中,主要位于会话层和传输层之间:在通用的计算机网络五层模型中,主要位于应用层和传输层之间. Linux So ...

最新文章

  1. PHP开发之递归算法的三种实现方法
  2. 微信小程序设置云函数使用的环境
  3. UVA 1451 Average 数形结合
  4. lsof/netstat命令的一个重要作用: 根据进程查端口, 根据端口查进程
  5. IE6的重定向页面无法跳转解决
  6. WIN10网络共享文件夹实战
  7. 如何更改计算机屏幕分辨率,如何修改电脑默认屏幕分辨率
  8. 龙之谷私服源码+搭建教程
  9. Tecplot 自定义色谱颜色
  10. 文献阅读【RNA-seq数据归一化】
  11. webpack 环境变量
  12. python流程控制工具
  13. 公众号h5获取手机号权限_微信公众号h5获取用户openId的方法和步骤
  14. ExtJS初级教程之ExtJS Tree(三)
  15. kubernetes1.5.2版本 yum install 方式安装部署 认证授权机制 安全模式 完整版
  16. 解决Discus!x3.1论坛安装插件提示无法运行通过zend加密应用
  17. 机器学习准备数据时如何避免数据泄漏
  18. jQuery Mobile-页面跳转
  19. 多种方法清理电脑内存,解决电脑卡问题
  20. B014 - ADC0809数字电压表可切换量程

热门文章

  1. 电脑上怎么绘制流程图?三分钟快速绘制流程图的秘诀
  2. 首位文博虚拟宣推官“文夭夭”上岗
  3. 若语句char a = ‘\72‘; 则变量a包含几个字符?‘\72‘是否在ASCII值的范围之内?
  4. 软件测试之测试用例的设计
  5. Python9-前端基础知识-day47
  6. 简单聊聊Long Short Term Memory Network (LSTM)和 Gated Recurrent Unit (GRU)两种强大的RNN变体
  7. 关于支付系统(支付通道,支付接口)
  8. C++描述 LeetCode 5677. 统计同构子字符串的数目
  9. 利用cobaltstrike加sqlmap拿下一个网站并提权
  10. leetcode之Isomorphic strings