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状态
```C
#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: hackxiaowang@qq.com
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

```C
/*************************************************************************
    > File Name: Network_test.c
    > Author: WangMinghang
    > Mail: hackxiaowang@qq.com
    > 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: hackxiaowang@qq.com
    > 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