C++ 之动手写 Reactor 服务器模型(一):网络编程基础复习总结
基础
IP 地址可以在网络环境中唯一标识一台主机。
端口号可以在主机中唯一标识一个进程。
所以在网络环境中唯一标识一个进程可以使用 IP 地址与端口号 Port 。
字节序
TCP/IP协议规定,网络数据流应采用大端字节序。
大端:低地址存高位,高地址存低位;
小端:低地址存低位,高地址存高位(x86采用小端存储)。
网络字节序,就是在网络中进行传输的字节序列,采用的是大端法。
主机字节序,就是本地计算机中存储数据采用的字节序列,采用的是小端法。
相关 API 函数
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//h = host n = network l = long s = short
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);#include <arpa/inet.h>
//点分十进制字符串转换为网络字节序
int inet_pton(int af, const char *src, void *dst);
//网络字节序转换为点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> //包含了<netinet/in.h>,后者包含了<sys/socket.h>typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};//将cp所指C字符串转换成一个32位的网络字节序二进制值,并通过inp指针来存储,成功返回1,失败返回0
int inet_aton(const char *cp, struct in_addr *inp);//将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串,由该函数的返回值所指向的
//字符串驻留在静态内存中,这意味着该函数是不可重入的
char *inet_ntoa(struct in_addr in);//inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序二进制值,如果参数char
//*cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也返回-
//1,255.255.255.255是一个有效的地址,不过inet_addr无法处理;
//返回值为32位的网络字节序二进制
in_addr_t inet_addr(const char *cp);//ok
in_addr_t inet_network(const char *cp);
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
常用结构体
man 7 ip
可以查看相应的结构体,也可以使用命令sudo grep -rn "struct sockaddr_in {" /usr
进行搜索。
struct sockaddr
{sa_family_t sa_family; /* address family, AF_xxx */char sa_data[14]; /* 14 bytes of protocol address */
};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 */
};
IPv4
和 IPv6
的地址格式定义在 netinet/in.h
中,IPv4
地址用 sockaddr_in
结构体表示,IPv6
地址使用 sockaddr_in6
结构体表示。UNIX Domain Socket
的地址格式定义在 sys/un.h
中,使用 sockaddr_un
结构体表示。所有的地址类型分别定义为常数 AF_INET
、AF_INET6
、AF_UNIX
。
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6/AF_UNIX;
addr.sin_port = htons/ntohs;
addr.sin_addr.s_addr = htonl/ntohl/inet_pton/inet_ntop
网络编程相关函数
socket 函数
创建套接字:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>//创建套接字函数
int socket(int domain, int type, int protocol);domain:AF_INET/AF_INET6/AF_UNIX
type:SOCK_STREAM/SOCK_DGRAM 前者默认是TCP,后者默认是UDP
protocol:传0表示使用默认协议
//函数返回值
成功,返回指向新创建的socket的文件描述符,失败返回-1,设置errno
bind 函数
因为服务器程序所监听的网络地址与端口号是固定不变的,所以需要使用bind函数进行绑定。bind 函数将 sockfd 与 addr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。
绑定 IP 与端口号:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//绑定服务的端口号与IP地址
sockfd:上面socket创建的套接字
addr:所要绑定的ip地址与端口号
addrlen:前面addr结构体的长度
//函数返回值
成功,返回指向新创建的socket的文件描述符,失败返回-1,设置errno
listen 函数
用来指定监听上限数值(允许同时多少个客户端与服务器建立连接),指定最大同时发起连接数。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);sockfd:socket创建的文件描述符
backlog:排队建立3次握手队列和刚刚建立3次握手队列的连接数和。
可以使用命令进行最大发起连接数限定值的查看:
cat /proc/sys/net/ipv4/tcp_max_syn_backlog 1
accept 函数
接收连接请求的函数,阻塞等待客户端发起连接。
如果客户端还没有来得及连接,此时 accept 函数会处于阻塞状态。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);sockfd:socket创建的文件描述符
addr:传出参数,返回连接客户端地址信息,包含IP地址与端口号
addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构
体的大小。
//函数返回值
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
connect 函数
客户端调用该函数,连接到服务器上。
发起连接:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd:是客户端自己使用socket得到的文件描述符。
addr:传入参数,指定服务器端地址信息,包含IP地址与端口号
addrlen:传入参数,传入sizeof(addr)大小
返回值:成功返回0,失败返回-1,设置errno
客户端需要调用 connect 连接服务器,connect 和 bind 的采纳数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。
close 函数
关闭套接字创建的文件描述符。
#include <unistd.h>int close(int fd);
客户端其实也是需要 bind 端口号与 IP 地址,如果没有显示绑定的话,操作系统会自动分配一个 IP 地址与端口号。但是服务器是不能不使用 bind 函数,让操作系统随机分配 IP 地址与端口号,这样的话客户端就不知道服务器的 IP 地址与端口号,就不知道怎么连接到服务器上了,也不知道连接到那个服务器上。
本地随机的有效数字类型的 IP,INADDR_ANY
。
INADDR_ANY
解析:转换过来就是 0.0.0.0,泛指本机的意思,表示本机的所有IP,因为有些电脑不止一块网卡,如果某个应用程序只监听某个端口,那么其他端口过来的数据就接收不了。
网络编程代码
逻辑示例图
端口复用
让同一个端口可以进行重复使用,不至于等待 2MSL
的时间:
#include <sys/types.h>
#include <sys/socket.h>int getsockopt(int sockfd, int level, int optname, void* optval, socklen_t* optlen);int setsockopt(int sockfd, int level, int optname,const void* optval, socklen_t optlen);int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
服务器端源码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666int main()
{int sfd, cfd;struct sockaddr_in serv_addr, clie_addr;socklen_t clie_addr_len;char buf[BUFSIZ], clie_IP[BUFSIZ];int nByte, idx;sfd = socket(AF_INET, SOCK_STREAM, 0);int opt = 1;setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//允许端口复用memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));listen(sfd, 128);clie_addr_len = sizeof(clie_addr);cfd = accept(sfd, (struct sockaddr*)&clie_addr, &clie_addr_len);printf("client IP: %s, port: %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP,sizeof(clie_IP)),ntohs(clie_addr.sin_port));while (1){nByte = read(cfd, buf, sizeof(buf));for (idx = 0; idx < nByte; ++idx){buf[idx] = toupper(buf[idx]);}write(cfd, buf, nByte);}close(sfd);close(cfd);return 0;
}
客户端源码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666int main()
{int cfd;struct sockaddr_in serv_addr;char buf[BUFSIZ];int nByte;cfd = socket(AF_INET, SOCK_STREAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));while (1){fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0write(cfd, buf, strlen(buf));nByte = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, nByte);}close(cfd);return 0;
}
read 返回值
1、大于0,实际读到的字节数,并且buf=1024
如果read读到的数据的长度等于buf,返回的就是1024
如果read读到的数据长度小于buf,那就是小于1024的数值。
2、返回值为0,数据读完(读到文件、管道、socket末尾 —对端关闭)
3、返回值为-1,表明出现异常
errno == EINTR 说明被信号中断 所以需要重启或者退出;
errno == EAGAIN(EWOULDBLOCK)非阻塞方式读,并且没有数据;
其他值的出现表示出现错误使用 perror 打印然后 exit 退出
readn/writen 函数的封装
因为以太网帧一次只能传送 1500 字节的数据,所以使用 read 函数一次最多只能读到 1500 字节,就返回退出。
ssize_t readn(int fd, void* vptr, size_t n)
{size_t nleft;//usigned int剩余未读取的字节数size_t nread;//int 实际读到的字节数char* ptr;nleft = n;//n未读取字节数ptr = vptr;while (nleft > 0){if ((nread = read(fd, ptr, nleft)) < 0){if (errno == EINTR){nread = 0;}else{return -1;}}else if (0 == nread){break;}nleft -= nread;ptr += nread;}return (n - nleft);
}
ssize_t writen(int fd, const void* vptr, size_t n)
{size_t nleft;size_t nwritten;const char* ptr;nleft = n;ptr = vptr;while (nleft > 0){if ((nwritten = write(fd, ptr, nleft)) <= 0){if (nwritten < 0 && errno == EINTR){nwritten = 0;}else{return -1;}}nleft -= nwritten;ptr += nwritten;}return n;
}
IO多路复用
概念与原理图
多进程与多线程并发服务器,不经常使用这种作为大型服务器开发的原因是,所有的监听与访问请求都由服务器操作。
可以使用多路IO转接服务器(也叫多任务IO服务器),思想:不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
select
接口解析
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,struct timeval* timeout);nfds:监控的文件描述符集里最大文件描述符 + 1,因为此参数会告诉内核检测前多少个文件描述符的状态。
readfs / writes / exceptfds : 监控有读数据 / 写数据 / 异常发生到达文件描述符集合,三个都是传入传出参数。
timeout : 定时阻塞监控时间,3种情况:
1、NULL,永远等下去
2、设置timeval,等待固定时间
3、设置timeval里时间均为0,检查描述字后立即返回,轮询。
fd_set:本质是个位图。
struct timeval
{long tv_sec; /* seconds */long tv_usec; /* microseconds */
};
返回值:
成功:所监听的所有的监听集合中,满足条件的总数。
失败:返回 -1.
void FD_ZERO(fd_set *set);//将set清空为0
void FD_SET(int fd, fd_set *set);//将fd设置到set集合中
void FD_CLR(int fd, fd_set *set);//将fd从set中清除出去
int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中
优缺点
1、文件描述符上限(1024),同时监听的文件描述符1024个,历史原因,不好修改,除非重新编译Linux内核。
2、当监听的文件描述符个数比较稀疏的时候(比如3, 600, 1023),循环判断比较麻烦,所以需要自定义数据结构:数组。
3、监听集合与满足监听条件的集合是同一个,需要将原有集合保存。
代码实现(C语言)
server 端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>#define SERV_PORT 8888int main()
{int listenfd, connfd, sockfd;struct sockaddr_in serv_addr, clie_addr;socklen_t clie_addr_len;int ret, maxfd, maxi, i, j, nready, nByte;fd_set rset, allset;int client[FD_SETSIZE];char buf[BUFSIZ], str[BUFSIZ];listenfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listenfd){perror("socket error");exit(-1);}int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);ret = bind(listenfd, (struct sockaddr*)&serv_addr,sizeof(serv_addr)); if (-1 == ret){perror("bind error");exit(-1);}ret = listen(listenfd, 128);if (-1 == ret){perror("listen error");exit(-1);}maxfd = listenfd;maxi = -1;for (i = 0; i < FD_SETSIZE; ++i){client[i] = -1;}FD_ZERO(&allset);FD_SET(listenfd, &allset);while (1){rset = allset;nready = select(maxfd + 1, &rset, NULL, NULL, NULL);if (nready < 0){perror("select error");exit(-1);}if (FD_ISSET(listenfd, &rset)){clie_addr_len = sizeof(clie_addr);connfd = accept(listenfd, (struct sockaddr*)&clie_addr,&clie_addr_len);if (-1 == connfd){perror("accept error");exit(-1);}printf("receive from %s from port %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str,sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < FD_SETSIZE; ++i){if (client[i] < 0){client[i] = connfd;break;}}if (i == FD_SETSIZE){fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset);if (connfd > maxfd){maxfd = connfd;}if (i > maxi){maxi = i;}if (--nready == 0){continue;}}for (i = 0; i <= maxi; ++i){if ((sockfd = client[i]) < 0){continue;}if (FD_ISSET(sockfd, &rset)){if ((nByte = read(sockfd, buf, sizeof(buf))) == 0){close(sockfd);FD_CLR(sockfd, &allset);client[i] = -1;}else if (nByte > 0){for (j = 0; j < nByte; ++j){buf[j] = toupper(buf[j]);}write(sockfd, buf, nByte);write(STDOUT_FILENO, buf, nByte);}if (--nready == 0){break;}}}}close(listenfd); close(connfd);return 0;
}
client 端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888int main()
{int cfd;struct sockaddr_in serv_addr;char buf[BUFSIZ];int nByte;cfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == cfd){perror("socket error");exit(-1);}memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));while (1){fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0write(cfd, buf, strlen(buf));nByte = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, nByte);}close(cfd);return 0;
}
poll
接口解析
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd
{int fd; /* file descriptor */short events; /* requested events */short revents; /* returned events */
};fds:文件描述符数组。
events:POLLIN/POLLOUT/POLLERR
nfds:监控数组中有多少文件描述符需要被监控。
timeout 毫秒级等待:-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏0:立即返回,不阻塞进程>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值。
函数返回值:满足监听条件的文件描述符的数目。
优缺点
优点:
1、突破文件描述符1024的上限
2、监听与返回的集合分离
3、搜索范围变小(已经知道是哪几个数组)
缺点:
1、监听1000个文件描述符,但是只有3个满足条件,这样也需要全部遍历,效率依旧低。
2、cat /proc/sys/fs/file-max 查看一个进程可以打开的文件描述符的上限数。
3、sudo vi /etc/security/limits.conf。在文件尾部写入以下配置,soft 软限制,hard 硬限制。
soft nofile 65536
hard nofile 100000
代码实现(C语言)
server 端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>#define SERV_PORT 8888
#define OPEN_MAX 1024int main()
{int i, j, n, maxi;int nready, ret;int listenfd, connfd, sockfd;char buf[BUFSIZ], str[INET_ADDRSTRLEN];struct sockaddr_in serv_addr, clie_addr;socklen_t clie_addr_len;struct pollfd client[OPEN_MAX];listenfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listenfd){perror("socket error");exit(-1);}int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);//本地字节序port与ip都要转换为网络字节序serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//因为要在网络上传输ret = bind(listenfd, (struct sockaddr*)&serv_addr,sizeof(serv_addr));if (-1 == ret){perror("bind error");exit(-1);}ret = listen(listenfd, 128);if (-1 == ret){perror("listen error");exit(-1);}client[0].fd = listenfd;client[0].events = POLLIN;for (i = 1; i < OPEN_MAX; ++i){client[i].fd = -1;//将数组初始化为-1}maxi = 0;while (1){nready = poll(client, maxi + 1, -1);if (nready < 0){perror("poll error");exit(-1);}if (client[0].revents & POLLIN){clie_addr_len = sizeof(clie_addr);connfd = accept(listenfd, (struct sockaddr*)&clie_addr,&clie_addr_len);//立即连接,此时不会阻塞等if (-1 == connfd){perror("accept error");exit(-1);}printf("received from %s at port %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, str,sizeof(str)),ntohs(clie_addr.sin_port));for (i = 1; i < OPEN_MAX; ++i){if (client[i].fd < 0)//因为初始化为-1,所以在此作为判断条件{client[i].fd = connfd;break;//直接跳出,免得继续判断,浪费时间}}if (i == OPEN_MAX)//select监听的文件描述符有上限,最大只能监听1024个{fputs("too many clients\n", stderr);exit(1);}client[i].events = POLLIN;if (i > maxi){maxi = i;//因为文件描述符有新增,导致自定义数组有变化,所以需要重新修改maxi的值}if (--nready == 0)//意思不明确{continue;}}for (i = 1; i <= maxi; ++i){if ((sockfd = client[i].fd) < 0){continue;}if (client[i].revents & POLLIN){if ((n = read(sockfd, buf, sizeof(buf))) < 0){if (errno == ECONNRESET){printf("client[%d] abort connect\n", i);close(sockfd);client[i].fd = -1;}else{perror("read n = 0 error");}}else if (n > 0){for (j = 0; j < n; ++j){buf[j] = toupper(buf[j]);}write(sockfd, buf, n);write(STDOUT_FILENO, buf, n);}else{close(sockfd);client[i].fd = -1;}if (--nready == 0){break;}}}}close(listenfd);close(connfd);return 0;
}
client 端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888int main()
{int cfd;struct sockaddr_in serv_addr;char buf[BUFSIZ];int nByte;cfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == cfd){perror("socket error");exit(-1);}memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));while (1){fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0write(cfd, buf, strlen(buf));nByte = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, nByte);}close(cfd);return 0;
}
epoll
接口解析
是Linux下IO多路复用接口select/poll的增强版本,能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要侦听的文件描述符集合,另一个原因是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历哪些被内核IO事件唤醒而加入Ready队列的描述符集合就行了。
#include <sys/epoll.h>int epoll_create(int size);size:参数size用来告知内核监听的文件描述符的个数,与内存大小有关。//控制某个epoll监控的文件描述符上的事件:注册、修改、删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);epfd:epoll_create函数返回的值
op:EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL
fd:将哪个文件描述符以op的方式加在以epfd建立的树上
event:告诉内核需要监听的事情。struct epoll_event
{uint32_t events;epoll_data_t data;
};typedef union epoll_data
{void* ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;//等待所监控文件描述符上有事件的产生
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);events:用来存内核得到事件的集合(这里是个传出参数)
maxevents:告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size
timeout:是超时时间-1:阻塞=0:立即返回,非阻塞>0:指定毫秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回 - 1
优缺点
优点:
1、文件描述符数目没有上限:通过 epoll_ctl() 来注册一个文件描述符,内核中使用红黑树的数据结构来
管理所有需要监控的文件描述符。
2、基于事件就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于 callback 的回调机制,迅速激活这个文件描述符,这样随着文件描述符数量的增加,也不会影响判定就绪的性能。
3、维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中,这样调用 epoll_wait 获取就绪文件描述符的时候,只要取队列中的元素即可,操作的时间复杂度恒为 O(1) 。
图解
类型区别
水平触发(level-triggered)
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知;当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知LT模式支持阻塞和非阻塞两种方式。
epoll
默认的模式是LT
。
边缘触发(edge-triggered)
当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知;当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知。
两种类型区别
两者的区别在哪里呢?
水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次。
LT(level triggered) 是缺省的工作方式,并且同时支持 block 和 no-block socket.
在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的 select/poll 都是这种模型的代表.
当设置了边缘触发以后,以可读事件为例,对“有数据到来”这事件为触发。
select/poll/epoll 除了应用于 fd 外,像管道、文件也是可以的。
代码实现(C语言)
server端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>#define SERV_PORT 8888
#define OPEN_MAX 5000int main()
{int listenfd, connfd, sockfd, epfd;struct sockaddr_in serv_addr, clie_addr;socklen_t clie_addr_len;int ret, i, j, nready, nByte;char buf[BUFSIZ], str[BUFSIZ];struct epoll_event evt, ep[OPEN_MAX];listenfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listenfd){perror("socket error");exit(-1);}int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);ret = bind(listenfd, (struct sockaddr*)&serv_addr,sizeof(serv_addr));if (-1 == ret){perror("bind error");exit(-1);}ret = listen(listenfd, 128);if (-1 == ret){perror("listen error");exit(-1);}epfd = epoll_create(OPEN_MAX);if (-1 == epfd){perror("epoll_create error");exit(-1);}evt.events = EPOLLIN;evt.data.fd = listenfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &evt);if (-1 == ret){perror("epoll_ctl error");exit(-1);}while (1){nready = epoll_wait(epfd, ep, OPEN_MAX, -1);if (nready < 0){perror("select error");exit(-1);}for (i = 0; i < nready; ++i){if (!(ep[i].events & EPOLLIN)){continue;}if (ep[i].data.fd == listenfd)//如果是连接事件{clie_addr_len = sizeof(clie_addr);connfd = accept(listenfd, (struct sockaddr*)&clie_addr,&clie_addr_len);if (-1 == connfd){perror("accept error");exit(-1);}printf("receive from %s from port %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str,sizeof(str)),ntohs(clie_addr.sin_port));evt.events = EPOLLIN;evt.data.fd = connfd;epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &evt);}else //不是连接建立事件,而是读写事件(信息传递事件){sockfd = ep[i].data.fd;nByte = read(sockfd, buf, sizeof(buf));if (nByte == 0){ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);if (-1 == ret){perror("epoll_ctl error");}close(sockfd);printf("client[%d] closed connection\n", sockfd);}else if (nByte < 0){perror("epoll_ctl error");ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);if (-1 == ret){perror("epoll_ctl error");}close(sockfd);}else{for (j = 0; j < nByte; ++j){buf[j] = toupper(buf[j]);}write(sockfd, buf, nByte);write(STDOUT_FILENO, buf, nByte);}}}}close(listenfd);close(connfd);return 0;
}
client端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888int main()
{int cfd;struct sockaddr_in serv_addr;char buf[BUFSIZ];int nByte;cfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == cfd){perror("socket error");exit(-1);}memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));while (1){fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0write(cfd, buf, strlen(buf));nByte = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, nByte);}close(cfd);return 0;
}
相关文章:

C++ 之动手写 Reactor 服务器模型(一):网络编程基础复习总结
基础 IP 地址可以在网络环境中唯一标识一台主机。 端口号可以在主机中唯一标识一个进程。 所以在网络环境中唯一标识一个进程可以使用 IP 地址与端口号 Port 。 字节序 TCP/IP协议规定,网络数据流应采用大端字节序。 大端:低地址存高位,…...

qt 在vs2022 报错记录
1,qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed 需要把SSL 相关的库加入进去,如ssleay32.dll,libeay32.dll。 2,在一个文件中已定义,编译器在链接时,在多处报 已在.*…...
【人工智能】TensorFlow和机器学习概述
一、TensorFlow概述 TensorFlow是由Google Brain团队开发的开源机器学习库,用于各种复杂的数学计算,特别是在深度学习领域。以下是对TensorFlow的详细概述: 1. 核心概念 张量(Tensor):TensorFlow中的基本…...
SQLALchemy 的介绍
SQLALchemy 的介绍 基本概述主要特点使用场景安装与配置安装 SQLAlchemy配置 SQLAlchemy示例:使用 SQLite 数据库连接到其他数据库 结论 总结 SQLAlchemy是Python编程语言下的一款开源软件,它提供了SQL工具包及对象关系映射(ORM)工…...

Java虚拟机:运行时内存结构
大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 035 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进…...
微信小程序子组件调用父组件的方法
来源:通义千文2.5 步骤 1: 定义父组件中的方法 首先,在父组件中定义一个方法(如 handleClick),并准备一个用于接收子组件传来的数据的方法。 父组件(Parent.wxml) html<!-- parent.wxml …...

【数据结构】TreeMap和TreeSet
目录 前言TreeMap实现的接口内部类常用方法 TreeSet实现的接口常用方法 前言 Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。 一般把搜索的数据称为关键字(Key), 和关键字对应的称为…...

前端react集成OIDC
文章目录 OpenID Connect (OIDC)3种 授权模式 【服务端】express 集成OIDC【前端】react 集成OIDCoidc-client-js库 原生集成react-oidc-context 库非组件获取user信息 OAuth 2.0 协议主要用于资源授权。 OpenID Connect (OIDC) https://openid.net/specs/openid-connect-core…...

JavaWeb—XML_Tomcat10_HTTP
一、XML XML是EXtensible MarkupLanguage的缩写,翻译过来就是可扩展标记语言。所以很明显,XML和HTML一样都是标记语言,也就是说它们的基本语法都是标签。 可扩展:三个字表面上的意思是XML允许自定义格式。但这不代表你可以随便写; 在XML基…...
中介者模式在Java中的实现:设计模式精解
中介者模式在Java中的实现:设计模式精解 中介者模式(Mediator Pattern)是一种行为型设计模式,用于定义一个中介者对象,以封装一系列对象之间的交互,从而使对象之间的交互不再直接发生,减少了系…...

PyQt编程快速上手
Python GUI安装 GUI就是图形用户界面的意思,在Python中使用PyQt可以快速搭建自己的应用,使得自己的程序看上去更加高大上,学会GUI编程可以使得自己的软件有可视化的结果。 如果你想用Python快速制作界面,可以安装PyQt:…...
Docker Swarm管理
Docker Swarm管理 前置知识点 Docker Swarm 是 Docker 公司 2014年出品的基于 Docker 的集群管理调度工具,能够将多台主机构建成一个Docker集群,并结合Overlay网络实现容器调度的互访 用户可以只通过 Swarm API 来管理多个主机上的 Docker Swarm 群集包…...

Python | Leetcode Python题解之第335题路径交叉
题目: 题解: class Solution:def isSelfCrossing(self, distance: List[int]) -> bool:n len(distance)# 处理第 1 种情况i 0while i < n and (i < 2 or distance[i] > distance[i - 2]):i 1if i n:return False# 处理第 j 次移动的情况…...

Ubuntu视频工具
1. VLC VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影音光…...

HBase snapshot+replication 测试
一、背景 画像标签服务(CDP)是核心服务,被公司其他系统如现金、电商、风控等核心业务调用。异常的话,影响范围大。 二、目标 存量数据测试通过 snapshot 迁移。增量数据测试通过 replication 同步。 三、测试 方案二测试&#x…...

代码随想录算法训练营第四十一天|图论基础、深度优先搜索理论基础、98. 所有可达路径、797. 所有可能的路径
图论基础 图的种类:有向图 和 无向图,加权有向图, 加权无向图 无向图中有几条边连接该节点,该节点就有几度。 在有向图中,每个节点有出度和入度。出度:从该节点出发的边的个数。入度:指向该节…...

STM32学习笔记09-SPI通信
目录 SPI通信简介 硬件电路 移位示意图 SPI基本时序单元 SPI时序 W25Q64简介 硬件电路 W25Q64框图 Flash操作注意事项 SPI外设简介 SPI框图 SPI基本结构 主模式全双工连续传输 非连续传输 软件/硬件波形对比 SPI应用 软件SPI读写W25Q64 硬件SPI读写W25Q64 SP…...

树------二叉树
什么是树: 树是一种特殊的结构,由多个节点连接构成,并且不包含回路,也可以认为树是不包含回路的无向连通图,具体如下图所示。 当我们要确定一棵树的形态时,要指定一个根节点,没有父亲节点的节点…...
如何对加密后的数据进行模糊查询(面试题)
目录 前言1. 基本知识2. 国内做法 前言 这道题在面试比较常见,但是在算法逻辑层面中,直接对加密数据进行模糊查询是不可行的,因为加密算法会使数据变成不可读的形式 需要在加密过程中采取特殊的策略来支持模糊查询 以下只是结合网上现有的资…...
【MYSQL】当前读和快照读
前言 复习下隔离级别: 1、读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。 2、读提交:一个事务提交之后,它做的变更会被其他事务看到 3、可重复读:一个事务执行过程中看到的数据,…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...

中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
Web中间件--tomcat学习
Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机,它可以执行Java字节码。Java虚拟机是Java平台的一部分,Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...

解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...