【C++】Web服务器项目所用到的函数详解
文章目录
- 1 Web服务器端监听
- 1.1 socket()函数
- 1.2 struct sockaddr和struct sockaddr_in结构体(INADDR_ANY)
- 1.3 bzero()函数
- 1.4 htonl()函数
- 1.5 setsockopt()函数
- 1.6 bind()函数
- 1.7 listen()函数
- 2 IO复用技术
- 2.1 epoll_event结构体(事件类型)
- 2.2 epoll_create函数
- 2.3 epoll_ctl()函数
- 2.4 epoll_wait()函数
- 2.5 socklen_t数据类型
- 3 Web服务器端接收
- 3.1 accept()函数
- 3.2 recv()函数
- 4 信号量
- 4.1 sem_init()函数
- 4.2 sem_wait()函数
- 4.3 sem_post()函数
- 4.4 sem_destroy()函数
- 5 线程池
- 5.1 pthread_t数据类型
- 5.2 pthread_create()函数
- 5.3 pthread_detach()函数
- 5.4 pthread_join()函数
- 5.5 pthread_exit()函数
- 6 互斥锁--与条件变量结合使用
- 6.1 pthread_mutex_init()函数
- 6.2 pthread_mutex_destroy()函数
- 6.3 pthread_mutex_lock()函数
- 6.4 pthread_mutex_unlock()函数
- 7 条件变量
- 7.1 pthread_cond_init()函数
- 7.2 pthread_cond_destroy()函数
- 7.3 pthread_cond_wait()函数
- 7.4 pthread_cond_timedwait()函数
- 7.5 pthread_cond_signal()函数
- 7.6 pthread_cond_broadcast()函数
- 8 Mysql接口API
- 8.1 头文件
- 8.2 MYSQL变量
- 8.3 结构体
- MYSQL_RES
- MYSQL_ROW
- 8.4 使用流程
- 8.5 mysql_init()函数
- 8.6 mysql_real_connect()函数
- 8.7 mysql_query()函数
- 8.8 mysql_store_result()函数
- 8.9 mysql_fetch_row()函数
- 8.10 mysql_free_result()函数
- 8.11 mysql_close()函数
- 9 Linux系统读写函数
- 9.1 fcntl()函数
- 9.2 stat()函数
- 9.3 mmap()函数
- 9.4 iovec 结构体
- 9.5 writev()函数
- 9.6 va_list结构体
- 9.7 常见ERRNO
- 9.8 send()函数
- 9.9 exit()函数
- 10 Linux系统信号机制函数
- 10.1 sigaction结构体
- 10.2 sigaction()函数
- 10.3 sigfillset()函数
- 10.4 SIGALRM、SIGTERM信号
- 10.5 alarm()函数
- 10.6 socketpair()函数
- 引用
1 Web服务器端监听
1.1 socket()函数
在网络编程结构下,在Server端和Client端都会有一个socket。通过将socket当作文件,我可以将我手中的数据写入socket:打开socket->写入数据可以读socket的数据:打开socket->读出数据。作为文件,socket当然有自己的文件描述符。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
该函数返回一个socket编号(文件描述符,唯一标识一个socket),是个int值(若失败就返回-1
)。
输入值(只关心前两个就行,第三个有点重复):
- domain:即协议域,又称为协议族(family)。 AF_INET 与AF_INET6最为常用,分别代指ipv4与ipv6协议。、在Linux中,理论上建立socket时是指定协议,应该用
PF_xxxx
,设置地址时应该用AF_xxxx
。当然AF_INET
和PF_INET
的值是相同的,混用也不会有太大的问题。 - type:流式套接字(SOCK_STREAM)(TCP) ,数据报套接字(SOCK_DGRAM)(UDP) ,原始套接字(SOCK_RAW),前两者较常用。
- protocol:当protocol为0时,会自动选择type类型对应的默认协议.
1.2 struct sockaddr和struct sockaddr_in结构体(INADDR_ANY)
sockaddr
是通用的socket地址,此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息,但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构
sockaddr_in(在netinet/in.h中定义):
struct sockaddr_in {short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
sin_family
指代协议族,在socket编程中只能是AF_INET
sin_port
存储端口号(使用网络字节顺序)
sin_addr
存储IP地址,使用in_addr
这个数据结构(in_addr.s_addr
就是32位IP地址),按照网络字节顺序存储IP地址
sin_zero
是为了让sockaddr
与sockaddr_in
两个数据结构保持大小相同而保留的空字节。
【Tips】这个地址结构根据地址创建socket时的地址协议族的不同,如ipv4对应的是:
struct sockaddr_in {sa_family_t sin_family; /*地址族 AF_INET*/in_port_t sin_port; /*端口号,网络字节序表示*/struct in_addr sin_addr; /*v4的地址结构体*/
};
struct in_addr {uint32_t s_addr; /*v4地址,网络字节序表示*/
};
ipv6对应的是:
struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id;
};struct in6_addr { unsigned char s6_addr[16];
};
【注意此处"将sin_addr.s_addr设置为INADDR_ANY"的含义】
INADDR_ANY
转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
1.3 bzero()函数
bzero函数是c++ string.h中的函数。功能描述:置内存(字符串)前n个字节为零且包括‘\0’。
#include <string.h>
void bzero(void *s, int n);//s为内存(字符串)指针,n 为需要清零的字节数。
现常用memset(&local, 0, sizeof(local));
的memset函数来代替!
1.4 htonl()函数
包含的头文件为:"winsock2.h"
htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
htonl(host to net unsigned long)就是把本机字节顺序转化为网络字节顺序
网络字节顺序(大尾顺序)就是指一个数在内存中存储的时候“高对低,低对高”(即一个数的高位字节存放于低地址单元,低位字节存放在高地址单元中)。
但对于不同CPU主机,其主机字节顺序是不同的,因此为了不同的机器直接通信,需要进行统一的转换。
举例来说,数值0x2211使用两个字节储存:高位字节是0x22,低位字节是0x11。顺序为大端,逆序的为小端。之所以有小端字节序是因为计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。
1.5 setsockopt()函数
这个函数用来给上面那个socket()函数返回的socket设置属性,作为服务端,可以设置重用地址和端口号。
int setsockopt(int sockfd , int level, int optname, void *optval, socklen_t *optlen);
- sockfd:要设置的套接字描述符。(创建的sockopt)
- level:选项定义的层次。或为特定协议的代码(如IPv4,IPv6,TCP,SCTP),或为通用套接字代码(SOL_SOCKET)。
- optname:选项名。level对应的选项,一个level对应多个选项,不同选项对应不同功能。选项【
SO_REUSEADDR
】:一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。即:允许端口被重复使用 - optval:指向某个变量的指针,该变量是要设置新值的缓冲区。可以是一个结构体,也可以是普通变量
- optlen:optval的长度。
1.6 bind()函数
这个函数用来给socket绑定地址信息。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen);
- sockfd:即为socket描述字,他是通过socket()函数创建的,唯一标识一个socket。bind函数就是将这个描述子绑定一个名字。
- addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
struct sockaddr
的结构里并没有提供存放ip地址,端口号的属性,所以需要用struct sockaddr_in
来强制类型转换。 - addrlen:对应的是地址的长度。
通常服务器在启动时会绑定一个总所周知的地址(ip地址+端口号),客户端不用指定系统自动分配,所以通常服务端在listen之前要调用bind(),而客户端不会调用,在connect()时由系统随机生成一个。
1.7 listen()函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- sockfd:即为socket描述字,为要监听的socket描述字
- backlog:相应socket可以排队的最大连接个数
注意这个函数调用的socket并不是连接使用的,如果我们这个socket用于通信了,那么接下来的客户连接怎么办,难道我们在绑定一个端口,那就冲突了。
2 IO复用技术
IO多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux下实现IO多路复用的系统调用主要有select. poll和epoll。其包含在头文件:
#include <sys/epoll.h>
2.1 epoll_event结构体(事件类型)
typedef union epoll_data {void *ptr;//是给用户自由使用的,用于附带自定义消息
int fd;
__uint32_t u32;
__uint64_t u64;} epoll_data_t;
struct epoll_event {__uint32_t events; /* epoll event */epoll_data_t data; /* User data variable */
};
- epoll_data中fd存放文件描述符,所以我们一般直接将监听的文件描述符赋值给
epoll_event.data.fd
- 其中events表示感兴趣的事件和被触发的事件,可能的取值为:加
|
就可以并选多个选项
选项 | 说明 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) |
EPOLLOUT | 表示对应的文件描述符可以写; |
EPOLLPRI | 表示对应的文件描述符有紧急的数可读(这里应该表示有带外数据到来) |
EPOLLERR | 表示对应的文件描述符发生错误; |
EPOLLRDHUP | 当socket接收到对方关闭连接时的请求之后触发,有可能是TCP连接被对方关闭,也有可能是对方关闭了写操作。(表示读关闭,内核不能再往内核缓冲区中增加新的内容。但是已经在内核缓冲区中的内容,用户态依然能够读取到。); |
EPOLLHUP | 表示对应的文件描述符被挂断(表示读写都关闭); |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的 |
EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里(以防多个线程相竞争) |
2.2 epoll_create函数
功能:该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围;
int epoll_create(int size)
2.3 epoll_ctl()函数
用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数返回值——0成功,-1失败。
- epfd:由epoll_create生成的epoll专用的文件描述符;
- op:要进行的操作,EPOLL_CTL_ADD注册(注册新的fd到epfd)、EPOLL_CTL_MOD修改(修改已经注册的fd的监听事件)、EPOLL_CTL_DEL删除(从epfd删除一个fd);
- fd:关联的文件描述符;
- event:指向epoll_event的指针,告诉内核需要监听的事件;
2.4 epoll_wait()函数
该函数用于轮询I/O事件的发生;
int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
该函数返回值——成功:返回发生的事件数;时间到时返回0;失败:-1
- epfd:由epoll_create生成的epoll专用的文件描述符;
- epoll_event:用来存内核得到事件的集合;
- maxevents:每次能处理的事件数;告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
- timeout:在没有检测到事件发生时最多等待的时间(单位为毫秒)——0:立即返回,不阻塞;-1 :无限阻塞,直到检测到fd数掘发生变化,解除阻塞;>0:指定毫秒
2.5 socklen_t数据类型
socklen_t是一种数据类型,它其实和int差不多,在32位机下,size_t和int的长度相同,都是32 bits,但在64位机下,size_t(32bits)和int(64 bits)的长度是不一样的,socket编程中的accept函数的第三个参数的长度必须和int的长度相同。于是便有了socklen_t类型。
其常指sockaddr_in
的长度的数据类型
3 Web服务器端接收
3.1 accept()函数
函数执行之后,socket就会等待客户端的连接。当连接建立之后,返回一个用于通信的新的socket,这个新的socket用于客户端与服务端之间的通信。因此,该函数返回的是已经连接的socket描述字。但是,如何失败,那么返回-1。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:服务器端的socket描述字,是服务器一开始调动socket()函数产生的(监听的)。
- addr:返回客户端的协议地址(注意对于
sockaddr_in
类型需要一步强制类型转换) - addrlen:表示地址的长度
【注意】accept()的第三个参数socklen_t *addrlen
和bind()的第三个参数socklen_t addrlen
不一样,accept需要一个指针(因此需要取地址符&
)
3.2 recv()函数
调用recv函数时:
recv
先等待SOCKET s
的发送缓冲
中的数据被协议传送完毕,如果协议在传送s
的发送缓冲
中的数据时出现网络错误,那么recv
函数返回SOCKET_ERROR
;- 如果
s
的发送缓冲区
中没有数据或者数据被协议成功发送完毕后,recv
先检查套接字s
的接收缓冲区; - 如果
s
的接收缓冲区
中没有数据或者协议正在接收数据,那么recv
就一直等待,直到协议把数据接收完毕; - 当协议把数据接收完毕,
recv
函数就把s
的接收缓冲区
中的数据copy到buf
中。(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv
函数仅仅是copy数据,真正的接收数据是协议来完成的),recv
函数返回其实际copy的字节数。如果recv
在copy时出错,那么它返回SOCKET_ERROR
; - 如果
recv
函数在等待协议接收数据时网络中断了,那么它返回0。
#include <pthread.h>
int recv( SOCKET s, char *buf, int len, int flags);
//返回值:<0 出错,=0 连接关闭,>0 接收到的数据长度大小
- s指定接收端套接字描述符;
- buf指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
- len指明buf的长度;
- flags一般置0。
当失败时,errno被设为以下的某个值 :
- EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 (对非阻塞socket而言,EAGAIN不是一种错误)
- EINTR:如果在读的过程中遇到了中断那么会返回-1,同时置errno为EINTR。
- EWOULDBLOCK与EAGAIN 等价(在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK)
【特别注意(代码也有体现)】当返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
的情况下认为连接是正常的,继续接收,即进入下次循环即可。
在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN)。从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。一次你,对于非阻塞而言,这个不是错误!
例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返 回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
4 信号量
linux 信号量相关函数都声明头文件 semaphore.h
头文件中,所以使用信号量之前需要先包含头文件。
而信号量的数据类型为结构sem_t
,它本质上是一个长整型的数。
4.1 sem_init()函数
该函数用于创建信号量:
int sem_init(sem_t *sem, int pshared, unsigned int value);
//返回值:创建成功返回0,失败返回-1
- sem:指向信号量结构的一个指针
- pshared:不为0时此sem信号量在进程间共享,为0时当前进程的所有线程共享
- value:信号量的初始值
4.2 sem_wait()函数
sem_wait 是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。若 s e m v a l u e > 0 sem_{value} > 0 semvalue>0,则该信号量值减去 1 并立即返回。若 s e m v a l u e = 0 sem_{value} = 0 semvalue=0,则阻塞直到 s e m v a l u e > 0 sem_{value} > 0 semvalue>0,此时立即减去 1,然后返回。
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
//操作成功返回0,失败则返回-1
下面的函数sem_trywait
是非阻塞的函数,它会尝试获取获取 s e m v a l u e sem_{value} semvalue值,如果 s e m v a l u e = 0 sem_{value} = 0 semvalue=0,不是阻塞住,而是直接返回一个错误 EAGAIN。
4.3 sem_post()函数
把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程。
int sem_post(sem_t *sem);
//返回值:操作成功返回0,失败则返回-1
4.4 sem_destroy()函数
释放信号量自己占用的一切资源 (被注销的信号量sem
要求:没有线程在等待该信号量了)
int sem_destroy(sem_t * sem)
//成功则返回 0,失败返回 -1
5 线程池
5.1 pthread_t数据类型
进程内部的每一个线程都有唯一标识,叫线程ID。线程ID会返回给pthread_create()
的调用者,一个线程可以通过pthread_self()获取自己的线程ID。
#include <pthread.h>
pthread_t pthread_self(void);
5.2 pthread_create()函数
该函数的主要用途为:创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);
// 返回值:0为成功;若是正值则出现错误
- thread表示线程id。其是指向线程标识符的指针,也就是线程对象的指针
- attr表示线程参数,用来设置线程属性。
- 第三个参数为线程运行函数的地址(新线程调用函数的名字),通俗理解线程要执行函数(线程做的事情的)指针。入口函数的返回值必须为void*,且入口函数必须为static(该函数,要求为静态函数。如果处理线程函数为类成员函数时,需要将其设置为静态成员函数。)。
- arg为该线程调用函数的参数列表,arg指向一个全局或者堆变量,也可以设置为Null,如果需要传递多个参数,可以将arg指向一个结构。
5.3 pthread_detach()函数
该函数的主要用途为:将某个线程分离。
线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性是
非分离状态
,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()
函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程
不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。
#include <pthread.h>
int pthread_detach(pthread_t thread);
//返回值:如果成功就返回0,否则返回一个正数
【注意】一旦线程处于分离状态,就不能使用pthread_join获取其状态,也无法返回“可连接”状态。
5.4 pthread_join()函数
该函数的主要用途为:以阻塞的方式等待由thread标识的线程终止,如果线程已经终止,该函数会立即返回。
代码中如果没有
pthread_join
主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join
后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。
#include <pthread.h>
int pthread_join(pthread_t thread,void **retval);
//返回值:返回0表示成功,返回正数表示失败
- 第一个参数表示等待线程的id。
- 第二个参数表示该线程结束时的返回值。若retval为一非空指针,将会保存线程终止时返回值的拷贝,该返回值即线程调用return或pthread_exit()时所制定的值。
joinable
(即上面提到的非可分离线程)的线程必须用pthread_join()
函数来释放线程所占用的资源,如果没有执行这个函数,那么线程的资源永远得不到释放。
5.5 pthread_exit()函数
该函数的主要用途为:终止调用线程,且其返回值可以通过调用pthread_join()来获取。调用pthread_exit()相当于在新线程函数start()中执行return,不同之处在于:
- Linux主线程里使用pthread_exit(val)结束时,只会使主线程结束,而由主线程创建的子线程并不会因此结束,他们继续执行。
- Linux主线程使用return结束时,那么子线程也就结束了。
#include <pthread.h>
void pthread_exit(void *retval);
- retval:制定线程的返回值。如果该参数value_ptr不为NULL,那么,此线程调用pthread_exit函数终止时,线程退出返回的值为*value_ptr。
6 互斥锁–与条件变量结合使用
下面系列函数都包含在头文件#include <pthread.h>
中。
6.1 pthread_mutex_init()函数
该函数的功能是初始化互斥锁。
pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr);
//成功返回0,失败返回errno
其中参数attr
指定了新建互斥锁的属性。如果参数attr为空,则使用默认的互斥锁属性,默认属性为快速互斥锁 。其共有四个类型:
PTHREAD_MUTEX_NORMAL
普通锁(默认)。当线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。PTHREAD_MUTEX_ERRORCHECK
检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。PTHREAD_MUTEX_RECURSIVE
递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。PTHREAD_MUTEX_DEFAULT
适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
6.2 pthread_mutex_destroy()函数
与pthread_mutex_init函数相配对的是pthread_mutex_destroy函数,当使用完互斥锁后将锁销毁。
pthread_mutex_destroy(pthread_mutex_t* mutex);
//使用完锁之后释放锁,常用于递归锁的时候
//成功返回0,失败返回errno
6.3 pthread_mutex_lock()函数
当pthread_mutex_lock()返回时,该互斥锁已被锁定。线程调用该函数让互斥锁上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用该线程将阻塞,直到该互斥锁变为可用为止。
pthread_mutex_lock(pthread_mutex_t mutex);//加锁
pthread_mutex_trylock(*pthread_mutex_t *mutex);
//加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待
//成功返回0.失败返回错误信息
6.4 pthread_mutex_unlock()函数
pthread_mutex_unlock是可以解除锁定 mutex 所指向的互斥锁的函数。
pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁
//成功返回0.失败返回错误信息
7 条件变量
下面系列函数都包含在头文件#include <pthread.h>
中。条件变量是线程同步的一种手段。条件变量用来自动阻塞一个线程,直到条件(predicate)满足被触发为止。通常情况下条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。而创建和销毁条件边量需要用到pthread_cond_init函数和pthread_cond_destroy函数(类似于上面说的pthread_mutex_init和pthread_mutex_destroy)。
7.1 pthread_cond_init()函数
初始化条件变量的函数。
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//函数成功返回0;任何其他返回值都表示错误。
//参数 2 条件变量的属性,一般传 NULL
结构pthread_condattr_t
是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用;如果选择为PTHREAD_PROCESS_SHARED则为多个进程间各线程公用。
7.2 pthread_cond_destroy()函数
销毁条件变量的函数。
int pthread_cond_destroy(pthread_cond_t *cond);//函数成功返回0;任何其他返回值都表示错误。
需要注意的是只有在没有线程在该条件变量上等待时,才可以注销条件变量,否则会返回EBUSY。
7.3 pthread_cond_wait()函数
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
需要注意的是:被阻塞的线程可以被pthread_cond_signal
函数,pthread_cond_broadcast
函数唤醒,也可能在被信号中断后被唤醒。也就是说pthread_cond_wait
函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。因此常常结合while
使用
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//函数成功返回0;任何其他返回值都表示错误。//函数将解锁mutex参数指向的互斥锁,并使当前线程阻塞在cond参数指向的条件变量上。
该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,互斥锁会再次被锁上. 也就是说函数内部会有一次解锁和加锁操作.
7.4 pthread_cond_timedwait()函数
函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数abstime指定。函数返回时,相应的互斥锁往往是锁定的,即使是函数出错返回。
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
// cond是指向pthread_cond_t结构的指针,mutex是互斥锁的标识符,abstime为指向timespec结构体的指针。
计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEDOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
7.5 pthread_cond_signal()函数
激活一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
//函数成功返回0;任何其他返回值都表示错误。
唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER
类型的,系统将根据线程的优先级唤醒线程。
7.6 pthread_cond_broadcast()函数
函数以广播的方式唤醒所有被pthread_cond_wait函数阻塞在某个条件变量上的线程,参数cond被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast函数无效。
int pthread_cond_broadcast(pthread_cond_t *cond);
//函数成功返回0;任何其他返回值都表示错误。
由于pthread_cond_broadcast
函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast
函数。
8 Mysql接口API
8.1 头文件
要记得先包含头文件#include <mysql.h>
(找不到mysql/mysql.h头文件的时候,需要安装一个库文件:sudo apt install libmysqlclient-dev
)
8.2 MYSQL变量
连接数据库前,必须先创建MYSQL变量,此变量在很多Mysql API函数会用到。它包含了一些连接信息等数据。
typedef struct st_mysql {NET net; /* Communication parameters 通讯参数,网络相关*/
unsigned char connector_fd; /* ConnectorFd for SSL 加密套接字协议层*/char *host,*user,*passwd,*unix_socket,*server_version,
*host_info,*info,*db;//数据库用户名,密码,主机名,Unix套接字,版本,主机信息unsigned int port,client_flag,server_capabilities;
unsigned int protocol_version;
unsigned int field_count;
unsigned int server_status;
unsigned long thread_id; /* Id for connection in server */
my_ulonglong affected_rows;
my_ulonglong insert_id; /* id if insert on table with NEXTNR */
my_ulonglong extra_info; /* Used by mysqlshow */
unsigned long packet_length;
enum mysql_status status;
MYSQL_FIELD *fields;
MEM_ROOT field_alloc;
my_bool free_me; /* If free in mysql_close */
my_bool reconnect; /* set to 1 if automatic reconnect */
struct st_mysql_options options;
char scramble_buff[9];
struct charset_info_st *charset;
unsigned int server_language;
} MYSQL;
8.3 结构体
MYSQL_RES
该结构体中包含了查询结果集,也就是从数据库中查询到的数据。这个结构代表返回行的一个查询的(SELECT, SHOW, DESCRIBE, EXPLAIN)
的结果。
返回的数据称为“数据集”
,在C的API里对应的就是MYSQL_RES,从数据库读取数据,最后就是从MYSQL_RES中读取数据。
typedef struct st_mysql_res {my_ulonglong row_count;MYSQL_FIELD *fields;MYSQL_DATA *data;MYSQL_ROWS *data_cursor;unsigned long *lengths; /* column lengths of current row */MYSQL *handle; /* for unbuffered reads */const struct st_mysql_methods *methods;MYSQL_ROW row; /* If unbuffered read */MYSQL_ROW current_row; /* buffer to current row */MEM_ROOT field_alloc;unsigned int field_count, current_field;my_bool eof; /* Used by mysql_fetch_row *//* mysql_stmt_close() had to cancel this result */my_bool unbuffered_fetch_cancelled; void *extension;
} MYSQL_RES;
MYSQL_ROW
这是一个行数据的类型安全(type-safe)的表示。当前它实现为一个计数字节的字符串数组。
typedef char **MYSQL_ROW;
8.4 使用流程
1、首先要包含mysql的头文件,并链接mysql动态库。
2、创建MYSQL变量。如:MYSQL mysql;
3、mysql_init()
初始化MYSQL变量
4、使用mysql_real_connect()
建立一个到mysql数据库的连接
5、使用mysql_query()
执行查询语句
6、使用result = mysql_store_result(mysql)
,通过返回的MYSQL_RES变量result
获取查询结果数据。
7、(可选)使用mysql_num_fields(result)
获取查询的列数,mysql_num_rows(result)
获取结果集的行数
8、调用mysql_fetch_row(result)
函数读取结果集数据。
9、使用mysql_free_result(result)
释放结果集所占内存,防止内存泄漏。
10、使用mysql_close(conn)
关闭连接
8.5 mysql_init()函数
函数用途:分配或初始化与mysql_real_connect()相适应的MYSQL对象
MYSQL *mysql_init(MYSQL *mysql)
- mysql为MYSQL类型的变量(注意整体输入是地址)。如果mysql是NULL指针,该函数将分配、初始化、并返回新对象。
【返回】初始化的MYSQL*句柄。如果无足够内存以分配新的对象,返回NULL。 错误,在内存不足的情况下,返回NULL。
8.6 mysql_real_connect()函数
连接数据库引擎,通过函数mysql_real_connect()尝试与运行在主机上的MySQL数据库引擎建立连接。
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag)
- mysql:前面定义的MYSQL变量;
- host:MYSQL服务器的地址;如果“host”是NULL或字符串"localhost",连接将被视为与本地主机的连接。如果操作系统支持套接字(Unix)或命名管道(Windows),将使用它们而不是TCP/IP连接到服务器。
- user:登录用户名;如果“user”是NULL或空字符串"",用户将被视为当前用户。在UNIX环境下,它是当前的登录名。
- passwd:登录密码;
- db:要连接的数据库,如果db为NULL,连接会将默认的数据库设为该值。
- port:MYSQL服务器的TCP服务端口;如果“port”不是0,其值将用作TCP/IP连接的端口号。注意,“host”参数决定了连接的类型。
- unix_socket:unix连接方式。如果unix_socket不是NULL,该字符串描述了应使用的套接字或命名管道。注意,“host”参数决定了连接的类型。
- clientflag:Mysql运行为ODBC数据库的标记,一般取0。
【返回】如果连接成功,返回值与第1个参数的值相同。如果连接失败,返回NULL
8.7 mysql_query()函数
查询数据库中的某一个表内容,通过函数mysql_query()来实现。(不会返回内容,需要通过下面的mysql_store_result()
函数来获取内容后,通过mysql_fetch_row()
函数来显示出来)
int mysql_query(MYSQL *mysql, const char *query)
//query为执行的SQL语句对应的字符长串(Null终结的字符串)
【返回】如果查询成功,返回0。如果出现错误,返回非0值。
注意,mysql_query()
不能用于包含二进制数据的查询,应使用mysql_real_query()
取而代之。
8.8 mysql_store_result()函数
显示查询数据库中数据表的内容,mysql_store_result()将mysql_query()查询的全部结果读取到客户端,分配1个MYSQL_RES
结构,并将结果置于该结构中。
MYSQL_RES *mysql_store_result(MYSQL *mysql)
【返回】具有多个结果的MYSQL_RES结果集合。如果出现错误,返回NULL。
8.9 mysql_fetch_row()函数
MYSQL_ROW mysql_fetch_row(MYSQL_RES* result)
【返回】下一行的MYSQL_ROW
结构。如果没有更多要检索的行或出现了错误,返回NULL。
其中,行内值的数目由mysql_num_fields(result)
给出。也就是流程中提到的第七步。
8.10 mysql_free_result()函数
释放由mysql_store_result()
、mysql_use_result()
、mysql_list_dbs()
等为结果集分配的内存。完成对结果集的操作后,必须调用mysql_free_result()
释放结果集使用的内存。
void mysql_free_result(MYSQL_RES *result)
8.11 mysql_close()函数
关闭前面打开的连接。如果句柄是由mysql_init()
或mysql_connect()
自动分配的,mysql_close()
还将解除分配由mysql指向的连接句柄。
void mysql_close(MYSQL *mysql)
9 Linux系统读写函数
9.1 fcntl()函数
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。(经常用这个fcntl函数改变非阻塞)
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:
命令名 | 描述 |
---|---|
F_DUPFD | 复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件 |
F_GETFD | 获得文件描述符标志 |
F_SETFD | 设置文件描述符标志(设置为第三个参数arg的最后一位) |
F_GETFL | 获取文件状态标志(用于改变非阻塞) |
F_SETFL | 设置文件状态标志(用于改变非阻塞) |
F_GETLK | 获取文件锁 |
F_SETLK | 设置文件锁 |
F_SETLKW | 类似F_SETLK,但等待返回 |
F_GETOWN | 获取当前接收SIGIO和SIGURG信号的进程ID和进程组ID |
F_SETOWN | 设置当前接收SIGIO和SIGURG信号的进程ID和进程组ID |
【返回值】fcntl()的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。如:
- F_DUPFD 返回新的文件描述符
- F_GETFD 返回相应标志
- F_GETFL , F_GETOWN 返回一个正的进程ID或负的进程组ID
9.2 stat()函数
stat()函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里,这里仅对其中用到的成员进行介绍。
#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>//获取文件属性,存储在statbuf中int stat(const char *pathname, struct stat *statbuf);struct stat {mode_t st_mode; /* 文件类型和权限 */off_t st_size; /* 文件大小,字节数*/
};
9.3 mmap()函数
用于将一个文件或其他对象映射到内存,提高文件的访问速度。
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);
//如同malloc之后需要free一样,mmap调用创建的映射区使用完毕之后,需要调用munmap去释放。
函数的输入参数含义如下:
- start:指向映射区的首地址,这是由系统内核所决定的,一般设为NULL或0(代表由系统决定映射区的起始地址);
- length:映射区的长度
- prot:期望的内存保护标志,不能与文件的打开模式冲突
- PROT_READ 表示页内容可以被读取
- PROT_EXEC 映射区域可被执行
- PROT_WRITE 映射区域可被写入
- PROT_NONE 映射区域不能存取
- flags:指定映射对象的类型,映射选项和映射页是否可以共享
- MAP_PRIVATE 建立一个写入时拷贝的私有映射,对映射区所作的修改不会反映到物理设备。
- MAP_FIXED:对映射区所作的修改会反映到物理设备,但需要调用msync()或者munmap();
- fd:创建的映射区的文件描述符,一般是由open()函数返回
- offset:被映射文件的偏移量,一般设为0,表示从头开始映射。
9.4 iovec 结构体
定义了一个向量元素,通常,这个结构用作一个多元素的数组。
struct iovec {void *iov_base; /* starting address of buffer */size_t iov_len; /* size of buffer */
};
- iov_base指向数据的地址,它指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。
- iov_len表示数据的长度
9.5 writev()函数
writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。
#include <sys/uio.h>
2ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
- filedes表示文件描述符
- iov为前述io向量机制结构体iovec
- iovcnt为结构体的个数
若成功则返回已写的字节数,若出错则返回-1(并正确设置errno)。writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。
writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。
特别注意: 循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len。
9.6 va_list结构体
VA_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。
【用法】
- 首先在函数里定义一具
VA_LIST
型的变量 ,这个变量是指向参数的指针 ,通过指针运算来调整访问的对象; - 然后用
VA_START
宏初始化变量刚定义的VA_LIST
变量 ,实际上 就是用VA_LIST
去指向函数的最后一个具名的参数; - 然后用
VA_ARG
宏返回可变的参数,VA_ARG
的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG
获取各个参数);因为栈地址是从高到低延伸的,所以加上你要的参数类型大小,就意味着栈顶指针指向你所要的参数,便可通过 底层 pop 得到。 - 最后用
VA_END
宏结束可变参数的获取,即清空 va_list 。
【问题】
- 因为 va_start , va_arg , va_end 等定义成宏,只是字符替换,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
- 另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
- 由于参数的地址用于 VA_START 宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。
但是C++11新特性可变参数可以在语言层面很好的解决上述问题
9.7 常见ERRNO
当linux中的 api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因,在实际编程中用这一招解决了不少原本看来莫名其妙的问题。
如:
#define EAGAIN 11 /* Try again */重试
#define EINTR 4 / Interrupted system call / 中断的系统调用
9.8 send()函数
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);
第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。
当套接字发送缓冲区变满时,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。
9.9 exit()函数
退出当前运行的程序,并将参数value返回给主调进程
exit(0);//表示程序正常退出;除了0之外,其他参数均代表程序异常退出,如下面
exit(1);
exit(-1);
exit
其实和return
是一样使用的,但其中的区别如下:
- return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
- return会跳出函数,而exit会结束程序。
10 Linux系统信号机制函数
10.1 sigaction结构体
struct sigaction {void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);
}
参数解释如下:
- sa_handler是一个函数指针,指向信号处理函数
- sa_sigaction同样是信号处理函数,有三个参数,可以获得关于信号更详细的信息
- sa_mask用来指定在信号处理函数执行期间需要被屏蔽的信号
- sa_flags用于指定信号处理的行为,其各自行为的定义如下:
- SA_RESTART,使被信号打断的系统调用自动重新发起
- SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
- SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
- SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
- SA_RESETHAND,信号处理之后重新设置为默认的处理方式
- SA_SIGINFO,使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
- sa_restorer一般不使用
10.2 sigaction()函数
检查或修改与指定信号相关联的处理动作(可同时两种操作)
#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//返回值,0 表示成功,-1 表示有错误发生。
- signum表示操作的信号。
- act表示对信号设置新的处理方式。
- oldact表示信号原来的处理方式。
10.3 sigfillset()函数
用来将参数set信号集初始化,然后把所有的信号加入到此信号集里。
#include <signal.h>
int sigfillset(sigset_t *set);
信号集是为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux系统中引入了信号集(信号的集合)。
10.4 SIGALRM、SIGTERM信号
#define SIGALRM 14 //由alarm系统调用产生timer时钟信号
#define SIGTERM 15 //终端发送的终止信号
10.5 alarm()函数
设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。
#include <unistd.h>;
unsigned int alarm(unsigned int seconds);
如果未设置信号SIGALRM的处理函数,那么alarm()默认处理终止进程
10.6 socketpair()函数
在linux下,使用socketpair函数能够创建一对套接字进行通信,项目中使用管道通信。
#include <sys/types.h>
#include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sv[2]);
//返回结果, 0为创建成功,-1为创建失败
- domain表示协议族,PF_UNIX或者AF_UNIX
- type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP
- protocol表示类型,只能为0
- sv[2]表示套节字柄对,该两个句柄作用相同,均能进行读写双向操作
引用
参考文章如下:
Mysql接口API相关函数详细使用说明
linux编程:pthread
多线程之信号量
互斥锁
recv详解
fcntl函数的用法总结
一文让你搞懂 C语言可变参数 VA_LIST原理详解
【C++】Web服务器项目所用到的函数详解相关推荐
- [转帖]ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解
ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解 https://www.cnblogs.com/vipyoumay/p/7525478.html ASP.NET C ...
- 哪个服务器支持高并发,IIS Web服务器支持高并发设置方法详解
这篇文章主要介绍了IIS Web服务器如何支持高并发,详细设置方法在下面,大家参考使用吧 适用的IIS版本:IIS 7.0, IIS 7.5, IIS 8.0 适用的Windows版本:Windows ...
- web 服务器安全维护,Web服务器安全攻击及防护机制详解
Web安全分为两大类: · Web服务器的安全性(Web服务器本身安全和软件配置). · Web应用程序的安全性(在Web服务器上运行的Java. ActiveX.PHP.ASP代码的安全). Web ...
- ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解
1.1. 名词解释 内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序. 用户态: 只能受限的访问内存, 且不允许访问外围设备. ...
- 淘宝网发起的Web服务器项目
>>>>>>>>>>>>>>>>>>>>>=== Tengin ...
- php output详解,PHP输出缓冲控制Output Control系列函数详解,output函数详解
PHP输出缓冲控制Output Control系列函数详解,output函数详解 概述 以前研究过PHP的输入输出缓冲,不过博客搬家以后,原来文章找不到了,今天看到一篇好文,顺便转载过来. 简介 说到 ...
- 脚本——web_custom_request函数详解
web_custom_request函数详解 在LR中当使用HTML录制方式时,录制的脚本中主要由函数web_link().web_submit_form().web_url().web_submit ...
- 让别人远程访问你的代码网站项目或临时演示你的项目给客户的方式详解即外网局域网访问自己的项目
让别人远程访问你的代码网站项目或临时演示你的项目给客户的方式详解 文章目录 让别人远程访问你的代码网站项目或临时演示你的项目给客户的方式详解 引言 一.创建一个你想要别人访问的项目 二.明确你想要将这 ...
- Java 回调函数详解及使用
Java 回调函数详解 前言: C语言中回调函数解释: 回调函数(Callback Function)是怎样一种函数呢? 函数是用来被调用的,我们调用函数的方法有两种: 直接调用:在函数A的函数体里通 ...
最新文章
- ASP.NET中的事务处理和异常处理
- unity三维向量变化为角度_UNITY3D两个物体相对位置、角度、相对速度方向
- 话里话外:从“种房子”谈流程与制度的差别
- 创建型模式——抽象工厂模式
- Python中字符串如何定义简单举例
- 代码太烂,可能是他离职的原因吧!
- 汽车行业要变天?数据告诉你,为什么说合资车企正在走向末路
- SpringCache @Cacheable 在同一个类中调用方法,导致缓存不生效的问题及解决办法...
- 前后端开源的一款简单的微信个人博客小程序
- 浙江省计算机二级办公软件高级应用技术真题,浙江省计算机二级办公软件高级应用技术考试题库.doc...
- 树莓派Raspberry pi 4B 运行 WuKong-Robot 智能语音对话机器人
- Excel中的透视表和vlookup的用法简单讲解
- html中的abbr有什么作用,html中关于abbr 标签的使用以及作用的详解
- java netcdf精度_NetCDF 介绍
- 【数分】7. AB实验篇
- HP1020打印机加入域后,域用户无法使用HP1020域打印功能
- 无线网卡ping时显示hardware error的原因及解决方法
- php8能否救命,虎皮兰干枯发黄惨兮兮!2大罪魁祸首,对症下药救命
- 产品一:葡萄籽的美肌功效
- 七夕一键生成表白墙源码
热门文章
- c1-02西班牙的语言,【图片】考试的同学看过来~DELE-C1两个月准备(实用经验+超详细流程)转【西班牙语吧】_百度贴吧...
- SAP STO With Billing流程与配置
- 在线学习Linux的网站
- 7-3 小孩子才做选择,大人全都要 (10 分)
- java程序设计高级教程答案_Java高级程序设计实战教程答案
- JSR303 数据效验
- java订单 并发_订单并发处理思路
- Jetson-DeepStream
- 2021最新Spring Security知识梳理
- 拍案惊奇——软件调试实战训练营暑期特别班(v2.1)