Linux——UDP epoll实现服务端和客户端
1.基于UDP epoll的服务端和客户端的实现
注:本篇文章中,客户端(Client)和服务端(Server)均为UDP协议下的实现。
什么是epoll? epoll是Linux内核提供的一种高效的I/O多路复用机制,可以监控多个文件描述符的状态,读写数据时非常高效,比select和poll性能更好,在高并发时表现尤其突出。
2. 实现流程
服务端
- 创建socket
- 绑定socket到指定端口
- 创建epoll文件描述符
- 将socket添加到epoll监控列表
- 创建一个数组,用于存储接收到的数据
- 不断循环,等待事件发生
- 如果有事件发生,判断事件类型并相应处理
- 如果是新连接,则将新连接socket添加到epoll监控列表
- 如果是已连接socket可读,则接收数据并存储到数组中
- 如果是已连接socket可写,则将存储的数据发送出去
客户端
- 创建socket
- 绑定socket到指定端口
- 创建epoll文件描述符
- 将socket添加到epoll监控列表
- 不断循环,等待事件发生
- 如果有事件发生,判断事件类型并相应处理
- 如果是socket可写,则将数据发送出去
- 如果是socket可读,则接收数据
3. 代码实现
服务端:
//g++ epoll_server_udp.cpp -o epoll_server_udp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define MAX_BUFFER_SIZE 1024
int set_nonblocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl(F_GETFL) failed");
return -1;
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) == -1) {
perror("fcntl(F_SETFL) failed");
return -1;
return 0;
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("usage: %s port", argv[0]);
return -1;
int port = atoi(argv[1]);
if (port <= 0 || port > 65535) {
printf("invalid port number");
return -1;
int server_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_sockfd == -1) {
perror("socket() failed");
return -1;
sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sockfd, (sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind() failed");
return -1;
int epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1() failed");
return -1;
epoll_event event, events[MAX_EVENTS];
memset(&event, 0, sizeof(event));
event.events = EPOLLIN;
event.data.fd = server_sockfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_sockfd, &event) == -1) {
perror("epoll_ctl(EPOLL_CTL_ADD) failed");
return -1;
char buffer[MAX_BUFFER_SIZE];
memset(buffer, 0, sizeof(buffer));
sockaddr_in client_addr;
socklen_t client_addr_len;
while (true) {
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
if (errno == EINTR) {
continue;
perror("epoll_wait() failed");
break;
for (int i = 0; i < nfds; i++) {
int sockfd = events[i].data.fd;
if ((sockfd == server_sockfd) && (events[i].events & EPOLLIN)) {
client_addr_len = sizeof(client_addr);
ssize_t len = recvfrom(sockfd, buffer, MAX_BUFFER_SIZE, 0, (sockaddr *)&client_addr, &client_addr_len);
if (len == -1) {
perror("recvfrom() failed");
continue;
printf("Received %zd bytes from %s:%d", len, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
printf("Content: %s", buffer);
memset(buffer, 0, sizeof(buffer));
close(epollfd);
close(server_sockfd);
return 0;
//g++ epoll_client_udp.cpp -o epoll_client_udp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define MAX_BUFFER_SIZE 1024
int set_nonblocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl(F_GETFL) failed");
return -1;
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) == -1) {
perror("fcntl(F_SETFL) failed");
return -1;
return 0;
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("usage: %s ip port", argv[0]);
return -1;
char *ip = argv[1];
int port = atoi(argv[2]);
if (port <= 0 || port > 65535) {
printf("invalid port number");
return -1;
int client_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (client_sockfd == -1) {
perror("socket() failed");
return -1;
if (set_nonblocking(client_sockfd) == -1) {
return -1;
sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_addr.sin_addr);
int epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1() failed");
return -1;
epoll_event event, events[MAX_EVENTS];
memset(&event, 0, sizeof(event));
event.events = EPOLLOUT;
event.data.fd = client_sockfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sockfd, &event) == -1) {
perror("epoll_ctl(EPOLL_CTL_ADD) failed");
return -1;
char buffer[MAX_BUFFER_SIZE];
memset(buffer, 0, sizeof(buffer));
strncpy(buffer, "Hello", sizeof(buffer) - 1);
while (true) {
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
if (errno == EINTR) {
continue;
perror("epoll_wait() failed"
);
break;
for (int i = 0; i < nfds; i++) {
int sockfd = events[i].data.fd;
if ((events[i].events & EPOLLOUT) && (sockfd == client_sockfd)) {
ssize_t len = sendto(sockfd, buffer, strlen(buffer), 0, (sockaddr *)&server_addr, sizeof(server_addr));
if (len == -1) {
perror("sendto() failed");
continue;
printf("Sent %zd bytes to %s:%d", len, ip, port);
memset(buffer, 0, sizeof(buffer));
if ((events[i].events & EPOLLIN) && (sockfd == client_sockfd)) {
ssize_t len = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
if (len == -1) {
perror("recv() failed");
continue;
printf("Received %zd bytes from %s:%d", len, ip, port);
printf("Content: %s", buffer);
close(epollfd);
close(client_sockfd);
return 0;
close(epollfd);
close(client_sockfd);
return 0;
4. 测试结果
在两台机器上分别编译运行服务端和客户端程序,通过UDP协议进行通信。其中,192.168.1.102为服务端IP地址,192.168.1.101为客户端IP地址。
服务端:
$ ./epoll_server_udp 1234
Received 5 bytes from 192.168.1.101:39136
Content: Hello
客户端:
$ ./epoll_client_udp 192.168.1.102 1234
Sent 5 bytes to 192.168.1.102:1234
Received 14 bytes from 192.168.1.102:1234
Content: Received Hello
5. 知识点:
1.socket套接字
创建套接字的函数是socket(),函数原型为:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
其中 “int domain”参数 表示套接字要使用的协议簇,协议簇的在“linux/socket.h”里有详细定义,常用的协议簇:
- AF_UNIX(本机通信)
- AF_INET(TCP/IP – IPv4)
- AF_INET6(TCP/IP – IPv6)
其中 “type”参数 指的是套接字类型,常用的类型有:
- SOCK_STREAM(TCP流)
- SOCK_DGRAM(UDP数据报)
- SOCK_RAW(原始套接字)
最后一个“ protocol”一般设置为“0 ”,也就是当确定套接字使用的协议簇和类型时,这个参数的值就为0,但是有时候创建原始套接字时,并不知道要使用的协议簇和类型,也就是domain参数未知情况下,这时protocol这个参数就起作用了,它可以确定协议的种类。
socket是一个函数,那么它也有返回值,当套接字创建成功时,返回套接字,失败返回“-1”,错误代码则写入“errno”中。
创建套接字:
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/socket.h>
int sock_fd_tcp;
int sock_fd_udp;
sock_fd_tcp = socket(AF_INET, SOCK_STREAM, 0);
sock_fd_udp = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd_tcp < 0){
perror("TCP SOCKET ERROR!
\n
");
exit(-1);
}
if(sock_fd_udp < 0){
perror("UDP SOCKET ERROR!
\n
");
exit(-1);
}
什么是Socket?举一个例子:Lewis跟Nico两人聊QQ,QQ是一个独立的应用程序,那么它对应了两个Socket,一个在Lewis的电脑上,一个在Nico的电脑上。当Lewis对Nico说:”周末我们去开卡丁车吧!“,这句话就是一段数据,这段数据会先储存在Lewis电脑Socket上,我们在” 分层网络模型 “一文中提到过,TCP存在于传输层,同时,我们在” 端口、IP协议 “一文中又提到了TCP传输过程(三次握手建立连接,三次握手关闭连接),当Lewis的QQ和Nico的QQ连接成功后,Lewis的Socket将这段话的数据发送到Nico的电脑中,但是Nico暂时还没看到,因为数据会先存放在Nico电脑的Socket当中,然后Socket会把数据呈现给Nico看。
到了这里不禁要问,数据传送过程中为什么要多出Socket这样东西?
答:因为不同的应用程序对应不同的Socket,而Socket保证了QQ的数据不会到处乱跑,不会一冲动跑到MSN上去了。因为QQ和MSN两个应用程序的Socket内容是完全不同的。那么Socket里面到底是什么?
答:Socket套接字地址!套接字地址是一个数据结构,我们仅基于TCP传输协议作为例子。套接字地址这个数据结构里面包含了:地址类型、端口号、IP地址、填充字节这4种数据。而它的数据结构原型为:
#include <netinet/in.h>
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
其中:
- sin_family表示地址类型,对于基于TCP/IP传输协议的通信,该值只能是AF_INET;
- sin_prot表示端口号,例如:21 或者 80 或者 27015,总之在0 ~ 65535之间;
- sin_addr表示32位的IP地址,例如:192.168.1.5 或 202.96.134.133;
- sin_zero表示填充字节,一般情况下该值为0;
Socket数据的赋值实例:
struct sockaddr_in Lewis;
Lewis.sin_family = AF_INET;
Lewis.sin_port = htons(80);
Lewis.sin_addr.s_addr = inet_addr("202.96.134.133");
memset(Lewis.sin_zero,0,sizeof(Lewis.sin_zero));
分析:我们设置了一个名叫Lewis的套接字地址,它基于TCP/IP协议,因此sin_family的值为AF_INET,这个是雷打不动的,只要使用TCP/IP协议簇,该值就是AF_INET;htons是端口函数,以后介绍,这就表示设置了端口号为80;
sin_addr是一个数据结构,原型是:
struct in_addr{
unsigned long s_addr;
};
因此,Lewis这个套接字地址的IP赋值格式是Lewis.sin_addr.s_addr,inet_addr函数也是日后再说,这里表示设置IP地址为202.96.134.133;而memset函数在这里起到给sin_zero数组清零的作用,它的原型是:
memset(void *s, int c, size_t n);
2. INADDR_ANY
3.epoll
1)epoll函数
Epoll 作为一种 IO 复用机制多应用与高并发领域,网上有很多如何使用 epoll 的基础教程,但对于 epoll 中很重要的结构体 epoll_event 讲的都模棱两可,这篇文章将做深入解析
在解析之前,先回顾一下 epoll 的使用方法。
-
首先调用
int epoll_create(int size);创建一个 epoll -
调用
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);为 epoll 注册事件(如果是新建的 epoll 一般 op 选项是EPOLL_CTL_ADD添加事件)
··· EPOLL_CTL_ADD(注册新的 fd 到epfd)
··· EPOLL_CTL_DEL(从 epfd 中删除一个 fd)
··· EPOLL_CTL_MOD(修改已经注册的 fd 监听事件)
-
调用
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);等待事件的到来,得到的结果存储在 event 中 -
完全处理完毕后,再次调用
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);删除已经注册的事件(op 选项是EPOLL_CTL_DEL)
值得注意的是
epoll_wait
函数
只能获取是否有注册事件发生
,至于这个事件到底是什么、从哪个 socket 来、发送的时间、包的大小等等信息,统统不知道。这就好比一个人在黑黢黢的山洞里,只能听到声响,至于这个声音是谁发出的根本不知道。因此我们就需要
struct epoll_event
来帮助我们读取信息。
2)struct 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 events /
epoll_data_t data; / User data variable */
epoll_event 结构体的定义如上所示,分为 events 和 data 两个部分。
events 是 epoll 注册的事件,比如
EPOLLIN
、
EPOLLOUT
等等,这个参数在
epoll_ctl
注册事件时,可以明确告知注册事件的类型。
第二个参数 data 是一个联合体,很多人搞不清除 data 拿来干嘛,网上给的解释一般是传递参数,至于怎么传?有什么用?都不清不楚。下面一个小节将用实例的方式分析。
3)struct epoll_event 使用实例
下面将从两个实际案例中,分析 epoll_event 的作用。
3.1 示例 1:服务器侦听客户端连接
//创建socket
nSocketListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定地址
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0.0所有地址都合法
local.sin_port = htons(TCP_PORT);
bind(nSocketListen, (struct sockaddr*) & local, sizeof(local))
//创建epoll
nListenEpoll = epoll_create(MAX_LISTEN_EVENTS);
//注册事件
struct epoll_event Ev;
memset(&Ev, 0, sizeof(epoll_event));
Ev.events= EPOLLIN | EPOLLET
Ev.data.fd = nSocketListen;
epoll_ctl(nListenEpoll, EPOLL_CTL_ADD, nSocketListen, &Ev);
int nFdNumber = epoll_wait(nListenEpoll, lpListenEvents, MAX_LISTEN_EVENTS, -1);
//处理侦听结果
for (int i = 0; i < nFdNumber; i++)
if (lpListenEvents[i].data.fd != nSocketListen) continue;
上述代码是网上很常见的 demo 片段,作用是建立一个服务器,侦听所有客户端的连接。具体过程是先建立了一个 socket,地址设为设为 0.0.0.0(所有人都可以连接),然后将这个 socket 的句柄 nSocketListen 附加在注册事件
Ev.data.fd
上。在 wait 等到结果后做一个判断,看看接收到和预设的是否一致
if (lpListenEvents[i].data.fd != nSocketListen)
continue;
这里联合体 data 中的 fd 起到了传递 socket 句柄的作用,这样我们就知道:等到的事件是不是我们想要的 socket 产生的。但是 这个网上很常见的 demo 其实并没有体现出 fd 传参的作用!
整个程序仅仅设置并注册了一个 socket 来连接所有 IP 地址
htonl(INADDR_ANY);
,因此 wait 收到的消息必然来自于这个唯一的 socket,所以这句判断根本是多此一举。
正确的使用方法是:我们可以建立三个 socket 管理不同的字段
| Socket 句柄 | 管理的 IP 地址范围 |
|---|
101 100-120
102 121-191
103 192-255
将这三个 socket 都注册进 epoll 里面,当 wait 到来时,我们就可以根据
Ev.data.fd
传进来的 socket 句柄来进行处理。比如上午 8 点到 10 点这个时间段,服务器只允许 100-120 范围的 IP 连接进来,就可以做一个判断
if (lpListenEvents[i].data.fd == 101)
,如果是再接受连接。
这个例子中,fd 传递了 socket 的句柄,帮助我们管理不同的网络连接。
3.2 示例 2:线程间通信
//线程A代码
struct epoll_event Ev;
memset(&Ev, 0, sizeof(Ev));
Ev.events= EPOLLOUT | EPOLLET | EPOLLERR | EPOLLHUP
Ev.data.ptr = lpCatList;
epoll_ctl(iClientEpoll, EPOLL_CTL_ADD, lpCatList->nClientSocket, &Ev);
//线程B代码
int nFdNumber = epoll_wait(iClientEpoll, lpEvent, MAX_CLIENT_EVENTS, -1);
IOPACKHEAD_LIST* RelpCatList = (IOPACKHEAD_LIST*)lpEvent[i].data.ptr;
上述 demo 展示了 epoll 在两个线程间协同工作。线程 A 功能相当于接线员,跟 3.1 节展示的服务器功能相同:监听客户的连接,accept 客户的请求,建立客户与服务器间的 socket 连接通道(此处的建立的 socket 句柄为 nClientSocket)。然后将这些客户连接注册到 iClientEpoll 中
这些通道建立后,客户一般不会时刻收发数据,也就是说客户可能 不定时 的使用为他们建立的 socket 连接通道,线程 B 的 iClientEpoll 就是用来监听有没有 已经建立连接的客户 需要收发数据的。
如果仅仅像 3.1 节所展示的那样用
Ev.data.fd
传一个客户 socket 的句柄,这样线程 B 能得到的信息太少了。所以我们需要使用结构体 lpCatList 来传参。
lpCatList 相当于一个令牌,他是一个指针,指向的地址存储了客户的信息(Socket 句柄,IP 地址,MAC 地址,请求时间等等),A 线程在接收客户连接后,将他们写到这个令牌中,一并注册到 iClientEpoll。B 线程就可以利用 Ev.data.ptr 包含的重要的地址信息。
这样 ptr 就相当于一个小纸条,A 线程通过 iClientEpoll 将这个小纸条交到 B 线程手中,B 线程就能了解 A 线程的信息,实现了线程间的通信。
下面我们打印一下线程 A 的 lpCatlist
(gdb) p lpCatList
$18 = (IOPACKHEAD_LIST *) 0x7ffff0001120
再打印一下线程 B 的 ptr,可以发现他们指向同一个地址 0x7ffff0001120,说明参数成功传递
(gdb) p lpEvent[0]
$14 = {events = 4, data = {ptr = 0x7ffff0001120, fd = -268431072, u32 = 4026536224,
u64 = 140737219924256}}
4.其他例子
/* 实现功能:通过epoll, 处理多个socket
* 监听一个端口,监听到有链接时,添加到epoll_event
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <unistd.h>
#define MYPORT 12345
//最多处理的connect
#define MAX_EVENTS 500
//当前的连接数
int currentClient = 0;
//数据接受 buf
#define REVLEN 10
char recvBuf[REVLEN];
//epoll描述符
int epollfd;
//事件数组
struct epoll_event eventList[MAX_EVENTS];
void AcceptConn(int srvfd);
void RecvData(int fd);
int main()
int i, ret, sinSize;
int recvLen = 0;
fd_set readfds, writefds;
int sockListen, sockSvr, sockMax;
int timeout;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
//socket
if((sockListen=socket(AF_INET, SOCK_STREAM, 0)) < 0)
printf("socket error\n");
return -1;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(MYPORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//bind
if(bind(sockListen, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0)
printf("bind error\n");
return -1;
//listen
if(listen(sockListen, 5) < 0)
printf("listen error\n");
return -1;
// epoll 初始化
epollfd = epoll_create(MAX_EVENTS);
struct epoll_event event;
event.events = EPOLLIN|EPOLLET;
event.data.fd = sockListen;
//add Event
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockListen, &event) < 0)
printf("epoll add fail : fd = %d\n", sockListen);
return -1;
//epoll
while(1)
timeout=3000;
//epoll_wait
int ret = epoll_wait(epollfd, eventList, MAX_EVENTS, timeout);
if(ret < 0)
printf("epoll error\n");
break;
else if(ret == 0)
printf("timeout ...\n");
continue;
//直接获取了事件数量,给出了活动的流,这里是和poll区别的关键
int i = 0;
for(i=0; i<ret; i++)
//错误退出
if ((eventList[i].events & EPOLLERR) ||
(eventList[i].events & EPOLLHUP) ||
!(eventList[i].events & EPOLLIN))
printf ( "epoll error\n");
close (eventList[i].data.fd);
return -1;
if (eventList[i].data.fd == sockListen)
AcceptConn(sockListen);
}else{
RecvData(eventList[i].data.fd);
close(epollfd);
close(sockListen);
return 0;
/**************************************************
函数名:AcceptConn
功能:接受客户端的链接
参数:srvfd:监听SOCKET
***************************************************/
void AcceptConn(int srvfd)
struct sockaddr_in sin;
socklen_t len = sizeof(struct sockaddr_in);
bzero(&sin, len);
int confd = accept(srvfd, (struct sockaddr*)&sin, &len);
if (confd < 0)
printf("bad accept\n");
return;
}else
printf("Accept Connection: %d", confd);
//将新建立的连接添加到EPOLL的监听中
struct epoll_event event;
event.data.fd = confd;
event.events = EPOLLIN|EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
//读取数据
void RecvData(int fd)
int ret;
int recvLen = 0;
memset(recvBuf, 0, REVLEN);
printf("RecvData function\n");
if(recvLen != REVLEN)
while(1)
//recv数据
ret = recv(fd, (char *)recvBuf+recvLen, REVLEN-recvLen, 0);
if(ret == 0)
recvLen = 0;
break;
else if(ret < 0)
recvLen = 0;
break;
//数据接受正常
recvLen = recvLen+ret;
if(recvLen<REVLEN)
continue;
//数据接受完毕
printf("buf = %s\n", recvBuf);
recvLen = 0;
break;