linux(or Windows) 异步网络编程 simple client-server-select 应用

一般的简单网络编程就是创建一个 server 和 一个 client,然后分别send&recv 数据:

如果是针对一对一的连接(即一个服务器一个客户端),则如下代码:

在 linux 中网络编程需要用到这么一些头文件:

#include "sys/socket.h"
#include "arpa/inet.h"
#include "string.h"
#include "unistd.h"
#include "netinet/tcp.h"
#include "errno.h"

创建一个 server,绑定地址,监听等:下面这段代码还没有 accept 客户端的连接,因为 accept 的调用是阻塞的,如果没有连接进来,程序会一直阻塞在 accept 调用的地方。关于如何不阻塞地 accept,见下文的 serverPoll 调用。


bool TcpServer::createServer(int port_num)
{
  int rc;
  bool rtn;
  const int reuse_addr = 1;
  socklen_t addrSize = 0;

  // 由socke系统调用创建的描述符需要保存起来,将这个 描述符 绑定到一个网络地址之后,对这个 描述符 的操作就是对这个服务器的操作!
  rc = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 != rc)
  {
    this->setSrvrHandle(rc);  // 保存这个描述符,用于后续操作。

    // 避免出现:"address already in use"
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val))

    // Initialize address data structure
    // sockaddr_ 是一个 sockaddr_in 结构体,也需要作为成员变量一直保存.
    memset(&this->sockaddr_, 0, sizeof(this->sockaddr_));
    this->sockaddr_.sin_family = AF_INET;
    this->sockaddr_.sin_addr.s_addr = INADDR_ANY;
    this->sockaddr_.sin_port = HTONS(port_num);

    addrSize = sizeof(this->sockaddr_);
    rc = bind(this->getSrvrHandle(), (sockaddr *)&(this->sockaddr_), addrSize);

    if (-1 != rc)
    {
      LOG_INFO("Server socket successfully initialized");

      rc = listen(this->getSrvrHandle(), 1);

      if (-1 != rc)
      {
        LOG_INFO("Socket in listen mode");
        rtn = true;
      }
      else
      {
        LOG_ERROR("Failed to set socket to listen");
        rtn = false;
      }
    }
    else
    {
      LOG_ERROR("Failed to bind socket, rc: %d", rc);
      close(this->getSrvrHandle());
      rtn = false;
    }

  }
  else
  {
    LOG_ERROR("Failed to create socket, rc: %d", rc);
    rtn = false;
  }

  return rtn;
}

上一步创建了一个服务器,并且保存了对其进行操作的描述符。这一步我们需要知道是否有客户端在连接,如果有则accept(第一步,判断是否有链接,第二步再accept,这样就不会阻塞了):

// ready: 表示是否有客户端连接(还未accept)
// error: 表示服务器是否有错误
// timeout: 0 表示马上返回,否则等待指定的时间(毫秒),直到超时返回。
bool TcpServer::serverPoll(int timeout, bool & ready, bool & error)
{
    timeval time;
    fd_set read, write, except;
    int rc = -1;
    bool rtn = false;
    ready = false;
    error = false;

    // The select function uses the timeval data structure
    time.tv_sec = timeout / 1000;
    time.tv_usec = (timeout % 1000) * 1000;

    FD_ZERO(&read);
    FD_ZERO(&write);
    FD_ZERO(&except);

    if(-1 == this->getSrvrHandle())
        return false;

    FD_SET(this->getSrvrHandle(), &read);  // 将服务器的描述符加入到可读的子集中.如果可读,则表示有客户端连接,然后我们就可以调用accept连接它了.
    FD_SET(this->getSrvrHandle(), &except);  // 将服务器的描述符加入到是否有异常的子集中.

    // If both fields of the timeval structure are zero, then select() returns immediately. (This is useful for polling.) If timeout is NULL (no timeout), select() can block indefinitely(释放cpu的block).
    rc = select(this->getSrvrHandle()+1, &read, &write, &except, &time);  // +1 是必须的。见下文关于select调用的分析。

    if (-1 != rc)
    {
        // 成功调用 select 之后我们判断 server 的描述符是否在可读的子集中,如果存在,则表示有客户端的连接进来了。
        if (FD_ISSET(this->getSrvrHandle(), &read)) {
          ready = true;
          rtn = true;
        }
        else if(FD_ISSET(this->getSrvrHandle(), &except)) {
          error = true;
          rtn = true;
        }
        else {
          LOG_WARN("Select returned, but no flags are set");
          rtn = true;
        }
    }
    else
    {
      LOG_ERROR("Socket select function failed", rc, errno);
      rtn = false;
    }
    return rtn;
}

在上一步判断有客户端连接之后,我们再 accept,这样可以实现非阻塞的 accept 的调用了:

bool TcpServer::makeConnect()
{
  bool rtn = false;
  int rc = -1;
  int disableNodeDelay = 1;
  int err = 0;

  if (!this->isConnected())  // 初始化为 false
  {
    this->setConnected(false);
    if (-1 != this->getSockHandle())
    {
      close(this->getSockHandle());
    }

    // accept 调用会返回一个描述符,表示这个服务器与客户端的连接,对这个描述符 send & recv 可以向该连接的客户端发送和接收数据。
    rc = accept(this->getSrvrHandle(), NULL, NULL);
    if (-1 != rc)
    {
      this->setSockHandle(rc);  // 保存这个描述符,因为每一次 accept 调用就会创建一个新的连接,而该描述符表示这个连接。如果有多个客户端连接,则创建一个队列,保存所有accept返回的描述符。
      LOG_INFO("Client socket accepted");

      // The set no delay disables the NAGEL algorithm
      rc = setsockopt(this->getSockHandle(), IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val))disableNodeDelay);
      err = errno;
      if (-1 == rc)
      {
        LOG_WARN("Failed to set no socket delay, errno: %d, sending data can be delayed by up to 250ms", err);
      }
      this->setConnected(true);  // 设置标志位
      rtn = true;
    }
    else
    {
      LOG_ERROR("Failed to accept for client connection");
      rtn = false;
    }
  }
  else
  {
    LOG_WARN("Tried to connect when socket already in connected state");
  }

  return rtn;

}

服务器这边就是先调用 creatServer,然后再循环的调用 serverPoll,判断 ready 是否为 true,如果 serverPoll 返回的 ready 为 true 表示有客户端在进行连接,则调用 makeConnect 连接该客户端。

如果服务器需要判断是否有新的消息到达并等待读取,也可以通过 select调用来判断。只不过这次是 select 的是 连接 的描述符(就是 accept 调用返回的描述符),select( this->getSockHandle()+1,...),而不是 server 的描述符。


客户端就简单了,创建socke,然后 connect 就行了:

#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  

#define MAXLINE 4096  


int main(int argc, char** argv)  
{  
    int    sockfd, n,rec_len;  
    char    recvline[4096], sendline[4096];  
    char    buf[MAXLINE];  
    struct sockaddr_in    servaddr;  


    if( argc != 2){  
    printf("usage: ./client <ipaddress>\n");  
    exit(0);  
    }  


    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){  
    printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);  
    exit(0);  
    }  


    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(8000);  
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){  
    printf("inet_pton error for %s\n",argv[1]);  
    exit(0);  
    }  


    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){  
    printf("connect error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  


    printf("send msg to server: \n");  
    fgets(sendline, 4096, stdin);  
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)  
    {  
    printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);  
    exit(0);  
    }  
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {  
       perror("recv error");  
       exit(1);  
    }  
    buf[rec_len]  = '\0';  
    printf("Received : %s ",buf);  
    close(sockfd);  
    exit(0);  
}  

需要注意的是select 调用有pollblock两种工作方式,当timeout参数的内容设置为 0 时是poll操作(select调用会立即返回,这种情况下需要轮询调用,如果没有适当的线程同步操作,会导致cpu占用高);而当timeout参数本身为 NULL 时是block操作,此时select调用在没有可读或者可写的socket的情况下会阻塞调用,导致线程睡眠,直到条件满足才会被唤醒。

适当的利用block模式下select调用的返回条件(select在有socket可读时返回),可以精确控制线程的运行和睡眠。这种情况适用于,当只有 一个 work thread,并且有许多工作需要这个thread做(例如,除了处理读socket操作外,还要处理写socket操作和其他任务)。

例如: 在 tacopie 网络库中,使用名为 “self-pipe”的 trick ,当其他任务需要被处理时,就触发一下self-pipe,使得select调用返回,线程能够运行其他任务。而且,为了加快响应,self-pipe的socket类型设置为UDP类型, see link and link.

另外还需要注意的一个问题,见链接 howto prevent a process from terminating when writing to a broken pipe

ref link:

值得注意的是,当需要监控大量的socket时 select 调用不是很高效(因此,select调用一般适用于client端,server端考虑epoll调用):

On the userspace side, generating and reading the bit arrays can be made to take time proportional to the number of fds that you provided for select(). But on the kernel side, reading the bit arrays takes time proportional to the largest fd in the bit array, which tends to be around the total number of fds in use in the whole program, regardless of how many fds are added to the sets in select().
也就是说,无论你需要监控的描述符有多少个,在内核中select调用永远监控最大描述符范围内所有描述符,这就有点浪费。

Comparing the performance for 100,000 monitoring operations:

operations    |  poll  |  select   | epoll
10            |   0.61 |    0.73   | 0.41
100           |   2.9  |    3.0    | 0.42
1000          |  35    |   35      | 0.53
10000         | 990    |  930      | 0.66

So using epoll really is a lot faster once you have more than 10 or so file descriptors to monitor.

当需要监控大量的socket时(10个以上),使用以下调用替换select:

  • Linux/Unix: epoll()
  • BSDs (including Darwin): kqueue()
  • Solaris: evports and /dev/poll

如果需要考虑统一的接口,可以使用 libevent 库。
- https://jvns.ca/blog/2017/06/03/async-io-on-linux–select–poll–and-epoll/
- https://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
- http://www.wangafu.net/~nickm/libevent-book/01_intro.html
- http://blog.csdn.net/tennysonsky/article/details/45745887

猜你喜欢

转载自blog.csdn.net/gw569453350game/article/details/56666144