LinuxC/C++ 实现简单的TCP服务端
服务器处理多个客户端请求的时候,有两种处理方式,一种是一个线程处理一个客户端请求,但是这种方式比较昂贵,现在已经弃用,另一种是使用epoll
来对客户端IO进行管理.
这篇文章我们将使用epoll
来实现一个简单的TCP服务器,不过在实现之前,我们有必要先了解一下epoll
.
epoll
epoll的内核事件表
epoll是Linux特有的I/O
复用函数。它在实现和使用上与select、poll 有很大差异. 首先,epoll 使用一组函数来完成任务,而不是单个函数. 其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像selet和poll那样每次调用都要重复传入文件描述符集或事件集. 但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表.
创建epoll文件描述符
#include <sys/epoll.h>
int epoll_create(int size);
size
参数现在并不起作用,只是给内核一个提示, 告诉它事件表需要多大. 该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数
,以指定要访问的内核事件表.
操作内核事件表
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
fd
参数是要操作的文件描述符,op
参数则指定操作类型。操作类型有如下3种:
EPOLL_CTL_ADD
: 往事件表中注册fd上的事件.EPOLL_CTL_MOD
: 修改fd上的注册事件.EPOLL_ CTL_ DEL
: 删除fd上的注册事件.
event
参数指定事件,它是epoll event
结构指针类型. epoll_ event
的定义如下:
struct epoll_event {
_uint32_t events; // epoll事件
epoll_data_t data; // 用户数据
};
其中events
成员描述事件类型. epoll 支持的事件类型和poll基本相同. 表示epoll事件类型的宏是在poll对应的宏前加上E
,比如epoll的数据可读事件是EPOLLIN
. 但epoll有两个额外的事件类型: EPOLLET
和EPOLLONESHOT
. 它们对于epoll的高效运作非常关键,我们将在后面讨论它们. data 成员用于存储用户数据,其类型epoll data_t
的定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_data_t
是一个联合体,其4个成员中使用最多的是fd
. 它指定事件所从属的目标文件描述符.
epoll ctl
成功时返回0
,失败则返回-1
并设置errno
.
epoll等待事件触发
epoll系列系统调用的主要接口是epoll_ wait
函数. 它在一段超时时间内等待一组文件描述符上的事件,其原型如下:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数成功时返回就绪的文件描述符的个数,失败时返回-1
并设置errno
.
关于该函数的参数,我们从后往前讨论. timeout
参数的含义与poll
接口的timeout
参数相同. maxevents
参数指定最多监听多少个事件,它必须大于0
.
epoll wait
函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events
指向的数组中. 这个数组只用于输出epoll wait
检测到的就绪事件,而不像select
和poll
的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件. 这就极大地提高了应用程序索引就绪文件描述符的效率.
LT 和 ET 模式
epoll对文件描述符的操作有两种模式: LT (Level Trigger, 电平触发)模式
和ET (Edge Trigger,边沿触发)模式
.
LT模式
是默认的工作模式,这种模式下epoll相当于一个效率较高的poll. 而当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll 将以ET模式
来操作该文件描述符. ET模式是epoll的高效工作模式.
对于采用LT工作模式
的文件描述符,当epoll_wait
检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件. 这样,当应用程序下一次调用epoll_wait
时,cpoll_ wait
还会再次向应用程序通告此事件,直到该事件被处理.
而对于采用ET工作模式
的文件描述符,当epoll_wait
检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait
调用将不再向应用程序通知这一事件. 可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高
.
具体实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUFFER_LENGTH 1024
#define EPOLL_SIZE 1024
int main(int argc,char* argv[]) {
if (argc < 2) {
printf("Parm Error\n");
return -1;
}
int port = atoi(argv[1]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
perror("bind");
return 2;
}
if (listen(sockfd, 5) < 0) {
perror("listen");
return 3;
}
// 创建一个epoll
int epfd = epoll_create(1);
struct epoll_event events[EPOLL_SIZE] = {0};
// 储存epoll监听的IO事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
// 把socket交给epoll去管理
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
// epfd: 指定哪一个epoll
// events: 指定监听事件的容器
// EPOLL_SIZE: 数组大小
// -1: 表示只要没有IO事件就不去处理,0表示有时间就去处理
// 返回处理的IO事件的个数
int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (nready == -1) {
continue;
}
// 依次处理IO事件
// events容器中会储存两种fd,一种是sockfd,一种是clientfd
int i = 0;
for (i = 0; i < nready; i++) {
// 触发IO事件的是sockfd,要进行accept处理
if (events[i].data.fd == sockfd) {
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = sizeof(client_addr);
// 建立连接之后得到新的clientfd
int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
// 确定事件的触发方式
// 水平触发(有数据就触发,可能会触发多次)和边沿触发(检测到状态的改变才会触发)
// 这里使用边沿触发
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
// clientfd交给epoll管理
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else {
// 触发的是clientfd,要进行读写操作
int clientfd = events[i].data.fd;
char buffer[BUFFER_LENGTH] = { 0 };
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (len < 0) {
close(clientfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
// 及时清除IO
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
}
else if (len == 0) {
close(clientfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
// 及时清除IO
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
}
else {
printf("Recv: %s, %d byte(s)\n", buffer, len);
}
}
}
}
return 0;
}
CMakeLists.txt
PROJECT(TCPSERVER)
ADD_EXECUTABLE(tcp tcpserver.c)
执行结果
首先执行二进制程序:
# 后面跟上端口号
./tcp 8888
然后用NetAssist
开三个客户端,向服务器发送请求:
服务端接收:
参考资料:
《Linux高性能服务器编程》