首页 > 系统相关 >Linux socket 通信和 select 以及 epoll 函数

Linux socket 通信和 select 以及 epoll 函数

时间:2023-11-13 16:49:08浏览次数:40  
标签:文件 socket epoll int 描述符 fd Linux addr

1.socket 通信

1.1 大小端转换

  • 主机字节序 16 位值 <==> 网络字节序 16 位值
  • 主机字节序 32 位值 <==> 网络字节序 32 位值
#include <arpa/inet.h>

// 主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);    // host to net unsigned short 可用端口转换
unit32_t htonl(unit32_t hostlong);     // host to net unsigned int 可用ip地址转换

// 网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort);
unit32_t ntohl(unit32_t netlong);

1.2 IP地址转换

  • 主机字节序的字符串IP地址  <==> 网络字节序的整形IP地址
#include <arpa/inet.h>

// 主机字节序IP to 网络字节序(大端)IP
int inet_pton(int af, const char* src, void* dst);
/*  参数:
        af: 地址族协议 AF_INET(ipv4), AF_INET6(ipv6)
        src: 主机字节序的字符串类型的IP地址,被转换的数据
        dst: 传出参数, 存储转换之后的大端的IP地址
    返回值: 成功0; 失败-1                */

const char *int_ntop(int af, const void *src, char *dst, socklen_t size);
/*  参数:
        af: 地址族协议 AF_INET; AF_INET6
        src: 传入参数, 要被转换的数据指针, 指向内存中存储的大端IP地址(整形数)
        dst: 传出参数, 指针指向主机字节序, 字符串类型的IP地址
        size: dst指向的内存的大小
    返回值: 
        成功: 返回指向 dst 指针指向的内存
        失败: NULL                          */

1.3 套接字相关函数

1.3.1 socket 创建

#include <arpa/inet.h>  // 该头文件包括了 <sys/socket.h>

int socket(int domain, int type, int protocol);
/* 参数:
        domain: AF_INET; AF_INET6
        type:
            SOCK_STREAM: 流式传输协议
            SOCK_DGRAM: 报式传输协议
        protocol: 默认写0
            流式传输默认 TCP
            报式传输默认 UDP
    返回值:
        成功: 返回文件描述符
        失败: 返回-1                      */

1.3.2 bind 绑定套接字

  将监听的套接字和本地IP和端口进行关联

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*  参数:
        sockfd: 用于监听的套接字, 通过socket创建
        addr: 将本地ip和端口初始化给该结构体(需要用大端)
            绑定的时候服务器一般ip使用宏 INADDR_ANY (0)
            0 表示绑定该主机的所有ip地址, 多个网卡可能有多个ip
        addrlen: 记录第二个指针指向内存的大小, sizeof(struct sockaddr)
    返回值:
        成功0, 失败-1                       */

1.3.3 listen 监听套接字

  给监听的套接字设置监听,开始检测客户端链接

int listen(int sockfd, int backlog);
/*  参数:
        sockfd: 监听的套接字, 设置监听前需要先绑定
        backlog: 可以同时检测的新的连接个数, 最大值128
    返回值:
        成功0, 失败-1                */

1.3.4 accept 接收客户端连接

  等待并接受客户端的连接,阻塞函数,没有客户端连接就阻塞,监听的文件描述符缓冲区没有数据就阻塞,有数据就解除阻塞建立连接,连接建立成功后,返回一个通信用的文件描述符

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*  参数:
        sockfd: 监听的文件描述符
        addr: 传出参数, 保存了建立连接的客户端的地址信息(ip 端口) -> 大端存储
            不需要客户端信息则填NULL
        addrlen: 传入传出参数, 传入addr指针指向的内存大小, 传出存储了客户端信息的addr内存大小
            addr为NULL,则该参数也填NULL                    
    返回值:
        文件描述符或-1                           */

1.3.5 read、recv 读数据

  读取数据,如果数据区空会读堵塞

ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
/*  参数:
        sockfd: 通信文件描述符
            服务器端: accept 返回值
            客户端: socket 创建得到, connect 初始化连接
        buf: 存储接收到的数据, 数据来自文件描述符对应的缓冲区
        size: buf 的内存容量
        flag: 默认属性0即可
    返回值:
        >0: 读到的字节数
        =0: 对方断开连接
        -1: 读异常, 失败                      */

1.3.6 write、send 写数据

  发送数据,如果数据区满会写阻塞

ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
/*  参数:
        fd: 通信的文件描述符
        buf: 要发送的数据缓冲区
        len: 缓冲区大小
        flags: 使用默认属性0即可             */

1.3.7 connect 客户端连接

  客户端连接服务器

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*  参数:
        sockfd: 通信文件描述符
        addr: 连接服务器的ip和端口信息(需要使用大端描述)
        addrlen: 参数addr指向的内存大小
    返回值:
        成功0; 失败-1                   */

1.4 套接字选项

  该函数用来设置套接字选项,端口复用、广播、组播等,下面是端口复用的参数解释

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
/*  参数
        sockfd: 监听的套接字
        level: SOL_SOCKET
        optname: SO_REUSEPORT
        optval: 实际类型int
            0 -> 端口不复用
            1 -> 端口复用
        optlen: optval 指针指向的内存大小 sizeof(int)
    返回值
        成功0, 失败-1                        */

2. IO多路复用

2.1 select

  • 构造一个文件描述符列表,将要监听的文件描述符添加到该列表中(最大支持1024,线性描述)
  • 调用一个函数,监听该表中的文件描述符,知道这些描述符中的一个进行IO操作时,函数返回(该函数为阻塞函数,检测由内核完成)
    • 读集合:检测文件描述符列表的读缓冲区
      • 监听的文件描述符:新客户端连接
      • 通信的文件描述符:新数据到达
    • 写集合:内核检测集合中文件描述符是否可写
      • 通信的文件描述符
    • 异常集合:检测文件描述符是否有异常
  • 返回时,告诉进程有哪些描述符需要进行IO操作
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*  参数:
        nfds: 下面三个集合中, 最大文件描述符值 + 1
        readfds: 传出传出参数,读集合,检测若干文件描述符的读缓冲区(新连接 / 新数据)
        writefds: 传入传出参数,写集合,检测若干文件描述符的写缓冲区(一般都可写,很少用)
        execptfds: 传入传出参数,异常集合
        timeout: 表示时间段,最长检测多长时间,超过这个时间还在阻塞就解除阻塞
            NULL 一直阻塞等待; 0 函数调用后立刻返回
    返回值:
        >0: 检测完成后,满足条件的总个数
        =0: 超时强制返回
        - 1: 失败                                            */

  timeval 结构体

struct timeval {
    time_t         tv-sec;
    suseconds_t    tv_usec;
};

  fd_set 文件描述符集合(位操作)操作函数

void FD_CLR(int fd, fd_set *set);     // 删除fd
int FD_ISSET(int fd, fd_set *set);    // 判断fd是否在集合
void FD_SET(int fd, fd_set *set);     // 添加fd
void FD_ZERO(fd_set *set);            // 清空fd(初始化)

2.2 epoll

  在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统。把原先的select/poll调用分成了3个部分:

1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

3)调用epoll_wait收集发生的事件的连接

如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

2.2.1 epoll_create 创建 epoll

#include <sys/epoll.h>
int epoll_create(int size);
/*  参数:
        size: 没有实际意义, 大于0即可
    返回值:
        成功: 返回一个文件描述符
                该文件描述符对应的指针存储了红黑树的根节点
        失败: -1                             */

2.2.2 epoll_ctl 操作epoll

  实现对 epoll 树上节点的操作(添加、修改、删除节点)

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/* 参数:
        epfd: epoll_create() 函数的返回值,找到对应的epoll实例
        op: 
            EPOLL_CTL_ADD: 添加新节点
            EPOLL_CTL_MOD: 修改已经添加到树上节点的属性(读改写)
            EPOLL_CTL_DEL: 删除节点
        fd: 要操作的文件描述符
            添加 / 修改 / 删除(监听、通信)
        event: 对应的事件(若删除填NULL)
            EPOLLIN: 读事件
            EPOLLOUT: 写事件                     */
  • epoll_data
typedef union epoll_data{
    void      *ptr;
    int        fd;         // 该联合体常用这个
    uint32_t   u32;
    uint64_t   u64;
} epoll_data_t;
  • epoll_event
    • event 是位操作,EPOLLIN 检测写缓冲区,EPOLLOUT 检测读缓冲区
    • data.fd 等于 epoll_ctl 函数调用的第三个参数
struct epoll_event{
    uint32_t    event;    // Epoll events;
    epoll_data_t data;    // User data variable
};

2.2.3 epoll_wait

  阻塞函数,委托内核检测epoll树上文件描述符的状态,如果没有状态变化,默认一直阻塞

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*  参数:
        epfd: epoll_create() 的返回值, 找到epoll实例
        event: 传出参数,记录了这轮检测到epoll模型中有状态变化的文件描述符(结构体数组地址)
        maxevent: events数组的容量
        timeout: 超时时长 ms(-1一直阻塞; 0立即返回)
    返回值:
        成功: 有多少文件描述符发生变化                        */

2.2.4 Level triggered 水平模式(默认)

  LT(level triggered)是缺省的工作方式,同时支持 block 和 no-block socket。这种模式下,内核会通知文件描述符是否就绪,如果不进行任何操作,内核会一直通知你该文件描述符就绪

2.2.5 Edge triggered 边沿模式

  ET(edge triggered)是高速工作模式,只支持 no-block socket。这种模式下,如果接到通知,但是没有把数据从缓冲区读完,epoll_wait不会再次通知;直到再次接收到新数据也一样通知一次,但是此时他会接着上次的缓冲区数据读。

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;  // 设置文件描述符为边沿模式
    ev.data.fd = lfd;

  使用边沿模式读数据需要在收到消息后我们一般需要 while(1) 死循环读取数据直到缓冲区数据读完,所以需要设置文件描述符为非阻塞状态,让read可以非阻塞读取数据,通过 read 的返回值判断是否结束该死循环

int fcntl(int fd, int cmd, ...);

int flag = fcntl(cfd, F_GETFL);
flag = flag | O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);    //设置文件描述符为非阻塞, read函数再读取不会阻塞

  最后因为这里已经设置为非阻塞,可以根据read的返回值判断是否已经读完缓冲区了,如果读完了会有errno EAGAIN的错误码,根据该错误码跳出循环即可

while(1)
{
    int len = recv(curfd, buf, sizeof(buf), 0);
    if(len > 0)
        printf("打印接收的数据");
    else if( len == 0)
        printf("断开连接");
    else
    {
        if(errno==EAGAIN)
        {
            printf("数据读完了");
            break; // 跳出循环
        }
        perror("接收错误");
        exit(0);
    }
}

2.2.4 服务器代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

int main()
{
    // 1. 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // 2.将 套接字 和 ip端口 绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;  // ipv4
    addr.sin_addr.s_addr= INADDR_ANY;   // 0地址(本地任意地址)
    addr.sin_port = htons(8989);    // 端口转为大端
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind error");
        exit(2);
    }

    // 3.设置监听
    ret = listen(lfd, 128);
    if(ret == -1)
    {
        perror("listen error");
        exit(3);
    }

    // 4.初始化检测的集合
    int epfd = epoll_create(1);
    if(epfd == -1)
    {
        perror("epoll_create error");
        exit(4);
    }

    // 5.将要检测的节点添加到epoll树中
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if(ret == -1)
    {
        perror("epoll_ctl");
        exit(5);
    }

    // 6.委托内核检测epoll树中的文件描述符状态
    struct epoll_event evs[1024];
    int size = sizeof(evs) / sizeof(evs[0]);
    while(1)
    {
        int num = epoll_wait(epfd, evs, size, -1);  // 把文件描述符发生变化的储存到 evs 数组中
        printf("num = %d\n", num);
        // 遍历evs数组
        for(int i=0; i<num; i++)
        {
            int curfd = evs[i].data.fd;
            if(curfd == lfd)    // lfd 套接字状态改变说明有新链接请求
            {
                int cfd = accept(lfd, NULL, NULL);
                ev.events = EPOLLIN;
                ev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);   // 把新的链接加入到epoll树中
            }
            else    // 其他套接字状态改变说明有新数据抵达
            {
                char buf[1024];
                memset(buf, 0, sizeof(buf));
                int len = recv(curfd, buf, sizeof(buf), 0);
                if(len == 0)
                {
                    printf("客户端断开了链接...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else if(len>0)
                {
                    printf("recv data: %s\n");
                    send(curfd, buf, len, 0);
                }
                else
                {
                    perror("recv error");
                    exit(6);
                }
            }
        }
    }
}

 

标签:文件,socket,epoll,int,描述符,fd,Linux,addr
From: https://www.cnblogs.com/stux/p/17816974.html

相关文章

  • Linux Capabilities 简介
    Linux是一种安全的操作系统,它把所有的系统权限都赋予了一个单一的root用户,只给普通用户保留有限的权限。root用户拥有超级管理员权限,可以安装软件、允许某些服务、管理用户等。作为普通用户,如果想执行某些只有管理员才有权限的操作,以前只有两种办法:一是通过sudo提升权限,如......
  • linux帮助命令
    内部命令helphelp命令的功能是用于显示帮助信息,能够输出Shell内部命令的帮助内容,但对于外部命令则无法使用,需要用man或info命令进行查看了。语法格式help[参数]命令名常用参数-d:显示命令的简短描述-m:使用man手册格式显示帮助信息-s:显示短格式的帮助信息参考示例help......
  • Linux信息系统相关命令
    查看进程及关闭进程命令1、通过ps命令查看mysql进程:ps -aux |grep mysql2、通过top命令查看当前系统中CPU占用前三的进程:top   按shift+p3、通过kill命令杀死进程:kill   -9    进程号 查看系统监听端口查端口、进程号:netstat -anptu | grep......
  • Linux修改文件名命令是什么?
    Linux命令是用于在Linux操作系统中执行各种任务和操作的指令。在Linux中,提供了很多命令可以帮助我们完成各种各样的操作,比如重启网卡、修改文件名、复制目录或文件等,那么Linux修改文件名命令是什么?我们简单来介绍一下。在Linux系统中,有多种命令可以用来修改文件名。以下是......
  • Veeam Agent for Linux 免费版
    免费的东西,多多推荐,个人和家用都不错,也有windows系统版本。首屈一指的Linux备份和恢复裸机恢复 备份整个Linux系统或特定文件控制台UI或命令行简单又免费的Linux备份—随时随地使用!备份和恢复 Linux实例 —无论是在内部还是云环境中—通常比较繁琐,需要较高成本和......
  • Linux认证 | RHCE是中级还是高级?含金量如何?
    红帽认证是一个完善的认证体系,分为三个等级:初级、中级和高级。其中,RHCE认证是中级认证。这意味着,获得RHCE认证需要先通过初级认证,即RedHatCertifiedSystemAdministrator(RHCSA)认证。通过RHCE认证后,可以进一步挑战高级认证,即RedHatCertifiedArchitect(RHCA)认证。下面我们就来了......
  • 前端建立WebSocket连接
    WebSockets是H5提供的在web应用程序中客户端与服务器端之间进行的非HTTP的通信机制。当服务器想向客户端发送数据时,可以立即将数据推送到客户端的浏览器中,无需重新建立连接。只要客户端有一个被打开的socket(套接字)并且与服务器建立了连接,服务器就可以把数据推送到这个socket上,......
  • 分析Linux kernel exception-基础篇【转】
    转自:https://blog.csdn.net/ldinvicible/article/details/50911947转载自MTKFAQ:KE概念AndroidOS由3层组成,最底层是kernel,上面是nativebin/lib,最上层是java层: 任何软件都有可能发生异常,比如野指针,跑飞、死锁等等。异常发生在kernel层,我们就叫它为KE(kernelexception),同理,......
  • linux 软件包
    安装rpm包RPM工具使用分为安装、查询、验证、更新、删除等操作参数:-i是install的意思,安装软件包-v显示附加信息,提供更多详细信息-V校验,对已经安装的软件进行校验-h--hash安装时输出####标记对已经安装过的软件包进行操作时,比如查找已经安装的某个包,卸载包等,使......
  • Linux服务器不自动杀死超内存Python程序导致服务器卡死掉线
    状态:Python处理大数据时,内存占用超过服务器可用内存,但是服务器并没有杀死该进程,而是被卡死无法通过ssh进入解决方向:一、设置系统内存限制:使用 ulimit-a查看系统参数ulimit-a 命令的结果中会显示各种资源的限制参数。以下是一些常见参数及其含义:corefilesize (ulimit......