socket网络编程的步骤
先给出大致流程:
服务端:
创建自己的socket连接
再打开自己的可以用于通信的端口,并把自己的ip告诉要通信的客户端
打开监听的socket,监听是否有客户端连接
接受客户端的连接
如果有客户端连接上来,则接收数据后,再回复
不用于通信则关闭socket
客户端:
创建自己的socket连接
向服务端发起连接请求
请求成功后,则发送信息,并接受服务端的信息
不用于通信则关闭socket
在建立连接的阶段,则会用到TCP的三次握手:
客户端:首先发送建立连接报文syn,表示想建立从客户端到服务端的连接
服务端:确认报文ack表示同意从客户端建立连接,并且向客户端发送建立连接报文syn,表示想从服务端建立到客户端的连接
客户端:回复收到确认报文ack,表示同意从服务端到客户端建立连接
自此连接建立。
由于客户端于服务端建立的连接是双向
的,所以,连接在断开的需要四次挥手。
客户端:
首先客户端先断开自己到服务端的连接。
向服务端发送一个断开连接请求FIN
服务端:
对FIN请求进行确认,表示同意断开由客户端到服务端的连接
然后服务端再向客户端发送断开连接请求FIN
客户端:
对服务端FIN进行回复确认,表示同意断开从服务端到客户端的连接
有了上面基础知识,我们就可以往下面写代码了。
首先是客户端:
int main()
{
int sockefd = socket(AF_INET,SOCK_STREAM,0);
if (sockefd==-1)
{
perror("socket");return -1;
}
struct hostent *h;
if ((h=gethostbyname(ip地址)==0)
{
cout<<"gethostbyname failed\n";close(sockefd);return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof (servaddr));
servaddr.sin_family=AF_INET;
memcpy(&servaddr.sin_addr.s_addr,h->h_addr,h->h_length);
servaddr.sin_port=htons(通信端口));
if (connect(sockefd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)
{
perror("connect");close(sockefd);return -1;
}
char buffer[1024];
for(int i=0;i<3;i++)
{
memset(&buffer,0,sizeof(buffer));
int iret;
sprintf(buffer,"这是%d号",i+1);
if (iret=send(sockefd,buffer,sizeof(buffer),0)<=0)
{
perror("send");break;
}
cout<<"发送"<<buffer<<endl;
memset(&buffer,0,sizeof(buffer));
if (iret=recv(sockefd,buffer,sizeof(buffer),0)<=0)
{
perror("recv");break;
}
cout<<"接收"<<buffer<<endl;
}
close(sockefd);
}
先建立socket没什么好说的,主要是struct hostent 与 struct sockaddr_in 这两个结构体。
我们首先来看struct sockaddr_in结构体,事实上他有两个“兄弟”。
struct sockaddr {
unsigned short sa_family;
// 协议族(地址类型)
unsigned char sa_data[14];
// 14字节的端口和地址。
};
struct sockaddr_in {
unsigned short sin_family;
// 协议族(地址类型)
unsigned short sin_port; // 16位端口号,大端序。用htons(整数的端口)转换。
struct in_addr sin_addr; // IP地址的结构体。
unsigned char sin_zero[8]; // 未使用,为了保持与struct sockaddr一样的长度而添加。
};
struct in_addr { // IP地址的结构体。
unsigned int s_addr; // 32位的IP地址,大端序。
};
sin_family:表示使用的是什么协议,IPV4 / IPV6
sin_port:表示用于通信的端口
sin_addr:表示用于通信的ip地址
sin_zero[8]:为了扩展而定义的参数
我们用sockaddr_in来处理用于通信的ip与端口号,当发现其结构体长度不够用的时候,我们会转换为sockaddr结构体,事实上最好每次都转化为sockaddr。
struct hostent
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; // 主机名。
char **h_aliases; // 主机所有别名构成的字符串数组,同一IP可绑定多个域名。
short h_addrtype; // 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。
short h_length; // 主机IP地址长度,IPV4地址为4,IPV6地址则为16。
char **h_addr_list; // 主机的ip地址,以网络字节序存储。
};
这个结构体主要用于把域名转化为用于网络通信的大端序ip。
struct hostent *h;
if ((h=gethostbyname(ip地址)==0)
{
cout<<"gethostbyname failed\n";close(sockefd);return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof (servaddr));
servaddr.sin_family=AF_INET;
memcpy(&servaddr.sin_addr.s_addr,h->h_addr,h->h_length);
servaddr.sin_port=htons(通信端口));
所以这部分代码,主要是绑定用于通信的ip与端口号。
if (connect(sockefd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)
{
perror("connect");close(sockefd);return -1;
}
这部分代码,用于向服务端发起连接请求。
if (iret=send(sockefd,buffer,sizeof(buffer),0)<=0)
{
perror("send");break;
}
cout<<"发送"<<buffer<<endl;
memset(&buffer,0,sizeof(buffer));
if (iret=recv(sockefd,buffer,sizeof(buffer),0)<=0)
{
perror("recv");break;
}
cout<<"接收"<<buffer<<endl;
这部分主要是接收服务端发过来的信息,以及向服务端发送信息。
再来看服务端:
int main()
{
int listenfd;
listenfd=socket(AF_INET,SOCK_STREAM,0);
if (listenfd==-1) {
perror("socket");return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(0));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(5005);
if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))!=0)
{
perror("bind");close(listenfd);return -1;
}
if (listen(listenfd,5)!=0)
{
perror("listen");close(listenfd);return -1;
}
int clientfd = accept(listenfd,0,0);
if (clientfd==-1)
{
perror("accept");close(listenfd);return -1;
}
cout<<"客户端已连接"<<endl;
char buffer[1024];
while (true)
{
int iret;
memset(buffer,0,sizeof(buffer));
if (iret=recv(clientfd,buffer,sizeof(buffer),0)<=0)
{
perror("recv");break;
}
strcpy(buffer,"ok");
if (iret=send(clientfd,buffer,sizeof(buffer),0)<=0)
{
perror("send");break;
}
cout<<"发送"<<buffer<<endl;
}
close(listenfd);
close(clientfd);
}
服务端首先建立监听的socket,用于处理客户端到来的socket连接。
int listenfd;
listenfd=socket(AF_INET,SOCK_STREAM,0);
if (listenfd==-1) {
perror("socket");return -1;
}
然后服务端开启自己通信的端口以及向所有客户端用于通信:
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(0));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(5005);
后面这部分的代码就是流程图的步骤了
if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))!=0)
{
perror("bind");close(listenfd);return -1;
}
if (listen(listenfd,5)!=0)
{
perror("listen");close(listenfd);return -1;
}
int clientfd = accept(listenfd,0,0);
if (clientfd==-1)
{
perror("accept");close(listenfd);return -1;
}
cout<<"客户端已连接"<<endl;
char buffer[1024];
while (true)
{
int iret;
memset(buffer,0,sizeof(buffer));
if (iret=recv(clientfd,buffer,sizeof(buffer),0)<=0)
{
perror("recv");break;
}
strcpy(buffer,"ok");
if (iret=send(clientfd,buffer,sizeof(buffer),0)<=0)
{
perror("send");break;
}
cout<<"发送"<<buffer<<endl;
}
close(listenfd);
close(clientfd);
}
注:有些函数细节没有给出详解,这个自己查查就明白,程序员就得会自己查资料。
学习网络编程推荐大家看看下面这套教程
select执行过程详解
select函数参数
FD_ZERO(fd_set* fdset): 将fd_set变量的所有位初始化为0。
FD_CLR(int fd,fd_set *set):清除fd_set集合中指定的fd文件描述符。
FD_SET(int fd,fd_set *set):在fd_set集合中注册文件描述符fd的信息。
FD_ISSET(int fd, fd_set* fdset):若参数fd_set指向的变量中包含文件描述符fd的信息,则返回真。
第一个参数:监视的文件描述的数量(通常是最大的那个文件描述符+1)
第二个参数:表示关心读事件
第三个参数:表示关心写事件
第四个参数:表示关心异常事件
第五个参数:超时时间
返回值:错误返回-1,超时返回0。当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
select执行流程
在了解select是如何执行之前,我们要明白,recv只能监视单个socket,而人们希望服务器一次性能够管理多个socket连接,所以才出现了select、poll、epoll这些技术。
在进行select方法之前,它与服务端建立单个连接的准备工作是相似的。
int sock = socket(AF_INET,SOCK_STREAM,0);
if (sock < 0)
{
perror("socket() failed"); return -1;
}
/*
int opt = 1; unsigned int len = sizeof(opt);
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
*/
if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
{
perror("bind() failed"); close(sock); return -1;
}
if (listen(sock,5) != 0 )
{
perror("listen() failed"); close(sock); return -1;
}
先不用管注释部分的代码,总之,就是先建立一个socket,然后进行绑定,再对连上来的客户端进行监听。
int listensock = initserver(端口号); //然后初始化监听端口
printf("listensock=%d\n",listensock);
fd_set readfds; // 需要监视读事件的socket的集合,大小为16字节(1024位)的bitmap。
FD_ZERO(&readfds); // 初始化readfds,把bitmap的每一位都置为0。
FD_SET(listensock,&readfds); // 把服务端用于监听的socket加入readfds。
int maxfd=listensock; // readfds中socket的最大值。
我们来看fd_set之后的代码:建立 一个读事件的socket集合,将该集合初始化,再把用于监听的socket:listensock加入该集合。也就是下图表示的含义:我们假设需要的socket有:3,6,8,9。首先在建立socket集合的时候会初始化bitmap也就是位图。
那么位图是什么呢? 在了解这个概念之前,我们应该明白,select可以监视的最大连接数是1024个,为了表示哪些socket我们需要监听,用一个数组来表示,数组下标为socket的值(文件描述符),用1表示需要监听,0表示不需要监听。而这个数组就是位图。比如我们所需要的3,6,8,9这些socket在bitmap[3,6,8,9]=1,其他不需要监听的为0。而完成上述动作的就需要调用FD_SET函数。
这里的maxfd表示最大的那个文件描述符。在之后会有详细介绍。
进行了初始化后我们就要对这些socket进行监视了: 在调用select函数的时候如果失败会返回一个负数,如果超时会返回一个0。
while (true)
{
// 在select()函数中,会修改bitmap,所以,要把readfds复制一份给tmpfds,再把tmpfds传select()。
fd_set tmpfds=readfds;
// 调用select() 等待事件的发生(监视哪些socket发生了事件)。
int infds=select(maxfd+1,&tmpfds,NULL,NULL,0);
// 如果infds<0,表示调用select()失败。
if (infds<0)
{
perror("select() failed"); break;
}
// 如果infds==0,表示select()超时。
if (infds==0)
{
printf("select() timeout.\n"); continue;
}
select函数的几个参数这里就不再讲解了,上面已经提过。(下面的代码接上面的部分)
// 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
for (int eventfd=0;eventfd<=maxfd;eventfd++)
{
if (FD_ISSET(eventfd,&tmpfds)==0) continue;
// 如果eventfd在bitmap中的标志为0,表示它没有事件,continue
// 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
if (eventfd==listensock)
{
/*
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0) { perror("accept() failed"); continue; }
printf ("accept client(socket=%d) ok.\n",clientsock);
*/
FD_SET(clientsock,&readfds);
// 把bitmap中新连上来的客户端的标志位置为1。
if (maxfd<clientsock) maxfd=clientsock; // 更新maxfd的值。
}
注释部分我们不看(在socket里面已经讲的很清楚了),主要关心select的流程。
这部分代码的含义是:检查是否有客户端的连接。如果有,那么就把连上来的客户端在位图里所表示的文件描述符置为1。同时更新需要处理的文件描述符的最大值。
下面代码继续接上面部分:
// 如果是客户端连接的socket有事件,表示接收缓存中有数据可以读(对端发送的报文已到达),或者有客户端已断开连接。
else
{
// 存放从接收缓冲区中读取的数据。
char buffer[1024];
memset(buffer,0,sizeof(buffer));
if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket
FD_CLR(eventfd,&readfds); // 把bitmap中已关闭客户端的标志位清空。
if (eventfd == maxfd)
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
{
for (int ii=maxfd;ii>0;ii--) // 从后面往前找。
{
if (FD_ISSET(ii,&readfds))
{
maxfd = ii; break;
}
}
}
}
如果 eventfd!=listensock 表示有客户端的事件发生:
(1)客户端断开连接;
(2)客户端发送数据过来了。
这里是客户端断开连接。
当客户端断开连接的时候(即recv返回值<=0),首先我们关闭监视的socket,然后在bitmap中把该socket所表示的位置置为0,表示不再监视这个socket。
这里重点来讲一下关于处理maxfd的代码部分,当发现我们要处理的最大socket恰好是我们要关闭的socket 的时候,我们要更新maxfd的值,如果不是我们则直接关闭。
这里我个人理解是:我们引入的maxfd是为了减少我们在bitmap中寻找的个数,bitmap处理1024是它的极限个数,而maxfd则表示实际我们真正需要处理socket的最多的那个值。
只有当我们发现所需要处理的那个socket值正好是我们最大的socket的值的时候,我们才会更新maxfd。如果比maxfd还小,那么在遍历寻找的时候,现有的maxfd完全可以把目前所有需要监视的socket包括进去。
下面代码接上面:
else
{
// 如果客户端有报文发过来。
printf("recv(eventfd=%d):%s\n",eventfd,buffer);
// 把接收到的报文内容原封不动的发回去。
send(eventfd,buffer,strlen(buffer),0);
}
}
}
}
return 0;
}
如果 不是断开连接,则发送数据过去就行。
以上所有代码:即可抽象成上述图片。
服务端:
(1)用户态:
1:把需要监听的socket创建一个集合(集合可以用数组表示),FD_ZERO()初始化位图为0;
2:FD_SET()把需要监听的socket在位图中置为1;
3:调用select()把位图从用户态拷贝到内核态,并阻塞select();
(2)内核态
4: 把需要等待数据的进程加入与其对应的socket阻塞队列;
客户端:
1:向服务端发送数据;
服务端:
1:网卡接收数据 -----> 同时把数据写入内存;
2:引发CPU中断;
3:CPU执行中断程序;
4:接收对应的socket数据;
5:把处于阻塞队列的进程重新加入执行队列;
6:把所有位图从内核态拷贝到用户态。
7:在用户态轮询查找对应进程的socket。
以上所有代码:
int main()
{
int listensock = initserver(5005);
printf("listensock=%d\n",listensock);
if (listensock < 0) { printf("initserver() failed.\n"); return -1; }
// 读事件:1)已连接队列中有已经准备好的socket(有新的客户端连上来了);
// 2)接收缓存中有数据可以读(对端发送的报文已到达);
// 3)tcp连接已断开(对端调用close()函数关闭了连接)。
// 写事件:发送缓冲区没有满,可以写入数据(可以向对端发送报文)。
fd_set readfds; // 需要监视读事件的socket的集合,大小为16字节(1024位)的bitmap。
FD_ZERO(&readfds); // 初始化readfds,把bitmap的每一位都置为0。
FD_SET(listensock,&readfds); // 把服务端用于监听的socket加入readfds。
int maxfd=listensock; // readfds中socket的最大值。
while (true) // 事件循环。
{
// 用于表示超时时间的结构体。
struct timeval timeout;
timeout.tv_sec=10; // 秒
timeout.tv_usec=0; // 微秒。
fd_set tmpfds=readfds; // 在select()函数中,会修改bitmap,所以,要把readfds复制一份给tmpfds,再把tmpfds传给select()。
// 调用select() 等待事件的发生(监视哪些socket发生了事件)。
int infds=select(maxfd+1,&tmpfds,NULL,NULL,0);
// 如果infds<0,表示调用select()失败。
if (infds<0)
{
perror("select() failed"); break;
}
// 如果infds==0,表示select()超时。
if (infds==0)
{
printf("select() timeout.\n"); continue;
}
// 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
for (int eventfd=0;eventfd<=maxfd;eventfd++)
{
if (FD_ISSET(eventfd,&tmpfds)==0) continue; // 如果eventfd在bitmap中的标志为0,表示它没有事件,continue
// 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
if (eventfd==listensock)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0) { perror("accept() failed"); continue; }
printf ("accept client(socket=%d) ok.\n",clientsock);
FD_SET(clientsock,&readfds); // 把bitmap中新连上来的客户端的标志位置为1。
if (maxfd<clientsock) maxfd=clientsock; // 更新maxfd的值。
}
else
{
// 如果是客户端连接的socke有事件,表示接收缓存中有数据可以读(对端发送的报文已到达),或者有客户端已断开连接。
char buffer[1024]; // 存放从接收缓冲区中读取的数据。
memset(buffer,0,sizeof(buffer));
if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket
FD_CLR(eventfd,&readfds); // 把bitmap中已关闭客户端的标志位清空。
if (eventfd == maxfd) // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
{
for (int ii=maxfd;ii>0;ii--) // 从后面往前找。
{
if (FD_ISSET(ii,&readfds))
{
maxfd = ii; break;
}
}
}
}
else
{
// 如果客户端有报文发过来。
printf("recv(eventfd=%d):%s\n",eventfd,buffer);
// 把接收到的报文内容原封不动的发回去。
send(eventfd,buffer,strlen(buffer),0);
}
}
}
}
return 0;
}
poll执行过程详解
poll函数参数
第一个参数:数组的元素首地址,元素类型是pollfd 结构体类型
第二个参数:需要关注的文件描述符的数目
第三个参数:阻塞等待的时间
timeout = -1,代表永久阻塞等待
timeout = 0,代表永久非阻塞等待
timeout > 0,代表先阻塞等待 timeout 毫秒,超过这个时间变为非阻塞等待,poll函数返回
返回值:poll函数调用出错,返回-1;
非阻塞模式下,返回0;
有事件就绪时,返回 有事件就绪的文件描述符的个数
第一个参数fd:希望内核帮你关注哪个文件描述符
第二个参数events:希望内核帮你关注该文件描述符上的什么事件(读/写事件),事件类型以及设置方式在下面介绍。
第三个参数revents:内核通知你该文件描述符上的某个事件就绪了。
poll执行流程
总的来说,poll与大体流程select一样,只不过相对于select来说,poll可以接受更高的并发度。主要的我们来看以下代码:
最主要的不同就是poll多了一个结构体来存储相应的文件描述符、要处理的事件以及实际发生的事件。
struct
{
int fd;
short events;
short revents;
}
首先还是做好初始化的工作:
int listensock = initserver(atoi(argv[1]));
printf("listensock=%d\n",listensock);
if (listensock < 0) { printf("initserver() failed.\n"); return -1; }
接下面代码:
pollfd fds[2048];
// fds存放需要监视的socket。
// 初始化数组,把全部的socket设置为-1,如果数组中的socket的值为-1,那么,poll将忽略它。
for (int ii=0;ii<2048;ii++)
fds[ii].fd=-1;
// 打算让poll监视listensock读事件。
fds[listensock].fd=listensock;
fds[listensock].events=POLLIN; // POLLIN表示读事件,POLLOUT表示写事件。
// fds[listensock].events=POLLIN|POLLOUT;
int maxfd=listensock; // fds数组中需要监视的socket的实际大小。
首先定义一个fds数组,用于存放我们要监视的socket;这里fds大小可以自己定义,但是并不是越大越好,当fds大小越大性能方面就会下降。
再初始化fds数组,然后把监听客户端连接的socket--listensock加入fds。
同时,maxfd指定fds 文件描述符的最大值。主要目的和select相同。
接下面代码:
while (true)
{
// 调用poll() 等待事件的发生(监视哪些socket发生了事件)。
int infds=poll(fds,maxfd+1,NULL);
// 如果infds<0,表示调用poll()失败。
if (infds < 0)
{
perror("poll() failed"); break;
}
// 如果infds==0,表示poll()超时。
if (infds == 0)
{
printf("poll() timeout.\n"); continue;
}
// 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
for (int eventfd=0;eventfd<=maxfd;eventfd++)
{
if (fds[eventfd].fd<0) continue; // 如果fd为负,忽略它。
if ((fds[eventfd].revents&POLLIN)==0) continue;
// 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
if (eventfd==listensock)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0) { perror("accept() failed"); continue; }
printf ("accept client(socket=%d) ok.\n",clientsock);
// 修改fds数组中clientsock位置的元素。
fds[clientsock].fd=clientsock;
fds[clientsock].events=POLLIN;
if (maxfd<clientsock) maxfd=clientsock; // 更新maxfd的值。
}
else
{
// 如果是客户端连接的socke有事件,表示有报文发过来了或者连接已断开。
char buffer[1024]; // 存放从客户端读取的数据。
memset(buffer,0,sizeof(buffer));
if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket。
fds[eventfd].fd=-1;
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
if (eventfd == maxfd)
{
for (int ii=maxfd;ii>0;ii--) // 从后面往前找。
{
if (fds[ii].fd!=-1)
{
maxfd = ii; break;
}
}
}
}
else
{
// 如果客户端有报文发过来。
printf("recv(eventfd=%d):%s\n",eventfd,buffer);
send(eventfd,buffer,strlen(buffer),0);
}
}
}
}
总体代码和select是一样的,流程也差不多,只不过把用位图该换为了结构体数组来存取监视的socket。流程可参考select。
select与poll小结
总的来说,select与poll大体流程是一样的,但是也有细微差别:
细微差别主要是poll结构体和select的位图所导致的。
epoll执行过程详解
epoll执行流程
开始先初始化:
int main()
{
int listensock = initserver(5005);
printf("listensock=%d\n",listensock);
if (listensock < 0) { printf("initserver() failed.\n"); return -1; }
接下面代码:
// 创建epoll句柄。
int epollfd=epoll_create(1);
// 为服务端的listensock准备读事件。
epoll_event ev; // 声明事件的数据结构。
ev.data.fd=listensock; // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。
ev.events=EPOLLIN; // 打算让epoll监视listensock的读事件。
// 把需要监视的socket和事件加入epollfd中。
epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev);
epoll_event evs[10]; // 存放epoll返回的事件。
首先创建一个epollfd句柄,即创建一个eventpoll模型,这个其实本质也是一个文件描述符。
eventpoll结构体里面的成员很多,我们主要关注其内部的:rdllist --->就绪队列;rb_root rbt-->红黑树的节点(即创建的eventpoll模型的文件描述符对应的数值);wq-->等待队列。
在进行下一步之前,我们要了解一些结构体,首先就是epitem,在调用epoll_ctl()的时候,我们会用到这个结构体。
epitem
{
rbn;
rdllink;
next;
ffd;
nwait;
pwqlist;
ep;
fllink;
event;
}
我们主要关心:rbn;rdllink;ffd;ep;event。
rbn:是我们文件描述符对应红黑树的节点
rdllink:是已经就绪的文件描述符队列
ffd:是文件描述符
ep:是eventpoll结构体指针 (当我们拿到了某个事件,我们就知道他是属于哪个eventpoll)
在往下进行下一步的代码之前,我们得要了解epoll大致步骤是如何处理socket的。
当服务端从网卡接收到数据的时候,会把数据写入内核缓冲区,然后,这个时候会调用epoll_ctl()函数,维护等待队列。我们假设A进程对应的文件描述符事件已经到达,这个时候,epoll区别于select与poll,他会有一个回调机制,这就使得可以不需要轮询去查找到底哪个socket有事件,把o(n)降到了o(1)。
当找到这个目标socket的对应事件后,会把相应的文件描述符节点加入到就绪队列,同时会等待epoll_wait()的调用。这里注意:并不是真正加到就绪队列,而是我们为了描述方便所以才这么表述,实际上只是在红黑树改变了节点的指向。
epoll_wait()调用该事件的时候,会把已经发生的事件节点拷贝回用户态,同时会唤起相对应的阻塞进程。这里只是拷贝了一次,而且区别于select、poll只拷贝已经发生了的事件节点。
了解了epoll的工作方式,我们接下来对于代码的理解就更为清楚了。
继续接上面代码:
while (true)
{
// 等待监视的socket有事件发生。
int infds=epoll_wait(epollfd,evs,10,-1);
// 返回失败。
if (infds < 0)
{
perror("epoll() failed"); break;
}
// 超时。
if (infds == 0)
{
printf("epoll() timeout.\n"); continue;
}
epoll_wait()
第一个参数:eventpoll的返回值,也就是eventpoll对应的文件描述符
第二个参数:把内核已经就绪的事件拷贝到某个数组
第三个参数:evs数组的元素个数
第四个参数:超时时间
咱们继续接着聊 ,接上面代码:
// 如果infds>0,表示有事件发生的socket的数量。
for (int ii=0;ii<infds;ii++) // 遍历epoll返回的数组evs。
{
// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
printf ("accept client(socket=%d) ok.\n",clientsock);
// 为新客户端准备读事件,并添加到epoll中。
ev.data.fd=clientsock;
ev.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
}
如果是listensock接收到了事件,表示有客户端连接,那么把客户端连接的socket(文件描述符)加入到epoll中,同时也为他分配红黑树的节点。 (接下面代码)
else
{
// 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
char buffer[1024]; // 存放从客户端读取的数据。
memset(buffer,0,sizeof(buffer));
if (recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0)
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
close(evs[ii].data.fd); // 关闭客户端的socket
// 从epollfd中删除客户端的socket,如果socket被关闭了,会自动从epollfd中删除,所以,以下代码不必启用。
// epoll_ctl(epollfd,EPOLL_CTL_DEL,evs[ii].data.fd,0);
}
如果不是listensock有事件:
(1)客户端发过来数据
(2)客户端连接已经断开
如果是客户端断开连接,我们需要从eventpoll中删除它。
接下面代码:
else
{
// 如果客户端有报文发过来。
printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);
处理客户端的报文
}
}
}
}
return 0;
}
继续处理客户端的报文。
以上就是epoll接收数据的整个流程。
所有代码:
int main(int argc,char *argv[])
{
// 初始化服务端用于监听的socket。
int listensock = initserver(5005);
printf("listensock=%d\n",listensock);
if (listensock < 0) { printf("initserver() failed.\n"); return -1; }
// 创建epoll句柄。
int epollfd=epoll_create(1);
// 为服务端的listensock准备读事件。
epoll_event ev; // 声明事件的数据结构。
ev.data.fd=listensock; // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。
// ev.data.ptr=(void*)"超女"; // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。
ev.events=EPOLLIN; // 打算让epoll监视listensock的读事件。
epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev); // 把需要监视的socket和事件加入epollfd中。
epoll_event evs[10]; // 存放epoll返回的事件。
while (true) // 事件循环。
{
// 等待监视的socket有事件发生。
int infds=epoll_wait(epollfd,evs,10,-1);
// 返回失败。
if (infds < 0)
{
perror("epoll() failed"); break;
}
// 超时。
if (infds == 0)
{
printf("epoll() timeout.\n"); continue;
}
// 如果infds>0,表示有事件发生的socket的数量。
for (int ii=0;ii<infds;ii++) // 遍历epoll返回的数组evs。
{
// printf("ptr=%s,events=%d\n",evs[ii].data.ptr,evs[ii].events);
// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
printf ("accept client(socket=%d) ok.\n",clientsock);
// 为新客户端准备读事件,并添加到epoll中。
ev.data.fd=clientsock;
ev.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
}
else
{
// 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
char buffer[1024]; // 存放从客户端读取的数据。
memset(buffer,0,sizeof(buffer));
if (recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0)
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
close(evs[ii].data.fd); // 关闭客户端的socket
// 从epollfd中删除客户端的socket,如果socket被关闭了,会自动从epollfd中删除,所以,以下代码不必启用。
// epoll_ctl(epollfd,EPOLL_CTL_DEL,evs[ii].data.fd,0);
}
else
{
// 如果客户端有报文发过来。
printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);
// 把接收到的报文内容原封不动的发回去。
处理客户端代码
}
}
}
}
return 0;
}
水平触发与边缘触发
所谓的水平触发,个人理解就是,当服务端发现有客户端的数据发送过来的时候,服务端会一直通知用户有数据到达,直到数据在缓冲区取走为止。类似于,一个很负责的快递员,当你有包裹到了,他会一直不停的给你发信息,叫你去拿,直到你所有的包裹被拿完位置,他才停止发信息。
边缘触发,个人理解就是,“不太可靠”的发信息的方式,类似于一个“不负责”快递员,当你有包裹到达了,他只会通知你一次,不管你拿没拿完,当下次还有包裹到达,他就再通知你一次。
总结
标签:listensock,socket,epoll,int,Linux,服务端,客户端 From: https://www.cnblogs.com/o-O-oO/p/18596203原创 Linux开发架构之路