前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入剖析Linux网络设计中网络IO的重要角色

深入剖析Linux网络设计中网络IO的重要角色

原创
作者头像
Lion Long
发布2024-08-17 21:56:22
920
发布2024-08-17 21:56:22
举报
文章被收录于专栏:后端开发技术

一、网络编程关注的四个方面

网络编程主要关注四个问题:连接的建立、断开连接、消息到达、消息发送。 不管使用什么样的网络模型,不管使用的是阻塞IO还是非阻塞IO,不管是同步IO还是异步IO,都需要关注这四个问题。

1.1、建立连接

连接有两种:服务器处理接收客户端的连接;服务器作为客户端主动连接第三方服务。

1.1.1 接收连接

接收连接主要使用accept()函数,用于从全连接队列中返回一个已完成的连接。如果成功,返回值大于0表示与一个客户端TCP建立了连接;返回值是由kernel自动生成的一个全新描述符。在非阻塞模式下,accept()返回-1表示全连接队列中没有已完成的客户端接入。 accept函数原型:

代码语言:javascript
复制
ACCEPT(2)                  Linux Programmer's Manual                 ACCEPT(2)
NAME
       accept, accept4 - accept a connection on a socket
SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

简单示例:

代码语言:javascript
复制
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>

#define LISTEN_PORT    8888

int main()
{
   

    int listenfd=socket(AF_INET,SOCK_STREAM,0);

    struct sockadd_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    serveraddr.sin_port=htons(LISTEN_PORT);

    bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));

    while(1)
    {
   
        struct sockaddr_in clientaddr;
        socklen_t len=sizeof(clientaddr);
        clientfd=accept(listenfd,&clientaddr,&len);

        /*......
        * 处理逻辑代码
        */
    }
    return 0;
}

1.1.2 主动连接

主动连接由connect()函数发起,主动连接服务器。成功返回0;失败则返回-1,并设置了全局变量errno,应该处理connect函数返回的错误码。

connect函数原型:

代码语言:javascript
复制
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

/*
* sockfd:socket文件描述符
* addr:指定服务器端地址信息,包括IP地址和端口。
* addrlen:指定地址信息的大小
*/

connect()和bind()参数形式一样,区别在于bind()参数的地址信息是自己的,connect()参数的地址信息是对方的地址信息。 失败时返回的错误码:

错误码

含义

EACCES,EPERM

用户在未启用套接字广播标志的情况下尝试连接到广播地址,或者由于本地防火墙规则,连接请求失败。

EADDRINUSE

本地地址已在使用中。

EADDRNOTAVAIL

套接字未绑定到地址,在尝试将其绑定到临时端口时,确定临时端口范围内的所有端口号当前都在使用中。

EAFNOSUPPORT

传递的地址在其sa_family字段中没有正确的地址族。

EAGAIN

路由缓存中的条目不足。

EALREADY

套接字是非阻塞的,以前的连接尝试尚未完成。

EBADF

文件描述符不是描述符表中的有效索引。

EconRefuse

没有人监听远程地址。

EFAULT

套接字结构地址在用户的地址空间之外。

EINPROGRESS

套接字是非阻塞的,无法立即完成连接。

EINTR

系统调用被捕获的信号中断;参见信号(7)。

EISCONN

套接字已连接。

ENETUNREACH

网络无法访问。

ENOTSOCK

文件描述符sockfd不引用套接字。

EPROTOTYPE

套接字类型不支持请求的通信协议。例如,在尝试将UNIX域数据报套接字连接到流套接字时,可能会发生此错误。

ETIMEDOUT

尝试连接时超时。服务器可能太忙,无法接受新连接。注意,对于IP套接字,当服务器上启用Syncookie时,超时可能很长。

简单示例:

代码语言:javascript
复制
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>

#define LISTEN_PORT 8888

int main()
{
   
    int connectfd=socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=htonl("127.0.0.1");//服务器IP
    serveraddr.sin_port=htons(LISTEN_PORT);//服务器端口

    int ret = connect(connectfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    if(ret ==1)
    {
   
        // ret == -1 && errno == EINPROGRESS 正在建立连接
        // ret == -1 && errno = EISCONN 连接建立成功
        switch(errno)
        {
   
        /*处理错误码*/

        }
    }

    /*处理逻辑*/

}

1.2 断开连接

断开分两种,主动断开和被动断开。

1.2.1 主动断开

主动断开主要调用close()函数。有些网络编程需要支持半关闭状态时,使用shutdown()函数。 close函数原型:

代码语言:javascript
复制
#include <unistd.h>

int close(int fd);

close()关闭文件描述符,使其不再引用任何文件,并可重复使用。成功返回0;失败则返回-1,并设置了全局变量errno。 失败错误码:

错误码

含义

EBADF

fd不是有效的打开文件描述符。

EINTR

close()调用被信号中断

EIO

发生I/O错误。

shutdown函数原型:

代码语言:javascript
复制
#include<sys/socket.h>

int shutdown(int fd,int flag);

成功则返回0, 失败返回-1, 错误码放在errno。 flag参数说明:

参数

含义

SHUT_RDWR

值为2,表示关闭读写段

SHUT_WR

值为1,表示关闭本地写段,对端读段

SHUT_RD

值为0,表示关闭本地读段,对端写段

使用方式:

代码语言:javascript
复制
//主动关闭
close(fd);
shoutdown(fd,SHUT_RDWR);

// 主动关闭本地读端,关闭对方写端
shutdown(fd,SHUT_RD);

// 主动关闭本地写端,关闭对方读端
shutdown(fd,SHUT_WR);

1.2.1 被动断开

主要依据recv/read、send/write判断。有的网络编程需要支持半关闭状态。

代码语言:javascript
复制
/*......*/
char buffer[1024]={
    0 };

// 被动,读端被关闭
int ret=recv(fd,buffer,1024,0);
if(ret==0)
{
   
    close(fd);
}

/*......*/

//被动,写端关闭
ret =send(fd,buffer,1024,0);
if(ret==0 && errno == EPIPE)
{
   
    close(fd);
}

/*......*/

recv和send函数原型:

代码语言:javascript
复制
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t send(int sockfd, void *buf, size_t len, int flags);

成功返回接收 / 发送的字节数;失败则返回-1,并设置errno以指示错误。 注意,recv也可能返回0。当流套接字对等端执行有序关闭时,返回值将为0;不同域(例如UNIX和Internet域)中的数据报套接字允许零长度数据报,当接收到这样的数据报时,返回值为0;如果从流套接字接收的请求字节数为0,则也可以返回值0。 recv的错误码:

错误码

含义

EAGAIN,EWOULDBLOCK

套接字标记为非阻塞,接收操作要求阻塞,或者设置了接收超时,并且在接收数据之前超时。

EBADF

参数sockfd是无效的描述符。

ECONREFUSED

远程主机拒绝允许网络连接(通常是因为它没有运行请求的服务)。

EFAULT

接收缓冲区指针指向进程地址空间之外。

EINTR

在任何数据可用之前,发送信号中断了接收。

EINVAL

传递的参数无效。

ENOMEM

无法为recvmsg()分配内存。

ENOTCONN

套接字与面向连接的协议关联,尚未连接。

ENOTSOCK

文件描述符sockfd不引用套接字。

send错误码:

错误码

含义

EACCES

对目标套接字文件的写入权限被拒绝,或者对路径前缀为的目录之一的搜索权限被拒绝。(对于UDP套接字)尝试发送到网络/广播地址,好像它是单播地址一样。

EAGAIN,EWOULDBLOCK

套接字标记为非阻塞,请求的操作要求阻塞。

EAGAIN

sockfd引用的套接字以前未绑定到地址,在尝试将其绑定到临时端口时,确定临时端口范围内的所有端口号当前都在使用中。

EBADF

指定的描述符无效。

EconReset

对等端重置连接。

EDESTADDRREQ

套接字不是连接模式,并且未设置对等地址。

EFAULT

为参数指定了无效的用户空间地址。

EINTR

在传输任何数据之前发生的信号。

EINVAL

传递的参数无效。

EISCONN

连接模式套接字已连接,但指定了收件人。(现在要么返回此错误,要么忽略收件人规范。)

EMSGSIZE

套接字类型要求以原子方式发送消息,而要发送的消息的大小使得这不可能。

ENOBUFS

网络接口的输出队列已满。这通常表示接口已停止发送,但可能是由瞬时拥塞造成的。(通常情况下,在Linux中不会发生这种情况。当设备队列溢出时,数据包会自动丢弃。)

ENOMEM

没有可用内存。

ENOTCONN

未连接套接字,且未指定目标。

ENOTSOCK

文件描述符sockfd不引用套接字。

EOPNOTSUPP

flags参数中的某些位不适用于套接字类型。

EPIPE

本地端已在面向连接的套接字上关闭。在这种情况下,进程也将接收一个SIGPIPE,除非设置了MSG_NOSIGNAL。

1.3 消息到达

接收消息使用recv / read函数。从缓冲区中读取数据。

代码语言:javascript
复制
//......

while(1)
{
   
    //......

    char buffer[1024]={
    0 };
    int ret =recv(fd,buffer,1024,0);
    if(ret<0)// ret==-1
    {
   
        if(errno==EINTR || errno == EWOULDBLOCK)
            break;
        // 四次挥手发送ack之前,还可以发送数据
        // send(....)
        close(fd);
    }
    else if(ret==0)
        close(fd);
    else
    {
   
        //处理buffer
    }

    //......
}

//......

1.4 消息发送

发送消息使用send / write函数。往写缓冲区写数据。

代码语言:javascript
复制
//......

char buffer[1024]={
    0 };
//......

int ret = send(fd,buffer,1024,0);
if(ret==-1)
{
   
    if(errno==EINTR || errno == EWOULDBLOCK)
        return;
    close(fd);
}
//......

二、操作IO

只能使用IO函数进行操作,有两者操作方式:阻塞IO和非阻塞IO。

2.1 操作方式

2.1.1 阻塞模式

一般情况下,fd默认是阻塞的。阻塞模式会阻塞在网络线程。比如,当调用recv,读缓冲区没有数据时,则一直阻塞,直到有数据可读才返回。注意,send函数不是把数据写完了才返回,而是只要写缓冲区有空间给它write数据就返回写成功,而不是写完数据才返回成功。 原理图如下:

2.1.2 非阻塞模式

连接的fd的阻塞属性决定了IO函数是否阻塞。默认情况下fd是阻塞的,要设置非阻塞模式,可以使用一下方式:

代码语言:javascript
复制
//......

int flag = fcntl(fd,F_GETFL,0);
flag|=O_NONBLACK;
fcntl(fd,F_SETFL,flag);

//......

设置了非阻塞模式后,调用IO函数时,不管有没有成功都返回。比如,当调用recv,读缓冲区没有数据时,返回-1,并设置errno,errno应该是EWOULDBLOCK。 原理如下:

2.1.3 两者区别

从上面原理图可以看出,差异主要在数据准备阶段。具体差异在:IO函数在数据未就绪时是否立刻返回。

2.2 非阻塞IO处理方式

2.2.1 建立连接

连接有两种:服务器处理接收客户端的连接;服务器作为客户端主动连接第三方服务。

2.2.1.1 主动连接

当服务器需要连接第三方服务,需要调用connect函数进行连接。 在非阻塞IO中,connect()会一直返回-1,同时设置errno;需要检查errno是EINPROGRESS(正在建立连接)还是EISCONN(已经建立连接)。 示例:

代码语言:javascript
复制
#define SERVER_PORT    8888
//......

struct sockaddr_in serv;
memset(&serv,0,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=htonl("127.0.0.1");//要连接的服务器ip地址
serv.sin_port=htons(SERVER_PORT);
while(1)
{
   
    int ret = connect(fd,(struct sockaddr *)&serv,sizeof(serv));
    if(ret==-1 && errno==EISCONN)
    {
   
        // ........
        break;
    }
}
// ......
2.2.1.1 接收连接

服务器通过accept()函数从全连接队列中获得已完成连接的客户端,并返回内核自动生成的文件描述符。 在非阻塞模式中,完成socket()、bind()、listen()的调用后,会循环调用accept()函数,如果返回值大于0,表示获取到一个已完成连接的客户端。 示例:

代码语言:javascript
复制
#define SERVER_PORT    8888
//......
int listenfd=socket(AF_INET,SOCK_STREAM,0);

struct sockaddr_in serv;
memset(&serv,0,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=htonl(INADDR_ANY);
serv.sin_port=htons(SERVER_PORT);

bind(listenfd,(struct sockaddr *)&serv,sizeof(serv));

listen(listenfd,10);

//......
while(1)
{
   
    struct sockaddr_in clientaddr;
    socklen_t len=sizeof(clientaddr);
    int ret = accept(fd,(struct sockaddr *)&serv,sizeof(serv));
    if(ret>0)
    {
   


        // ........
        break;
    }
}
// ......

2.2.2 断开连接

如1.2所描述。

2.2.3 消息到达

在非阻塞模式中,如果读缓冲区没数据,recv/read函数返回-1,并且设置errno为EWOULDBLOCK。如1.3所描述。

2.2.4消息发送

如1.4所描述。

2.3 IO函数说明

IO函数既有检测IO功能也有操作IO功能。 例如:

IO函数

IO操作功能

IO检测功能

accept

从全连接队列中取出一个已完成连接的节点,并返回内核自动生成文件描述符以及客户端的ip地址和端口等信息

检测全连接队列中是否有已完成的连接的节点。

recv

从读缓冲区中读取数据到用户态

检测读缓冲区是否有数据

send

拷贝数据到写缓冲区

检测写缓冲区是否可写

注意,IO函数只能检测一条连接就绪的状态以及操作一条连接的IO数据

三、IO多路复用检测IO

IO多路复用不会操作IO,只检测IO的就绪状态。 但是IO多路复用可以检测多个IO的就绪状态。IO多路复用主要有:select、poll、epoll。IO多路复用只能检测比较笼统的事件(比如 读事件、写事件、错误事件),IO函数可以检测具体的事件。 IO多路复用检测IO模型:

以epoll为例,epoll主要有三个函数:epoll_create、epoll_wait、epoll_ctl。 epoll函数原型:

代码语言:javascript
复制
#include <sys/epoll.h>

/*相关数据结构*/
struct eventpoll {
   
    // ...
    struct rb_root rbr; // 红黑树,管理 epoll 监听的事件
    struct list_head rdllist; // 链表,保存着 epoll_wait返回满⾜条件的事件
    // ...
};
struct epitem {
   
    // ...
    struct rb_node rbn; // 红⿊树节点
    struct list_head rdllist; // 双向链表节点
    struct epoll_filefd ffd; // 事件句柄信息
    struct eventpoll *ep; // 指向所属的eventpoll对 象
    struct epoll_event event; // 注册的事件类型
    // ...
};
struct epoll_event {
   
    __uint32_t events; // epollin ,epollout ,epollel(边缘触发)
    epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
   
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

/*相关接口*/

int epoll_create(int size);

/*
* op:
*     EPOLL_CTL_ADD    添加事件
*     EPOLL_CTL_MOD    修改事件
*     EPOLL_CTL_DEL    删除事件
*
* event:
*     EPOLLIN        注册读事件
*     EPOLLOUT    注册写事件
*     EPOLLET        注册边沿触发,默认是水平触发
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

/*
* events[i].event:
*     EPOLLIN        触发读事件
*     EPOLLOUT    触发写事件
*     EPOLLERR    触发错误事件
*     EPOLLRDHUP    连接读端关闭
*     EPOLLHUP    连接读写端关闭
*
* timeout:
*     -1,体现阻塞特性,直到有事件触发才返回
*     0,体现非阻塞特性,立刻返回
*     >0,超时时间,最多等待timeout时间,如果还没有事件触发就返回;单位是ms。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

调用epoll_create会创建一个epoll对象; 调用epoll_ctl添加到epoll中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用触发函数(ep_poll_callback),将触发的事件拷贝到双向链表(rdllist)中; 调用epoll_wait会从双向链表中就绪事件拷贝到用户态中。

那么,IO多路复用是怎么检测IO事件的呢?以epoll为例。

3.1 建立连接

连接有两种方式:主动连接和接受连接。

3.1.1 主动连接

主动连接主要通过connect()函数建立。 首先,通过socket()函数创建一个socket对象; 然后,epoll(IO多路复用器)监听写事件,调用connect函数,在三次握手阶段,客户端向服务端发送ack(在第三次)的同时发送写就绪信号给epoll(IO多路复用器); 这就实现了epoll(IO多路复用器)检测到主动连接完成。

3.1.2 接受连接

接受连接主要通过socket()、bind()、listen()、accept()函数。 首先,通过socket()函数创建一个socket对象,bind()绑定地址,listen()监听端口,完成一个listenfd的创建和设置; 其次,epoll(IO多路复用器)监听listenfd的读事件,三次握手成功后全连接队列会产生一个节点,同时发送信号告诉epoll(IO多路复用器),触发读事件;这时说明连接完成。 然后,调用accept()函数,执行操作IO功能。 简单示例:

代码语言:javascript
复制
int init_sock(short port) {
   

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(fd, F_SETFL, O_NONBLOCK);

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    if (listen(fd, 20) < 0) {
   
        printf("listen failed : %s\n", strerror(errno));
        return -1;
    }

    printf("listen server port : %d\n", port);
    return fd;
}
int main()
{
   
    int epfd=epoll_create(1);
    int listenfd=init_sock(8888);

    struct epoll_event ep_ev = {
   0, {
   0}};
    ep_ev.data.fd=listenfd;
    ep_ev.events=EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ep_ev);
    while(1)
    {
   
        struct epoll_event ep_ev_client[1024];
        int n = epoll_wait(epfd,ep_ev_client,1024,-1);
        int i=0;
        for(i=0;i<n;i++)
        {
   
            if(ep_ev_client[i].events & EPOLLIN)
            {
   
                //处理读事件......
            }
            if(ep_ev_client[i].events & EPOLLOUT)
            {
   
                //处理写事件......
            }
        }
    }
    return 0;
}

3.2 连接断开

IO多路复用器检测的是被动断开。 当epoll返回EPOLLRDHUP表示服务器读端关闭了;当epoll返回EPOLLHUP表示服务器读写端都关闭了。

3.3 消息到达

epoll(IO多路复用器)检测客户端fd的读事件。 当客户端发送数据到服务器的读缓冲区时,会发送信号给epoll(IO多路复用器),epoll(IO多路复用器)就会触发读事件,说明读缓冲区填充有数据;此时就可以调用recv/read函数操作IO。

3.4 消息发送

epoll(IO多路复用器)检测客户端fd的写事件。 当写缓冲区可写(即写缓冲区有空间可以写数据)时,它会发信号告诉epoll(IO多路复用器),epoll(IO多路复用器)触发写事件,这时调用send/write函数操作IO。

四、总结

一定要熟悉网络编程的四个关注点(建立连接、消息到达、消息发送、断开连接),深入理解操作IO和检测IO,这样才能很好的理解网络编程的源码,设计出高效的网络模型。 特别需要理解TCP的三次握手和四次挥手过程。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、网络编程关注的四个方面
    • 1.1、建立连接
      • 1.1.1 接收连接
      • 1.1.2 主动连接
    • 1.2 断开连接
      • 1.2.1 主动断开
      • 1.2.1 被动断开
    • 1.3 消息到达
      • 1.4 消息发送
      • 二、操作IO
        • 2.1 操作方式
          • 2.1.1 阻塞模式
          • 2.1.2 非阻塞模式
          • 2.1.3 两者区别
        • 2.2 非阻塞IO处理方式
          • 2.2.1 建立连接
          • 2.2.2 断开连接
          • 2.2.3 消息到达
          • 2.2.4消息发送
        • 2.3 IO函数说明
        • 三、IO多路复用检测IO
          • 3.1 建立连接
            • 3.1.1 主动连接
            • 3.1.2 接受连接
          • 3.2 连接断开
            • 3.3 消息到达
              • 3.4 消息发送
              • 四、总结
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档