学习笔记13-学习《精通UNIX下C语言编程及项目实践》

 

十七章 并发 Socket 程序设计

  非阻塞并发模型

  I/O 阻塞是影响进程并发的重要原因 , 进程一旦进入阻塞 , 就不能再执行任何操作 . 比如进程调用输入函数后 , 在默认情况下必须一直阻塞到产生满足条件的数据为止 .

  套接字也使一种 I/O 设备 , 它有四类阻塞性交易 , 分别是输入类交易 (read, recv recvfrom ), 输出类交易 (write send ), 连接申请交易 (connect) 和连接处理交易 (accept), 其中 UDP 协议数据发送函数 sendto 不产生阻塞 .

  当套接字调用以上函数时 , 将导致进程阻塞 . 因此把套接字函数的阻塞模式更改为非阻塞模式 , 是实现并发套接字程序的一种方法 .

  (1) 非阻塞套接字系统调用

  函数 fcntl 设置套接字描述符的 O_NONBLOCK 标志后 , 即可将 I/O 方式更改为非阻塞方式 .

  下面代码将套接字描述符 nSock 设置为非阻塞方式 .

int val;

val = fcntl(nSock, F_GETFL, 0);

fcntl(nSock, F_SETFL, val | O_NONBLOCK);

  此时 , 当调用函数的成功条件不满足时 , 将立即返回 -1 并置 errno EAGAIN EINPROGRESS 错误 .

  (2) 非阻塞套接字程序设计流程

  非阻塞套接字的程序一般包含一段循环代码 , 在相互中采取轮询的方式 , 分别调用套接字的输入 , 输出 , 申请连接或连接处理函数 , 从而得到并发处理多个套接字的目的 .

  这里设计一个运行在非阻塞方式下的服务器端套接字程序 , 说明通过非阻塞方式实现并发处理的流程 . 它首先在端口 PORT 上创建一个 TCP 侦听套接字 , 然后一边处理客户端的套接字连接申请 , 一边从已连接的客户端接收数据信息 .

   /*------------------- 定义设置非阻塞模式宏 --------------------*/

#define Fsetnonblock(a) /

   { val = fcntl(a, F_GETFL, 0); fcntl(nSock, F_SETFL, val | O_NONBLOCK); }

 

int nLisSock;

int i, n = 0, nSockVar[MAX];

int val;

char buf[1024];

 

   /*------------------- 主流程 --------------------*/

CreateSock(&nLisSock, PORT, MAX);

Fsetnonblock(nLisSock);

   /*--- 循环代码 , 论询 nLisSock 套接字的 accept 函数和连接套接字的 read 函数 ---*/

while(1){

        /*--- 创建套接字描述符 nSockVar[n] 与客户端套接字建立连接 ---*/

    if((nSockVar[n] = accept(nLisSock, NULL, NULL)) > 0){

           /*--- 设置新创建的套接字描述符的非阻塞属性 ---*/

        Fsetnonblock(nSockVar[n++]);

    }

       /*--- 遍历每一个套接字 , 并从客户端套接字中读入数据 ---*/

    for(i=0;i<n;i++){

        read(nSockVar[i], buf, sizeof(buf));

           /*--- 其他的处理代码 ---*/

        ... ... ... ...

    }

}

  从上面可以看到 , 进程虽然能够同时处理一个套接字的侦听和 n 个套接字的数据接收操作 , 却存在两个缺陷 :

  a. 此代码采用了非阻塞轮询的方式 , 极大地浪费了 CPU 时间 .

  b. 此代码没有解决循环的跳出问题 , 进程很有可能一直循环下去 .

     信号驱动并发模型

  有过 Windows 下开发经验的读者都知道消息驱动的概念 . 比如使用 Win32 程序 , 平时挂起或执行其他的操作 , 当套接字有输入发生时 , Windows 产生消息并将此消息发送到目标进程 . 进程接到此消息 , 自动调用响应的处理代码 , 完成套接字数据的读取过程 .

  当文件描述符就绪时 , UNIX 内核会向进程发送 SIGIO 信号 , 进程获取该信号并调用预先准备的处理函数执行 I/O 操作 . 当函数调用完毕后 , 进程回到接收信号前的代码处继续工作 . 在一个套接字上实现信号驱动的步骤如下 :

  (1) 为信号 SIGIO 设计处理函数被捕获信号

Void func(int sig){              // 信号处理函数

     ... ... ... ...

    signal(SIGIO, func);

}

... ... ... ...

signal(SIGIO, func);             // 捕获信号 SIGIO

  (2) 设定套接字的归属主 , 设置接收 SIGIO 信号的进程或进程组 .

Fcntl(nSock, F_SETOWN, getid());          // 设置接收信号的进程

或者

fcntl(nSock, F_SETOWN, 0 – getpgrp());  // 设置接收信号的进程组

  (3) 设置套接字的异步 I/O 属性 .

int val;

val = fcntl(nSock, F_GETFL, 0);

fcntl(nSock, F_SETFL, val | O_ASYNC);

  然而 , 套接字信号驱动的设置尽管比较简单 , 但在实践中却难以实现 . 因为 UNIX 内核并不仅仅在套接字有输入时才发送 SIGIO 信号 , 在套接字连接 , 输出 , 错误和其他状态变化时均发送该信号 , 这样就导致了进程在接收信号后无法正确地判断下一步的行为 .

  超时并发模型

  超时也是防止阻塞的一种手段 , 它可以保证进程不被永远挂起 阻塞函数的超时时间长度决定了进程的并发程度 , 超时时间越小 , 并发度越高 , 超时时间越大 , 则并发度越低 .

  UNIX 下的阻塞函数一般都有在进程接收到任意信号后终端返回的传统 , 套接字函数也不例外 , 利用这个特性使用定时器信号 SIGALRM 实现套接字的超时处理 .

  下面我们使用信号加跳转方式设置超时 :

  (1) 定义超时标志和跳转结构 .

  (2) 为信号 SIGALRM 设计处理函数

  (3) 记录跳转点

    (4) 超时判断

  (5) 捕获信号 SIGALRM 并设置定时器

  (6) 调用套接字阻塞处理函数 , 比如套接字接收 , 发送 , 连接申请或者接收连接等

  (7) 取消定时器并忽略信号 SIGALRM.

  本处设计一个套接字连接函数 connect 超时处理的例子 tcpto1.c, 它从命令行输入 IP 地址和端口 , 程序向该 IP 地址和端口建立连接 , 如果连接失败 , 10 秒钟后超时退出 , 代码如下

#include "tcp.h"

 

static int nTimeout = 0;                       // (1) 定义超时标志设置

jmp_buf env;                               // (1) 定义跳转结构

 

void OnTimeOut(int nSignal){                 // (2) 信号处理函数

        signal(nSignal, SIG_IGN);

        nTimeout = 1;

        longjmp(env, 1);

        return;

}

 

int main(int argc, char *argv[])

{

        int nSock = -1, ret;

       

        if(argc != 3)

                 return 1;

        nTimeout = 0;

        setjmp(env);                         // (3) 记录跳转点

        if(nTimeout == 1)                    // (4) 超时判断

                 printf("Connect Timeout./n");

        else{

                 signal(SIGALRM, OnTimeOut);      // (5) 捕获信号 SIGALRM

                 alarm(3);                        // (5) 发送定时器信号 SIGALRM

                 ret = ConnectSock(&nSock, atoi(argv[2]), argv[1]);   // (6) 执行函数

                 alarm(0);                        // (7) 取消定时器

                 signal(SIGALRM, SIG_IGN);        // (7) 忽略信号 SIGALRM

                 if(ret == 0)

                          printf("Connect Success./n");

                 else

                          printf("Connect Error./n");

        }

        if(nSock != -1)

                 close(nSock);

 

        return 0;

}

  其中 , 用到了一个头文件 tcp.h, 其中整合了 ConnectSock 函数 , 代码如下 :

[root@billstone Unix_study]# cat tcp.h

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <signal.h>

#include <setjmp.h>

#include <stdio.h>

#include <errno.h>

#include <fcntl.h>

#include <assert.h>

 

int ConnectSock(int *pSock, int nPort, char *pAddr){

        struct sockaddr_in addrin;

        long lAddr;

        int nSock;

 

         assert(pSock != NULL && nPort > 0 && pAddr != NULL);

        assert((nSock = socket(AF_INET, SOCK_STREAM, 0)) > 0);

        memset(&addrin, 0, sizeof(addrin));

 

        addrin.sin_family = AF_INET;

        addrin.sin_addr.s_addr = inet_addr(pAddr);

         addrin.sin_port = htons(nPort);

        if(connect(nSock, (struct sockaddr*)&addrin, sizeof(addrin)) >= 0){

                *pSock = nSock;

                return 0;

        }

        else{

                close(nSock);

                return 1;

         }

}

 

[root@billstone Unix_study]#

  编译程序 , 分别连接百度网站上的已打开和未打开的端口 , 结果如下

[root@billstone Unix_study]# gcc -o tcpto1 tcpto1.c

[root@billstone Unix_study]# ping www.baidu.com

PING www.a.shifen.com (202.108.22.5) 56(84) bytes of data.

 

--- www.a.shifen.com ping statistics ---

1 packets transmitted, 0 received, 100% packet loss, time 0ms

 

[root@billstone Unix_study]# ./tcpto1 202.108.22.5 80           // 连接存在的 80 HTTP 端口

Connect Success.

[root@billstone Unix_study]# ./tcpto1 202.108.22.5 1234         // 连接不存在的 1234 端口

Connect Timeout.                                  // 延迟 3

[root@billstone Unix_study]#

  多路复用并发模型

  多路复用函数 select 把一些文件描述符集合在一起 , 如果某个文件描述符的状态发生编号 , 比如进入 写就绪 或者 读就绪 状态 , 函数 select 会立即返回 , 并且通知进程读取或写入数据 ; 如果没有 I/O 到达 , 进程将进入阻塞 , 知道函数 select 超时退出为止 .

  利用多路复用 , 进程可以同时监控多个套接字信息 , 在多个套接字上并发地执行操作 .

  几种常见的使用 select 的套接字进程如下 :

  (1) 交互式进程

  程序一边处理客户的交互式输入输出 , 一边使用套接字 . 多路复用标准输入 , 标准输出和套接字文件描述符 .

  (2) 多套接字进程

  程序同时使用侦听套接字和大量的连接套接字 .

  (3) 多协议进程

  程序同时使用 TCP 套接字和 UDP 套接字

  (4) 多服务进程

  程序同时应用多种服务 , 完成多种应用协议 , 比如 inetd 守护进程等 .

  下面以多路复用读套接字文件描述符为例 , 说明并发程序设计的步骤 :

  (1) 创建套接字文件描述符集合

fd_set fdset;                   // 文件描述符集合

FD_ZERO(&fdset);              // 清空集合中的元素

FD_SET(nLisSock, &fdset);       // 监控侦听套接字

FD_SET(nSockVal, &fdset);       // 监控连接套接字

  (2) 准备超时时间

Struct timeval wait;             // 定义超时时间

wait.tv_sec = 0;                // 超时时间为 0.1

wait.tv_usec = 100000;

  (3) 调用函数 select 并检测应答结果

  假设只复用读集合中的套接字描述符 , 其中 MAXSOCK 是集合 fdset 中最大的描述符号 .

Int ret;

ret = select(MAXSOCK+1, &fdset, NULL, NULL, &wait);

if (ret == 0)  ... ... ... ...                        // 超时

else if (ret == -1)  ... ... ... ...                  // 错误

else ... ... ... ...                               // 产生了套接字连接或数据发送请求

  (4) 检测套接字文件描述符的状态并处理之

  如果是侦听套接字 , 其操作流程一般为 :

If (FS_ISSET(nLisSock, &fdset)) {                       // 侦听套接字 , 处理连接申请

    if ((nSockVal = accept(nLisSock, NULL, NULL)) > 0)  ... ... ... ...

}

  如果是连接套接字操作 , 其流程一般为 :

If (FS_ISSET(nLisSock, &fdset)) {                       // 连接套接字 , 读取传输数据

    read (nSockVar[i], buf, sizeof(buf));

}

  采用 select 实现套接字的并发处理 , 有如下优点 :

  (1) 在监控套接字描述符状态变化的过程中 , 函数 select 以阻塞的方式执行 , 这样可以节省 CPU 时间 .

  (2) 当规定的时间到达后 , 套接字仍然没有连接申请 , 接收或发送 , 函数 select 将自动返回 , 这样可以预防进程一直阻塞下去 .

  (3) 函数 select 能够同时监控多个套接字描述符的状态 , 实现套接字的并发处理 .

     多进程并发模型

  服务器套接字进程经常既需要接收客户端的连接申请 , 又要接收客户端发送的数据信息 , 遗憾的是函数 accept 和函数 recv read 都会引起进程阻塞 , 服务器宽进程常常顾此失彼 .

  多进程方法正好可以弥补这个缺陷 , 它创建专门的进程来处理每一个阻塞的套接字函数 , 比如父进程只执行函数 accept 等待并完成客户端的连接申请 , 子进程则执行函数 recv 等待客户端的信息发送 . 虽然每个进程都处于阻塞状态 , 但一旦某个套接字描述符的状态发送变化时 , 它所在的进程都能在第一时间被激活并完成响应操作 , 这样救灾整个进程组中实现了套接字的并发处理 .

  (1) 不固定进程数的并发模型

  多进程实现套接字并发处理最常见的方式是 accept 后创建子进程 , 父进程继续 accept, 子进程完成后续工作 .

  不固定进程数的并发模型的服务器端流程如下 :

  a. 创建侦听套接字 nLisSock (socket, bind listen)

  b. 进程转后台运行

  c. 等待客户端的连接申请 (accept), 创建与客户端的通信连接 nSock

  d. 创建子进程

  e. 父进程关闭套接字 nSock, 此时用于子进程仍然打开此套接字 , 故父进程的关闭操作并不真正断开连接 , 只是把连接上减少 1

  f. 父进程回到步骤 c 继续进行

  g. 子进程关闭侦听套接字 nLisSock, 用于父进程仍然打开着侦听套接字 , 故实际上此套接字仅仅把连接数减少 1, 并不真正关闭它

  h. 子进程执行 recv send 等操作与客户端进行数据交换

  i. 数据交换完毕 , 关闭套接字 , 子进程结束

  (2) 固定进程数的并发模型

  固定进程数的并发模型是一种介于单进程与多进程之间的这种方案 , 服务器父进程在创建侦听套接字后 fork 子进程 , 由子进程等待客户端的 connect 并完成与客户端的通信交换等工作 , 父进程的功能只是维持子进程的数量不变 .

  1) 父进程流程

  固定进程数并发模型的服务器端父进程的流程如下 :

  a. 创建侦听套接字 nLisSock

  b. 进程转后台运行

  c. 创建子进程

  d. wait 子进程 . 如果有子进程退出 , 则立即创建子进程 , 保证子进程在数量上不变 .

  2) 子进程流程

  子进程继续父进程的侦听套接字 , 按下面流程运行 :

  a. 等待客户端的连接申请 (accept), 并与客户端建立通信套接字 nSock 连接

  b. 执行 recv send 等操作与客户端进行数据交换

  c. 数据交换完毕 , 关闭套接字 nSock

  d. 回到步骤 a, 继续执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值