您现在的位置是:首页 >其他 >从零开始学习Linux网络编程C++ Day3网站首页其他

从零开始学习Linux网络编程C++ Day3

scipig_ 2026-06-27 00:01:04
简介从零开始学习Linux网络编程C++ Day3

今天开始学习一些套接字的属性。

构建简单的客户端和服务端可以看从零开始学习Linux网络编程C++ Day1

封装socket类可看从零开始学习Linux网络编程C++ Day2

阻塞IO:

       当用户线程发起 I/O 请求之后,内核会去查看数据是否就绪。如果没有就绪,内核会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。

       对于前两天的学习中出现的函数,服务端的accept()函数位置就可能会发生阻塞,因为如果没有客户端发送连接过来,accept()函数就会一直等待,程序就挂起,然后交出CPU。recv()函数也可能会发生阻塞,如果客户端已经连过来了,但是一直不发送数据,程序也会一直等待,然后挂起,交出CPU。对于服务端来说,send()函数也是肯能会发生阻塞的,因为发送缓冲区已满(如接收方处理速度慢、网络拥塞或 TCP 流量控制限制),导致数据无法立即写入。

      对于客户端来说,connect()可能会发生阻塞,因为在建立 TCP 连接时需要完成三次握手的过程,如果服务器响应延迟、网络拥塞或目标地址不可达,客户端会等待直到超时或连接成功,而默认情况下connect()是以阻塞模式运行,因此在这段时间内程序会被暂停,直到连接建立完成或失败为止。send()recv()也可能会发生阻塞,原因和服务端一样。

      对于套接字socket,我们创建出来的时候就会默认为一个阻塞的IO模型。

非阻塞IO:

       非阻塞 I/O 是指当一个进程发起 I/O 请求时,无论数据是否准备好,操作系统都会立即返回。如果数据尚未准备好,返回的结果通常是一个特定的状态码(如 "未准备好" 或 "错误"),而不是实际的数据。在非阻塞 I/O 模型中,用户线程需要不断地轮询内核数据是否就绪,也就是说非阻塞 I/O 不会交出 CPU,而会一直占用 CPU。

       在轮询的模式下,我们一般会采用非阻塞的IO模型。

如何将socket由默认的阻塞方式设置为非阻塞

在上一章的socket.h中添加如下代码:

bool set_non_blocking();//由阻塞变为非阻塞

在socket.cpp中添加如下代码:

bool Socket::set_non_blocking(){
    int flags=fcntl(m_sockfd,F_GETFL,0);//F_FETFL获取状体
    if(flags<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_set_non_blocking_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "socket set_non_blocking failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    flags |= O_NONBLOCK;//设置非阻塞标志
    if(fcntl(m_sockfd,F_SETFL,flags)<0){//F_SETFL设置状态
        // 获取错误信息
        const char* error_message = strerror(errno);
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_set_non_blocking_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "socket set_non_blocking failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return true;
}
  • fcntl 是一个通用的文件控制函数,可以对文件描述符进行多种操作。
  • 在这里,F_GETFL 和 F_SETFL 分别用于获取和设置文件状态标志。
  • O_NONBLOCK 是一个标志,表示将文件或套接字设置为非阻塞模式。

发送缓冲区 

       socket 没法直接将数据发送到网卡,所以只能先将数据发送到操作系统数据发送缓冲区。然后网卡从数据发送缓冲区中获取数据,再发送到接收方。

       如果用户程序发送数据的速度比网卡读取的速度快,那么发送缓冲区将会很快被写满,这个时候 send 会被阻塞,也就是写入发生阻塞。

设置接收缓存区使用setsockopt()这个函数

//socket.h
bool set_send_buffer(int size);//设置发送缓冲区大小
//socket.cpp
bool Socket::set_send_buffer(int size){
    int buf_size=size;
    if(setsockopt(m_sockfd,SOL_SOCKET,SO_SNDBUF,&buf_size,sizeof(buf_size))<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_set_send_buffer_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "socket set_send_buffer failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return true;
}

 接收缓存区

       首先接收方机器网卡接收到发送方的数据后,先将数据保存到操作系统接收缓冲区。用户程序感知到操作系统缓冲区的数据后,主动调用接收数据的方法来获取数据。 如果数据接收缓冲区为空,这个时候 recv 会被阻塞,也就是读取发生阻塞。

 修改接收缓存区的代码和修改发送缓存区的是一样的,就是讲SO_SNDBUF参数改为SO_RCVBUF。

SO_LINGER

       设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。

bool Socket::set_linger(bool active,int seconds){
    struct linger l;
    memset(&l,0,sizeof(l));
    l.l_onoff=active?1:0;
    l.l_linger=seconds;
    if(setsockopt(m_sockfd,SOL_SOCKET,SO_LINGER,&l,sizeof(l))<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_set_linger_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "socket set_linger failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
}

SO_KEEPALIVE

不论是服务端还是客户端,一方开启 KeepAlive 功能后,就会自动在规定时间内向对方发送心跳包,而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。

bool Socket::set_keepalive(){
    int flag=1;
    if(setsockopt(m_sockfd,SOL_SOCKET,SO_KEEPALIVE,&flag,sizeof(flag))<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_set_keepalive_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "socket set_keepalive failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return true;
}

SO_REUSEADDR

SO_REUSEADDR 是一个很有用的选项,一般服务器的监听 socket 都应该打开它。它的大意是允许服务器 bind 一个地址,即使这个地址当前已经存在已建立的连接。

bool Socket::set_reuseaddr(){
        int flag=1;
    if(setsockopt(m_sockfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag))<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_set_keepalive_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "socket set_keepalive failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return true;
}

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。