突破网络瓶颈:提升性能的必备技术——Linux网络IO与select详解

简介: 本文通过对Linux网络IO和select的详细讨论,帮助读者深入理解了这些关键概念,并展示了select函数在构建高效网络应用中的重要性和灵活性。对于想要提升网络编程技能的开发者来说,这些知识将会是宝贵的参考和实践指南。

一、IO的定义

IO 即“Input”和“Output”的组合,即输入/输出,IO用来处理设备之间的数据传输。socket/fd也是一种IO。

二、socket的定义

socket 的译意是“插座”,在计算机通信领域,socket 也被翻译为“套接字”,它是计算机之间进行通信的一种方式。通过 socket ,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

三、一对一服务器设计

image.png

第一步:创建socket。
函数原型

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

这个函数建立一个协议族、协议类型、协议编号的socket文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
domain参数值含义:

名称 含义
PF_UNIX,PF_LOCAL 本地通信
AF_INET,PF_INET IPv4协议
PF_INET6 IPv6协议
PF_NETLINK 内核用户界面设备
PF_PACKET 底层包访问

type参数值含义:

名称 含义
SOCK_STREAM TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM UDP连接
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
:SOCK_PACKET 专用类型
SOCK_RDM 提供可靠的数据报文,不保证数据有序
SOCK_RAW 提供原始网络协议访问

protocol参数含义:
通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;如果协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
第二步:设置参数
通过struct sockaddr_in结构体指定协议族,指定绑定地址,指定监控的端口号。
使用的成员:sin_family、sin_addr.s_addr、sin_port
第三步:绑定--> bind
函数原型:

#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第2个参数my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
第3个参数addrlen是my_addr结构的长度,可以设置成sizeof(struct sockaddr)。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败
第四步:监听--> listen
函数原型:

#include<sys/socket.h>
int listen(int sockfd, int backlog);

参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第二个参数规定了内核应该为相应套接字排队的最大连接个数。
第五步:接收连接--> accept
函数原型:

#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

参数说明:
sockefd:套接字描述符,该套接字在listen()后监听连接。
addr:(可选)指针。指向一个缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针。输入参数,配合addr一起使用,指向存有addr地址长度的整形数。
第六步:接收数据--> recv
函数原型:

#include<sys/types.h>
#include<sys/socket.h>
int recv( int fd, char *buf, int len, int flags);

参数说明:
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
第七步:发送数据-->send
函数原型:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:
sockfd:向套接字中发送数据
buf:要发送的数据的首地址
len:要发送的数据的字节
int flags:设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和write一样
返回值:成功返回实际发送的字节数,失败返回 -1

完整示例:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#define BUFF_LENGTH 128
int main(void)
{
   
   
    int listen_fd=socket(AF_INET,SOCK_STRAM,0);
    if(lisenfd==-1)
        return -1;
    printf("lisenfd: %d\n",lisenfd);

    struct sockaddr_in servaddr;
    servaddr.sin_family=AF_INET;//指定协议族,INET是IPv4,INET6是IPv6
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//指定地址
    servaddr.sin_port=htons(9999);//将整型变量从主机字节顺序转变成网络字节顺序

    //bind(listenfd,&servaddr,sizeof(servaddr));
    if(-1==bind(lisenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)))
    {
   
   
        return -2;
    }

    listen(listenfd,10);

    struct sockaddr_in client;
    socklen_t len=sizeof(client);
    int clientfd=accept(lisenfd,(struct sockaddr*)&client,&len);
    printf("client: %d\n",clientfd);

    while(1){
   
   
        unsigned char buffer[BUFF_LENGTH] = {
   
    0 };
        int ret = recv(clientfd,buffer,BUFF_LENGTH,0);
        printf("buffer: %s,ret=%d\n",buffer,ret);

        ret=send(clientfd,buffer,ret,0);
        printf("send buffer: %s,ret=%d\n",buffer,ret);
    }
    return 0;
}

四、设置非阻塞

默认的连接是阻塞方式的,可以使用fcntl函数进行设置非阻塞模式。
函数原型:

#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
// 返回值:成功依赖cmd的值,失败返回-1;

cmd参数说明:

参数 含义
F_GETFL 获取文件状态标志
F_SETFL 设置文件状态标志
F_GETFD 获取文件描述符标志
F_SETFD 设置文件描述符标志
F_GETLK 获取文件锁
F_SETLK 设置文件锁
F_DUPFD 复制文件描述符
F_GETOWN 取当前接受SIGIO和SIGURG信号的进程ID和进程组ID.正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程中ID
F_SETOWN 设置当前接受SIGIO和SIGURG信号的进程ID和进程组ID.

状态标志:

标志 含义
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 为读、写打开
O_APPEND 每次写时追加
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(数据)
O_RSYNC 同步读、写
O_FSYNC 等待写完成(进FreeBSD和Mac OS X)
O_ASYNC 异步I/O(进FreeBSD和Mac OS X)

注意: 非阻塞要在accpt函数之前设置才能生效。
使用示例:

int flag=fcntl(listenfd,F_GETFL,0);
flg|=O_NONBLOCK
fcntl(listenfd,F_SETFL,0);

五、多对一服务器设计

5.1、多线程方案

使用多线程方案,来一个连接请求则创建一个线程。
image.png

pthread_create函数原型:

#include <pthread.h>
int pthread_create(
                 pthread_t *restrict tidp,                   //新创建的线程ID指向的内存单元。
                 const pthread_attr_t *restrict attr,        //线程属性
                 void *(*start_rtn)(void *),                 //线程函数的地址
                 void *restrict arg                         //线程函数所需的参数
                  );

完整示例:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

#define BUFFER_LENGTH    128

// thread --> fd
void *routine(void *arg) 
{
   
   
    int clientfd = *(int *)arg;
    while (1) {
   
   
        unsigned char buffer[BUFFER_LENGTH] = {
   
   0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
        if (ret == 0) 
        {
   
   
            close(clientfd);
            break;
        }
        printf("buffer : %s, ret: %d\n", buffer, ret);
        ret = send(clientfd, buffer, ret, 0); 
    }
}

int main() {
   
   
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 
    if (listenfd == -1) return -1;

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
   
   
        return -2;
    }

#if 0 // nonblock
    int flag = fcntl(listenfd, F_GETFL, 0);
    flag |= O_NONBLOCK;
    fcntl(listenfd, F_SETFL, flag);
#endif

    listen(listenfd, 10);

    while (1) {
   
   
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);

        pthread_t threadid;
        pthread_create(&threadid, NULL, routine, &clientfd);
    }
    return 0;
}

5.2、io多路复用——select

image.png

什么是IO多路复用? 通俗的讲就是一个线程,通过记录IO流的状态来管理多个IO。解决创建多个进程处理IO流导致CPU占用率高的问题。
select是io多路复用的一种方式,其他的还有poll、epoll等。
函数原型:

#include <sys/types.h>
#include <unistd.h>

int select(int maxfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select函数共有5个参数,其中参数和返回值:
maxfds:监视对象文件描述符数量。
readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值。
writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值。
exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值。
timeout:调用select后,为防止陷入无限阻塞状态,传递超时信息。
返回值:错误返回-1,超时返回0。当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

完整示例:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_LENGTH    128

int main() {
   
   

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 
    if (listenfd == -1) return -1;
// listenfd
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
   
   
        return -2;
    }

#if 0 // nonblock
    int flag = fcntl(listenfd, F_GETFL, 0);
    flag |= O_NONBLOCK;
    fcntl(listenfd, F_SETFL, flag);
#endif

    listen(listenfd, 10);

    fd_set rfds, wfds, rset, wset;
    FD_ZERO(&rfds);
    FD_SET(listenfd, &rfds);
    FD_ZERO(&wfds);

    int maxfd = listenfd;

    unsigned char buffer[BUFFER_LENGTH] = {
   
   0}; // 0 
    int ret = 0;
    // int fd, 
    while (1) {
   
   
        rset = rfds;
        wset = wfds;

        int nready = select(maxfd+1, &rset, &wset, NULL, NULL);
        if (FD_ISSET(listenfd, &rset)) {
   
   

            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);

            FD_SET(clientfd, &rfds);

            if (clientfd > maxfd) maxfd = clientfd;
        } 

        int i = 0;
        for (i = listenfd+1; i <= maxfd;i ++) {
   
   

            if (FD_ISSET(i, &rset)) {
   
    //

                ret = recv(i, buffer, BUFFER_LENGTH, 0);
                if (ret == 0) {
   
   
                    close(i);
                    FD_CLR(i, &rfds);

                } else if (ret > 0) {
   
   
                    printf("buffer : %s, ret: %d\n", buffer, ret);
                    FD_SET(i, &wfds);
                }

            } else if (FD_ISSET(i, &wset)) {
   
   

                ret = send(i, buffer, ret, 0); // 

                FD_CLR(i, &wfds); //
                FD_SET(i, &rfds);
            }

        }

    }

    return 0;
}

步骤:
1、定义io管理状态变量:fd_set rfds,wfds;
2、初始化变量:FD_ZERO();
3、设置io流状态,最初只有监听的fd,将其设置:FD_SET(listenfd,rfds);
4、在循环中select
5、FD_ISSET()判断端口是否有连接
6、FD_ISSET()判断可读、可写状态

总结

本文通过对Linux网络IO和select的详细讨论,帮助读者深入理解了这些关键概念,并展示了select函数在构建高效网络应用中的重要性和灵活性。对于想要提升网络编程技能的开发者来说,这些知识将会是宝贵的参考和实践指南。

  1. 网络IO的重要性:理解网络IO是构建高效网络应用的基础。通过有效管理数据的输入和输出,可以实现更好的性能和可伸缩性。

  2. Linux中的网络IO模型:介绍了阻塞IO、非阻塞IO、多路复用IO和异步IO等不同的网络IO模型。特别地,我们重点讨论了多路复用IO模型中的select函数。

  3. select函数的作用:select函数是一种常用的多路复用机制,它可以同时监视多个文件描述符的状态变化,并通知应用程序哪些描述符可以进行读写操作。

  4. 使用select函数的优势:通过使用select函数,可以在一个线程内管理多个连接,减少了线程创建和销毁的开销,提升了系统的性能和资源利用率。

  5. select函数的工作原理:详细解释了select函数的工作原理,包括文件描述符集合的准备、调用select函数并处理返回结果的流程。

  6. select函数的限制:虽然select函数具有一定的优点,但也存在一些限制,如最大文件描述符数量的限制,每次调用都需要遍历整个描述符集合等。

  7. select函数的应用示例:通过一个实际的案例,演示了如何使用select函数实现多个TCP连接的并发处理,展示了其在网络编程中的具体应用。

image.png

相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
6天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
19 2
|
24天前
|
运维 监控 网络协议
|
1月前
|
网络协议 前端开发 Java
网络协议与IO模型
网络协议与IO模型
网络协议与IO模型
|
19天前
|
网络协议 物联网 API
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第26天】Python 是一门功能强大且易于学习的编程语言,Twisted 框架以其事件驱动和异步IO处理能力,在网络编程领域独树一帜。本文深入探讨 Twisted 的异步IO机制,并通过实战示例展示其强大功能。示例包括创建简单HTTP服务器,展示如何高效处理大量并发连接。
39 1
|
19天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
192 2
|
25天前
|
人工智能 Cloud Native Java
云原生技术深度解析:从IO优化到AI处理
【10月更文挑战第24天】在当今数字化时代,云计算已经成为企业IT架构的核心。云原生作为云计算的最新演进形态,旨在通过一系列先进的技术和实践,帮助企业构建高效、弹性、可观测的应用系统。本文将从IO优化、key问题解决、多线程意义以及AI处理等多个维度,深入探讨云原生技术的内涵与外延,并结合Java和AI技术给出相应的示例。
86 1
|
28天前
|
Ubuntu Linux 虚拟化
Linux虚拟机网络配置
【10月更文挑战第25天】在 Linux 虚拟机中,网络配置是实现虚拟机与外部网络通信的关键步骤。本文介绍了四种常见的网络配置方式:桥接模式、NAT 模式、仅主机模式和自定义网络模式,每种模式都详细说明了其原理和配置步骤。通过这些配置,用户可以根据实际需求选择合适的网络模式,确保虚拟机能够顺利地进行网络通信。
|
1月前
|
开发者
什么是面向网络的IO模型?
【10月更文挑战第6天】什么是面向网络的IO模型?
21 3
|
1月前
|
数据挖掘 开发者
网络IO模型
【10月更文挑战第6天】网络IO模型
38 3
|
1月前
|
缓存 Java Linux
硬核图解网络IO模型!
硬核图解网络IO模型!

热门文章

最新文章