如果处理简单的客户端请求单线程即可处理,此方法较为简单不做阐述。如果需要处理复杂的工作逻辑或者需要模拟机器人请求大量的服务器,需要两个线程,一个任务线程,一个工作线程,可达到每秒平均处理数千的并发量。
任务线程:
从任务队列获取任务,创建socket并设置非阻塞,将该socket加入epoll句柄,connect目标服务器。派棚返
sockfd = socket(AF_INET, SOCK_STREAM, 0)
fcntl(sockfd, F_SETFL, fcntl (socket_fd, F_GETFL,0) | O_NONBLOCK)
struct epoll_event ev
ev.events = EPOLLIN | EPOLLOUT | EPOLLET
ev.data.ptr = my_ev(my_ev保存了后续处理的所有数据);
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev)
connect(sockfd,(struct sockaddr*)&my_ev->dest,sizeof(sockaddr_in))(无需判断connect返回值,后续会在epoll事件中触发)
工作线程:
等待epoll事件触发,如果尘饥返回(EPOLLHUP | EPOLLERR),则证明连接出错(目标ip不存在,目标端口不在监听状态等原因)。如果返回EPOLLOUT,则证明连接成功,可以进行写 *** 作。如果返回EPOLLIN | EPOLLOUT,则证明可读可写,这时候就需要按照自定义的数据来判断了,如过接收完成则进行处理后续工作逻辑。无论事件成功或者失败处理完之后均要做后续清理工作。
epoll_wait(epfd,events,YOUR_SIZE,-1)
error_handle()
do_write()
do_read()
clean()
https特殊性:
http在tcp连接成功之后直接开始与对端交互,比较简单,此处省略,需要注意的是所有的收发数据均在非阻塞模式下进行,关于非阻塞socket的使用可以参考另一篇文章 非阻塞socket使用 。此文所使用的https库为openssl,tcp建立连接成功后进行ssl连接,连接时间可能较长,由于全部使用非阻塞,所以ssl连接是否成功需要事件返回后多次判断。
连接准备工作(ev为自定义数据的指针)
ev->ctx = SSL_CTX_new(SSLv23_client_method())
ev->ssl = SSL_new(ev->ctx)
SSL_set_mode(ev->ssl,SSL_MODE_ENABLE_PARTIAL_WRITE)
SSL_set_fd(ev->ssl,ev->sockfd)
ssl连接(需要多次判断)
int ssl_conn_ret = SSL_connect(ev->ssl)
if (1 == ssl_conn_ret)
{
// ssl连接成功,开始和对端交互
}
//连接失败,根据错误码判断是需要更多时间来完成连接还是连接失败。
int ssl_conn_err = SSL_get_error(ev->ssl,ssl_conn_ret)
if (SSL_ERROR_WANT_WRITE == ssl_conn_err||SSL_ERROR_WANT_READ == ssl_conn_err)
{
// 需要更多时间来完成ssl连接
}
else
{
//连接失,做清理工作
}
ssl读写判断
while((rtn = SSL_write(ev->ssl,ev->send_buf,ev->send_len)) <= 0)
{
和喊 int ssl_conn_err = SSL_get_error(ev->ssl,rtn)
if (SSL_ERROR_WANT_WRITE == ssl_conn_err &&rtn != 0)
{
continue
}
// 写失败
return -1
}
返回值判断
rtn = send_len:发送成功
rtn <send_len:发送失败
do
{
rtn = SSL_read(ev->ssl,recv_buf+size,1024)
if(rtn <0)
{
int ssl_conn_err = SSL_get_error(ev->ssl,rtn)
if (SSL_ERROR_WANT_READ == ssl_conn_err)
{
continue
}
//读失败
free(recv_buf)
return -1
}
size += rtn
}
while(rtn >0 &&size <1024*1024)
接收判断
size = 0:对端关闭
size >0:接收成功
http读写
发送
while((size = send(ev->sockfd,ev->send_buf,ev->send_len,0)) <0)
{
if(errno == EINTR || errno == EAGAIN)
{
continue
}
//失败
return -1
}
返回值判断
size = send_len:发送成功
size <send_len:发送失败
接收
do
{
rtn = recv(ev->sockfd,recv_buf+size,1024,0)
if(rtn <0)
{
if((errno == EINTR || errno == EAGAIN))
{
continue
}
//接收失败
free(recv_buf)
return -1
}
size += rtn
}
while(rtn >0 &&size <1024*1023)
接收判断
size = 0:对端关闭
size >0:接收成功
注意:
send分为阻塞和非阻塞,阻塞模式下,如果正常的话,会直到把你所需要发送的数据发完再返回;非阻塞,会根据你的socket在底层的可用缓冲区的大小,来将你的缓冲区当中的数据拷贝过去,有多大缓冲区就拷贝多少,缓冲区满了就立即返回,这个时候的返回值,只表示拷贝到缓冲区多少数据,但是并不代表发送多少数据,此时本程序直接认为发送失败,具体原因未知,可能缓冲区满了或者内存不够什么原因,
最近一段时间看epoll的源码,看的抓耳挠腮。本着分享的原则,分享一下我对epoll的理解,注意:本文并不能让你从零开始学epoll,而是希望在你看epoll源码也学的抓耳挠腮的时候,看到本文能对你有一丢小小的帮助。
本篇文章并不会设计到具体源码,只是涉及到epoll的整个数据交互的流程。
一个应用程序,想要使用epoll模型,首先会创建一个epoll模型。这里是在内核创建一个epoll模型,你可以把哪祥仿这个模型看做一个java对象。这个对象里有一个阻塞列表,一个就绪列表,一个红黑树。
每一次通讯,客户端都会建立一个socket,socket有一个文件描述符fd,系统通过这个fd去 *** 作socket,系统会封装一个epitem(红黑树的节点),这个epitem包含socket和一个回调事件,当然还有其他属性,我们暂且不提。
然后内核会把这个epitem添加到红黑树,当然,也可以修改和删除事件。
用户进程去就绪列表拿就绪事件。拿到就返回,拿不到就进入epoll阻塞列表,当然也可以设置超时参数为0表示拿不到也立即返回。
回调事件会根据监听的事件类型,把fd放到就绪列表。然后去通知阻塞进程。阻塞进程从就绪列表拿走就绪事件,也就是把就绪事件从内核空间拷贝到用户空间。
这是我对epoll模型的大概描述,接下宴唤来,从交互的层面再说一说:
当一个数据包从网络传输过来,包含了协议,发送端ip 和端口,目标端ip和接口。内核根据这五个要素去找到对应的socket,就可以拿到对应的fd,有了fd,就可以去红黑树里找epitem,找到了epitem就可以去触发回调事件。回调事件就可以把就绪事件放到就绪列表。
输入netstat命名就可以查看李纤系统的所有活跃的socket链接:
例子里,本地机器有两个socket链接和172.217.27.42.443端口建立了链接,当网络数据包传输过来,发送端ip和端口号就是172.217.27.42.443,接收端可能是54040和54035端口,用发送端ip和端口号为参数去对应foreign address,用接收端参数去对应local address,就可以找到具体的哪一个socket连接。
如果在拷贝就绪事件的时候,出现了新的就绪事件怎么办,其实还有一个备用链表,拷贝事件的时候,如果有就绪事件产生,先放到备用链表,拷贝完成再把备用链表的就绪事件放到就绪列表里。
如果想看细节,还是得自己撸源码,别人讲只能听个大概。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)