Linux C語言 connect 的超時設置(含源碼和例程)

0x01 關鍵步驟和相關函數

網絡編程中默認情況下進入connect函數,會一直等待連接結束。超時等待設置關鍵在於
1、將socket置爲非阻塞後
2、設定超時等待時間
3、時間結束後讀取socket狀態,進行判斷

1、設置socket爲非阻塞

記錄下兩種設置socket爲非阻塞方式,分別是fcntl()ioctl() 兩個函數

fcntl()

#include <fcntl.h>
#include <unistd.h>

/*********************************************************************
 * Function     : fcntl
 * Description  : 根據文件描述符操作文件特性
 * Parameter    :
 *      @fd         文件描述符
 *      @cmd        操作命令:
 *                      F_DUPFD     : 複製文件描述詞。
 *                      FD_CLOEXEC  : 設置close-on-exec標誌
 *                      F_GETFD     : 讀取文件描述詞標誌
 *                      F_SETFD     : 設置文件描述詞標誌
 *                      F_GETFL     : 讀取文件狀態標誌
 *                      F_SETFL     : 設置文件狀態標誌
 *                      ...
 *      @arg        供命令使用的參數
 *
 * Return   : int
 *
 * Usage    :
 *      int fcntl(int fd, int cmd); 
 *      int fcntl(int fd, int cmd, long arg); 
 *      int fcntl(int fd, int cmd, struct flock *lock);
 * 
 * 參考 man 或:
 * https://blog.csdn.net/weixin_34362875/article/details/86340074
 *
 *********************************************************************/
int fcntl(int fd, int cmd, ... /* arg */ );

ioctl()

#include <sys/ioctl.h>

/*********************************************************
 * Function     : ioctl
 * Description  : 設備驅動程序中對設備的I/O通道進行管理的函數
 * Parameter    :
 *      @fd         文件描述符
 *      @request    操作命令:
 *                      FIONBIN     : 設置/ 清除非阻塞I/O 標誌
 *                      FIOASYNC    : 設置/ 清除信號驅動異步I/O 標誌
 *                      FIONREAD    : 獲取接收緩存區中的字節數
 *                      FIOSETOWN   : 設置文件的進程ID 或進程組ID
 *                      FIOGETOWN   : 獲取文件的進程ID 或進程組ID
 *                      ...
 *      @arg        供命令使用的參數
 *
 * Return   : int
 * 
 * 參考 man 或:
 * https://www.cnblogs.com/tdyizhen1314/p/4896689.html
 * 上文詳細介紹了該函數的用途
 * 以及request對應參數所需要提供的arg類型
 *********************************************************/
int ioctl(int fd, unsigned long request, .../* arg */);

2、超時等待

select() 多路複用

#include <sys/select.h>

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

/*********************************************************
 * Function     : select
 * Description  : 同步I/O多路複用
 * Parameter    :
 *      @nfds       文件描述符+1
 *      @readfds    可讀文件描述符詞組
 *      @writefds   可寫文件描述符詞組
 *      @exceptfds  例外文件描述符詞組
 *      @timeout    等待時間
 *
 * Return   : int
 * 
 * 參考 man 或:
 * https://blog.csdn.net/lucykingljj/article/details/43198889
 *
 *********************************************************/
int select(int nfds, fd_set *readfds, fd_set *writefds,
          fd_set *exceptfds, struct timeval *timeout);

/**
 * 相關函數
 */
void FD_ZERO(fd_set *set);                  // 清除描述符詞組set的全部位
void FD_CLR(int fd, fd_set *set);           // 清除描述符詞組set中相關fd的位
void FD_SET(int fd, fd_set *set);           // 設置描述符詞組set中相關fd的位
int  FD_ISSET(int fd, fd_set *set);         // 測試描述符詞組set中相關fd的位是否爲真

3、獲取socket狀態

#include <sys/types.h>
#include <sys/socket.h>

/*********************************************************
 * Function     : getsockopt
 * Description  : 返回指定socket的狀態
 * Parameter    :
 *      @sockfd     socket的文件描述符
 *      @level      操作的網絡層, 一般設置爲SOL_SOCKET
 *      @optname    操作的選項:
 *          SO_DEBUG        打開或關閉排錯模式
 *          SO_REUSEADDR    允許在bind ()過程中本地地址可重複使用
 *          SO_TYPE         返回socket 形態.
 *          SO_ERROR        返回socket 已發生的錯誤原因
 *          SO_DONTROUTE    送出的數據包不要利用路由設備來傳輸.
 *          SO_BROADCAST    使用廣播方式傳送
 *          SO_SNDBUF       設置送出的暫存區大小
 *          SO_RCVBUF       設置接收的暫存區大小
 *          SO_KEEPALIVE    定期確定連線是否已終止.
 *          SO_OOBINLINE    當接收到OOB 數據時會馬上送至標準輸入設備
 *          SO_LINGER       確保數據安全且可靠的傳送出去.
 *      @optval     保存結果的內存地址
 *      @optlen     optval空間的大小
 *
 * Return   : 成功則返回0, 若有錯誤則返回-1, 錯誤原因存於errno
 *              errno:
 *                  EBADF 參數s 並非合法的socket 處理代碼
 *                  ENOTSOCK 參數s爲一文件描述詞, 非socket
 *                  ENOPROTOOPT 參數optname指定的選項不正確
 *                  EFAULT 參數optval指針指向無法存取的內存空間
 * 
 * 參考 man 或:
 * http://www.cnblogs.com/dpf-learn/p/6124170.html
 *
 *********************************************************/
int getsockopt(int sockfd, int level, int optname,
              void *optval, socklen_t *optlen);

0x02 例程代碼

本例程爲一份檢測本地與目標端口連接是否活躍的程序,只封裝了建立一個普通TCP帶有超時參數的客戶端connect函數。

頭文件 - Network.h

/*************************************************************************
    > File Name: Network.h
    > Author: WangMinghang
    > Mail: [email protected]
    > Blog: https://www.wangsansan.com
    > Created Time: 2019年04月29日 星期一 10時32分21秒
 ************************************************************************/


#ifndef __USER_NETWORK_H__
#define __USER_NETWORK_H__

/************************************************************
 * Function     : create_connect
 * Description  : 創建一個TCP連接的客戶端socket, 並設置超時時間
 * Parameter    :
 *                  @host       主機域名或IP地址
 *                  @port       目標端口
 *                  @s          連接超時時間, 單位: 秒
 * Return   : 
 *          連接成功 - 返回socket描述符
 *          連接失敗 - 返回值小於0
 *                      -1 : 創建socket失敗
 *                      -2 : 連接超時
 *
 ************************************************************/
int create_connect(const char *host, int port, int s);

#endif

main 主程序 - Network_test.c

/*************************************************************************
    > File Name: Network_test.c
    > Author: WangMinghang
    > Mail: [email protected]
    > Blog: https://www.wangsansan.com
    > Created Time: 2019年04月29日 星期一 10時32分21秒
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "Network.h"

#define TIMEOUT             3
#define SERVER_PORT         80
#define SERVER_HOST         "www.wangsansan.com"

void delay_ms(int timeout);

int main(int argc, char **argv)
{
    int sock_fd = -1;

    while(1)
    {
        sock_fd = create_connect(SERVER_HOST, SERVER_PORT, TIMEOUT);
        if(sock_fd <= 0){
            printf("Connect failed - %d \n", sock_fd);
        }else{
            close(sock_fd);
            printf("Connect success - %d \n", sock_fd);
        }

        /* 在網絡通暢時, 如果不加延時會消耗資源 */
        delay_ms(1000);
    }
}

/* 毫秒定時器 */
void delay_ms(int timeout)
{
    struct timeval timer;
    timer.tv_sec        = 0;    // 0秒
    timer.tv_usec       = 1000*timeout; // 1000us = 1ms
    select(0, NULL, NULL, NULL, &timer);
}

源代碼 - Network.c

/*************************************************************************
    > File Name: Network.c
    > Author: WangMinghang
    > Mail: [email protected]
    > Blog: https://www.wangsansan.com
    > Created Time: 2019年04月29日 星期一 10時32分21秒
 ************************************************************************/

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <netdb.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include "Network.h"

int socket_create();
int socket_timeout(int sockfd, int s);
int socket_connect(int sockfd, const char* server, int port);

int create_connect(const char *host, int port, int s)
{
    int re = -1;
    int sock_fd = -1;
    unsigned long ul;

    // 創建 socket
    sock_fd = socket_create();
    if(sock_fd <= 0){
        return -1;
    }

    // 設置非阻塞
    ul = 1;
    ioctl(sock_fd, FIONBIO, &ul);

    // 連接 socket
    re = socket_connect(sock_fd, host, port);
    if(re == 1){
        // 設置爲阻塞
        ul = 0;
        ioctl(sock_fd, FIONBIO, &ul);
        return sock_fd;
    }

    // 設置超時時間
    re = socket_timeout(sock_fd, s);
    if(re <= 0){
        close(sock_fd);
        return -2;
    }

    // 設置爲阻塞
    ul = 0;
    ioctl(sock_fd, FIONBIO, &ul);

    return sock_fd;
}

int socket_timeout(int sockfd, int s)
{
    int re = 0;
    fd_set set;
    struct timeval tm;

    int len;
    int error = -1;

    tm.tv_sec = s;
    tm.tv_usec = 0;
    FD_ZERO(&set);
    FD_SET(sockfd, &set);

    re = select(sockfd + 1, NULL, &set, NULL, &tm);
    if(re > 0){
        len = sizeof(int);

        // 獲取socket狀態
        getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *)&len);
        if(error == 0){
            re = 1;
        }else{
            re = -3;
        }
    }else{
        re = -2;
    }

    return re;
}

int socket_connect(int sockfd, const char *server, int port)
{
    int re = -1;
    struct hostent *host;
    struct sockaddr_in cliaddr;
 
    // 域名解析
    host = gethostbyname(server);
    if(host == NULL){
        printf("gethostbyname(%s) error:%s\n", server, strerror(errno));
        re = -1;
        return re;
    }

    // 填充socket的IP與端口
    bzero(&cliaddr, sizeof(struct sockaddr));
    cliaddr.sin_family  = AF_INET;
    cliaddr.sin_port    = htons(port);
    cliaddr.sin_addr    = *((struct in_addr *)host->h_addr);
 
    // 客戶端連接
    re = connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(struct sockaddr));
    if(re >= 0){
        return 1;
    }

    return re;
}

int socket_create()
{
    int sockfd;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0){
        perror("create socket error");
        return -1;
    }

    return sockfd;
}

關鍵位置都有註釋說明,非阻塞設置方式使用的是ioctl()函數,在連接完成之後又將socket設置爲阻塞,有興趣的可以在後續代碼中添加send和recv的瘋轉,對socket進行操作。


參考:
https://blog.csdn.net/wangyifei0822/article/details/2171314

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章