翼度科技»论坛 云主机 LINUX 查看内容

Linux socket 通信和 select 以及 epoll 函数

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
1.socket 通信

1.1 大小端转换


  • 主机字节序 16 位值  网络字节序 16 位值
  • 主机字节序 32 位值  网络字节序 32 位值
  1. #include <arpa/inet.h>
  2. // 主机字节序转换为网络字节序
  3. uint16_t htons(uint16_t hostshort);    // host to net unsigned short 可用端口转换
  4. unit32_t htonl(unit32_t hostlong);     // host to net unsigned int 可用ip地址转换
  5. // 网络字节序转换为主机字节序
  6. uint16_t ntohs(uint16_t netshort);
  7. unit32_t ntohl(unit32_t netlong);
复制代码
1.2 IP地址转换


  • 主机字节序的字符串IP地址   网络字节序的整形IP地址
  1. #include <arpa/inet.h>
  2. // 主机字节序IP to 网络字节序(大端)IP
  3. int inet_pton(int af, const char* src, void* dst);
  4. /*  参数:
  5.         af: 地址族协议 AF_INET(ipv4), AF_INET6(ipv6)
  6.         src: 主机字节序的字符串类型的IP地址,被转换的数据
  7.         dst: 传出参数, 存储转换之后的大端的IP地址
  8.     返回值: 成功0; 失败-1                */
  9. const char *int_ntop(int af, const void *src, char *dst, socklen_t size);
  10. /*  参数:
  11.         af: 地址族协议 AF_INET; AF_INET6
  12.         src: 传入参数, 要被转换的数据指针, 指向内存中存储的大端IP地址(整形数)
  13.         dst: 传出参数, 指针指向主机字节序, 字符串类型的IP地址
  14.         size: dst指向的内存的大小
  15.     返回值:
  16.         成功: 返回指向 dst 指针指向的内存
  17.         失败: NULL                          */
复制代码
1.3 套接字相关函数

1.3.1 socket 创建
  1. #include <arpa/inet.h>  // 该头文件包括了 <sys/socket.h>
  2. int socket(int domain, int type, int protocol);
  3. /* 参数:
  4.         domain: AF_INET; AF_INET6
  5.         type:
  6.             SOCK_STREAM: 流式传输协议 TCP
  7.             SOCK_DGRAM: 报式传输协议 UDP
  8.         protocol: 默认写0
  9.             流式传输默认 TCP
  10.             报式传输默认 UDP
  11.     返回值:
  12.         成功: 返回文件描述符
  13.         失败: 返回-1                      */
复制代码
1.3.2 bind 绑定套接字

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

  给监听的套接字设置监听,开始检测客户端链接
  1. int listen(int sockfd, int backlog);
  2. /*  参数:
  3.         sockfd: 监听的套接字, 设置监听前需要先绑定
  4.         backlog: 可以同时检测的新的连接个数, 最大值128
  5.     返回值:
  6.         成功0, 失败-1                */
复制代码
1.3.4 accept 接收客户端连接

  等待并接受客户端的连接,阻塞函数,没有客户端连接就阻塞,监听的文件描述符缓冲区没有数据就阻塞,有数据就解除阻塞建立连接,连接建立成功后,返回一个通信用的文件描述符
  1. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  2. /*  参数:
  3.         sockfd: 监听的文件描述符
  4.         addr: 传出参数, 保存了建立连接的客户端的地址信息(ip 端口) -> 大端存储
  5.             不需要客户端信息则填NULL
  6.         addrlen: 传入传出参数, 传入addr指针指向的内存大小, 传出存储了客户端信息的addr内存大小
  7.             addr为NULL,则该参数也填NULL                    
  8.     返回值:
  9.         文件描述符或-1                           */
复制代码
1.3.5 read、recv 读数据

  读取数据,如果数据区空会读堵塞
  1. ssize_t read(int sockfd, void *buf, size_t size);
  2. ssize_t recv(int sockfd, void *buf, size_t size, int flags);
  3. /*  参数:
  4.         sockfd: 通信文件描述符
  5.             服务器端: accept 返回值
  6.             客户端: socket 创建得到, connect 初始化连接
  7.         buf: 存储接收到的数据, 数据来自文件描述符对应的缓冲区
  8.         size: buf 的内存容量
  9.         flag: 默认属性0即可
  10.     返回值:
  11.         >0: 读到的字节数
  12.         =0: 对方断开连接
  13.         -1: 读异常, 失败                      */
复制代码
1.3.6 write、send 写数据

  发送数据,如果数据区满会写阻塞
  1. ssize_t write(int fd, const void *buf, size_t len);
  2. ssize_t send(int fd, const void *buf, size_t len, int flags);
  3. /*  参数:
  4.         fd: 通信的文件描述符
  5.         buf: 要发送的数据缓冲区
  6.         len: 缓冲区大小
  7.         flags: 使用默认属性0即可             */
复制代码
1.3.7 recvfrom / sendto 发送接收


  • 报式传输协议发送
  1. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  2. /*  参数:
  3.         sockfd: 通信文件描述符
  4.         buf: 一块有效内存地址
  5.         len: 参数buf指向的内存地址大小
  6.         flags: 默认属性0即可
  7.         src_addr: 传出参数, 保存发送端的IP和端口(网络字节序), 不感兴趣可以NULL
  8.         addrlen: 传入传出参数, src_addr指针指向内存空间的大小, 如果src_addr为NULL, 则填NULL
  9.     返回值:
  10.         >0: 接收到的字节数;  -1: 失败                     */
复制代码

  • 报式传输协议接收
  1. ssize_t sendto(int sockfd, void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t *addrlen);
  2. /*  参数:
  3.         sockfd: 通信文件描述符
  4.         buf: 待发送的数据地址
  5.         len: 参数buf指向的内存地址大小
  6.         flags: 默认属性0即可
  7.         dest_addr: 传入参数, 接收端的IP和端口信息(网络字节序)
  8.         addrlen: 传入参数, src_addr指针指向内存空间的大小
  9.     返回值:
  10.         >0: 发送的字节数;  -1: 失败                     */
复制代码
1.3.8 connect 客户端连接

  客户端连接服务器
  1. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  2. /*  参数:
  3.         sockfd: 通信文件描述符
  4.         addr: 连接服务器的ip和端口信息(需要使用大端描述)
  5.         addrlen: 参数addr指向的内存大小
  6.     返回值:
  7.         成功0; 失败-1                   */
复制代码
1.4 套接字选项

  该函数用来设置套接字选项,端口复用、广播、组播等,下面是端口复用的参数解释
  1. int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  2. /*  参数
  3.         sockfd: 监听的套接字
  4.         level: SOL_SOCKET
  5.         optname: SO_REUSEPORT
  6.         optval: 实际类型int
  7.             0 -> 端口不复用
  8.             1 -> 端口复用
  9.         optlen: optval 指针指向的内存大小 sizeof(int)
  10.     返回值
  11.         成功0, 失败-1                        */
复制代码
2. IO多路复用

2.1 select


  • 构造一个文件描述符列表,将要监听的文件描述符添加到该列表中(最大支持1024,线性描述)调用一个函数,监听该表中的文件描述符,知道这些描述符中的一个进行IO操作时,函数返回(该函数为阻塞函数,检测由内核完成)

    • 读集合:检测文件描述符列表的读缓冲区

      • 监听的文件描述符:新客户端连接
      • 通信的文件描述符:新数据到达




    • 写集合:内核检测集合中文件描述符是否可写

      • 通信的文件描述符

    • 异常集合:检测文件描述符是否有异常

  • 返回时,告诉进程有哪些描述符需要进行IO操作
  1. #include <sys/select.h>
  2. int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  3. /*  参数:
  4.         nfds: 下面三个集合中, 最大文件描述符值 + 1
  5.         readfds: 传出传出参数,读集合,检测若干文件描述符的读缓冲区(新连接 / 新数据)
  6.         writefds: 传入传出参数,写集合,检测若干文件描述符的写缓冲区(一般都可写,很少用)
  7.         execptfds: 传入传出参数,异常集合
  8.         timeout: 表示时间段,最长检测多长时间,超过这个时间还在阻塞就解除阻塞
  9.             NULL 一直阻塞等待; 0 函数调用后立刻返回
  10.     返回值:
  11.         >0: 检测完成后,满足条件的总个数
  12.         =0: 超时强制返回
  13.         - 1: 失败                                            */
复制代码
  timeval 结构体
  1. struct timeval {
  2.     time_t         tv-sec;
  3.     suseconds_t    tv_usec;
  4. };
复制代码
  fd_set 文件描述符集合(位操作)操作函数
  1. void FD_CLR(int fd, fd_set *set);     // 删除fd
  2. int FD_ISSET(int fd, fd_set *set);    // 判断fd是否在集合
  3. void FD_SET(int fd, fd_set *set);     // 添加fd
  4. 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
  1. #include <sys/epoll.h>
  2. int epoll_create(int size);
  3. /*  参数:
  4.         size: 没有实际意义, 大于0即可
  5.     返回值:
  6.         成功: 返回一个文件描述符
  7.                 该文件描述符对应的指针存储了红黑树的根节点
  8.         失败: -1                             */
复制代码
2.2.2 epoll_ctl 操作epoll

  实现对 epoll 树上节点的操作(添加、修改、删除节点)
  1. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  2. /* 参数:
  3.         epfd: epoll_create() 函数的返回值,找到对应的epoll实例
  4.         op:
  5.             EPOLL_CTL_ADD: 添加新节点
  6.             EPOLL_CTL_MOD: 修改已经添加到树上节点的属性(读改写)
  7.             EPOLL_CTL_DEL: 删除节点
  8.         fd: 要操作的文件描述符
  9.             添加 / 修改 / 删除(监听、通信)
  10.         event: 对应的事件(若删除填NULL)
  11.             EPOLLIN: 读事件
  12.             EPOLLOUT: 写事件                     */
复制代码

  • epoll_data
  1. typedef union epoll_data{
  2.     void      *ptr;
  3.     int        fd;         // 该联合体常用这个
  4.     uint32_t   u32;
  5.     uint64_t   u64;
  6. } epoll_data_t;
复制代码

  • epoll_event

    • event 是位操作,EPOLLIN 检测写缓冲区,EPOLLOUT 检测读缓冲区
    • data.fd 等于 epoll_ctl 函数调用的第三个参数

  1. struct epoll_event{
  2.     uint32_t    event;    // Epoll events;
  3.     epoll_data_t data;    // User data variable
  4. };
复制代码
2.2.3 epoll_wait

  阻塞函数,委托内核检测epoll树上文件描述符的状态,如果没有状态变化,默认一直阻塞
  1. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  2. /*  参数:
  3.         epfd: epoll_create() 的返回值, 找到epoll实例
  4.         event: 传出参数,记录了这轮检测到epoll模型中有状态变化的文件描述符(结构体数组地址)
  5.         maxevent: events数组的容量
  6.         timeout: 超时时长 ms(-1一直阻塞; 0立即返回)
  7.     返回值:
  8.         成功: 有多少文件描述符发生变化                        */
复制代码
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不会再次通知;直到再次接收到新数据也一样通知一次,但是此时他会接着上次的缓冲区数据读。
  1.     struct epoll_event ev;
  2.     ev.events = EPOLLIN | EPOLLET;  // 设置文件描述符为边沿模式
  3.     ev.data.fd = lfd;
复制代码
  使用边沿模式读数据需要在收到消息后我们一般需要 while(1) 死循环读取数据直到缓冲区数据读完,所以需要设置文件描述符为非阻塞状态,让read可以非阻塞读取数据,通过 read 的返回值判断是否结束该死循环
  1. int fcntl(int fd, int cmd, ...);
  2. int flag = fcntl(cfd, F_GETFL);
  3. flag = flag | O_NONBLOCK;
  4. fcntl(cfd, F_SETFL, flag);    //设置文件描述符为非阻塞, read函数再读取不会阻塞
复制代码
  最后因为这里已经设置为非阻塞,可以根据read的返回值判断是否已经读完缓冲区了,如果读完了会有errno EAGAIN的错误码,根据该错误码跳出循环即可
  1. while(1)
  2. {
  3.     int len = recv(curfd, buf, sizeof(buf), 0);
  4.     if(len > 0)
  5.         printf("打印接收的数据");
  6.     else if( len == 0)
  7.         printf("断开连接");
  8.     else
  9.     {
  10.         if(errno==EAGAIN)
  11.         {
  12.             printf("数据读完了");
  13.             break; // 跳出循环
  14.         }
  15.         perror("接收错误");
  16.         exit(0);
  17.     }
  18. }
复制代码
3. 代码示例

3.1 服务器(TCP、epoll)
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. #include <arpa/inet.h>
  6. #include <sys/epoll.h>
  7. int main()
  8. {
  9.     // 1. 创建套接字
  10.     int lfd = socket(AF_INET, SOCK_STREAM, 0);
  11.     if(lfd == -1)
  12.     {
  13.         perror("socket error");
  14.         exit(1);
  15.     }
  16.     // 2.将 套接字 和 ip端口 绑定
  17.     struct sockaddr_in addr;
  18.     addr.sin_family = AF_INET;  // ipv4
  19.     addr.sin_addr.s_addr= INADDR_ANY;   // 0地址(本地任意地址)
  20.     addr.sin_port = htons(8989);    // 端口转为大端
  21.     int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
  22.     if(ret == -1)
  23.     {
  24.         perror("bind error");
  25.         exit(2);
  26.     }
  27.     // 3.设置监听
  28.     ret = listen(lfd, 128);
  29.     if(ret == -1)
  30.     {
  31.         perror("listen error");
  32.         exit(3);
  33.     }
  34.     // 4.初始化检测的集合
  35.     int epfd = epoll_create(1);
  36.     if(epfd == -1)
  37.     {
  38.         perror("epoll_create error");
  39.         exit(4);
  40.     }
  41.     // 5.将要检测的节点添加到epoll树中
  42.     struct epoll_event ev;
  43.     ev.events = EPOLLIN;
  44.     ev.data.fd = lfd;
  45.     ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
  46.     if(ret == -1)
  47.     {
  48.         perror("epoll_ctl");
  49.         exit(5);
  50.     }
  51.     // 6.委托内核检测epoll树中的文件描述符状态
  52.     struct epoll_event evs[1024];
  53.     int size = sizeof(evs) / sizeof(evs[0]);
  54.     while(1)
  55.     {
  56.         int num = epoll_wait(epfd, evs, size, -1);  // 把文件描述符发生变化的储存到 evs 数组中
  57.         printf("num = %d\n", num);
  58.         // 遍历evs数组
  59.         for(int i=0; i<num; i++)
  60.         {
  61.             int curfd = evs[i].data.fd;
  62.             if(curfd == lfd)    // lfd 套接字状态改变说明有新链接请求
  63.             {
  64.                 int cfd = accept(lfd, NULL, NULL);
  65.                 ev.events = EPOLLIN;
  66.                 ev.data.fd = cfd;
  67.                 epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);   // 把新的链接加入到epoll树中
  68.             }
  69.             else    // 其他套接字状态改变说明有新数据抵达
  70.             {
  71.                 char buf[1024];
  72.                 memset(buf, 0, sizeof(buf));
  73.                 int len = recv(curfd, buf, sizeof(buf), 0);
  74.                 if(len == 0)
  75.                 {
  76.                     printf("客户端断开了链接...\n");
  77.                     epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
  78.                     close(curfd);
  79.                 }
  80.                 else if(len>0)
  81.                 {
  82.                     printf("recv data: %s\n");
  83.                     send(curfd, buf, len, 0);
  84.                 }
  85.                 else
  86.                 {
  87.                     perror("recv error");
  88.                     exit(6);
  89.                 }
  90.             }
  91.         }
  92.     }
  93. }
复制代码
3.2 服务器UDP


  • UDP服务器需要创建套接字、绑定端口、接收数据、根据接收数据的客户端发送数据
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. #include <arpa/inet.h>
  6. int main()
  7. {
  8.     // 1.创建通信套接字
  9.     int fd = socket(AF_INET, SOCK_DGRAM, 0);
  10.     if(fd==-1)
  11.     {
  12.         perror("socket");
  13.         exit(0);
  14.     }
  15.     // 2.接收数据需要绑定固定的端口
  16.     struct sockaddr_in addr;
  17.     addr.sin_family = AF_INET;
  18.     addr.sin_port = htons(8989);
  19.     addr.sin_addr.s_addr = INADDR_ANY;
  20.     int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
  21.     if(ret==-1)
  22.     {
  23.         perror("bind");
  24.         exit(0);
  25.     }
  26.     // 通信
  27.     char ip[24];
  28.     char buf[1024];
  29.     struct sockaddr_in cliaddr;
  30.     int clilen = sizeof(cliaddr);
  31.     while(1)
  32.     {
  33.         // 3.接收数据
  34.         int len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&cliaddr, &clilen); // 把发送端数据保存在cliaddr中
  35.         if(len==-1)
  36.         {
  37.             break;
  38.         }
  39.         printf("client ip: %s, port: %d\n",
  40.                 inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
  41.                 ntohs(cliaddr.sin_port));   // 打印发送端ip和port
  42.         printf("client say: %s\n", buf);    // 打印发送端发送的内容
  43.         // 4.回复数据
  44.         sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, clilen);
  45.     }
  46.     close(fd);
  47.     return 0;
  48. }
复制代码
3.3 客户端UDP


  • UDP客户端相对于服务器端减少了手动绑定ip端口的步骤
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. #include <arpa/inet.h>
  6. int main()
  7. {
  8.     // 1.创建通信套接字
  9.     int fd = socket(AF_INET, SOCK_DGRAM, 0);
  10.     if(fd==-1)
  11.     {
  12.         perror("socket");
  13.         exit(0);
  14.     }
  15.     // 服务器地址
  16.     struct sockaddr_in serveraddr;
  17.     serveraddr.sin_family = AF_INET;
  18.     serveraddr.sin_port = htons(8989);
  19.     inet_pton(AF_INET, "10.0.2.15", &serveraddr.sin_addr.s_addr);
  20.     // 通信
  21.     char ip[24];
  22.     char buf[1024];
  23.     int num=0;
  24.     while(1)
  25.     {   
  26.         // 2.发送数据
  27.         sprintf(buf, "Hello World!, %d\n", num++);
  28.         sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
  29.         // 3.接收数据
  30.         memset(buf, 0, sizeof(buf));
  31.         int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); // 把发送端数据保存在cliaddr中
  32.         if(len==-1)
  33.         {
  34.             break;
  35.         }
  36.         printf("client say: %s\n", buf);    // 打印发送端发送的内容
  37.     }
  38.     close(fd);
  39.     return 0;
  40. }
复制代码
 

来源:https://www.cnblogs.com/stux/p/17816974.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具