
本文共 19634 字,大约阅读时间需要 65 分钟。
广播和组播只在UDP里面有
广播——只适用于局域网
服务端通过广播地址向固定端口发送数据,在局域网内的每个客户端都会收到信息。
注意:即使客户端不绑定9898端口,也能收到服务端发来的信息,只不过没有写接收数据的客户端程序,所以获得的数据就被当成垃圾数据处理掉了。除非你把网线拔了,不然都会收到数据。
广播地址
xxx.xxx.xxx.255
比如当前网段是123网段。我们设置123网段之内的机器就能收到信息,则设置为
xxx.xxx.123.255
255是广播地址,没人会拿来做私人地址
补充——什么是网关
xxx.xxx.xxx.1 是网关(网关用于不同网段的人之间进行数据交换。比如123网段的信息要从xxx.xxx.123.1这个网关出去,再从xxx.xxx.122.1这个网关进去,然后再去寻找进行通信的客户端的ip地址)
UDP数据报的大小是有上限的,文件太大不分开发,则用于发送数据的函数会调用失败
广播的写代码的流程
服务端——想广播的话,需要设置广播权限
1、创建套接字 - socket
2、fd绑定服务器IP和端口
3、初始化客户端IP和端口信息
struct sockaddr_in cli; cli.sin_family = af_inet; cli.port = htons(9898); //指定端口,本地机器测试的时候server端和客户端绑定的端口不能一样inet_pton(af_inet, "xxx.xxx.123.255", &cli.adr); //将点分十进制转化成大端整型
4、发送数据
sendto(fd, buf, len, 0, cli) //第五个参数是客户端的ip和端口 //第三个参数是要发送的数据,第四个参数是数据的长度
5、设置广播权限(端口复用的时候,用的就是这个函数)
setsockopt();
客户端
1、创建套接字
2、显示绑定IP和端口
bind();
3、接收数据 - server数据
recvform();
广播的实现代码
服务端
#include#include #include #include #include #include #include int main(int argc, const char* argv[]){ // 创建套接字,UDP通信,第二个参数一定要写SOCK_DGRAM int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd == -1){ perror("socket error"); exit(1); } // 绑定server的iP和端口 struct sockaddr_in serv; memset(&serv, 0, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(8787); // server端口 serv.sin_addr.s_addr = htonl(INADDR_ANY); //ip int ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv)); if(ret == -1){ perror("bind error"); exit(1); } // 初始化客户端地址信息 struct sockaddr_in client; memset(&client, 0, sizeof(client)); client.sin_family = AF_INET; client.sin_port = htons(6767); // 客户端要绑定的端口 // 使用广播地址给客户端发数据 inet_pton将点分十进制转成大端 inet_pton(AF_INET, "192.168.123.255", &client.sin_addr.s_addr); // 给服务器开放广播权限 int flag = 1; setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag)); // 通信 while(1){ // 一直给客户端发数据 static int num = 0; char buf[1024] = {0}; sprintf(buf, "hello, udp == %d\n", num++); //字符串拼接 //发送数据 int ret = sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client)); if(ret == -1) { perror("sendto error"); break; } printf("server == send buf: %s\n", buf); sleep(1); } close(fd); return 0;}
客户端
#include#include #include #include #include #include #include int main(int argc, const char* argv[]){ int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd == -1){ perror("socket error"); exit(1); } // 绑定iP和端口 struct sockaddr_in client; memset(&client, 0, sizeof(client)); client.sin_family = AF_INET; //地址族协议 client.sin_port = htons(6767); //端口 //不知道ip,所以用"0.0.0.0"来适配当前机器的ip inet_pton(AF_INET, "0.0.0.0", &client.sin_addr.s_addr); int ret = bind(fd, (struct sockaddr*)&client, sizeof(client)); if(ret == -1){ perror("bind error"); exit(1); } // 接收数据 while(1){ char buf[1024] = {0}; //UDP int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); if(len == -1){ perror("recvfrom error"); break; } printf("client == recv buf: %s\n", buf); } close(fd); return 0;}
组播——适用于局域网和广域网
组播使用组播地址,发送到客户端对应的端口上。
不管你的ip地址是多少,只要你加入了我的组,就能收到数据。
server需要指定组播权限。
组播地址
组播地址有四个网段(用的时候查一下就行)
- 224.0.0.0~224.0.0.255——预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
- 224.0.1.0~224.0.1.255——公用组播地址,可以用于Internet;欲使用需申请(要花钱)。
- 224.0.2.0~238.255.255.255——用户可用的组播地址(临时组地址),全网范围内有效;
- 239.0.0.0~239.255.255.255——本地管理组播地址,仅在特定的本地范围内有效。
组播代码实现
组播需要用到的结构体
struct ip_mreqn{ // 组播组的IP地址. struct in_addr imr_multiaddr; // 本地某一网络设备接口的IP地址。 struct in_addr imr_interface; int imr_ifindex; // 网卡编号};struct in_addr { in_addr_t s_addr;};
服务端
#include#include #include #include #include #include #include #include int main(int argc, const char* argv[]){ // 创建套接字 SOCK_DGRAM int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd == -1){ perror("socket error"); exit(1); } // 绑定server的iP和端口 struct sockaddr_in serv; memset(&serv, 0, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(8787); // server端口 serv.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv)); if(ret == -1){ perror("bind error"); exit(1); } // 初始化客户端地址信息 struct sockaddr_in client; memset(&client, 0, sizeof(client)); client.sin_family = AF_INET; client.sin_port = htons(6767); // 客户端要绑定的端口 // 使用组播地址给客户端发数据 点分十进制转成大端的int类型的值 inet_pton(AF_INET, "239.0.0.10", &client.sin_addr.s_addr); // 给服务器开放组播权限 struct ip_mreqn flag; // init flag 点分十进制转成大端的int类型的值 inet_pton(AF_INET, "239.0.0.10", &flag.imr_multiaddr.s_addr); // 组播地址 inet_pton(AF_INET, "0.0.0.0", &flag.imr_address.s_addr); // 本地IP //if_nametoindex函数可以通过网卡的名字找到网卡的编号,该函数协议头文件 #include flag.imr_ifindex = if_nametoindex("ens33"); //网卡编号,通过ifconfig命令可以查到网卡的名字 //IP_MULTICAST_IF 指定外出接口 setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &flag, sizeof(flag)); // 通信 while(1){ // 一直给客户端发数据 static int num = 0; char buf[1024] = {0}; sprintf(buf, "hello, udp == %d\n", num++); int ret = sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client)); if(ret == -1){ perror("sendto error"); break; } printf("server == send buf: %s\n", buf); sleep(1); } close(fd); return 0;}
客户端
#include#include #include #include #include #include #include #include int main(int argc, const char* argv[]){ int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd == -1){ perror("socket error"); exit(1); } // 绑定iP和端口 struct sockaddr_in client; memset(&client, 0, sizeof(client)); client.sin_family = AF_INET; client.sin_port = htons(6767); inet_pton(AF_INET, "0.0.0.0", &client.sin_addr.s_addr); int ret = bind(fd, (struct sockaddr*)&client, sizeof(client)); if(ret == -1){ perror("bind error"); exit(1); } // 加入到组播地址 struct ip_mreqn fl; inet_pton(AF_INET, "239.0.0.10", &fl.imr_multiaddr.s_addr); //组播地址 inet_pton(AF_INET, "0.0.0.0", &fl.imr_address.s_addr); //本地IP地址 fl.imr_ifindex = if_nametoindex("ens33"); //通过网卡名来获取 网卡编号 //IP_ADD_MEMBERSHIP 加入到多播组 setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &fl, sizeof(fl)); // 接收数据 while(1){ char buf[1024] = {0}; int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); if(len == -1) { perror("recvfrom error"); break; } printf("client == recv buf: %s\n", buf); } close(fd); return 0;}
本地套接字通信
管道文件(前头是p,类比于目录文件的前头是d。管道的实质就是一块内核缓冲区)和套接字文件(前头是s)都是伪文件(即无论你怎么操作他,他的大小永远都是0,因为伪文件存储在内核的一块缓冲区中了)
补充:使用有名管道进程没有血缘关系的进程间通信
本地套接字实现没有血缘关系的进程间通信有两种方案——tcp和udp
本地套接字实现没有血缘关系的进程间通信的基本流程(TCP为例)
我们需要用到结构体 sockaddr_un
#include#define UNIX_PATH_MAX 108struct sockaddr_un { __kernel_sa_family_t sun_family; char sun_path[UNIX_PATH_MAX]; //套接字的文件路径};
服务端
1、创建套接字,得到用于监听的文件描述符
int lfd = socket(AF_LOCAL, sock_stream, 0);
2、绑定
struct sockaddr_un serv;serv.sun_family = af_local;strcpy(serv.sun_path, "server.socket"); //把路径赋值给sun_path //其实server.socket是一个档期不存在的文件bind(lfd, (struct sockaddr8)&serv, len); //绑定成功之后,server.socket才会被创建出来
3、设置监听
listen();
4、等待接收连接请求
struct sockaddr_un client;int len = sizeof(client);int cfd = accept(ldf, &client, &len);
5、通信
sendrecv
6、断开连接
close(cfd);close(lfd);
客户端
1、创建套接字
int fd = socket(af_local,sock_stream, 0);
2、绑定一个套接字文件
struct sockaddr_un client;client.sun_family = af_local;strcpy(client.sun_path, "client.socket"); //client.socket现在还不存在bind(fd, (struct sockaddr*)&client, len); //绑定成功后,client.socket才被创建出来套接
3、连接服务器
struct sockaddr_un serv;serv.sun_family = af_local;strcpy(serv.sun_path, "server.socket"); - -现在还不存在connect(fd, &serv, sizeof(server));
4、通信
recvsend
5、关闭
close(fd);
本地套接字进程通信框架
server和client分别绑定一个套接字文件,套接字文件分别对应一块内核缓冲区,内核中数据如何交互不需要我们去研究。
不同机器之间实现tcp通信操作的是一个文件描述符,这个文件描述符对应一个内核缓冲区。本地套接字通信操作的是一个套接字文件,这个套接字文件也是对应了一块内核缓冲区。
本地进程间通信的代码实现
服务端
#include#include #include #include #include #include #include #include int main(int argc, const char* argv[]){ //TCP 使用SOCK_STREAM int lfd = socket(AF_LOCAL, SOCK_STREAM, 0); if(lfd == -1){ perror("socket error"); exit(1); } // 如果套接字文件存在, 删除套接字文件,不写的话,再次启动程序时会显示already in use unlink("server.sock"); // 绑定 struct sockaddr_un serv; serv.sun_family = AF_LOCAL; strcpy(serv.sun_path, "server.sock"); int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv)); if(ret == -1) { perror("bind error"); exit(1); } // 监听 ret = listen(lfd, 36); if(ret == -1){ perror("listen error"); exit(1); } // 等待接收连接请求 struct sockaddr_un client; socklen_t len = sizeof(client); int cfd = accept(lfd, (struct sockaddr*)&client, &len); if(cfd == -1){ perror("accept error"); exit(1); } printf("======client bind file: %s\n", client.sun_path); // 通信 while(1){ char buf[1024] = {0}; int recvlen = recv(cfd, buf, sizeof(buf), 0); if(recvlen == -1) { perror("recv error"); exit(1); } else if(recvlen == 0) { printf("clietn disconnect ....\n"); close(cfd); break; } else { printf("recv buf: %s\n", buf); send(cfd, buf, recvlen, 0); } } close(cfd); close(lfd); return 0;}
客户端
#include#include #include #include #include #include #include #include int main(int argc, const char* argv[]){ int fd = socket(AF_LOCAL, SOCK_STREAM, 0); if(fd == -1){ perror("socket error"); exit(1); } //如果套接字文件存在, 删除套接字文件,不写的话,再次启动程序时会显示already in use unlink("client.sock"); // 给客户端绑定一个套接字文件 struct sockaddr_un client; client.sun_family = AF_LOCAL; //地址族协议 strcpy(client.sun_path, "client.sock"); //客户端套接字文件 int ret = bind(fd, (struct sockaddr*)&client, sizeof(client)); if(ret == -1){ perror("bind error"); exit(1); } // 初始化server信息 struct sockaddr_un serv; serv.sun_family = AF_LOCAL; //地址族协议 strcpy(serv.sun_path, "server.sock"); //服务端套接字文件 // 连接服务器,server的地址 connect(fd, (struct sockaddr*)&serv, sizeof(serv)); // 通信 while(1){ char buf[1024] = {0}; //从终端获取数据 fgets(buf, sizeof(buf), stdin); //发送数据 send(fd, buf, strlen(buf)+1, 0); // 接收数据 recv(fd, buf, sizeof(buf), 0); printf("recv buf: %s\n", buf); } close(fd); return 0;}
心跳包——判断客户端和服务器是否处于连接状态
心跳机制
- 不会携带大量的数据
- 每隔一定时间 服务器->客户端 客户端->服务器 发送一个数据包
心跳包看成一个协议
- 应用层协议
判断网络是否断开
- 有多个连续的心跳包没收到回复时,说明断开连接
- 关闭通信的套接字
重连
- 重新初始套接字
- 继续发送心跳包
--乒乓包
- 比心跳包携带的数据多一些
- 除了知道连接是否存在, 还能获取一些信息
epoll反应堆模型
- 在往epoll树上挂节点时,需要准备一个epoll_event类型的结构体
- 使用myevent_s记录要挂在树上的节点的信息
- 使用epoll_data的ptr指针指向myevent_s的结构体变量
struct epoll_event{ unit32_t events; //文件描述符对应的事件 epoll_data_t data;};typedef union epoll_data{ void *ptr; //可以指向任意一块内存 int fd; unit32_t u32; unit64_t u64;}epoll_data_t;/* 描述就绪文件描述符相关信息 epoll反应堆的结构体模型 */ struct myevent_s { int fd; //要监听的文件描述符 int events; //对应的监听事件 void *arg; //泛型参数结 void (*call_back)(int fd, int events, void *arg); //回调函数 int status; //是否在监听 char buf[BUFLEN]; int len; long last_active; //记录每次加入红黑树 g_efd 的时间};
epoll反应堆工作流程
epoll的读流程
- 在server上 创建树的根节点
- 在树上添加需要监听的节点 读事件
- 读事件有返回
- 通信(此时如果写缓冲区满了,则数据就写不进去了,则数据发送不出去)
- 挂到树上之后通过epoll_wait去检测
epoll反应堆模型的工作流程
- 在server上 创建树的根节点
- 在树上添加需要监听的节点读事件
- 读事件有返回
- 通信(通信过程:将这个fd从树上删除 -> 监听写事件 (即判断写缓冲区是否可写)-> 写操作(此时可以给对方发送数据) -> fd从树上摘下来 -> 继续监听fd的读事件(即判断读缓冲区是否有内容))
- 挂到树上之后通过epoll_wait去检测
EPLLOUT被触发的时机
设置 ev.events = EPLLOUT;时,epoll模型是如何工作的?
在水平模式下,若设置 ev.events = EPLLOUT; ,则他会一直返回,不管你写没写东西,因为写事件并不需要epoll_wait来通知我我写了东西,自己写了东西自己是知道的。EPLLOUT的真正作用是检测你的写缓冲区是否可以写。 EPLLOUT会一直检测写缓冲区是否可写,如果可写,他会一直通知你还能写,如果写缓冲区满了, EPLLOUT就不会返回了。
在边沿模式下, 若设置 ev.events = EPLLOUT;,在开始工作时会有返回值,因为刚开始启动epoll模型时,写缓冲区是空,则会有返回值。之后直到写缓冲区被写满,都不会有返回值,直到写满的缓冲区又出现了可写空间时才会发生返回,而且只返回一次。
epoll反应堆完整代码(建议粘贴下来放在visual studio里面看,这里)
/* * epoll基于非阻塞I/O事件驱动 */#include#include #include #include #include #include #include #include #include #include //监听上限数#define MAX_EVENTS 1024 #define BUFLEN 4096#define SERV_PORT 8080void recvdata(int fd, int events, void *arg);void senddata(int fd, int events, void *arg);/* 描述就绪文件描述符相关信息 epoll反应堆的结构体模型 */struct myevent_s { //要监听的文件描述符 int fd; //对应的监听事件 int events; //泛型参数,arg指向了myevent_s结构体的首地址。 //因为存数据的时候存储在myevent_s结构体的buf中,所以需要通过arg指针拿到buf void *arg; //回调函数/ void(*call_back)(int fd, int events, void *arg); //是否在监听:1->在红黑树上(监听), 0->不在(不监听) int status; char buf[BUFLEN]; int len; //记录每次加入红黑树 g_efd 的时间值(客户端1分钟不工作,则删除客户端) long last_active; };//全局变量, 保存epoll_create返回的文件描述符int g_efd; //自定义结构体类型数组. +1-->listen fdstruct myevent_s g_events[MAX_EVENTS + 1]; /*将结构体 myevent_s 成员变量 初始化结构体数组*///eventset(&g_events[i], cfd, recvdata, &g_events[i]); void eventset(struct myevent_s *ev, int fd, void(*call_back)(int, int, void *), void *arg) { ev->fd = fd; ev->call_back = call_back; ev->events = 0; ev->arg = arg; ev->status = 0; //memset(ev->buf, 0, sizeof(ev->buf)); //ev->len = 0; //默认情况下,结构体不在树上 ev->last_active = time(NULL); //调用eventset函数的时间 return;}/* 向 epoll监听的红黑树 添加一个 文件描述符 *///eventadd(g_efd, EPOLLIN, &g_events[i]); void eventadd(int efd, int events, struct myevent_s *ev) { struct epoll_event epv = { 0, {0} }; int op; //ev是eventadd(g_efd, EPOLLIN, &g_events[i]); 中的g_events[i],被初始化好的结构体变量 epv.data.ptr = ev; epv.events = ev->events = events; //events是 EPOLLIN 或 EPOLLOUT if (ev->status == 1) { //已经在红黑树 g_efd 里 op = EPOLL_CTL_MOD; //修改其属性 } else { //不在红黑树里 op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1 ev->status = 1; } if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //实际添加/修改 printf("event add failed [fd=%d], events[%d]\n", ev->fd, events); else printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events); return;}/* 从epoll 监听的 红黑树中删除一个 文件描述符*/void eventdel(int efd, struct myevent_s *ev) { struct epoll_event epv = { 0, {0} }; if (ev->status != 1) //不在红黑树上 return; epv.data.ptr = ev; ev->status = 0; //修改状态 epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //从红黑树 efd 上将 ev->fd 摘除 return;}/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */// 回调函数 - 监听的文件描述符发送读事件时被调用void acceptconn(int lfd, int events, void *arg) { struct sockaddr_in cin; socklen_t len = sizeof(cin); int cfd, i; //cfd对应的结构体是myevent_s if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) { if (errno != EAGAIN && errno != EINTR) { /* 暂时不做出错处理 */ //EAGAIN读了一个没有数据的fd,如果程序是阻塞的,不会出现这种错误。 //如果是非阻塞的,那么会因为中断而出现这种错误 } //__func__ 通过宏打印函数名 printf("%s: accept, %s\n", __func__, strerror(errno)); return; } //do {} while(0) 这个只执行一次,为什么这么写?因为 //函数用于将获取的文件描述符挂在epoll数上 //do while循环中有break,说明只要满足条件,就不进行下面的操作,do while(0) 是一个小技巧 do { for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素 if (g_events[i].status == 0) //类似于select中找值为-1的元素 break; //跳出 for if (i == MAX_EVENTS) { printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS); break; //跳出do while(0) 不执行后续代码 } int flag = 0; if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { //将cfd也设置为非阻塞 printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno)); break; } /* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */ //将数据初始化 //recvdata是一个回调函数,用于处理用户发来的数据 //从结构体数组(g_events[i])中去找一个没有被占用的,然后用cfd相关的信息初始化g_events[i] eventset(&g_events[i], cfd, recvdata, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件(EPOLLIN是读事件) //当检测到 EPOLLIN, 说明客户端发来了数据 eventadd(g_efd, EPOLLIN, &g_events[i]); } while (0); printf("new connect [%s:%d][time:%ld], pos[%d]\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i); return;}// 回调函数 - 通信的文件描述符发生读事件时候被调用,在main函数的for循环中标被调用的void recvdata(int fd, int events, void *arg) { //arg是myevent_s中指向结构体自身的那个指针,这个指针的作用是: //因为myevent_s结构体中有buf,如果想修改buf,那么就需要一个指针来找到这个buf struct myevent_s *ev = (struct myevent_s *)arg; int len; //读存入myevent_s成员buf中的数据 len = recv(fd, ev->buf, sizeof(ev->buf), 0); eventdel(g_efd, ev); //将该节点从红黑树上摘除 /* 将获取的数据初始化后挂在树上 */ if (len > 0) { //数据被成功接收 ev->len = len; ev->buf[len] = '\0'; //手动添加字符串结束标记 printf("C[%d]:%s\n", fd, ev->buf); //设置该 fd 对应的回调函数为 senddata(过去是receivedata),该函数用于给对方发送数据 eventset(ev, fd, senddata, ev); //将fd加入红黑树g_efd中,监听其写事件(EPOLLIN改成了EPOLLOUT),检测写缓冲区是否可写 eventadd(g_efd, EPOLLOUT, ev); } else if (len == 0) { close(ev->fd); /* ev-g_events 地址相减得到偏移元素位置 */ printf("[fd=%d] pos[%ld], closed\n", fd, ev - g_events); } else { close(ev->fd); printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno)); } return;}// 回调函数 - 通信的文件描述符发生写事件时候被调用void senddata(int fd, int events, void *arg) { struct myevent_s *ev = (struct myevent_s *)arg; int len; //直接将收到的buf中的数据 回写给客户端。未作处理 len = send(fd, ev->buf, ev->len, 0); /* printf("fd=%d\tev->buf=%s\ttev->len=%d\n", fd, ev->buf, ev->len); printf("send len = %d\n", len); */ if (len > 0) { printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf); eventdel(g_efd, ev); //从红黑树g_efd中移除 eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上, 设为监听读事件 } else { close(ev->fd); //错误处理,发生错误时,关闭链接 eventdel(g_efd, ev); //从红黑树g_efd中移除 printf("send[fd=%d] error %s\n", fd, strerror(errno)); } return;}/*创建 socket, 初始化lfd *///efd是epoll树的根节点 port是端口void initlistensocket(int efd, short port) { int lfd = socket(AF_INET, SOCK_STREAM, 0); //创建监听的套接字 fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞 /* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */ //acceptconn是一个函数名,有新的事件需要处理时,是该函数去处理的 eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]); //初始化用户自定义的结构体数组 /* void eventadd(int efd, int events, struct myevent_s *ev) */ //把初始化好的东西挂在epoll树上,也就是使得ptr指向初始化好的结构体变量 eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin)) sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port); bind(lfd, (struct sockaddr *)&sin, sizeof(sin)); listen(lfd, 20); return;}int main(int argc, char *argv[]) { unsigned short port = SERV_PORT; if (argc == 2) port = atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口 SERV_PORT g_efd = epoll_create(MAX_EVENTS + 1); //创建红黑树,返回给全局 g_efd MAX_EVENTS=1024 if (g_efd <= 0) //__func__是一个宏,用于打印当前函数的名字 printf("create efd in %s err %s\n", __func__, strerror(errno)); initlistensocket(g_efd, port); //初始化监听socket,initlistensocket是个自定义函数 struct epoll_event events[MAX_EVENTS + 1]; //保存已经满足就绪事件的文件描述符数组 printf("server running:port[%d]\n", port); int checkpos = 0, i; while (1) { /* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */ //获取当前时间 long now = time(NULL); //一次循环检测100个。 使用checkpos控制检测对象。每次检测100个 //MAX_EVENTS的值是1025,分组检测,每次检测100个,并不是每次都检测1025个 //checkpos对应的是检测是g_events全局结构体数组的一个下标,此时检测完了 for (i = 0; i < 100; i++, checkpos++) { if (checkpos == MAX_EVENTS) checkpos = 0; if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上 continue; //客户端不活跃的时间,last_active是一直在被更新的 //每次对文件描述符做操作时,都会通过eventset函数更新 long duration = now - g_events[checkpos].last_active; if (duration >= 60) { close(g_events[checkpos].fd); //关闭与该客户端链接 printf("[fd=%d] timeout\n", g_events[checkpos].fd); eventdel(g_efd, &g_events[checkpos]); //将该客户端 从红黑树 g_efd移除 } } /*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/ int nfd = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000); if (nfd < 0) { printf("epoll_wait error, exit\n"); break; } //多少个fd发生了变化 for (i = 0; i < nfd; i++) { /*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/ struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; //读就绪事件,则调用读回调函数——recvdata //函数指针指向谁,call_back就调用谁,这两个call_back调用的不是一样的 if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { ev->call_back(ev->fd, events[i].events, ev->arg); } //写就绪事件,则调用写回调函数——senddata //每个文件描述符都对应一个myevent_s结构体,每个结构体中call_back指向的函数不同,因此调用的不同 if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { ev->call_back(ev->fd, events[i].events, ev->arg); } } } /* 退出前释放所有资源 */ return 0;}
发表评论
最新留言
关于作者
