I/O多路复用-select函数

I/O多路复用(select,poll)函数及示例程序 I/O多路复用 在网络编程中,不得不提的一个概念就是I/O多路复用机制,采用I/O多路复用机制,使得内核一旦发现进程指定的一个或多个I/O条件就绪,就通知进程进行处理,能够同时处理多个描述符。 目前Linux上的I/O多路复用机制主要有三种select,poll,epoll,因为select与poll比较相似,因此这篇文章先讲select和poll函数。 接下来分别从函数、结构、注意点分别进行说明... 阅读详情
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检测写事件

Transformer模型在语义通信中的实战应用:从信源编码到端到端优化 本文深入探讨了Transformer模型在语义通信中的实战应用,从信源编码到端到端优化。文章分析了Transformer凭借其强大的全局依赖建模能力,如何成为实现高效语义提取与传输的理想架构,并提供了构建可端到端训练系统的具体实现思路与评估方法,旨在为下一代智能通信系统提供技术参考。 阅读详情

相关推荐

滞回比较器电路详细分析

滞回比较器是一种用于检测两个不同电压值并使比较器输出高电平或低电平的电路。滞回比较器的特点是具有迟滞特性,即当输入电压高于某一阈值时,比较器输出高电平,当输入电压低于另一阈值时,比较器输出低电平,而在这两个阈值之间,比较器的输出保持不变。比较器通常有两个模拟电压输入端UIN+和UIN-以及一个数字状态输出端UOUT。当UIN+大于UIN-时,UOUT为高电平;当UIN+小于UIN-时,UOUT为低电平。根据具体的系统定义,这两种电位的具体数值可以有所不同。

LuDvei的博客 2万+

多路复用之——select

系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序同时监视多个文件描述符的状态变化;程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化;

waywt1的博客 2189

使用 Unity Sentis 和 Compute Shader,det_10g.onnx 进行高效人脸五官定位

在计算机视觉领域,人脸五官定位是一个重要的任务。本文将介绍如何使用 Unity Sentis 和 Compute Shader,结合模型,实现高效的人脸五官定位。我们将详细讲解每一步骤,并提供完整的代码示例。

m0_55632444的博客 2393

Windows Sockets 函数api (微软官方文档)

以下函数api来自:microsoft Documentation,本博客仅提供索引链接。 提示:可以借助浏览器自动翻译功能阅读参考文档。(英语水平高的人请忽略????‍♂️) 目录 Winsock.h 1. api接口 2. 数据结构信息(Structures) Winsock2.h 1. api接口 2. 数据结构信息(Structures) --------------------------------------------------------- 附录Winsock.h &

专注于C/C++/Linux领域创作 2877

从RadixTree到哈希表:深入对比SGLang和vLLM的Prefix Cache实现差异

本文深入对比了SGLang与vLLM在大语言模型推理服务中实现Prefix Cache(前缀缓存)的两种核心方案。SGLang采用基于Radix Tree的RadixAttention,支持任意长度前缀的精确匹配与灵活复用;vLLM则采用基于哈希表的方案,深度集成于PagedAttention,实现简洁但要求块级精确对齐。文章剖析了二者在设计哲学、内存管理、适用场景及集群级优化(如Prefix Cache Aware Routing)上的关键差异,为高性能LLM服务的技术选型提供决策依据。

blockchain9miner的博客 955

Voxel Hashing技术解析:如何解决TSDF内存爆炸问题?

本文深入解析Voxel Hashing技术如何有效解决TSDF内存爆炸问题。通过动态内存分配和空间哈希函数设计,该技术将内存占用降低96.6%,同时保持高精度重建。文章详细介绍了GPU加速实现、现代系统演进及实践指南,为实时3D重建提供工程解决方案。

m2n3b4v5c6的博客 713

【网络编程】IO 多路复用 select

文章以【网络编程】TCP socket 单机通信实验 为基础,尝试对 IO 多路复用 select 接口进行使用

_山月 2788

Claude Code 错误解决方案 使用策略拒绝 Usage Policy refusal

摘要:本文分析了Claude Code API请求被拒绝的问题,当违反Usage Policy时会显示错误提示。根因包括请求内容违规、模型敏感度差异或历史消息影响。解决方案包括修改请求措辞、切换至Sonnet 4.5模型或开启新会话。文章详细追踪了错误链路,提供了核心代码分析,并给出验证方法和最佳实践建议,帮助用户避免触发API拒绝机制。

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 972

谈谈对IO多路复用的select机制的理解

谈谈自己对IO多路复用的select机制的理解,理解了select机制,再理解epoll机制就比较容易了。

qq_34798605的博客 1851

多线程开发的基本概念

在Android开发中,我们常常会遇到多线程开发的需求。由于Android中的,我们不能在主线程处理耗时逻辑,否则会导致卡顿,甚至出现ANR(应用无响应)等异常情况。因此,我们需要将网络请求等耗时操作放到中去执行,完成后再更新UI。多线程开发因此成为Android开发中的一个重要部分。随着Android开发技术的不断进步,从早期的使用Handler,到后来的RxJava,再到如今的Kotlin协程,切换线程以及并发编程的代码写起来是越来越简洁了。

喻志强的博客 1017

79、昇腾系列服务器/昇腾系列推理卡/昇腾系列嵌入式开发板+Dify+one api部署DeepSeek-R1-Distill-Qwen-32BW8A8+RAG本地模型知识库和负载均衡双实例

阶段,安装过程缺什么,,在docker外面git下源码,进入容器内部进行量化,这里的容器建议在创建个8卡的容器,双卡容器量化会显示npu显存不够,除非你用cpu转模型,我就懒得创建容器了,使用cpu量化吧,巨慢;创建容器,进入容器,计划使用两张昇腾NPU卡推理DeepSeek-R1-Distill-Qwen-32B的W8A8模型,所以构建的容器用两张卡,选6、7卡吧,0-6号卡可以跑文本嵌入模型、重排序模型;直接进入量化阶段,在容器外面操作即可,环境不用管,因为系统已经默认配置了环境,直接跳到。

sxj731533730 6118

select函数--IO多路复用详解

本章基于网络编程,需要学习网络编程的可以看我这篇博客: socket编程(超简单、详细、可运行)–实现一个简单的聊天程序 什么是多路复用IO呢? 多路io:允许同时对多个I/O进行控制 可能这样说还是有点抽象,stm32下面有GPIO口,如果我需要一个LED屏幕连接在32开发板上面,是不是又很多的接线,每个接线接着板子上面的对应引脚上面,既然我们接线这个引脚了,是不是首先需要让这个引脚处于使能状态,这就是开时钟,多个引脚我们是不是要开多个引脚的时钟。配置好后我们的开发板的处理器是不是会按照需要去对这些IO口

玫瑰花店的博客 1760

python爬取银行年报2

python爬取公司年报(巨潮资讯网)

bboysteed的博客 4730

I/O多路复用及select函数解析及实例

概述 在进行解释之前,首先要说明几个概念: - 用户空间和内核空间 - 进程切换 - 进程的阻塞 - 文件描述符 - 缓存 I/O 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能

Megustas_JJC的博客 912

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

I/O多路复用及select函数

、

qq_43341187的博客 152

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

I/O多路复用-select函数详解

文章目录为什么要使用I/O多路复用什么是I/O多路复用select函数I/O事件select函数参数select例子:select函数缺点 为什么要使用I/O多路复用 在原来的程序中,我们可以使用fgets方法等待标准输入,但是一旦这么做,就没有办法在套接字有数据的时候读数据(read);我们也可以使用read方法等待套接字有数据时返回,但这么做了,就没有办法在标准输入(fget)有数据时,读入数...

k5722166的博客 527

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

day2 I/O多路复用select函数

是三个集合中编号最高的文件描述符,加上1;

m0_58066972的博客 552

为什么要使用多路复用I/O?多路复用I/O的实现——select函数模型

多路复用IO-select函数模型的使用

嵌入式 204

I/O多路复用之select函数分析

转载前注明出处,欢迎转载分享 如果一堆的client.c与server.c进行连接的话一定会产生很多的进程,也就是说每当来一个客户端请求,就会产生一个进程来服务,然而进程不可能无限制的产生,所以为了解决一对一占用资源的连接方式,引入了IO复用。 即:一个进程可以同时对多个客户请求进行服务。 I/O复用的适用如下: 当客户处理多个文件句柄时必须使用。 当一个客户同时处理多个套接口时,而这种情况

Keep coding 904

掌握select()函数:高效I/O多路复用

本文介绍了Linux系统调用select()和pselect()的使用方法,重点分析了文件描述符集合(fd_set)的操作、参数含义以及套接字就绪条件。文章指出select()最多监听1024个文件描述符,建议使用poll或epoll替代。详细说明了readfds、writefds和exceptfds三种事件集合的用法,包括读/写/异常就绪的具体判定条件。特别强调了timeout参数在不同系统中的差异性问题,以及套接字低水位标记、带外数据等概念。最后列出了select()可能返回的错误码。

m0_51566880的博客 717

lv8 嵌入式开发-网络编程开发 15I/O多路复用及select函数

* 秒 *//* 微秒 */select是一个用于多路复用 I/O 的系统调用函数,它可以同时监视多个文件描述符的可读、可写和异常事件。(特殊用法:也可以用于阻塞微秒级,其他文件描述符功能都设为NULL不使用)。nfds:监视的文件描述符的数量,即需要检查的最大文件描述符值加一。readfds:用于检查可读事件的文件描述符集合。(常用,一般多路监听都是可读)writefds:用于检查可写事件的文件描述符集合。exceptfds:用于检查异常事件的文件描述符集合。timeout:超时时间。

m0_60718520的博客 234

C语言 I/O多路复用 epoll函数和select函数 示例

flags:0 = epoll_creat,EPOLL_CLOEXEC = 子进程没有访问父进程的权限(fork()出的任何子进程,都不会继承其父进程和父进程所拥有的文件描述符,因为若没有设置EPOLL_CLOEXEC标志,fork()会把父进程所拥有的描述符复制给子进程,会导致多个进程同时持有同一描述符,当其中的一个或多个进程close()了某个共有的文件描述符时,会导致程序的不可用,或实现过程不在我们的预期之内)size:告知内核要监控的文件描述符个数,有助于内核决定epoll的大小。

YoAron的博客 444

[I/O多路复用] select 函数

它允许一个线程同时监视多个文件描述符,判断哪些描述符已经可以进行读、写,或者发生异常。对于 socket 来说,可读通常表示有数据到达、对端关闭连接,或者监听 socket 上有新连接到来。是异常事件集合,常用于检测 socket 的带外数据等异常情况,实际使用较少。第三,内核和用户态之间需要来回复制 fd 集合,fd 数量大时开销明显。调用返回后,集合中只保留已经就绪的 fd。需要监听的文件描述符数量,是所有文件描述符中最大值+1。,调用后还要线性扫描所有 fd,效率较低。第一,文件描述符数量有限。

m0_52540136的博客 53

I/O多路复用- select函数

I/O 多路复用 阻塞函数在完成其指定的任务以前不允许程序继续向下执行。如:当服务器运行到accept语句时而没有客户端请求连接,服务器就会停止在accept语句上等待连接请求的到来,这种情况称为堵塞,而非堵塞操作则可以立即完成。如:如果你希望服务器仅仅检查是否有客户在等待连接,有就接受连接,否则就继续做其他的事情,则可以通过使用select系统调用来实现,除此之外,select还可以同时监

root_robot的博客 385

I/O多路复用——select函数与poll函数

1 区别   同:(1)机制类似,本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。(2)包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。   异:poll没有最大文件描述符数量的限制。 2 select函数原型   该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有

coderCong的博客 552

select函数:I/O多路复用

I/O 多路复用 阻塞函数在完成其指定的任务以前不允许程序继续向下执行。如:当服务器运行到accept语句时而没有客户端请求连接,服务器就会停止在accept语句上等待连接请求的到来,这种情况称为堵塞,而非堵塞操作则可以立即完成。如:如果你希望服务器仅仅检查是否有客户在等待连接,有就接受连接,否则就继续做其他的事情,则可以通过使用select系统调用来实现,除此之外,select还可以同时监听多

qq_35535992的博客 362
上一篇: Web客户/服务器程序
下一篇: 原始套接字-SOCK_RAW
暴风雨中的白杨
博客等级 码龄6年 1070粉丝 162原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暴风雨中的白杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值