linux中epoll+socket实战

这篇具有很好参考价值的文章主要介绍了linux中epoll+socket实战。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

参考

Linux Epoll使用详解
利用Socket网络编程实现TCP时遇到的无法立刻建立第二次连接传输数据的问题
TCP面试常见题:time_wait状态产生的原因,危害,如何避免
计算机网络 | C++实现TCP/UDP的socket通信
爱编程的大丙

前言

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。

相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明:

#define __FD_SETSIZE    1024

表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

linux中epoll+socket实战

案例

  • 单线程epoll:redis
    纯粹内容操作,只有一个epoll,没有多线程加锁以及切换
  • 多线程epoll:nettyserver
  • 多核epoll:ntyco
  • 多进程epoll:nginx

一、epoll的基本使用

epoll的接口非常简单,一共就三个函数:

  • epoll_create:创建一个epoll的句柄 ,相当于聘请了一个快读员
  • epoll_ctl:epoll的事件注册函数,相当于快递员对住户的增删改查
  • epoll_wait:等待事件的产生,相当于快读员等待不停的收发快递

首先是epoll_create函数:

int epoll_create (int __size)

它只有一个参数,用来告诉系统这个监听的数目一共有多大,但这个参数不同于select中的第一个参数。而是我们真实要监听的文件数量(事实上,在新版的Linux内核中,该值已经被忽略,只要大于0即可)

需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭

然后是epoll_ctl函数:

int epoll_ctl (int __epfd, int __op, int __fd,epoll_event *__event)

它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

  • 第一个参数是epoll_create的返回值
  • 第二个参数表示要执行的动作,有以下几种参数可填:
    EPOLL_CTL_ADD:注册新的文件标识符到epfd中
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件
    EPOLL_CTL_DEL:从epfd中删除一个文件标识符
  • 第三个参数是需要监听的文件标识符
  • 第四个参数是告诉内核需要监听什么事,

结构体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_PACKED;

其中events参数可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

而data则一般用于携带附加数据,比如网络编程中的套接字文件的fd

最后是epoll_wait函数:

TCP接收到事件时会回调触发epoll_wait 中事件

int epoll_wait (int __epfd, struct epoll_event *__events,int __maxevents, int __timeout)
  • 第一个参数就是前面epoll_create函数的返回值
  • 参数events用来从系统内核得到事件的集合
  • maxevents告知内核这个events有多大,不能大于创建epoll_create()时的size
  • 参数timeout是超时时间(毫秒,0会立即返回,-1将永久阻塞(直到有事件发生)
  • 该函数返回需要处理的事件数目,如返回0表示已超时。

关于ET(边沿触发)、LT(水平触发)两种工作模式可以得出这样的结论:

ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.

二、使用

前提先包含一个头文件:

#include <sys/epoll.h> 

然后通过create_epoll来创建一个epoll,其参数为你epoll所支持的最大数量(已忽略)。

这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close来关闭这个创建出来的epoll句柄

之后在你的网络主循环里面,每次调用epoll_wait来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd为用epoll_create创建之后的句柄
events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。
max_events是当前需要监听的所有socket句柄数。
最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件发生,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则等待。
一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

epoll_wait函数之后应该是一个循环,遍利所有的事件。

代码简易实现


#include<sys/socket.h> // socket依赖
#include<arpa/inet.h> // socket依赖
#include <unistd.h> // close依赖
#include<sys/epoll.h>
#include<cstring>
#include<iostream>

#include "log.h"
using namespace std;

#define MAX_LISTEN_SOCKET 10
#define SOCKET_PORT 5000

int main(){

    int sockfd = 0;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    LOG_INFO("监听套接字文件描述符:%d\n", sockfd);

    sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(SOCKET_PORT);
	// addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_addr.s_addr = htons(INADDR_ANY);

	// 一般在一个端口释放后需要等一段时间才能重新启用,因此需要借助SO_REUSEADDR来使端口重新启用。解决服务端异常退出之后,再次启动服务端,客户端无法使用同一个端口连接socket的问题
	int out = 1;
    int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &out, sizeof(out));
	if (ret < 0) {
		LOG_ERROR("setsockopt");
		return -1;
	}
	ret = bind(sockfd, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		LOG_ERROR("绑定失败!\n");
		return -1;
	}
    ret = listen(sockfd, 5);
    if (ret == -1) {
		LOG_ERROR("监听失败!\n");
		return -1;
	}
	sockaddr_in cliAddr;
	socklen_t len = sizeof(cliAddr);


    // 创建epoll ,int epoll_create(int size); size参数 相当于提供给内核一个提示,当前需要监听的fd个数
    int fdEp = epoll_create(MAX_LISTEN_SOCKET);
	epoll_event eve;
	eve.data.fd = sockfd;
	eve.events = EPOLLIN ;

    // 注册事件
	ret = epoll_ctl(fdEp, EPOLL_CTL_ADD, sockfd, &eve);
    if(ret == -1) {
        LOG_ERROR("epoll注册失败\n");
        close(sockfd);
		return -1;
    }
	LOG_INFO("开始监听!");
    epoll_event events[MAX_LISTEN_SOCKET];
    while (1) {
		/* timeout 500 epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
		*   __epfd:由epoll_create 生成的epoll专用的文件描述符 
			__events:分配好的epoll_event结构体数组 用于接收fd对象的缓冲区
			__maxevents:每次能够处理的事件数 这一次调用可以接收多少准备好的fd对象,通常设置为events参数的长度
			__timeout:如果没有准备好的事件对象,那么等待多久返回,-1阻塞,0非阻塞
			返回值:返回events缓冲区中有效的fd个数,也即准备好事件的fd个数
		*/
		int eventCount = epoll_wait(fdEp, events, MAX_LISTEN_SOCKET, 500);
        if (eventCount == -1) {
			LOG_ERROR("select 出错!\n");
			break;
		}else if (eventCount == 0) {
			continue;
		}
		LOG_INFO("监听到事件数量:%d\n",eventCount);
        for (int i = 0; i < eventCount; i++) {
			// 如果是服务器fd并且是读事件,则接收连接
			if (events[i].data.fd == sockfd) {
				// 是否读事件
				if(events[i].events & EPOLLIN){
					int clisock = accept(sockfd, (sockaddr*)&cliAddr, &len);
					if (clisock == -1) {
						LOG_ERROR("接收客户端错误\n");
						continue;
					}else{
						LOG_INFO("接收到客户端连接");
					}

					eve.data.fd = clisock;
					eve.events = EPOLLIN;
					epoll_ctl(fdEp, EPOLL_CTL_ADD, clisock, &eve);
				}else{
					LOG_INFO("服务器其他事件");
				}

			}
			else {
				// 对非服务器socket进行处理
				if (events[i].events & EPOLLIN) {
					char buf[0xFF]{};
					size_t len = recv(events[i].data.fd, buf, 0xFF, 0);
					if (len < 0) {
						LOG_ERROR("客户端异常!");
						epoll_ctl(fdEp, EPOLL_CTL_DEL, events[i].data.fd, NULL);
						close(events[i].data.fd);
						break;
					}else if (len == 0) {
						LOG_INFO("客户端已经断开连接!");
						epoll_ctl(fdEp, EPOLL_CTL_DEL, events[i].data.fd, NULL);
						close(events[i].data.fd);
						break;
					}else{
						LOG_INFO("客户端接收到数据:%s",buf);
					}
					send(events[i].data.fd, buf, len, 0);
				}
				else if(events[i].events & EPOLLOUT) {
					LOG_INFO("客户端 EPOLLOUT");
				}
				else {
					LOG_INFO("客户端其他事件");
				}
				
			}
		}
    }
    close(fdEp);
	close(sockfd);
    return 0;
}

三、注意问题

因TIME_WAIT导致的客户端断开之后无法立马重连

问题描述

在测试时发现,当客户端和服务器端建立第一次连接并成功发送数据后,关闭连接,想要立刻建立第二次连接发送数据时,客户端会无法再次连接。大约2分钟(MSL)之后才可以重新连接

time_wait状态产生的原因

  • 1)为实现TCP全双工连接的可靠释放
    由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。

  • 2)为使旧的数据包在网络因过期而消失
    为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。

  • 3)总结
    具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。

原因分析

  • 1、TCP的可靠连接
    众所周知,TCP是一种可靠的连接,而断开连接需要“四次挥手”,为保证传输的可靠性,需要有一个阶段来保证能够对出错或丢失的数据包进行重发,那么这个阶段就是TIME_WAIT状态。

  • 2、允许老的重复分解在网络中消失
    关于这句话,看起来可能没那么容易理解,按通俗的话来讲就是:在第一次使用bind()函数绑定端口后,再次启动程序,导致地址复用,从而bind()函数error。

  • MSL(Maximum Segment Lifetime)最大报文生存时间
    每个TCP实现必须选择一个MSL。它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL时间。RFC 793指出MSL为2分钟,现实中常用30秒或1分钟。

  • 2MSL
    当TCP执行主动关闭,并发出最后一个ACK,该链接必须在TIME_WAIT状态下停留的时间为2MSL。这样可以(1)让TCP再次发送最后的ACK以防这个ACK丢失(被动关闭的一方超时并重发最后的FIN);保证TCP的可靠的全双工连接的终止。(2)允许老的重复分节在网络中消失。参考文章《unix网络编程》(3)TCP连接的建立和终止 在TIME_WAIT状态 时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置 SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

有了这个思路,那么如果能够让一个端口在短时间内连续使用,我们的问题不就可以解决了吗?

SO_REUSEADDR,用于对TCP套接字处于TIME_WAIT状态下的socket,当你想要在端口被释放后立即可以使用,那么应该在bind之前调用SO_REUSEADDR。

int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &out, sizeof(out));

总结:一般在一个端口释放后需要等一段时间才能重新启用,因此需要借助SO_REUSEADDR来使端口重新启用。

四、socket

缓冲区大小

建立一个socket,通过getsockopt获取缓冲区的值如下:
发送缓冲区大小:SNDBufSize = 16384
接收缓冲区大小:RCVBufSize = 87380
通过代码设置缓存区

bool Socket::set_send_buffer(int size)
{
    int buff_size = size;
    if (setsockopt(m_sockfd, SOL_SOCKET, SO_SNDBUF, &buff_size, sizeof(buff_size)) < 0)
    {
        LOG_ERROR("socket set send buffer error: errno=%d errstr=%s", errno, strerror(errno));
        return false;
    }
    return true;
}

bool Socket::set_recv_buffer(int size)
{
    int buff_size = size;
    if (setsockopt(m_sockfd, SOL_SOCKET, SO_RCVBUF, &buff_size, sizeof(buff_size)) < 0)
    {
        LOG_ERROR("socket set recv buffer error: errno=%d errstr=%s", errno, strerror(errno));
        return false;
    }
    return true;
}

如果接收缓存区设置过小,比如设置10,会导致recv循环读取返回值为-1,此时socket并没有断开,循环继续是可以读取到数据的,但是会导致比如上传文件时速度变慢,所以缓存区大小建议默认或者设置大一些文章来源地址https://www.toymoban.com/news/detail-444965.html

set_keep_alive

set_linger

/* 优雅关闭: 直到所剩数据发送完毕或超时 */

到了这里,关于linux中epoll+socket实战的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 02-Linux-IO多路复用之select、poll和epoll详解

    前言: 在linux系统中,实际上所有的 I/O 设备都被抽象为了文件这个概念,一切皆文件,磁盘、网络数据、终端,甚至进程间通信工具管道 pipe 等都被当做文件对待。 在了解多路复用 select、poll、epoll 实现之前,我们先简单回忆复习以下两个概念: 一、什么是多路复用: 多路

    2024年02月10日
    浏览(58)
  • Linux之epoll理解

    IO多路复用有几种实现方式:select poll和epoll。本篇文章对epoll进行总结理解。 IO多路复用的含义,我个人的理解是通过一个线程实现对多个socket的侦听,epoll与select和poll的区别是epoll效率最高。 select的最高管理1024个socket并且是通过轮询的方式实现的管理,管理的socket个数越多

    2024年02月07日
    浏览(24)
  • linux--epoll

    参考文献 https://www.cnblogs.com/lojunren/p/3856290.html https://www.51cto.com/article/717096.html linux下的I/O复用epoll详解 要深刻理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、链表。 首先需要了解什么是IO多路复用 IO多路复用是一种同步的IO模型。利用IO多路复用模型,可以实现一个

    2024年02月12日
    浏览(18)
  • Linux_epoll

    一个顾客来就餐,一个服务员在顾客点菜期间全程陪同服务! 一个人来就餐,一个服务员去服务,然后客人会看菜单,点菜。 服务员将菜单给后厨。 二个人来就餐,二个服务员去服务…… 五个人来就餐,五个服务员去服务…… 一百个人呢? 如果你是餐厅的老板,你改怎么

    2024年02月08日
    浏览(18)
  • 【Linux】多路转接 -- epoll

    epoll系统调用和select以及poll是一样的,都是可以让我们的程序同时监视多个文件描述符上的事件是否就绪。 epoll在命名上比poll多了一个poll,这个e可以理解为extend, epoll就是为了同时处理大量文件描述符而改进的poll。 epoll在2.5.44内核中被引进,它几乎具备了select和poll的所有

    2024年02月14日
    浏览(53)
  • Linux多路IO复用:epoll

            epoll是为克服select、poll每次监听都需要在用户、内核空间反复拷贝,以及需要用户程序自己遍历发现有变化的文件描述符的缺点的多路IO复用技术。 epoll原理 创建内核空间的红黑树; 将需要监听的文件描述符上树; 内核监听红黑树上文件描述符的变化; 返回有变化

    2024年02月04日
    浏览(49)
  • linux poll,epoll,select的区别

    epoll中红黑树的作用? 红黑树(rbtree)、以及epoll的实现原理_epoll 红黑树_For Nine的博客-CSDN博客 红黑树和epoll_wait的关系? epoll_wait/就绪list和红黑树的关系 - 知乎 其他区别: 1. select 在linux内核中限制了能监听的数目上限。32位是1024,64位是2048 2. poll是将监听的对象改成了链表

    2024年02月03日
    浏览(40)
  • 【Linux】高级IO --- 多路转接,select,poll,epoll

    所有通过捷径所获取的快乐,无论是金钱、性还是名望,最终都会给自己带来痛苦 1. 后端服务器最常用的网络IO设计模式其实就是Reactor,也称为反应堆模式,Reactor是单进程,单线程的,但他能够处理多客户端向服务器发起的网络IO请求,正因为他是单执行流,所以他的成本就

    2024年02月09日
    浏览(66)
  • Linux网络编程(epoll的ET模式和LT模式)

    本篇文章主要来讲解epoll的ET模式和LT模式,epoll中有两种模式可以选择一种是ET模式(边缘触发模式),另一种是LT模式(水平触发模式) 在水平触发模式下,当一个文件描述符上的I/O事件就绪时,epoll会立即通知应用程序,然后应用程序可以对就绪事件进行处理。即,只要文件描述

    2024年02月12日
    浏览(38)
  • 高性能网络设计秘笈:深入剖析Linux网络IO与epoll

    本文分享自华为云社区《高性能网络设计秘笈:深入剖析Linux网络IO与epoll》,作者: Lion Long 。 epoll是Linux内核中一种可扩展的IO事件处理机制,可替代select和poll的系统调用。处理百万级并发访问性能更佳。 (1) 文件描述符越多,性能越差。 单个进程中能够监视的文件描述

    2024年02月16日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包