I/O多路复用-select函数

简介: I/O多路复用-select函数
I/O多路复用-select函数


简介

套接口默认情况都是阻塞的,如connect、accept、recv、recvfrom函数等

当进程执行到这些函数时,会被阻塞,直到有对应的事件发生,函数才会返回,进程继续执行。

WinSock,select函数有5个参数:第一个参数没有使用,只是为了与FreeBSD兼容;中间3个参数用于描述符集合,属于值-结果参数;最后一个参数可以让进程设置等待的超时时间。

进程与系统通过描述符集合来交换信息,调用函数时,进程可以在描述符集合中指定一个或多个需要系统监测的描述符;函数返回时,系统把满足条件的描述符放置到集合中,进程通过检查描述符集合来确定哪些描述符上发生了它感兴趣的事件,如读、写、异常。

描述符集合在不同的系统上有不同的实现方式

  • FreeBSD上使用的是一个整数数组,每一位表示一个描述符,即数组的第一个元素对应描述符0~31;第二个元素对应描述符32~63,依此类推。集合中没有记录已经设置了多少个描述符,这是通过select函数的第一个参数告诉内核的。通常定义一个32个元素的数组就可以表示1024个描述符。
  • 在WinSock中,描述符可以是除INVALID_SOCKET之外的任意整数,完全是用数组实现的,数组中的每个元素表示一个描述符。
  • 对外提供的是一致的编程接口:一个数据类型fd_set和4个宏
void FD_ZERO(SOCKET fd, fd_set *set)
void FD_CLR(SOCKET fd, fd_set *set)
void FD_SET(SOCKET fd, fd_set *set)
int FD_ISSET(SOCKET fd, fd_set *set)
数据结构fd_set

描述符集合,集合中存放的是套接口描述符,fd_set的清空、元素的加入、删除以及检查元素是否在集合中都是用宏来完成的。

成员fd_count是集合中已经设置的套接口描述符的数量

fd_array数组保存已经设置的套接口描述符

typedef struct fd_set {
        u_int fd_count;                  /* 集合中的元素个数 */
        SOCKET  fd_array[FD_SETSIZE];    /* 描述符数组 */
} fd_set;

winsock2.h中FD_SETSIZE的定义

#ifndef FD_SETSIZE
#define FD_SETSIZE      64
#endif /* FD_SETSIZE */

没有定义FD_SETSIZE时,把它定义为64,这是默认值。

包含WinSock2.h头文件之前,可将FD_SETSIZE定义为更大的值,来加大select函数使用的描述符集合的大小

WinSock对每个进程可以使用的描述符集合大小不做限制,但是通常很少有程序使用的描述符个数超过64

FD_ZERO

把集合初始化为0,更确切地说,是把集合中的元素个数初始化为0,并不修改描述符数组。

使用集合前,必须用FD_ZERO初始化,否则集合在栈上作为自动变量分配时,fd_set中将是随机值,导致不可预测的问题。

#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)

说明:

  1. 数组中的描述符一定是从下标0开始,并且顺序存放,无法根据数组中的值判断描述符是否有效
  2. 函数select的第一个参数nfds将被忽略,它最初的意义是被测试的描述符个数,WinSock的fd_set中包含了描述符个数,因此这个参数就没有必要了。
FD_SET

向集合中加入一个套接口描述符

当套接口描述符已经在集合中或者集合中的描述符个数已经达到最大值FD_SETSIZE时,FD_SET对集合不执行任何操作

只有当这两个条件不满足时,才把描述符加入集合中,并把描述符的个数加1。

#define FD_SET(fd, set) do { \
      u_int __i; \
      for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
           if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
               break; \
           } \
      } \
      if (__i == ((fd_set FAR *)(set))->fd_count) { \
           if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
               ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
               ((fd_set FAR *)(set))->fd_count++; \
           } \
      } \
} while(0)
  1. 宏的定义,FD_SET需要多条语句才能完成,用do … while把代码包裹起来,成为一个语句块,局部变量只在这个语句块中起作用,另外,当使用这个宏时,可以在宏的后面加上‘;’,符合C语言的语法
  2. 套接口描述符没在集合中,且小于FD_SETSIZE,把描述符加入集合中,集合元素数增加1
FD_CLR

从集合中移除一个套接口描述符。

在集合中查找对应的描述符,找到对应的描述符后,把后面的描述符依次前移一个位置,最后把描述符的个数减少

#define FD_CLR(fd, set) do { \
      u_int __i; \
      for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
           if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
               while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                   ((fd_set FAR *)(set))->fd_array[__i] = \
                       ((fd_set FAR *)(set))->fd_array[__i+1]; \
                   __i++; \
               } \
               ((fd_set FAR *)(set))->fd_count--; \
               break; \
           } \
      } \
} while(0)

这个宏的实现效率不高,可以把它做如下修改:

#define FD_CLR(fd, set) do { \
     u_int __i; \
     for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
          if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
              ((fd_set FAR *)(set))->fd_array[__i] = \
    ((fd_set FAR *)(set))->fd_array[--((fd_set FAR *)(set))->fd_count]; \
              break; \
          } \
     } \
} while(0)
FD_ISSET

检查描述符是否在集合中,如果在集合中返回非0值,否则返回0。

extern int PASCAL FAR __WSAFDIsSet(SOCKET, fd_set FAR *);
#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))

这个宏没有在头文件中直接实现,而是定义成了一个函数__WSAFDIsSet。这样做主要是用户可以直接判断函数的返回值,而用宏直接实现不太方便。

推断它的实现:

int PASCAL FAR __WSAFDIsSet(SOCKET fd, fd_set FAR *set)
{
    u_int i = set->fd_count;
    while (i--)
    {
         if (set->fd_array[i] == fd)
             return 1;
    }
    return 0;
}

在操作和检查fd_set集合中的内容时,还是尽量使用系统提供的宏定义,这样能保证不同编程环境中软件的可移植性。

select函数

允许进程规定一个或多个描述符集合,并指明对哪些描述符上发生的哪些事件感兴趣,当描述符集合中对应的事件没有发生时,进程要等待多长时间。

int WSAAPI select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds,
              fd_set FAR *exceptfds, const struct timeval FAR * timeout);
  • 返回值大于0时,为包含在fd_set结构中就绪的套接口句柄总数;返回0时,为程序设定的时间超时;失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。
  • 参数nfds,WinSock不关心这个参数的值,它的存在主要是为了保持与FreeBSD的兼容性。在FreeBSD系统上,nfds要设置为最大描述符加1,因此它的参数名是maxfdp1。WinSock中描述符的个数记录在结构fd_set中,这个参数不是必要的。
  • readfds、writefds、exceptfds是描述符集合,都是值-结果参数。调用select函数时,指定了进程要让系统监测的套接口描述符及描述符上的哪些事件:读、写、异常。
    某一事件的描述符集合是由fd_set结构定义的,对于普通的文件,读和写都比较简单,但套接口涉及与对方的交互以及底层协议的状态转换,可读和可写的判断比较复杂。
  • 等待时间,最后一个参数timeout规定了3个描述符集合中任意一个套接口上有事件发生时要等待多长时间。参数timeout类型为struct timeval
struct timeval {
        long    tv_sec;         /*秒 */
        long    tv_usec;        /* 微秒 */
};
  • 等待确定的时间
    timeout规定的时间内,集合中有一个或多个套接口上有事件发生时,select函数返回已经就绪的套接口个数。到了timeout的时间,集合中任何套接口上都没有事件发生,select返回0,告诉进程已经超时,这是select最常用的方式。
  • 一直等待
    把timeout设为空指针时,没有超时时间,进程一直等待,直到集合中有套接口上发生事件时才返回,服务器程序常用这种方式。
  • 不等待
    参数timeout中的tv_sec和tv_sec都设置为0,select检查集合中的套接口是否有事件发生后立即返回,这是一种轮询的方式,效率较低,通常不这样使用。
可读性
  • 参数readfds中规定,系统应检查可读性的套接口。套接口正在侦听,如果有客户的连接请求到达,TCP协议与客户端成功建立连接后,套接口是可读的。调用accept能够立即完成,不发生阻塞。
  • 套接口接收队列缓冲区中的字节数大于0,调用接收函数recv或recvfrom返回可以接收的字节数。
  • 面向连接的套接口,当对方正常关闭连接,即发送了FIN时,执行半关闭,recv将立即返回,返回值为0。如果连接被重置,对方发送了RESET,recv立即返回,错误码为WSAECONNRESET。
可写性
  • 参数writefds中规定,系统应检查可写性的套接口。如果套接口正在连接,已经调用了connect,一旦连接建立成功,套接口是可写的。
  • 套接口没有处理connect调用,可写意味着send、sendto可以成功,但如果发送数据超过协议栈可用的缓冲区大小,调用将被阻塞,阻塞的时间是不确定的。
异常
  • 参数exceptfds中规定,系统应检查异常事件的套接口。套接口选项SO_OOBINLINE是FALSE,收到带外数据(也称快速数据)时,被认为满足异常条件。
  • 面向连接的套接口,由于Keepalive失败,对方终止了连接。
  • 非阻塞套接口正在做connect操作,连接失败,被看做异常事件。

说明:

  1. 函数select 3个集合中的任意两个可以为NULL,至少要有一个集合不为NULL,任何不是NULL的集合至少要包含一个套接口描述符。
  2. 成功时select返回满足条件的套接口个数,并把集合中没有发生事件的套接口清除,可以用FD_ISSET测试哪个集合中的套接口有事件发生。
  3. 发生超时,返回0。失败时返回SOCKET_ERROR,可以调用WSAGetLastError得到错误码。
  4. 因为select会对集合进行更新,所以每次调用select时都要对集合重新初始化,并调用FD_SET把用户关心的套接口设置到对应的集合中。
Demo
MSP协议

Message Send Protocol(MSP),翻译为“消息发送协议”,正式发布的标准是RFC1312。

MSP向指定主机上的用户发送一条短消息,并显示在终端上。在一些主机上,把这项服务称做“SEND”。

发送的消息由8部分组成,每一部分都必须存在,整个消息的长度不能超过512字节,既可以使用TCP也可以使用UDP,侦听端口都是18

第一个字节是版本号,余下部分都是以‘\0’结尾的8位字符序列,使用ISO 8859/1字符集,各部分的内容可以为空,比较时不区分大小写。

  1. 版本号:第一个字节是协议的版本,RFC1312的版本值为十进制66,对应字母‘B’。协议有变化时,版本值也要调整。
  2. 接收者:消息接收者的用户名,如果为空,消息可以被交给目的系统上的任意用户。
  3. 接收终端:接收消息的终端名,这份规范没有对终端名做出定义。如果为空,由接收者来选择正确的终端;为‘*’,表示目的系统上的所有终端。
  4. 消息:实际的内容,消息文本中应该只包含可打印的字符,加上回车、换行和制表符。如果包含非法字符,服务器可以拒绝这条消息或把非法字符过滤掉再显示。当消息为空时,服务器可以把收到的内容丢弃。
  5. 发送者:发送者的用户名,不能为空。当它为空时,服务器可以接受、拒绝或忽略这条消息。
  6. 发送终端:发送者的终端名,可以为空。不为空时,答复消息发向这个终端。
  7. Cookie:必须存在,只对UDP服务有意义,与发送方UDP端口组合出的串应该是唯一的,不包含结束符的最大长度是32字节。它的主要用处是为了确保消息被接收到,客户端可以把同一消息发送多次。服务器可用Cookie与端口的组合识别重复的消息,通常用时间字符串作为Cookie。
  8. 签名:服务器使用签名验证发送者的身份。

基于TCP的消息发送服务

服务器在端口18上侦听,客户端一旦与服务器建立了连接,就向服务器发送一条消息,服务器收到消息后向客户端发送响应,指明对消息的处理结果,响应消息格式是:单个字符‘+’,正确认,或者‘-’,负确认,后跟可选的消息说明信息,以‘\0’结尾。

正确认表示消息被成功地交给了用户的终端,负确认表示消息没有被交付给任何终端,可以包含一些信息,说明消息为什么没有交给终端。

一个TCP连接上面可以发送多条消息,客户应该先关闭连接,服务器发送完响应后不应该立刻关闭连接,它可以启动一个定时器,超时后再关闭连接。

基于UDP的消息发送服务

使用UDP的服务器也在端口18上侦听,服务器收到消息后向客户端发送一条响应消息。

使用UDP的一个主要特点是可以向指定的用户或所有用户广播消息服务器只允许向单播地址发送响应,不能向广播地址发送响应消息,否则广播的客户端将收到多个服务器的应答消息。

MSP服务器程序

MSP服务器提供了下面的功能:

  1. 默认的侦听端口是18,可以在命令行中对其进行更改。如果机器上的18端口已经被占用,服务器在bind时会失败,这时可以在命令行中将其更换到其他的端口号。
  2. 是一个并发服务器程序,可以同时处理多个客户的请求,不仅支持TCP类型的套接口,而且支持UDP类型的套接口
  3. 接收到客户的消息后,检查版本是否匹配,不匹配时返回版本错误消息,再解析协议其他字段的值,并把收到的信息显示在屏幕上。最后向客户端发回应答信息,有错误时,发出错误的原因;成功处理该消息时,发送消息被接受了

MSP_Server.h 文件

#ifndef _MSP_SERVER_
#define _MSP_SERVER_
#define MSG_SEND_VERS       'B' /* 版本号 */
#define MSG_SEND_PORT       18  /* 消息发送协议默认端口*/
#define MSG_BUF_SIZE       512  /* 缓冲区的大小        */
/* 函数返回值 */
#define MSG_SUCCESS         0
#define MSG_VERSION_ERR    -1
#define MSG_FIELD_ERR      -2
/* 定义消息字段的枚举值 */
// 解析从客户端接收到的消息时
//把协议各个字段的信息保存到枚举值对应的数组元素中
enum msp_field
{
    MSP_VERSION,
    MSP_RECIP,
    MSP_RECIP_TERM,
    MSP_MESSAGE,
    MSP_SENDER,
    MSP_SEND_TERM,
    MSP_COOKIE,
    MSP_SIGNAT,
    MSP_TOTAL
};
struct field_name
{
    enum msp_field field;
    char *name;
};
#endif /* _MSP_SERVER_ */
  1. 每个协议字段的枚举值对应一个协议中定义的名字,在输出信息时使用

MSP_Server.c文件

#include <winsock2.h>
#include <stdio.h>
#include "MSP_Server.h"
#pragma comment(lib, "ws2_32.lib")  /* WinSock使用的库函数 */
/* 协议中字段的枚举值及对应的名字 */
static struct field_name msp_info[MSP_TOTAL] = {
    { MSP_VERSION,     "VERSION"    },
    { MSP_RECIP,       "RECIPIENT"  },
    { MSP_RECIP_TERM,  "RECIP-TERM" },
    { MSP_MESSAGE,     "MESSAGE"    },
    { MSP_SENDER,      "SENDER"     },
    { MSP_SEND_TERM,   "SENDER-TERM"},
    { MSP_COOKIE,      "COOKIE"     },
    { MSP_SIGNAT,      "SIGNATURE"  }
};
static SOCKET msp_client_soc[FD_SETSIZE]; /* 记录接受的客户连接 */
static int client_max;  /* 记录数组已经使用下标的最大值 */ 
/**
* 接收客户端的连接请求
* @param lstn_soc 监听套接口
* @param soc_set 新接受的套接口需要加入的集合
* @return success: 新接受的套接口  fail: INVALID SOCKET 
*/ 
SOCKET msp_new_client(SOCKET lstn_soc, fd_set *soc_set)
{
    int i;
    struct sockaddr_in faddr;
    int addr_len = sizeof(faddr);
    SOCKET acpt_soc;
    acpt_soc = accept(lstn_soc, (struct sockaddr *)&faddr, &addr_len);
    if (acpt_soc == INVALID_SOCKET)
        return INVALID_SOCKET;
    printf("[MSP] accept connection : %s\n", inet_ntoa(faddr.sin_addr));
    for (i = 0; i < FD_SETSIZE; i++)
    {
           if (msp_client_soc[i] == INVALID_SOCKET)
           {
               msp_client_soc[i] = acpt_soc;
               break;
           }
    }
    if (i == FD_SETSIZE) /* 已经满了 */
    {
        closesocket(acpt_soc);
        return INVALID_SOCKET;
    }
    if (i > client_max)
        client_max = i;
    FD_SET(acpt_soc, soc_set);
    return acpt_soc;
}
/**
* 关闭客户端的套接口,把它从soc_set中删除,全局数组中对应位置清空 
*/
void msp_close_client(int index, fd_set *soc_set)
{
      SOCKET sock = msp_client_soc[index];
      closesocket(sock);
      FD_CLR(sock, soc_set);
      msp_client_soc[index] = msp_client_soc[client_max]; // 和最后一个交换 
      msp_client_soc[client_max--]= INVALID_SOCKET;  // 赋值为一个无效值 
}
/**
*接收客户端发送的消息
* @param i 客户端fd在msp_client_soc中的下标
* @param buf 接收缓存区 
* @param len 缓存区大小
* @param from 地址结构体
* @return 接收字符个数 
*/ 
int msp_recv_data(int i, char *buf, int len, struct sockaddr_in *from)
{
      int result, size = sizeof(*from);
      SOCKET soc = msp_client_soc[i];
      // i==0 存放server的套接字描述符 
      if (i == 0) /* 基于UDP服务的socket */
           result = recvfrom(soc, buf, len, 0, (struct sockaddr *)from, &size);
      else
          result = recv(soc, buf, len, 0); /* 基于TCP服务的socket */
      return result;
}
/**
*解析处理请求
* @param buf 数据缓存区
* @param len buf的长度 
* @return 成功返回MSG_SUCCESS,失败返回小于0 
*/ 
int msp_process_request(char *buf, int len)
{
  // 协议中除了版本号,其他的字段都是以0结尾的字符串 
    char *sep, *buf_end = buf + len;
    char *msp_field[MSP_TOTAL] = { NULL };
    int i = MSP_RECIP;
    if (buf[0] != MSG_SEND_VERS) /* 检查版本 */
           return MSG_VERSION_ERR;
    /* 解析其他字段值 */
    msp_field[MSP_RECIP] = ++buf;
    sep = memchr(buf, 0, buf_end - buf); // 指向第一个字符串结尾位置 
    while (sep && (sep < buf_end))
    {
        msp_field[++i] = sep + 1;
        if (i == MSP_SIGNAT)
            break;
        buf = sep + 1;
        sep = memchr(buf, 0, buf_end - buf);
    }
    if (i != MSP_SIGNAT)
        return MSG_FIELD_ERR;
    /* 在屏幕上输出收到的信息 */
    printf("________________________________\n");
    for (i = MSP_RECIP; i < MSP_TOTAL; i++)
    {
        printf("%s : %s\n", msp_info[i].name,
            msp_field[i][0] ? msp_field[i] : "Empty");
    }
    printf("________________________________\n");
    return MSG_SUCCESS;
}
/**
* 向客户端发送响应
* @param i 客户端套接字在msp_client_soc中的下标
* @param error 对接受数据的处理结果
* @param from 客户端地址 , UDP时会使用
* @return 成功:返回发送的字节数,失败:返回SOCKET_ERROR 
*/ 
int msp_send_reply(int i, int error, struct sockaddr_in *from)
{
    int result, msg_len, from_len = sizeof(*from);
    SOCKET client_soc = msp_client_soc[i]; /* 与客户交互的socket */
    char *msg, *reply_desc[] = { "+ Message is accepted.",
                                 "- Version is not match.",
                                 "- Message format is wrong.",
                                 "- Unknow error." };
    msg = reply_desc[-error];
    msg_len = strlen(msg);
    if (i == 0) /* 基于UDP的服务器 */
        result = sendto(client_soc, msg, msg_len, 0,
                    (struct sockaddr *)from, from_len);
    else
      result = send(client_soc, msg, msg_len, 0); /* 基于TCP的服务器 */
    return result;
}
int main(int argc, char **argv)
{
    WSADATA wsa_data;
    SOCKET srv_soc_t = 0, srv_soc_u; /* 服务器的socket句柄 */
    struct sockaddr_in srv_addr, clnt_addr;
    unsigned short port = MSG_SEND_PORT;
    int i, result, ready_cnt, addr_len = sizeof(srv_addr);
    char recv_buf[MSG_BUF_SIZE+1];
    SOCKET new_soc, client_soc;
    fd_set read_all, read_set;
    if (argc == 2)
        port = atoi(argv[1]);
    /* 初始化客户连接的socket */
    for (i = 0; i < FD_SETSIZE; i++)
        msp_client_soc[i] = INVALID_SOCKET;
    WSAStartup(WINSOCK_VERSION, &wsa_data); /* 初始化WinSock资源 */
    srv_soc_t = socket(AF_INET, SOCK_STREAM, 0);/* 基于TCP socket */
    /* 消息发送协议服务器地址 */
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(port);
    srv_addr.sin_addr.s_addr = INADDR_ANY;
    /* 绑定服务器地址和端口 */
    result = bind(srv_soc_t, (struct sockaddr *)&srv_addr, addr_len);
    if (result == SOCKET_ERROR)
    {
        printf("[MSP] fail to bind: %d\n", WSAGetLastError());
        closesocket(srv_soc_t);
        return -1;
    }
    listen(srv_soc_t, SOMAXCONN);
    srv_soc_u = socket(AF_INET, SOCK_DGRAM, 0); /* 基于UDP socket */
    bind(srv_soc_u, (struct sockaddr *)&srv_addr, addr_len);
    FD_ZERO(&read_all);
    FD_SET(srv_soc_t, &read_all);
    FD_SET(srv_soc_u, &read_all);
    msp_client_soc[0] = srv_soc_u; /* UDP socket固定保存在第一个位置 */
    printf("[MSP] server is running ... ...\n");
    while (1)
    {
        read_set = read_all;
        ready_cnt = select(0, &read_set, NULL, NULL, NULL);
        if (ready_cnt == SOCKET_ERROR)
        {
            printf("[MSP] select error: %d\n", WSAGetLastError());
            break;
        }
        if (FD_ISSET(srv_soc_t, &read_set)) /* 检查TCP的侦听socket */
        {
            new_soc = msp_new_client(srv_soc_t, &read_all);
            if (--ready_cnt <= 0)
                continue;
        }
        for (i = 0; (i <= client_max) && (ready_cnt > 0); i++)
        {
            client_soc = msp_client_soc[i];
            if (!FD_ISSET(client_soc, &read_set))
                continue;
            /* 接收数据 */
            result = msp_recv_data(i, recv_buf, MSG_BUF_SIZE, &clnt_addr);
            if (result <= 0)
            {
                msp_close_client(i, &read_all);
                --ready_cnt;
                continue;
            }
            result = msp_process_request(recv_buf, result); /* 处理请求 */
            result = msp_send_reply(i, result, &clnt_addr); /* 发送应答 */
            if (result == SOCKET_ERROR)
                msp_close_client(i, &read_all);
            --ready_cnt;
        }
    }
    /* 关闭与客户端的socket */
    for (i = 0; i <= client_max; i++)
        closesocket(msp_client_soc[i]);
    closesocket(srv_soc_t);
    WSACleanup();
    return 0;
}
MSP客户端程序

客户端程序接收用户输入信息,把用户的输入组织成MSG协议的格式发送给服务器,收到服务器数据时,显示查询结果。

#include <winsock2.h>
#include <stdio.h>
#include <time.h>
#pragma comment(lib, "ws2_32.lib")  /* WinSock使用的库函数 */
#define MSG_SEND_VERS         'B' /* 版本     */
#define MSG_TERMINAL    "Console" /* 终端名称 */
#define MSG_SEND_PORT         18  /* 消息发送协议默认端口*/
#define MSG_BUF_SIZE         512  /* 缓冲区的最大长度    */
#define MSG_EACH_SIZE        160  /* 每一部分最大长度    */
#define MSG_COOKIE_LEN        15  /* Cookie的最小长度   */
enum msg_status /*输入的状态 */
{
    MSG_RECIP,
    MSG_CONTENT,
    MSG_SENDER,
    MSG_TOTAL
};
/**
* 根据日期和时间生成cookie
* @param buf 保存数据的缓冲区
* @param len 缓冲区长度
* @return 成功数据长度, 失败 0 
*/
int msp_make_cookie(char *buf, int len)
{
    int result;
    time_t t;
    struct tm *time_p;
    if (len < MSG_COOKIE_LEN)
        return 0;
    t = time(NULL);
    time_p = localtime(&t);
    result = sprintf(buf, "%04d%02d%02d%02d%02d%02d", time_p->tm_year+1900,
            time_p->tm_mon + 1, time_p->tm_mday, time_p->tm_hour,
            time_p->tm_min, time_p->tm_sec);
    return result;
}
/**
* 接收输入并生成放松数据
* @param msg_soc 套接口描述符
* @param prompt 是否显示菜单选择信息
* @return Success: 发送数据长度  fail: <= 0 
*/
int msp_input_send(SOCKET msg_soc, int prompt)
{
    int offset, result, menu, status = MSG_RECIP;
    int term_len = strlen(MSG_TERMINAL) + 1;
    char send_buf[MSG_BUF_SIZE], *msg = send_buf;
    /* 请用户选择是否继续? */
    if (prompt)
    {
        printf("\n------- MENU -------\n");
        printf("*  1 Send Message  *\n");
        printf("*  2 Bye            *\n");
        printf("--------------------\n");
        scanf("%d", &menu);
        if (menu != 1)
            return 0;
        fflush(stdin);
    }
    *msg++ = MSG_SEND_VERS; /* 版本 */
    // 状态机 
    printf("\nTo : ");
    while (fgets(msg, MSG_EACH_SIZE, stdin) != NULL)
    {
        offset = strlen(msg);
        *(msg + offset -1) = 0; /* 去掉结尾的 '\r' */
        switch (status)
        {
        case MSG_RECIP:
            strcpy(msg + offset, MSG_TERMINAL);
            offset += term_len; /* 终端名的长度, 包括结束符 */
            printf("Message : ");
            break;
        case MSG_CONTENT:
            printf("From : ");
            break;
        case MSG_SENDER:
            strcpy(msg + offset, MSG_TERMINAL);
            offset += term_len; /* 终端名的长度, 包括结束符 */
            /* 构造COOKIE */
            msg += offset;
            result = msp_make_cookie(msg, send_buf + MSG_BUF_SIZE - msg);
            msg += (result + 1);
            *msg = 0; /* SIGNATURE为空 */
            offset = 1;
            break;
        default:
            break;
        }
        msg += offset;
        if (++status == MSG_TOTAL)
            break;
    }
    /* 发送数据 */
    result = msg - send_buf;
    result = send(msg_soc, send_buf, result, 0);
    if (result == SOCKET_ERROR)
        printf("[MSP] Fail to send %d\n", WSAGetLastError());
    return result;
 }
 
int main(int argc, char **argv)
{
    WSADATA wsa_data;
    SOCKET msg_soc = 0; /* 客户端socket句柄 */
    struct sockaddr_in srv_addr;   /* 服务端地址 */
    unsigned short port = MSG_SEND_PORT;
    int result = 0, ready, addr_len = sizeof(srv_addr);
    u_long non_block = 1;
    char data_buf[MSG_BUF_SIZE], *ip_addr = "127.0.0.1";
    fd_set read_all, read_set, write_all, *write_fds = NULL;
    struct timeval timeval = { 60, 0 };
    /* 解析命令行参数 */
    switch (argc)
    {
    case 3: // 注意没有break 
        port = atoi(argv[2]);
    case 2:
        ip_addr = argv[1];
        break;
    }
    WSAStartup(WINSOCK_VERSION, &wsa_data); /* 初始化WinSock资源 */
    msg_soc = socket(AF_INET, SOCK_STREAM, 0);
    result = ioctlsocket(msg_soc, FIONBIO, &non_block);
    /* 设置socket集合 */
    FD_ZERO(&read_all);
    FD_ZERO(&write_all);
    FD_SET(msg_soc, &read_all);
    /* 填写要连接的服务器 */
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(port);
    srv_addr.sin_addr.s_addr = inet_addr(ip_addr);
    result = connect(msg_soc, (struct sockaddr *)&srv_addr, addr_len);
    if (result == 0) {/* 连接成功 */
        write_fds = NULL;
        msp_input_send(msg_soc, 0);
    }
    else if (WSAGetLastError() == WSAEWOULDBLOCK)
    {
        write_fds = &write_all;
        FD_SET(msg_soc, &write_all);
    }
    else
    {
        closesocket(msg_soc);
        return -1;
    }
    while (1)
    {
        read_set = read_all;
        ready = select(0, &read_set, write_fds, NULL, &timeval);
        if (ready <= 0) /* 出错或超时 */
        {
            printf("[MSP] Select error.\n");
            break;
        }
        
        /* 连接成功的socket是可写的 */
        if (write_fds && FD_ISSET(msg_soc, write_fds))
        {
            msp_input_send(msg_soc, 0);
            write_fds = NULL;
        }
        
        if (FD_ISSET(msg_soc, &read_set)) /* socket有数据可以读 */
        {
            result = recv(msg_soc, data_buf, MSG_BUF_SIZE, 0);
            if (result > 0)
            {
                data_buf[result] = 0;
                printf("[MSP] Receive : %s\n", data_buf);
            }
            else
                break;
            result = msp_input_send(msg_soc, 1); /* 重新输入并发送 */
            if (result <= 0)
                break;
        }
    }
    closesocket(msg_soc);
    WSACleanup();
    return 0;
}

非阻塞套接口调用connect函数会有三种情况:

  1. 连接成功,直接发送数据
  2. 阻塞,返回WSAEWOULDBLOCK,需要等待连接成功事件
    把套接字增加到write集合中,让select检测写事件
相关文章
|
6月前
IO多路转接 ——— select、poll、epoll(下)
IO多路转接 ——— select、poll、epoll
IO多路转接 ——— select、poll、epoll(下)
浅谈select,poll和epoll的区别
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! select,poll和epoll其实都是操作系统中IO多路复用实现的方法。 select select方法本质其实就是维护了一个文件描述符(fd)数组,以此为基础,实现IO多路复用的功能。
浅谈select,poll和epoll的区别
|
4月前
|
存储 消息中间件 Kubernetes
多路复用I/O-select
多路复用I/O-select
33 0
|
4月前
|
Unix Linux API
select和poll的区别
select和poll的区别
|
4月前
|
监控 Linux
IO多路复用,epoll和select的区别
IO多路复用,epoll和select的区别
20 0
|
5月前
|
Linux
Linux网络编程(多路IO复用select函数使用)
Linux网络编程(多路IO复用select函数使用)
34 0
|
6月前
IO多路转接 ——— select、poll、epoll(上)
IO多路转接 ——— select、poll、epoll
|
12月前
|
Linux Windows
【Linux网络编程】select多路复用
【Linux网络编程】select多路复用
|
Linux
一文搞懂select、poll和epoll区别
一文搞懂select、poll和epoll区别
630 1
一文搞懂select、poll和epoll区别
|
网络协议 Linux C语言
关于select,epoll的优缺点说明
关于select,epoll的优缺点说明
144 0