4.1 网络基础之网络IO

一、编写基本服务程序流程

下面介绍一个最最简单的服务程序的编写流程,先按照顺序介绍各个函数的参数和使用。然后在第三节用一对简单的程序对客户端与服务端通信过程进行演示。下面所有代码均在linux平台实现,所以可能与windows上的编程有所区别,主要是相关头文件、编译方式上。

1、创建套接字

// 头文件
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);/*
* 参数domain:通讯协议族
* PF_INET       IPv4互联网协议族(常用)
* PF_INET6      IPv6互联网协议族
* PF_LOCAL      本地通信的协议族
* PF_PACKET     内核底层的协议族
* PF_IPX        IPX Novell协议族
* IPv6尚未普及,其它的不常用
*//*
* 参数type:数据传输的类型
* SOCK_STREAM    面向连接的socket   数据不会丢失、顺序不会错乱、双向通道
* SOCK_DGRAM     无连接的socket     数据可能会丢失、顺序可能会错乱、传输效率更高
*//*
* 参数protocol:最终使用的协议
* 在IPv4网络协议家族中:
* 数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP
* 数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP
* 本参数也可以填0,编译器自动识别
*//*
* socket返回值:
* 成功返回一个有效的socket,失败返回-1,errno被设置
*/

2、端口复用

// 这个步骤非必须
int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);

这里简单介绍下SO_REUSEADDR的使用场景:

1、主动断开连方的socket1处于TIME_WAIT状态时,新创建的socket2想要绑定相同于socket1的IP和端口,就要设置SO_REUSEADDR。

2、SO_REUSEADDR允许同一端口上启动同一服务器的多个实例(即多个进程)。但每个实例绑定的IP是不能相同的。有多块网卡或用IP Alias机器可以试试。

3、SO_REUSEADDR允许单个进程绑定相同端口到多个socket上,但每个socket绑定的IP不同。

4、SO_REUSEADDR允许完全相同的IP和端口重复绑定。但只用于UDP多播,不用于TCP。

3、设置IP和端口

// 头文件
#include <netdb.h>// 申请变量,用于存放协议、端口和IP地址
struct sockaddr_in servaddr;// 初始化
memset(&servaddr,0,sizeof(servaddr));// 设置协议族
servaddr.sin_family = AF_INET;// 设置IP,本机的所有IP都可用
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// 指定用于监听的端口
servaddr.sin_port = htons(m_port);

4、绑定IP和端口

// 失败返回-1,成功返回0
// 注意第二个参数要强制转换类型
int bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

5、开始监听客户端

// 参数s:监听的描述符listen_fd
// 参数backlog:已经完成连接正等待应用程序接收的套接字队列长度(linux中)
// 失败返回-1,成功返回0
int listen(int s, int backlog);

6、接受连接上的客户端

struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connet_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len);
// 这里需要注意,同样涉及类型强制转换
// 最后一个参数必须传指针
// 返回一个 用于处理客户端请求的 套接字描述符

7、收发数据完成业务逻辑

// 此处开启一个新的线程,来处理客户端的请求
#include <pthread.h>
pthread_t pid;
pthread_create(&pid, NULL, deal_request, (void*)(long)client_fd);/*
* 处理客户端请求的逻辑:
* 读取数据成功,先输出,再直接发送过去
* 读取数据失败,关闭套接字
*/
void *deal_request(void* arg){// 处理流程return (void*)0;
}

二、收发数据函数简介

1、发送数据

send

ssize_t send(int sock_fd, const void *buf, size_t len, int flags);
/*
* 参数:
* sock_fd:发送给对方的网络套接字
* buf:待发送的数据的起始地址
* len:要发送的数据大小,发送最多不超过len大小的字节
* flags:用于控制发送行为,默认传0
*/// 返回值是ssize_t,表示实际发送成功字节数。返回-1表示出错

write

ssize_t write(int sock_fd, const void *buf, size_t count);
/*
* 参数:
* sock_fd:发送给对方的网络套接字
* buf:待发送的数据的起始地址
* count:最大写入字节数
*/

补充

套接字为阻塞模式时,如果发送缓冲区无法容纳发送的数据,程序会阻塞在send和write方法。

send、write函数向套接字发送数据时,函数调用后不代表数据已经发送出去。网络协议栈有一个发送缓冲区,先将数据拷贝到发送缓冲区,然后网络协议栈将发送缓冲数据通过网卡驱动转为电信号给发送出去。

阻塞模式下,发送缓冲区空间不够,程序阻塞在send、write函数,直到发送缓冲区数据发送出去腾出空间,将剩下数据再拷贝到腾出的空间,直接到数据全部拷贝进发送缓冲区,函数返回。

2、接收数据

recv

ssize_t recv(int sock_fd, void *buf, size_t len, int flags);/*
* 参数:
* sock_fd:从哪个套接字接收数据
* buf:接收到的数据保存到以buf为起始的地址
* len:本次最多接收多少字节的数据
* flags:控制套接字接收行为,默认传0
*//*
* 返回值:
* -1:出错,可以在errno取到对应的错误信息
* 大于0:实际读取到的字节数
* 0(EOF):对端没有更多数据发送了,可能对端已经把连接关闭
*/

read

ssize_t read (int sock_fd, void *buf, size_t count);/*
* 参数:
* sock_fd:从哪个套接字接收数据
* buf:接收到的数据保存到以buf为起始的地址
* count:本次最多接收多少字节的数据
*//*
* 返回值:
* -1:出错,可以在errno取到对应的错误信息
* 大于0:实际读取到的字节数
* 0(EOF):对端没有更多数据发送了,可能对端已经把连接关闭
*/

补充

套接字阻塞模式下,如果调用read和recv函数时,套接字没有数据可读,程序会阻塞在read或者recv函数,直到有数据可读。

调用read、recv函数时,从接收缓冲区读数据。所以不能保证每次调用read、recv函数时一定能读出所有数据。因为,数据可能还在对端发送缓冲区,也可能还在各个中间设备(路由器、交换机、电信主干网)。

接收缓冲区中有个读指针和写指针,当调用read和recv函数时,读指针会往后移,下次读取就从新的指针处开始读,读指针移动的长度就是read和recv函数返回的实际读取到的字节长度。

三、客户端与服务端通信示例

1、server_base.c

编译为可执行文件,命令为:gcc -o server server.c -lpthread

运行时,执行命令:./server 5555。这个5555是给main的参数,端口。

#include <stdio.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <arpa/inet.h>/*
* 功能: 线程函数,处理客户端的请求
*       这里打印接收到的信息,打印后再返回给客户端
*/
void *deal_request(void *arg) {int fd = (int)(long)arg;	// linux中指针为long型char buffer[1024];while (1) {memset(buffer, 0, sizeof(buffer));int res = recv(fd, buffer, sizeof(buffer), 0);if (res == 0) {		// 客户端退出连接close(fd);printf("客户端[%d]退出\n", fd);break;} else {printf("接收到客户端[%d]的数据是: %s\n", fd, buffer);send(fd, buffer, sizeof(buffer), 0);}}return (void*)0;
}int main(int argc, char args*[]) {if (argc != 1 + 1) {printf("请给一个端口参数:\n");return -1;}// 1、创建套接字int listen_fd = socket(AF_INET, SOCK_STREAM, 0);// 2、设置端口复用int opt = 1;unsigned int len = sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);// 3、设置服务端的IP和端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// 本机任何IP皆可// server_addr.sin_addr.s_addr = inet_addr("192.168.237.10");	// 指定IPserver_addr.sin_port = htons(atoi(args[1]));// 4、将套接字和IP、端口绑定int ret;ret = bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));if (ret == -1) {printf("bind failed\n");close(listen_fd);return -1;}// 5、开始监听ret = listen(listen_fd, 5);if (ret == -1) {printf("listen failed\n");close(listen_fd);return -1;}// 服务器不间断的接受客户端请求while (1) {// 6、接受连接上的客户端struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);int connet_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len);if (connet_fd == -1) {printf("accept failed\n");close(listen_fd);return -1;}printf("客户端[%d]连接上\n", connet_fd);// 7、创建一个线程来处理当前客户端的请求pthread_t pid;pthread_create(&pid, NULL, deal_request, (void*)(long)connet_fd);}return 0;
}

2、client.c

对于客户端如果想不写代码,可以使用NetAssist网络调试助手。如下图,设置好网络协议、服务端IP和端口,点击连接,即可通信。在这之前,确保服务端开放了设定的端口号,或者直接关闭防火墙。

关闭防火墙,可写成close_fire.sh文件,如下,然后运行./close_fire.sh。

#!/bin/bash
systemctl stop firewalld.service

如果也想通过代码进行通信,client.c代码则如下:

#include <stdio.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <arpa/inet.h>int main(void) {// 1、创建套接字int client_fd = socket(AF_INET, SOCK_STREAM, 0);// 2、设置服务端的IP和端口unsigned short port = 5555;	// 指定服务端的通信端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr("192.168.237.10");	// 指定服务端的IPserver_addr.sin_port = htons(port);// 3、向服务端发起连接int ret;ret = connect(client_fd,(struct sockaddr *)&servaddr,sizeof(servaddr));if (ret == -1){ printf("connect failed\n");close(sockfd);return -1; }// 4、与服务端进行通讯char buffer[1024];for(int i = 0;i < 3;i ++) { // 此处假设与将与服务端进行三次通讯int iret;memset(buffer,0,sizeof(buffer));sprintf(buffer,"服务端你好,这是第%d次通信。",i + 1);  // 生成报文内容// 向服务端发送请求报文iret = send(client_fd, buffer, strlen(buffer), 0);if (iret == -1){printf("send failed\n");close(client_fd);return -1;}memset(buffer, 0, sizeof(buffer));// 接收服务端响应报文,如果服务端没有发送响应报文,recv()函数将阻塞等待ret = recv(client_fd, buffer, sizeof(buffer), 0);if (ret == 0) {printf("服务端已经关闭");close(client_fd);return -1;}printf("接收到服务端信息:%s\n", buffer);sleep(1);	// 休眠1秒}// 5、正常关闭socket,释放资源close(sockfd);return 0;
}

3、客户端服务端代码流程对比

四、一些注意点

1、分包粘包问题

分包,例如:对方发送helloworld,我方收到hello和world。

粘包,例如:对方发送hello和world,我方收到helloworld。

解决办法:TCP协议中,数据以字节流方式传输,被发送的数据可能不是一次性发完,可能是被拆成很多个小段,一段段发出去。有个重要前提,就是TCP协议可以保证数据的顺序性。因此,可以采用报文长度 + 报文内容的方法。也可以使用特殊分隔符,如http协议采用\r\n

2、阻塞与非阻塞

阻塞:阻塞I/O调用recv、read时,程序切换到内核态。若I/O中(套接字)没有数据可读,就阻塞。直到有数据可读,内核将数据复制到用户态,复制完再返回。最后recv、read函数再返回读取到的字节数。缺点是这种方式比较占CPU。

非阻塞:非阻塞模式下,没有读取到数据立即返回-1,为了区别阻塞模式下返回-1,可以查看错误码errno设置成了EAGAIN。缺点就是需要间隔一段时间就读一下数据涉及系统调用,还是比较消耗资源。设置代码如下:

// 头文件
#include <unistd.h>
#include <fcntl.h>fcntl(fd, F_SETFL, O_NONBLOCK);    // fd为想要设置的套接字描述符

五、几种IO复用模型

1、select

原理

select方法告诉内核程序自己关心哪些I/O描述符,当内核程序发现有I/O准备好(可写/可读/异常),内核程序将数据复制到用户态并从select返回。用户拿着准备好的IO再调用recv就一定能拿到数据。

用法和参数

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout
);/*
* 参数:
* maxfdp1:当前待监听描述符基数。若监听的描述符最大值是3,则maxfdp1就为4(因为描述符从0开始)
* fd_set:通常有读、写、异常三种情况,readset、writeset、exceptset分别对应I/O的读、写和异常。表示当前关心readset里描述符是否可读,writeset里描述符是否可写,exceptset中描述符是否有异常。
* timeout:struct timeval {long tv_sec;  // 秒long tv_usec; // 微秒}- 传NULL,如果没有I/O可以处理,一直等待;- 设置成对应的秒或微秒,等待相应时间后若没有I/O可以处理就返回;- 将tv_sec和tv_usec都设置成0,表示不用等待立即返回。
*//*
* select返回值:
* -1表示出错;
* 0表示超时;
* 大于0表示可操作的I/O数量。
*/

使用流程 (服务端)

// 头文件
#include <sys/select.h>// 1、创建监听的fd集合并初始化
fd_set readfds;    // 大小16字节,1024位
FD_ZERO(&readfds); // 每一位置0// 2、把listen的socket加入集合
FD_SET(listensock, &readfds);  // 起初还未有客户端加进来// 3、while循环中调用select
fd_set tmpfds = readfds;	// 复制一份fd集合,因为系统在判断时会更改送进去的集合参数
int infds = select(maxfd + 1, &tmpfds, NULL, NULL, 0);// 4、当select返回值大于0
// 用FD_ISSET判断每个socket是否有事件
FD_ISSET(eventfd, &tmpfds)/*
对于新连接进来的客户端加入到事件集合
FD_SET(clientsock, &readfds);
*//*
对于断开的客户端,将其清除
FD_CLR(eventfd, &readfds);
并将套接字关闭
close(eventfd); 
*/

触发方式

采用水平触发,如果报告fd事件没有被处理或数据没有被全部读取,下次select时会再次报告该fd事件。

缺点

1、select支持文件描述符数量太小,默认1024

2、每次调整select都需要把fdset从用户态拷贝到内核

3、在线的大量客户端同时有事件发生的可能性小,但还是需要遍历fdset,因此随着监视的描述符数量增长,效率也会线性下降。

完整服务程序server_select.c

该程序中只监听了可读的事件,并在liunx中运行,编译命令:gcc -o server server_select.c,运行命令:./server 5555。代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <string.h>int main(int argc, char *args[]) {if (argc != 1 + 1) {  // 需要传入参数端口printf("please give port!\n");return 0;}// 1、创建套接字int listen_fd = socket(AF_INET, SOCK_STREAM, 0);// 2、设置端口复用int opt = 1;unsigned int len = sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);// 3、设置服务端的IP和端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(atoi(args[1]));// 4、将套接字和IP、端口绑定bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));// 5、开始监听listen(listen_fd, 5);// 6、初始化fd集和fd_set rfd;FD_ZERO(&rfd);FD_SET(listen_fd, &rfd);int maxfd = listen_fd;while(1) {fd_set tmp_fd = rfd;// 7、调用selectint num = select(maxfd + 1, &tmp_fd, NULL, NULL, 0);if (num == -1) {printf("error select\n");close(listen_fd);break;} if (num == 0) {		// 没有事件,继续continue;}// 如果listen_fd有事件if (FD_ISSET(listen_fd, &tmp_fd)) {struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);// 8、接受连接上的客户端int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len);if (client_fd == -1) {printf("accept error\n");} else {printf("client %d was connected!\n", client_fd);FD_SET(client_fd, &rfd);if (client_fd > maxfd) maxfd = client_fd;	// 如果新的fd大于maxfd,则替换}}// 检查后面的fd是否有事件int fd = listen_fd + 1;for (;fd <= maxfd;fd ++) {if (FD_ISSET(fd, &tmp_fd) == 0) {  // 无事件continue;} else { // 9、处理已经连接上的客户端的请求char buffer[1024];memset(buffer, 0, sizeof(buffer));int res = recv(fd, buffer, sizeof(buffer), 0);if (res == 0) { // 对方断开连接close(fd);printf("client [%d] disconnected !\n", fd);FD_CLR(fd, &rfd);	// 从集和中清除} else {printf("recv data from [%d] is: %s\n", fd, buffer);send(fd, buffer, sizeof(buffer), 0);}}}}return 0;
}

2、poll

原理

与select本质上没有差别,管理多个描述符也进行轮询,根据描述符状态进行处理,但是poll没有最大文件描述符数量的限制。

用法和参数

int poll(struct pollfd *fdarr,unsigned long nfds,int timout
);/*
* 参数:
* fdarr:要监听的I/O描述符事件集合,其结构如下:struct pollfd {int fd; // 描述符short events;  // 监听描述符发生的事件short revents; // 已经发生的事件};
* nfds:要监听的套接字数量
* timeout:超时时间,一般有三种传值的方式- -1表示在有可用描述符之前一直等待;- 0表示不管有没有可用描述符都立即返回;- 大于0表示超过对应毫秒即使没有事件发生也会立即返回
*//*
* poll返回值:
* -1表示出错;
* 0表示超时;
* 大于0表示可操作的I/O数量。
*/

使用流程 (服务端)

// 1、声明struct pollfd类型数组fds
int maxfd = 2047;    // linux中,超过了2047有限制,需要设置内核参数
struct pollfd fds[maxfd + 1]; // 2、初始化fds所有位置为-1,表示忽略该元素,poll在查找事件时就不会遍历这个元素
for (int = 0; i <= maxfd; i++)fds[i].fd = -1;// 3、将服务端套接字放到fds第一个位置,并设置监听POLLRDNORM事件
fds[0].fd = listensock;
fds[0].events = POLLIN;  // 读事件// 4、循环里调用poll函数
int infds = poll(fds, maxfd + 1, 10); // 超时时间为10毫秒/*
* 5、当poll返回值大于0
* 先判断fds[i].fd是否为-1,若不为,再通过fds[eventfd].revents&POLLIN
* 判断某个描述符是否有读事件
*//*
* 如果是listensock
* 接受新的客户端连接,将新套接字
* 找个-1的空位置存放起来,并监听POLLRDNORM事件
*//*
* 如果是原先存在的客户端事件,则做相应的业务处理:
* 读取数据成功,继续相应的业务处理操作
* 读取失败,将该位置设置为-1,fds[i].fd = -1;
* 且关闭套接字,close(fds[i].fd)
*/

与select异同

相同点:poll和select都会遍历所有描述符,在连接数非常大时有性能问题,而epoll就很好的解决该个问题。

不同点:

1、select采用fd_set和bitmap,而poll采用数组;

2、在声明pollfd结构数据的时候,可以自行指定大小,linux超过1024需要设置内核参数;

3、select会修改fd_set,因此需要复制一份。poll不会修改pollfd,它通过pollfd的events指定要监听的事件,再通过revents保存已发生事件用于在poll返回时判断都有哪些事件发生。poll用两个short整型来保存监听事件,和已经发生的事件。意味着,调用poll之前不需要将监听的事件复制一份。I/O设置监听事件使用events,判断是否有事件发生使用revents。

缺点

与select类似,poll文件描述符数组被整体复制于用户态和内核态的地址空间之间,不论这些文件描述符是否有事件,开销随着文件描述符数量增加而线性增大。poll返回后,也需要历遍整个描述符数组才能得到有事件的描述符。

完整服务程序server_poll.c

编译:gcc -o server server_select.c

运行:./server 5555

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <string.h>int main(int argc, char *args[]) {if (argc != 1 + 1) {	// 接受端口参数printf("please give port!\n");return 0;}// 1、创建套接字int listen_fd = socket(AF_INET, SOCK_STREAM, 0);// 2、设置端口复用int opt = 1;unsigned int len = sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);// 3、设置服务端的IP和端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(atoi(args[1]));// 4、将套接字和IP、端口绑定bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));// 5、开始监听listen(listen_fd, 50);// 6、声明和初始化struct pollfd类型数组int maxfd = 1023;struct pollfd fds[maxfd + 1];int i = 0;for(;i < maxfd + 1;i ++) {fds[i].fd = -1;}// 7、将listen_fd放入首位置并监听读事件fds[0].fd = listen_fd;fds[0].events = POLLIN;while (1) {int num = poll(fds, maxfd + 1, 50);	// 等待50msif (num == -1) {	// 出错printf("poll error\n");close(listen_fd);break;} else if (num == 0) {	// 没有事件continue;} else {// 先判断是否有新的客户端连接进来if(fds[0].revents & POLLIN) {struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);int connet_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len);printf("client [%d] is connected!\n", connet_fd);int i = 1;for(;i < maxfd + 1;i ++) {		// 找个空位置把新的客户端监听起来if (fds[i].fd == -1) {fds[i].fd = connet_fd;fds[i].events = POLLIN;break;}}}// 判断其他的df是否有事件int i = 1;for(;i < maxfd + 1;i ++) {if(fds[i].fd == -1) {continue;}if(fds[i].revents & POLLIN) {char buffer[1024];memset(buffer, 0, sizeof(buffer));int res = recv(fds[i].fd, buffer, sizeof(buffer), 0);if(res == 0) {close(fds[i].fd);printf("client [%d] is disconnected!\n", fds[i].fd);fds[i].fd = -1;} else {printf("recv data from client [%d] is %s\n", fds[i].fd, buffer);send(fds[i].fd, buffer, sizeof(buffer), 0);}}}}}return 0;
}

3、epoll

原理

epoll原理比select、poll复杂得多,后面单独用一篇文章介绍。

用法参数及使用流程

// 1、创建epoll实例
int epoll_create(int size);
int epoll_create1(int flags);
// 参数:一般传0即可
// 返回值:大于0表示epoll实例,-1表示出错// 2、注册要监听的fd和事件
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event
);
/*
* 参数:
* epfd:使用poll_create创建出的epoll实例
* op:表示增、删、改分别对应:EPOLL_CTL_ADD:向epoll实例注册文件描述符对应的事件;EPOLL_CTL_DEL:删除epoll实例中文件描述符对应的事件;EPOLL_CTL_MOD:修改epoll实例中文件描述符对应的事件。
* fd:要注册事件的描述符,这里指网络套接字。
* event:是一个结构体,如下:struct epoll_event {uint32_t events;   // epoll事件epoll_data_t data;};- typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;} epoll_data_t;对于events一般设置如下:这里的事件与poll的基本一样下面是在使用epoll的时候,常用的事件类型:EPOLLIN:表示描述符可读EPOLLOUT:表示描述符可写EPOLLRDHUP:表示描述符一端已经关闭或者半关闭EPOLLHUP:表示对应描述符被挂起EPOLLET:边缘触发模式edge-triggered,不设置默认使用
*//*
* epoll_ctl返回值:
* 0表示成功
* -1表示出错
*/// 3、等待事件发生
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout
);
/*
* 参数:
* epfd:使用poll_create创建出的epoll实例
* events:要处理的I/O事件,是个数组,大小是epoll_wait的返回值,每一个元素是一个待处理的I/O事件。
* maxevents:epoll_wait可以返回的最大事件
* timeout:超时时间,和select基本是一致的。- 如果设置-1表示不超时;- 设置0表示立即返回;
*//*
* epoll_wait返回值:大于0表示事件个数;0表示超时;-1表示出错。
*/

与poll区别

1、epoll需要使用poll_create创建一个实例,后续所的操作都基于这个实例。

2、epoll不再是将fd设置成-1来表示忽略当前描述,而是关心哪个就设置哪个,使用epoll_ctl函数。

// 例如:
struct epoll_event event;
event.data.fd = sock_fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &event)

3、events返回所有实际产生事件集合,大小就是epoll_wait返回值。所以,epoll_wait返回,就可以确定从0到read_num所有位置都是有事件发生。而poll每次都从0遍历到最大描述字。这中间有很多没有事件发生的描述符。这种实现绕不开它背后的数据结构红黑树。

触发方式

边沿触发:只在第一次有数据可读的情况下通知一次。后面的处理就完全靠自己了,很显然这种触发方式能够明显减少触发次数,从而减轻内核的压力,这在一些大数据量的传输场景下非常有用。

水平|条件触发(epoll默认):每次有数据可读时都会触发事件,某些情况下会造成内核频发触发事件。

完整服务程序server_epoll.c

编译:gcc -o server server_epoll.c

运行:./server 5555

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>int main(int argc, char *args[]) {if (argc != 1 + 1) {printf("please give port!\n");return 0;}// 1、创建套接字int listen_fd = socket(AF_INET, SOCK_STREAM, 0);// 2、设置端口复用int opt = 1;unsigned int len = sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);// 3、设置服务端的IP和端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(atoi(args[1]));// 4、将套接字和IP、端口绑定bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));// 5、开始监听listen(listen_fd, 50);// 6、创建epoll实例int epfd = epoll_create1(0);// 7、注册listen_fd和事件struct epoll_event event;event.data.fd = listen_fd;event.events = EPOLLIN | EPOLLET;  // 可读事件 | 边缘触发epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event);// 8、申请epoll_event数组int maxfd = 1024;struct epoll_event events[maxfd];while (1) {// 9、循环中调用epoll_wait来等待事件发生int num = epoll_wait(epfd, events, maxfd, 20);int i = 0;for (;i < num;i ++) {if (listen_fd == events[i].data.fd) {  // listen的socket有事件struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len);if (conn_fd == -1) {perror("accept error.");continue;} else {printf("client [%d] is connected !\n", conn_fd);struct epoll_event event_tmp;event_tmp.data.fd = conn_fd;event_tmp.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &event_tmp);}} else {	// 已经连接的客户端socket有事件int client_fd = events[i].data.fd;char buffer[1024];memset(buffer, 0, sizeof(buffer));int res = recv(client_fd, buffer, sizeof(buffer), 0); if (res == 0) {close(client_fd);printf("client [%d] is disconnected !\n", client_fd);} else {printf("recv data from client [%d] is %s\n", client_fd, buffer);send(client_fd, buffer, sizeof(buffer), 0);}}}}return 0;
}

六、几种IO模型对比

1、一请求一线程

对应上面的完整代码server_base.c

特点:在一个while循环中,一直"accept"客户端的连接。来一个客户端,就为其分配一个线程,去处理请求。 每个线程里面,也是while循环不断处理每个客户端的请求。

优点:逻辑简单。

缺点:不适合大量的客户端请求,无法突破C10K。(client 10 k量级的连接)

2、select模型

特点:maxfd有最大限制,1024。通过多设置几个select,相比方法1,能突破C10K,但是难以突破C1000K,因为每次调用fd_set需要copy进内核,然后返回再copy出来,涉及系统调用,当大量copy时,还是有限制的。

3、poll模型

特点:与select差不多,都是采用轮询,比较消耗资源,只不过maxfd没有了1024的限制。如果超过1024可能需要设置一下内核参数。

4、epoll模式

特点:不再是将fd设置成-1来表示忽略当前描述f符,而是关心哪个就设置哪个。events返回所有实际产生事件集合,大小就是epoll_wait返回值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/170779.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

app测试和web测试有什么区别

1.性能方面: web页面可能更关注响应时间&#xff0c;而app更关注流量、电量、QPS。 2.系统架构方面&#xff1a; web项目&#xff0c;一般都是b/s架构&#xff0c;基于浏览器的&#xff0c;而app则是c/s的&#xff0c;必须要有客户端。在系统测试的时候就会产生区别了。首从…

虹科直播 | CDS网络与数据安全专题技术直播重磅来袭,11.2起与您精彩相约

文章来源&#xff1a;虹科网络安全 阅读原文&#xff1a;https://mp.weixin.qq.com/s/T-CgU28hmYy4YV5SV9QGhg 虹科数据加密解决方案 虹科终端安全防护方案 虹科是在各细分专业技术领域内的资源整合及技术服务落地供应商&#xff0c;虹科网络安全事业部的宗旨是&#xff1a;让…

mac电脑怎么永久性彻底删除文件?

Mac老用户都知道在我们查看Mac内存时都会发现有一条“其他文件”占比非常高&#xff0c;它是Mac储存空间中的“其他”数据包含不可移除的移动资源&#xff0c;如&#xff0c;Siri 语音、字体、词典、钥匙串和 CloudKit 数据库、系统无法删除缓存的文件等。这些“其他文件”无用…

红米电脑硬盘剪切

Redmi R14 2023版固态硬盘剪切 工具准备操作结尾语 首先要说明&#xff0c;本文所说的操作不一定适合你的电脑&#xff0c;因为电子产品更新换代过快&#xff0c;你的硬盘不一定能剪切&#xff0c;在操作前一定要仔细观察硬盘的型号&#xff0c;是否为同款&#xff0c;我上了图…

如何保障单病种上报的填报效率、质量监控及数据安全

在国家平台对单病种病例进行手工直报&#xff0c;是大多数医院最初获知《关于进一步加强单病种质量管理与控制工作的通知》后的首选方式。随着医院对上报流程与内容的逐步熟练&#xff0c;质控管理的需求开始凸显并占据主要地位&#xff0c;同时为了能更好地适应国家平台的频繁…

测试用例的设计方法(全):等价类划分方法

一.方法简介 1.定义 是把所有可能的输入数据,即程序的输入域划分成若干部分&#xff08;子集&#xff09;,然后从每一个子集中选取少数具有代表性的数据作为测试用例。该方法是一种重要的,常用的黑盒测试用例设计方法。 2.划分等价类&#xff1a; 等价类是指某个输入域的…

【PG】PostgreSQL客户端认证pg_hba.conf文件

目录 文件格式 连接类型(TYPE) 数据库&#xff08;database&#xff09; 用户(user) 连接地址&#xff08;address&#xff09; 格式 IPv4 IPv6 字符 主机名 主机名后缀 IP-address/IP-mask auth-method trust reject scram-sha-256 md5 password gss sspi …

正点原子嵌入式linux驱动开发——外置RTC芯片PCF8563

上一章学习了STM32MP1内置RTC外设&#xff0c;了解了Linux系统下RTC驱动框架。一般的应用场合使用SOC内置的RTC就可以了&#xff0c;而且成本也低&#xff0c;但是在一些对于时间精度要求比较高的场合&#xff0c;SOC内置的RTC就不适用了。这个时候需要根据自己的应用要求选择合…

VR全景拍摄市场需求有多大?适用于哪些行业?

随着VR全景技术的成熟&#xff0c;越来越多的商家开始借助VR全景来宣传推广自己的店铺&#xff0c;特别是5G时代的到来&#xff0c;VR全景逐渐被应用在我们的日常生活中的各个方面&#xff0c;VR全景拍摄的市场需求也正在逐步加大。 通过VR全景技术将线下商家的实景“搬到线上”…

HTML5语义化标签 header 的详解

&#x1f31f;&#x1f31f;&#x1f31f; 专栏详解 &#x1f389; &#x1f389; &#x1f389; 欢迎来到前端开发之旅专栏&#xff01; 不管你是完全小白&#xff0c;还是有一点经验的开发者&#xff0c;在这里你会了解到最简单易懂的语言&#xff0c;与你分享有关前端技术和…

SILKYPIX Developer Studio Pro 11E for Mac: 掌握数码照片处理的黄金标准

在当今的数字时代&#xff0c;照片处理已经成为我们日常生活的一部分。无论是社交媒体分享&#xff0c;还是个人相册制作&#xff0c;我们总是希望我们的照片能够展现出最佳的效果。然而&#xff0c;这并非易事。幸运的是&#xff0c;SILKYPIX Developer Studio Pro 11E for Ma…

redis缓存击穿 穿透

我们之前写了一把分布式锁 并且用redis写的, redis内部实现是比较完善的&#xff0c;但是我们公司用的时候 redis 至少都是主从&#xff0c;哨兵,cluster 很少有单机的 呢么我们分布式锁基于集群问题下会有什么问题 比如说当第一个线程设置一个key过来进行加锁&#xff0c;加锁…

云计算模式的区域LIS系统源码,基于ASP.NET+JQuery、EasyUI+MVC技术架构开发

云计算模式的区域LIS系统源码 云LIS系统源码&#xff0c;自主版权 LIS系统是专为医院检验科的仪器设备能与计算机连接。可通过LIS系统向仪器发送指令&#xff0c;让仪器自动操作和接收仪器数据。并快速的将检验仪器中的数据导入到医生工作站中进行管理&#xff0c;且可将检验结…

CI/CD:GitLab-CI 自动化集成/部署 JAVA微服务的应用合集

CI/CD&#xff1a;GitLab-CI 自动化集成/部署 JAVA微服务的应用合集 CI/CD&#xff1a;GitLab-CI 自动化集成/部署 JAVA微服务的应用合集安装DockerGitLabGitLab-Runner阿里云容器仓库 GitLab-CIJava微服务的GitLab-CI应用 其他问题Maven本地仓库缓存 CI/CD&#xff1a;GitLab-…

贪心算法学习——最大数

目录 ​编辑 一&#xff0c;题目 二&#xff0c;题目接口 三&#xff0c;解题思路级代码 一&#xff0c;题目 给定一组非负整数 nums&#xff0c;重新排列每个数的顺序&#xff08;每个数不可拆分&#xff09;使之组成一个最大的整数。 注意&#xff1a;输出结果可能非常大…

虹科分享 | 买车无忧?AR带来全新体验!

文章来源&#xff1a;虹科数字化与AR 阅读原文&#xff1a;https://mp.weixin.qq.com/s/XsUFCTsiI4bkEMBHcGUT7w 新能源汽车的蓬勃发展&#xff0c;推动着汽车行业加速进行数字化变革。据数据显示&#xff0c;全球新能源汽车销售额持续上升&#xff0c;预计到2025年&#xff0…

港联证券:2万元股票一进一出手续费?

股市生意中的手续费是出资者无法避免的一项费用。关于许多出资者来说&#xff0c;手续费的多少对出资收益有着重要的影响。本文将从多个视点分析2万元股票一进一出手续费&#xff0c;并讨论其对出资者和商场的影响。 首先&#xff0c;从出资者的视点来看&#xff0c;2万元股票…

计算机网络第3章-运输层(2)

可靠数据传输原理 可靠数据传输依靠数据在一条可靠信道上进行传输。 TCP也正是依靠可靠信道进行传数据&#xff0c;从而数据不会被丢失。 而实现这种可靠数据传输服务是可靠数据传输协议的责任 构造可靠数据传输协议 1.经完全可靠信道的可靠数据传输&#xff1a;rdt1.0 在…

眨个眼就学会了PixiJS

本文简介 带尬猴&#xff0c;我是德育处主任 当今的Web开发中&#xff0c;图形和动画已经成为了吸引用户注意力的重要手段之一。而 Pixi.js 作为一款高效、易用的2D渲染引擎&#xff0c;已经成为了许多开发者的首选&#xff08;我吹的&#xff09;。本文将为工友们介绍PixiJS的…

【uniapp】小程序开发7:自定义组件、自动注册组件

一、自定义轮播图组件、自动注册 以首页轮播图组件为例。 1、创建组件文件src/components/my-swipper.vue 代码如下&#xff1a; <template><view><view class"uni-margin-wrap"><swiper class"swiper" circular :indicator-dots…