将套接字设置为非阻塞的

1.使用socket函数创建的套接字,将其设置为非阻塞的
  1. 使用socket函数创建套接字时,指定SOCK_NONBLOCK标志位
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (listenfd == -1) {
    exit(-1);
}
  1. 使用fcntl函数实现
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (listenfd == -1) {
    exit(-1);
}

int flags = fcntl(listenfd, F_GETFD, 0);
flags |= O_NONBLOCK;
fcntl(listenfd, F_SETFD, flags);
2.服务端开发中,将用于通信的套接字设置为非阻塞的
  1. 在接收连接请求时使用accept4函数
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept4(listenfd, (struct sockaddr*)&client_addr, &client_addr_len, SOCK_NONBLOCK);
  1. 在接收连接请求时使用accept函数,然后使用fcntl函数将用于通信的套接字设置为非阻塞的。

实现字节序转换函数

1.字节序
  1. 大端序:又称网络字节序。数据的高位字节存储在内存低地址
  2. 小端序:又称主机字节序。数据的高位字节存储在内存高地址
2.字节序的判断
#include <stdio.h>

union 
{
    char c[4];
    unsigned int n;
}host_order = {'0','1','2','3'};

#define LITTLE_ENDIAN ('0' == (char)host_order.n)
#define BIG_ENDIAN ('3' == (char)host_order.n)
int main() {
    // 0x33323130
    printf("%#x\n",host_order.n);
    
    if (LITTLE_ENDIAN) {
        printf("本主机使用的字节序为小端字节序\n");
    } else if (BIG_ENDIAN) {
        printf("本主机使用的字节序为大端序\n");
    }

    return 0;
}
3.字节序转换
  1. 16位整数从主机字节序转换为网络字节序
// 将16位的端口号从主机字节序转换为网络字节序
unsigned short int m_htons(unsigned short int port) {
    if LITTLE_ENDIAN {
        return port << 8 | port >> 8;
    }
    return port;
}
  1. 将32位的整数从主机字节序转换为网络字节序
// 将32位的IP从主机字节序转换为网络字节序
unsigned int m_htonl(unsigned int ip) {
    if LITTLE_ENDIAN {
        return (ip << 24) |
            (ip << 8 & 0x00ff0000) | 
            (ip >> 8 & 0x0000ff00) |
            (ip >> 24);
    }
    return ip;
}

非阻塞的connect

  1. 使用非阻塞的套接字向服务端发起连接会理解返回,此时需要利用一些方法得到connect函数调用的返回结果
  2. 方法1:
    1. 将套接字设置为非阻塞的
    2. 判断connect函数的返回值,如EINTR、EINPROGRESS。如果返回0则表示连接成功,直接返回,无34两部。
    3. 如果错误码为EINPROGRESS,则使用select进一步判断是否可写来判断连接是否成功建立。
    4. 使用getsockopt判断是否出错,不出错则表示由连接成功触发的可写事件,从而表示连接成功建立
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <sys/select.h>
    
    #define SEVER_ADDRESS "127.0.0.1"
    #define SERVER_PORT 8000
    
    bool connect(int timeout = 6) {
        // 创建非阻塞的套接字
        int clientfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
        if (clientfd == -1) {
            perror("create socket error");
            return false;
        }
    
        struct sockaddr_in serv_addr;
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(SERVER_PORT);
        serv_addr.sin_addr.s_addr = inet_addr(SEVER_ADDRESS);
        while (1) {
            int ret = connect(clientfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
            // 连接成功
            if (ret == 0) {
                close(clientfd);
                return true;
            } else if (ret == -1) {
                // 被信号中断
                if (errno == EINTR) {
                    printf("连接过程中被信号中断...\n");
                    continue;
                } else if (errno == EINPROGRESS) {
                    printf("连接正在进行中...\n");
                    break;
                } else {
                    close(clientfd);
                    return false;
                }
            }
        }
    
        fd_set writefds;
        FD_ZERO(&writefds);
        FD_SET(clientfd, &writefds);
    
        struct timeval tv;
        tv.tv_sec = timeout;
        tv.tv_usec = 0;
    
        if (select(clientfd + 1, nullptr, &writefds, nullptr, &tv) <= 0) {
            perror("连接失败");
            close(clientfd);
            return false;
        }
    
        // clientfd上写事件就绪
        int err;
        socklen_t len = static_cast<socklen_t>(sizeof err);
        //调用getsockopt检测此时socket是否出错
        if (getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0 || err != 0)
        {
            close(clientfd);
            return false;
        }
    
        // 无错则表示连接成功触发的写事件就绪
        if (err == 0) {
            printf("连接成功\n");
            close(clientfd);
            return true;
        } else
            perror("连接失败");
            close(clientfd);
            return false;
        
        
    }
    int main() {
        bool flag = connect();
        printf("flag:%s\n", flag == true ? "连接成功" : "连接失败");
        return 0;
    }
    
  3. 方法2:使用poll代替方法1中的select

常见的socket选项

  1. SO_LINGER选项
    1. 这个选项用于控制close系统调用在关闭TCP连接时的行为。可以通过调用setsockopt函数设置SO_LINGER选项的值,示例如下:
    #include <sys/socket.h>
    struct linger {
        int l_onoff;    // 0表示关闭该选项,非0表示开启该选项
        int l_linger; // 滞留时间
    }
    
    
    1. close系统调用可能产生的不同的行为:
  2. TCP_NODELAY:这个选项与Nagle算法有关,设置了这个选项表示禁止Nagle算法。
  3. SO_KEEPALIVE:发送周期性保活报文以维持连接。套接字设置了这个选项,将默认每隔两个小时发送一个心跳检测包给对端。两个小时太长,可以通过设置TCP_KEEPIDLE、TCP_KEEPINTVL 和 TCP_KEEPCNT选项进行更改。示例代码如下:
// 设置SO_KEEPALIVE选项
int val = 1
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val));

//发送 keepalive 报文的时间间隔
val = 7200;
// IPPROTO_TCP表示TCP选项
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val));

//两次重传报文的时间间隔
int interval = 75;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));

// 重传次数
int cnt = 9;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
  1. SO_RCVBUF/SO_SNDBUF:TCP接收缓冲区和发送缓冲区大小
  2. 设置超时时间的选项
    1. SO_RCVTIMO:对于阻塞套接字,设置接收数据阻塞时的时长
    2. SO_SNDTIMEO:对于阻塞套接字,设置发送数据阻塞时的时长或者connect系统调用阻塞的时长。对于connect系统调用,系统调用超时后返回-1,错误码为EINPROGRESS。示例如下:
    int timeout_connect(const char* ip, int port, int timeout) {
        int ret = 0;
        struct sockaddr_in address;
        bzero(&address, sizeof(address));
        address.sin_family = AF_INET;
        inet_pton(AF_INET, ip, &address.sin_addr);
        address.sin_port = htons(port);
    
        int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    
        struct timeval time;
        time.tv_sec = timeout;
        time.tv_usec = 0;
        socklen_t len = sizeof(time);
        setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &time, len);
        ret = connect(sockfd, (const sockaddr*)&address, sizeof(address));
        if (ret == -1) {
            if (errno == EINPROGRESS) {
                printf("连接超时\n");
                // 这里可以执行一些定时任务
                return -1;
            }
            printf("错误发生\n");
            return -1;
        }
        return sockfd;
    }
    
  3. SO_REUSEADDR/SO_REUSEPORT:分别设置IP地址或者端口的复用
  4. SO_ERROR:套接字上的错误

bind函数

  1. 在编写服务端时,通过bind函数绑定0IP地址。系统将会选择一个网卡地址
  2. 在编写服务端时,通过bind函数绑定0号端口。系统将会选择一个端口号
  3. 在编写客户端时,调用bind函数绑定指定的端口与服务端通信。客户端将以指定的端口号运行。

listen函数

  1. listen函数的第二参数backlog:表示未接受处理请求连接的最大长度,pending connections将放在一个队列上,这个backlog参数即为这个队列的长度

服务端如何保证在关闭连接前将数据包发送给客户端

这是一个半关闭问题

  1. 调用send/write发送数据
  2. 设置SO_LINGER选项
  3. 启动TCP_NODELAY选项
  4. 调用shutdown函数

epoll模型的触发方式

监听套接字是用于监听客户端连接请求的套接字,通信套接字是用于和客户端数据通信的套接字。epoll模型的触发方式有如下几种:

  1. 侦听套接字是非阻塞的水平触发模式:这种情况可行
  2. 通信套接字是阻塞的水平触发模式:这种情况与通信套接字是非阻塞的水平触发模式效果一样。
  3. 通信套接字是非阻塞的水平触发模式:这种情况可行
  4. 侦听套接字是非阻塞的边缘触发模式:这种情况不可行,可能会出现部分客户端连接不上的情况
  5. 通信套接字是阻塞的边缘触发模式:这种情况不可行,因为通信套接字是阻塞的,则在收发数据时会阻塞网络线程,导致无法处理新的客户端的连接请求。
  6. 通信套接字是非阻塞的边缘触发模式:这种情况下可行,需要在循环中收取数据。
    总结:epoll的水平触发模式既可以使用阻塞IO,也可以使用非阻塞IO;边缘触发模式只能使用非阻塞io,即将用于和客户端通信的套接字设置成非阻塞的。