菜单

Linux 的 Socket IO 模型趣解

2020年2月13日 - 计算机服务器
Linux 的 Socket IO 模型趣解

前言

之前有看到用很幽默的方式讲解Windows的socket
IO模型,借用这个故事,讲解下linux的socket IO模型;

老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。
他们的信会被邮递员投递到他们小区门口的收发室里。这和Socket模型非常类似。

下面就以老陈接收信件为例讲解linux的 Socket I/O模型。

JAVA NIO概述(一),javanio概述

NIO是jdk1.4加入的新功能,我们一般成为非阻塞IO,在1.4之前,JAVA中的都是BIO(堵塞IO),BIO有以下几个缺点:

我们会经常听到 同步(synchronous) IO和异步(asynchronous)
IO,那么阻塞(blocking)
IO和非阻塞(non-blocking)IO,,同步(synchronous)
IO和异步(asynchronous) IO,阻塞(blocking)
IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?  
我们先对UNIX常用的I/O模型做一个简单的介绍.Linux会把所有外部设备都当做一个文件来操作,对文件的读写会返回一个file
descriptor(fd,文件描述符)。对socket读写也会返回相应的描述符,称作socketfd(socket
描述符),描述符是一个数组,指向内核中的一个结构体(文件路径,数据区等一些属性)
UNIX提供了5中I/O模型:堵塞I/O模型,非堵塞I/O模型,I/O复用模型,信号驱动I/O模型,异步I/O。
对于一个network
IO,以read操作来举例,它会涉及到两个系统对象,1.调用这个IO的process or
thread,2.系统内核(kernel),当一个read操作发生时,它会经过两个步骤:
上面5种IO模型就是在这两个阶段上各自有不同的情况。      

一、同步阻塞模型

老陈的女儿第一次去外地工作,送走她之后,老陈非常的挂心她安全到达没有;
于是老陈什么也不干,一直在小区门口收发室里等着她女儿的报平安的信到;

这就是linux的同步阻塞模式;

在这个模式中,用户空间的应用程序执行一个系统调用,并阻塞,
直到系统调用完成为止(数据传输完成或发生错误)。

Socket设置为阻塞模式,当socket不能立即完成I/O操作时,进程或线程进入等待状态,直到操作完成。

如图1所示:

图片 1

/*
 * brief
 * tcp client
 */

#include 
#include 
#include 
#include 
#include 
#define SERVPORT 8080
#define MAXDATASIZE 100

int main(int argc, char *argv[])
{
  int sockfd, recvbytes;
  char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */
  char snd_buf[MAXDATASIZE];
  struct hostent *host;             /* struct hostent
                                     * {
                                     * char *h_name; // general hostname
                                     * char **h_aliases; // hostname's alias
                                     * int h_addrtype; // AF_INET
                                     * int h_length; 
                                     * char **h_addr_list;
                                     * };
                                     */
  struct sockaddr_in server_addr;

  if (argc < 3)
  {
    printf("Usage:%s [ip address] [any string]n", argv[0]);
    return 1;
  }

  *snd_buf = '';
  strcat(snd_buf, argv[2]);

  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  {
    perror("socket:");
    exit(1);
  }

  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVPORT);
  inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
  memset(&(server_addr.sin_zero), 0, 8);

  /* create the connection by socket 
   * means that connect "sockfd" to "server_addr"
   * 同步阻塞模式 
   */
  if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
    perror("connect");
    exit(1);
  }

  /* 同步阻塞模式  */
  if (send(sockfd, snd_buf, sizeof(snd_buf), 0) == -1)
  {
    perror("send:");
    exit(1);
  }
  printf("send:%sn", snd_buf);

   /* 同步阻塞模式  */
  if ((recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, 0)) == -1)
  {
    perror("recv:");
    exit(1);
  }

  rcv_buf[recvbytes] = '';
  printf("recv:%sn", rcv_buf);

  close(sockfd);
  return 0;
}

显然,代码中的connect, send, recv都是同步阻塞工作模式,

在结果没有返回时,程序什么也不做。
这种模型非常经典,也被广泛使用。

优势在于非常简单,等待的过程中占用的系统资源微乎其微,程序调用返回时,必定可以拿到数据;但简单也带来一些缺点,程序在数据到来并准备好以前,不能进行其他操作,需要有一个线程专门用于等待,这种代价对于需要处理大量连接的服务器而言,是很难接受的。

blocking IO 

 在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

图片 2

 

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network
io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整
个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除
block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

二、同步非阻塞模型

收到平安信后,老陈稍稍放心了,就不再一直在收发室前等信,而是每隔一段时间就去收发室检查信箱,这样,老陈也能在间隔时间内休息一会,或喝杯荼,看会电视,做点别的事情,这就是同步非阻塞模型。

同步阻塞 I/O 的一种效率稍低的变种是同步非阻塞
I/O,在这种模型中,系统调用是以非阻塞的形式打开的,这意味着 I/O
操作不会立即完成,
操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或
EWOULDBLOCK),非阻塞的实现是 I/O
命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。

这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。因为数据在内核中变为可用到用户调用
read 返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。

如图2所示:

图片 3

/*
 * brief
 * tcp client
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERVPORT 8080
#define MAXDATASIZE 100

int main(int argc, char *argv[])
{
  int sockfd, recvbytes;
  char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */
  char snd_buf[MAXDATASIZE];
  struct hostent *host;             /* struct hostent
                                     * {
                                     * char *h_name; // general hostname
                                     * char **h_aliases; // hostname's alias
                                     * int h_addrtype; // AF_INET
                                     * int h_length; 
                                     * char **h_addr_list;
                                     * };
                                     */
  struct sockaddr_in server_addr;
  int flags;
  int addr_len;

  if (argc < 3)
  {
    printf("Usage:%s [ip address] [any string]n", argv[0]);
    return 1;
  }

  *snd_buf = '';
  strcat(snd_buf, argv[2]);

  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  {
    perror("socket:");
    exit(1);
  }

  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVPORT);
  inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
  memset(&(server_addr.sin_zero), 0, 8);
  addr_len = sizeof(struct sockaddr_in);

  /* Setting socket to nonblock */
  flags = fcntl(sockfd, F_GETFL, 0);
  fcntl(sockfd, flags|O_NONBLOCK);

  /* create the connection by socket 
   * means that connect "sockfd" to "server_addr"
   * 同步阻塞模式  
  */
  if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
    perror("connect");
    exit(1);
  }

  /* 同步非阻塞模式 */
  while (send(sockfd, snd_buf, sizeof(snd_buf), MSG_DONTWAIT) == -1)
  {
    sleep(10);
    printf("sleepn");
  }
  printf("send:%sn", snd_buf);

  /* 同步非阻塞模式 */
  while ((recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, MSG_DONTWAIT)) == -1)
  {
    sleep(10);
    printf("sleepn");
  }

  rcv_buf[recvbytes] = '';
  printf("recv:%sn", rcv_buf);

  close(sockfd);
  return 0;
}

这种模式在没有数据可以接收时,可以进行其他的一些操作,比如有多个socket时,可以去查看其他socket有没有可以接收的数据;实际应用中,这种I/O模型的直接使用并不常见,因为它需要不停的查询,而这些查询大部分会是无必要的调用,白白浪费了系统资源;非阻塞I/O应该算是一个铺垫,为I/O复用和信号驱动奠定了非阻塞使用的基础。

我们可以使用 fcntl(fd, F_SETFL, flag |
O_NONBLOCK);将套接字标志变成非阻塞,调用recv,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would
block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。

这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

while(1)
{
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;

非阻塞read(设备2);
if(设备2有数据到达)
处理数据;

…………………………
}

如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处理。非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了,在实际应用中非阻塞I/O模型比较少用

non-blocking IO

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking
socket执行读操作时,流程是这个样子:

图片 4

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
从用户进程角度讲
,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次
发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system
call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,用户进程其实是需要不断的主动询问kernel数据好了没有。

 

三、I/O 复用(异步阻塞)模式

频繁地去收发室对老陈来说太累了,在间隔的时间内能做的事也很少,而且取到信的效率也很低.

于是,老陈向小区物业提了建议;

小区物业改进了他们的信箱系统:

住户先向小区物业注册,之后小区物业会在已注册的住户的家中添加一个提醒装置,每当有注册住房的新的信件来临,此装置会发出
“新信件到达”声,提醒老陈去看是不是自己的信到了。这就是异步阻塞模型。

在这种模型中,配置的是非阻塞 I/O,然后使用阻塞 select 系统调用来确定一个
I/O 描述符何时有操作。使 select
调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。对于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知

I/O复用模型能让一个或多个socket可读或可写准备好时,应用能被通知到;

I/O复用模型早期用select实现,它的工作流程如下图:

图片 5

用select来管理多个I/O,当没有数据时select阻塞,如果在超时时间内数据到来则select返回,再调用recv进行数据的复制,recv返回后处理数据。

下面的C语言实现的例子,它从网络上接受数据写入一个文件中:

/*
 * brief
 * tcp client
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#define SERVPORT 8080
#define MAXDATASIZE 100
#define TFILE "data_from_socket.txt"

int main(int argc, char *argv[])
{
  int sockfd, recvbytes;
  char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */
  char snd_buf[MAXDATASIZE];
  struct hostent *host;             /* struct hostent
                                     * {
                                     * char *h_name; // general hostname
                                     * char **h_aliases; // hostname's alias
                                     * int h_addrtype; // AF_INET
                                     * int h_length; 
                                     * char **h_addr_list;
                                     * };
                                     */
  struct sockaddr_in server_addr;

  /* */
  fd_set readset, writeset;
  int check_timeval = 1;
  struct timeval timeout={check_timeval,0}; //阻塞式select, 等待1秒,1秒轮询
  int maxfd;
  int fp;
  int cir_count = 0;
  int ret;

  if (argc < 3)
  {
    printf("Usage:%s [ip address] [any string]n", argv[0]);
    return 1;
  }

  *snd_buf = '';
  strcat(snd_buf, argv[2]);

  if ((fp = open(TFILE,O_WRONLY)) < 0)    //不是用fopen
  {
    perror("fopen:");
    exit(1);
  }

  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  {
    perror("socket:");
    exit(1);
  }

  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVPORT);
  inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
  memset(&(server_addr.sin_zero), 0, 8);

  /* create the connection by socket 
   * means that connect "sockfd" to "server_addr"
   */
  if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
    perror("connect");
    exit(1);
  }

  /**/
  if (send(sockfd, snd_buf, sizeof(snd_buf), 0) == -1)
  {
    perror("send:");
    exit(1);
  }
  printf("send:%sn", snd_buf);

  while (1)
  {
    FD_ZERO(&readset);            //每次循环都要清空集合,否则不能检测描述符变化
    FD_SET(sockfd, &readset);     //添加描述符       
    FD_ZERO(&writeset);
    FD_SET(fp,     &writeset);

    maxfd = sockfd > fp ? (sockfd+1) : (fp+1);    //描述符最大值加1

    ret = select(maxfd, &readset, NULL, NULL, NULL);   // 阻塞模式
    switch( ret)
    {
      case -1:
        exit(-1);
        break;
      case 0:
        break;
      default:
        if (FD_ISSET(sockfd, &readset))  //测试sock是否可读,即是否网络上有数据
        {
          recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, MSG_DONTWAIT);
          rcv_buf[recvbytes] = '';
          printf("recv:%sn", rcv_buf);

          if (FD_ISSET(fp, &writeset))
          {
            write(fp, rcv_buf, strlen(rcv_buf));   // 不是用fwrite
          }
          goto end;
        }
    }
    cir_count++;
    printf("CNT : %d n",cir_count);
  }

end:
  close(fp);
  close(sockfd);
  return 0;
}

perl实现:

#! /usr/bin/perl
###############################################################################
# File
#  tcp_client.pl
# Descript
#  send message to server
###############################################################################
use IO::Socket;
use IO::Select;

#hash to install IP Port
%srv_info =(

#"srv_ip"  => "61.184.93.197",
      "srv_ip"  => "192.168.1.73",
      "srv_port"=> "8080",
      );

my $srv_addr = $srv_info{"srv_ip"};
my $srv_port = $srv_info{"srv_port"};

my $sock = IO::Socket::INET->new(
      PeerAddr => "$srv_addr",
      PeerPort => "$srv_port",
      Type     => SOCK_STREAM,
      Blocking => 1,
#     Timeout  => 5,
      Proto    => "tcp")
or die "Can not create socket connect. $@";

$sock->send("Hello server!n", 0) or warn "send failed: $!, $@";
$sock->autoflush(1);

my $sel = IO::Select->new($sock);
while(my @ready = $sel->can_read)
{
  foreach my $fh(@ready)
  {
    if($fh == $sock)
    {
      while()
      {
        print $_;
      }
      $sel->remove($fh);
      close $fh;
    }
  }
}
$sock->close();

IO multiplexing

IO复用模型,也就是linux中常说的select、epoll,有一些地方也称为事件驱动模型。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

图片 6

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个
socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking
IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system
call (select 和 recvfrom),而blocking IO只调用了一个system call
(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用
select/epoll的web server不一定比使用multi-threading + blocking IO的web
server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing
Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被
block的。只不过process是被select这个函数block,而不是被socket
IO给block。

 

linux中一般选用epoll作为轮询和网络事件通知,因为它相比select有以下改进:

 

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图