一、前言
手把手教你從0開始編寫TCP服務(wù)器程序,體驗(yàn)開局一塊磚,大廈全靠壘。
為了避免篇幅過長使讀者感到乏味,對(duì)【TCP服務(wù)器的開發(fā)】進(jìn)行分階段實(shí)現(xiàn),一步步進(jìn)行優(yōu)化升級(jí)。
本節(jié),在上一章節(jié)的基礎(chǔ)上,將IO多路復(fù)用機(jī)制select改為更高效的IO多路復(fù)用機(jī)制epoll,使用epoll管理每個(gè)新接入的客戶端連接,實(shí)現(xiàn)發(fā)送和接收。
epoll是Linux內(nèi)核中一種可擴(kuò)展的IO事件處理機(jī)制,可替代select和poll的系統(tǒng)調(diào)用。處理百萬級(jí)并發(fā)訪問性能更佳。
select的局限性:
(1) 文件描述符越多,性能越差。 單個(gè)進(jìn)程中能夠監(jiān)視的文件描述符存在最大的數(shù)量,默認(rèn)是1024(在linux內(nèi)核頭文件中定義有 #define _FD_SETSIZE 1024),當(dāng)然也可以修改,但是文件描述符數(shù)量越多,性能越差。
(2)開銷巨大 ,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生了巨大的開銷(內(nèi)核/用戶空間內(nèi)存拷貝問題)。
(3)select需要遍歷整個(gè)句柄數(shù)組才能知道哪些句柄有事件。
(4)如果沒有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符的IO操作,那么每次調(diào)用select還是會(huì)將這些文件描述符通知進(jìn)程,即水平觸發(fā)。
(5)poll使用鏈表保存監(jiān)視的文件描述符,雖然沒有了監(jiān)視文件數(shù)量的限制,但是其他缺點(diǎn)依舊存在。
由于以上缺點(diǎn),基于select模型的服務(wù)器程序,要達(dá)到十萬以上的并發(fā)訪問,是很難完成的。因此,epoll出場了。
二、新增使用API函數(shù)
2.1、epoll_create()函數(shù)
函數(shù)原型:
#include <sys/epoll.h>
int epoll_create(int size);
功能:創(chuàng)建epoll的文件描述符。
參數(shù)說明:size表示內(nèi)核需要監(jiān)控的最大數(shù)量,但是這個(gè)參數(shù)內(nèi)核已經(jīng)不會(huì)用到,只要傳入一個(gè)大于0的值即可。 當(dāng)size<=0時(shí),會(huì)直接返回不可用,這是歷史原因保留下來的,最早的epoll_create是需要定義一次性就緒的最大數(shù)量;后來使用了鏈表以便便維護(hù)和擴(kuò)展,就不再需要使用傳入的參數(shù)。
返回:返回該對(duì)象的描述符,注意要使用 close 關(guān)閉該描述符。
2.2、epoll_ctl()函數(shù)
函數(shù)原型:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epoll_ctl對(duì)應(yīng)系統(tǒng)調(diào)用sys_epoll_ctl
功能:操作epoll的文件描述符,主要是對(duì)epoll的紅黑樹節(jié)點(diǎn)進(jìn)行操作,比如節(jié)點(diǎn)的增刪改查。
參數(shù)說明:
參數(shù) | 含義 |
---|---|
epfd | 通過 epoll_create 創(chuàng)建的文件描述符 |
op | 對(duì)紅黑樹的操作,比如節(jié)點(diǎn)的增加、修改、刪除,分別對(duì)應(yīng)EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL |
fd | 需要添加監(jiān)聽的文件描述符 |
event | 事件信息 |
注意:epoll_ctl是非阻塞的,不會(huì)被掛起。
2.3、struct epoll_event結(jié)構(gòu)體
struct epoll_event結(jié)構(gòu)體原型:
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64
};
struct epoll_event{
uint32_t events;
epoll_data_t data;
}
events成員代表要監(jiān)聽的epoll事件類型
events成員:
成員變量 | 含義 |
---|---|
EPOLLIN | 監(jiān)聽fd的讀事件 |
EPOLLOUT | 監(jiān)聽fd的寫事件 |
EPOLLRI | 監(jiān)聽緊急數(shù)據(jù)可讀事件(帶外數(shù)據(jù)到來) |
EPOLLRDHUP | 監(jiān)聽套接字關(guān)閉或半關(guān)閉事件 |
EPOLLET | 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式 |
data成員:
data 成員時(shí)一個(gè)聯(lián)合體類型,可以在調(diào)用 epoll_ctl 給 fd 添加/修改描述符監(jiān)聽的事件時(shí)攜帶一些數(shù)據(jù),方便后面的epoll_wait可以取出信息使用。
2.4、epoll_wait()函數(shù)
函數(shù)原型:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞一段時(shí)間,等待事件發(fā)生
返回:返回事件數(shù)量,事件集添加到events數(shù)組中。也就是遍歷紅黑樹中的雙向鏈表,把雙向鏈表中的節(jié)點(diǎn)數(shù)據(jù)拷貝出來,拷貝完畢后把節(jié)點(diǎn)從雙向鏈表中移除。
返回值 | 含義 |
---|---|
大于0 | 事件個(gè)數(shù) |
等于0 | 超時(shí)時(shí)間timeout到了 |
小于0 | 出錯(cuò),可通過errno查看出錯(cuò)原因 |
參數(shù)說明:
參數(shù) | 含義 |
---|---|
epfd | 通過 epoll_create 創(chuàng)建的文件描述符 |
events | 存放就緒的事件集合,是輸出參數(shù) |
maxevents | 最大可存放事件數(shù)量,events數(shù)組大小 |
timeout | 阻塞等待的時(shí)間長短,單位是毫秒,-1表示一直阻塞等待 |
三、實(shí)現(xiàn)步驟
epoll的優(yōu)點(diǎn):
- 不需要輪詢所有的文件描述符。
- 每次取就緒集合,都在固定位置。
- 事件的就緒和IO觸發(fā)可以異步解耦。
(1)創(chuàng)建socket。
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
(2)綁定地址。
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
(3)設(shè)置監(jiān)聽。
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
(4)創(chuàng)建epoll。
int epfd=epoll_create(1);
if(epfd==-1)
{
perror("epoll_create error");
return SOCKET_EPOLL_CREATE_FAILED;
}
(5)添加listen fd 到epoll。
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listenfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev)==-1)
{
perror("epoll_ctl error");
return SOCKET_EPOLL_CTL_FAILED;
}
(6)監(jiān)聽事件。
int nready=epoll_wait(epfd,evs,EVENTS_LENGTH,-1);
(7)如果監(jiān)聽套接字有新連接請(qǐng)求,處理新連接。
int curfd=evs[i].data.fd;
if(curfd==listenfd)
{
// accept
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&clientlen);
if(clientfd==-1)
{
perror("accept error");
continue;
}
//printf("client %s:%d connected\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
printf("client %s:%d connected\n", inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events=EPOLLIN;
ev.data.fd=clientfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
(8)處理客戶端發(fā)來的數(shù)據(jù)和發(fā)送數(shù)據(jù)到客戶端。
if(evs[i].events&EPOLLIN)
{
//read
int ret=recv(curfd,rbuff,BUFFER_LENGTH,0);
if(ret>0)
{
printf("recv from %d: %s\n",curfd,rbuff);
rbuff[ret]='\0';
memcpy(wbuff,rbuff,BUFFER_LENGTH);
ev.events=EPOLLOUT;
ev.data.fd=curfd;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,curfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
else if(ret==0)// 連接關(guān)閉
{
printf("client %d disconnected\n", evs[i].data.fd);
// 將連接從epoll實(shí)例中刪除
if(epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, NULL)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
close(evs[i].data.fd);
}
else if (ret == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue; // 數(shù)據(jù)已讀完
}
perror("read error");
break;
}
else{
printf("read error,unknow type %d\n",ret);
}
}
else if(evs[i].events&EPOLLOUT)
{
//write
send(curfd,wbuff,BUFFER_LENGTH,0);
ev.events=EPOLLIN;
ev.data.fd=curfd;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,curfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
四、完整代碼
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/epoll.h>
#define LISTEN_PORT 9999
#define BLOCK_SIZE 10
#define BUFFER_LENGTH 1024
#define EVENTS_LENGTH 128
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_BIND_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4,
SOCKET_SELECT_FAILED=-5,
SOCKET_EPOLL_CREATE_FAILED=-6,
SOCKET_EPOLL_CTL_FAILED=-7,
SOCKET_EPOLL_WAIT_FAILED=-8
};
char rbuff[BUFFER_LENGTH] = { 0 };
char wbuff[BUFFER_LENGTH] = { 0 };
int main(int argc,char **argv)
{
// 1.
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
// 2.
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
// 3.
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
printf("listen port: %d\n",LISTEN_PORT);
// 4.
int epfd=epoll_create(1);
if(epfd==-1)
{
perror("epoll_create error");
return SOCKET_EPOLL_CREATE_FAILED;
}
struct epoll_event ev,evs[EVENTS_LENGTH];
ev.events=EPOLLIN;
ev.data.fd=listenfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev)==-1)
{
perror("epoll_ctl error");
return SOCKET_EPOLL_CTL_FAILED;
}
printf("start epoll_wait. epoll fd = %d\n",epfd);
while(1)
{
int nready=epoll_wait(epfd,evs,EVENTS_LENGTH,-1);
if (nready == -1) {
perror("epoll_wait error");
exit(SOCKET_EPOLL_WAIT_FAILED);
}
for(int i=0;i<nready;i++)
{
int curfd=evs[i].data.fd;
if(curfd==listenfd)
{
// accept
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&clientlen);
if(clientfd==-1)
{
perror("accept error");
continue;
}
//printf("client %s:%d connected\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
printf("client %s:%d connected\n", inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events=EPOLLIN;
ev.data.fd=clientfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
else if(evs[i].events&EPOLLIN)
{
//read
int ret=recv(curfd,rbuff,BUFFER_LENGTH,0);
if(ret>0)
{
printf("recv from %d: %s\n",curfd,rbuff);
rbuff[ret]='\0';
memcpy(wbuff,rbuff,BUFFER_LENGTH);
ev.events=EPOLLOUT;
ev.data.fd=curfd;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,curfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
else if(ret==0)// 連接關(guān)閉
{
printf("client %d disconnected\n", evs[i].data.fd);
// 將連接從epoll實(shí)例中刪除
if(epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, NULL)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
close(evs[i].data.fd);
}
else if (ret == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue; // 數(shù)據(jù)已讀完
}
perror("read error");
break;
}
else{
printf("read error,unknow type %d\n",ret);
}
}
else if(evs[i].events&EPOLLOUT)
{
//write
send(curfd,wbuff,BUFFER_LENGTH,0);
ev.events=EPOLLIN;
ev.data.fd=curfd;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,curfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
}
}
close(listenfd);
return 0;
}
編譯命令:
gcc -o server server.c
五、TCP客戶端
5.1、自己實(shí)現(xiàn)一個(gè)TCP客戶端
自己實(shí)現(xiàn)一個(gè)TCP客戶端連接TCP服務(wù)器的代碼:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_CONN_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4
};
int main(int argc,char** argv)
{
if(argc<3)
{
printf("Please enter the server IP and port.");
return 0;
}
printf("connect to %s, port=%s\n",argv[1],argv[2]);
int connfd=socket(AF_INET,SOCK_STREAM,0);
if(connfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
struct sockaddr_in serv;
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=inet_addr(argv[1]);
serv.sin_port=htons(atoi(argv[2]));
socklen_t len=sizeof(serv);
int rwfd=connect(connfd,(struct sockaddr*)&serv,len);
if(rwfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
close(rwfd);
return SOCKET_CONN_FAILED;
}
int ret=1;
while(ret>0)
{
char buf[BUFFER_LENGTH]={0};
printf("Please enter the string to send:\n");
scanf("%s",buf);
send(connfd,buf,strlen(buf),0);
memset(buf,0,BUFFER_LENGTH);
printf("recv:\n");
ret=recv(connfd,buf,BUFFER_LENGTH,0);
printf("%s\n",buf);
}
close(rwfd);
return 0;
}
編譯:
gcc -o client client.c
5.2、Windows下可以使用NetAssist的網(wǎng)絡(luò)助手工具
下載地址:http://old.tpyboard.com/downloads/NetAssist.exe
小結(jié)
至此,我們最終確定使用IO多路復(fù)用器epoll處理高并發(fā)。但是,上面的epoll實(shí)現(xiàn)的TCP服務(wù)器存在一些問題:
- 所有的連接都是使用相同的讀寫緩存(rbuff和wbuff),這會(huì)導(dǎo)致數(shù)據(jù)覆蓋。
- 沒有分包能力。
下一章節(jié)會(huì)解決這些問題,構(gòu)建一個(gè)reactor網(wǎng)絡(luò)模型。文章來源:http://www.zghlxwxcb.cn/news/detail-797716.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-797716.html
到了這里,關(guān)于【TCP服務(wù)器的演變過程】使用IO多路復(fù)用器epoll實(shí)現(xiàn)TCP服務(wù)器的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!