提升性能的必备技术:Linux网络IO与select详解

2024-08-10 22:36:54 浏览数 (1)

一、IO的定义

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

二、socket的定义

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

三、一对一服务器设计

图片图片

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

代码语言:javascript复制
#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 函数原型:

代码语言:javascript复制
#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 函数原型:

代码语言:javascript复制
#include<sys/socket.h>
int listen(int sockfd, int backlog);

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

代码语言:javascript复制
#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 函数原型:

代码语言:javascript复制
#include<sys/types.h>
#include<sys/socket.h>
int recv( int fd, char *buf, int len, int flags);

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

代码语言:javascript复制
#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

完整示例:

代码语言:javascript复制
#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: %dn",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: %dn",clientfd);

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

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

四、设置非阻塞

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

代码语言:javascript复制
#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函数之前设置才能生效。 使用示例:

代码语言:javascript复制
int flag=fcntl(listenfd,F_GETFL,0);
flg|=O_NONBLOCK
fcntl(listenfd,F_SETFL,0);

五、多对一服务器设计

5.1、多线程方案

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

图片图片

pthread_create函数原型:

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

完整示例:

代码语言:javascript复制
#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: %dn", 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

图片图片

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

代码语言:javascript复制
#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的值,该值是发生事件的文件描述符数。

完整示例:

代码语言:javascript复制
#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: %dn", 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连接的并发处理,展示了其在网络编程中的具体应用。

0 人点赞