工程师纯干货总结:TCP/IP网络编程

嵌入式ARM 2019-07-17 15:40

授权转载自公众号“呆呆熊一点通”,作者:呆呆


开篇语


前两年, 就买了《TCP/IP网络编程》这本书, 由于自身基础薄弱, 只是走马观花翻阅了几张。


后来工作了这些年, 越来越感到瓶颈期已经来临, 再花式的 curd 也俘获不了领导的芳心了。


于是, 打算仔细学习下 《TCP/IP网络编程》, 为了让自己更深刻记忆, 特做笔记。


创建套接字(socket)


#include <sys/scoket.h>

int socket(int domain, int type, int protocol)

domain : 套接字中实用的协议族信息
type : 套接字数据传输类型信息
protocol : 计算机通信中实用的协议信息


1. domain参数 协议族


名称协议族
PF_INETIPv4互联网协议族
PF_INET6IPv6互联网协议族
PF_LOCAL本地通信unix协议族


2. type参数 套接字类型


2.1 面向链接的套接字类型 (SOCK_STREAM)


传输方式特征:


1.1 传输过程数据不会丢失
1.2 按序传输数据
1.3 不存在数据边界

这几个特性其实就是我们常说的 TCP协议。


缓冲区概念:


收发数据的套接字内部有缓冲(buffer), 简言之就是字节数组. 通过套接字传输的数据将保存到该数组。因此, 我们 read、write其实读取缓冲区的内容。


那么当缓冲区满, 会发生什么情况呢。在ICP/IP网络编程书中介绍, 如果read函数读取的速度比接收数据的速度慢, 则缓冲区有可能填满。此时套接字将无法再接收数据, 传输端套接字将停止传输。


2.2 面向消息的套接字类型 (SOCK_STREAM)


传输方式特征:


1. 强调快速传输而非传输顺序
2. 传输数据可能丢失也可能毁损
3. 传输的数据存在数据边界

其实就是我们常说的UDP协议


3. protocol参数 协议最终选择


这里我们不做选择, 为0即可。


4. 最终我们使用TCP链接模式写法


//创建套接字(IPv4协议族, TCP套接字, TCP协议)
int sock = socket(PF_INET, SOCK_STREAM, 0);

返回的为 文件描述符, 失败返回-1


向套接字分配网络地址(bind)


#include <sys/socket.h>

int bind(int socketfd, struct sockaddr *myaddr, socklen_t addrlen);

socketfd 要分配的套接字文件描述符
myaddr  存储地址信息的结构体变量地址值
addrlen 第二个结构体变量的长度


1. socketfd 参数


socketfd 不用多说, 即是我们的socket函数返回的文件描述符


2. myaddr 参数


struct sockaddr {
    __uint8_t       sa_len; 
    sa_family_t     sa_family; //地址组
    char            sa_data[14]; //地址信息
}; 

在sa_data一个成员里,包含了ip、port的地址信息, 这样写起来很麻烦, 所以有了新的结构体 sockaddr_in (IP和端口进行了拆分)


sockaddr_in结构体


struct sockaddr_in {
    __uint8_t       sin_len;
    sa_family_t     sin_family; //地址族
    in_port_t       sin_port; // TCP/UDP端口号
    struct  in_addr sin_addr; //IP地址
    char            sin_zero[8];
};


在上面的结构体中, 又嵌套了 in_addr 结构体,记录 IP 地址


struct in_addr {
    in_addr_t s_addr; //32位IPv4地址
};


结构体 sockaddr_in 的成员分析


成员 sin_family


地址族含义
AF_INETIPv4互联网使用的地址族
AF_INET6IPv6互联网使用的地址族
AF_LOCAL本地通信unix使用的地址族

成员 sin_port


16位端口号


成员 sin_addr


32位 ip 地址信息, 以网络字节序保存


成员 sin_zero


无特殊含义, 为与sockaddr 大小保持一致, 写入0 即可。


3. addrlen参数


传递地址信息的长度


4. 最终我们使用bind绑定地址方式


//分配内存-构造服务端地址端口
memset(&serv_addr, 0sizeof(serv_addr));
//IPv4中的地址族
serv_addr.sin_family = AF_INET;
//32位的IPv4地址, INADDR_ANY表示当前ip
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//16位tcp/udp端口号
serv_addr.sin_port = htons(atoi(argv[1]));  

//分配地址
if (bind(serv_sock, (struct sockaddr*) &serv_addr,sizeof(serv_addr) )==-1){
    printf("bind() error");
    exit(0); 
}

bind函数之前, 构造了 sockaddr_in 结构体的数据, 其中介绍几个点.

  1. INADDR_ANY 会自动获取当前服务器的IP

  2. 我们看到使用到了 htonl、htons 函数,构造IP地址和端口

为什么构造结构体地址时候使用了 htonl、htons对IP、端口进行了转换?

首先我们来看下这几个函数的含义


地址族含义
htons把short型数据从主机字节序转化为网络字节序
htonl把long型数据从主机字节序转化为网络字节序
ntohs把short型数据从网络字节序转化为主机字节序
ntohl把long型数据从网络字节序转化为主机字节序

数据传输采用的网络字节序, 那在传输前应直接把数据转换成网络字节序, 接收的数据也需要转换城主机字节序再保存
上面这句话是有问题的, 原因是数据收发过程中是有自动转换机制的.


除了 socketaddr_in 结构体变量手动填充数据转换外, 其他情况不需要考虑字节序问题。


说了这么多字节序, 那到底什么是网络字节序,什么是主机字节序


1.主机字节序:主机内部内存中数据的处理方式。


2.网络字节序:网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian(大端)排序方式。


天啦撸, 大端又是啥, 我们从两种网络字节顺序说起


字节序:是指整数在内存中保存的顺序。


cpu向内存保存数据字节序有两种实现方式:


  • 小端字节序(little endian):低字节数据存放在内存低地址处,高字节数据存放在内存高地址处。


  • 大端字节序(bigendian):高字节数据存放在低地址处,低字节数据存放在高地址处。


图例:



大字节序更符合我们的阅读习惯。但是我们的主机使用的是哪种字节序取决于CPU,不同的CPU型号有不同的选择。


当我们两台计算机是需要网络通信时, 规范统一约定为大端序进行通讯处理.


客户端代码分析


我们在服务端设置ip时候, 使用了 INADDR_ANY 会自动获取当前服务器的IP,
我们看下客户端的连接代码


struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;

char message[30];
//创建套接字(IPv4协议族, TCP套接字, TCP协议)
int sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1 ){
    printf("socket() error");
    exit(1);
}

//分配内存-构造服务端地址端口
memset(&serv_addr, 0sizeof(serv_addr));
//IPv4中的地址族
serv_addr.sin_family = AF_INET;
//32位的IPv4地址
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
//16位tcp/udp端口号
serv_addr.sin_port = htons(atoi(argv[2]));  

if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) {
    printf("connect() error");
    exit(1);
}

int length = read(sock, message, sizeof(message)-1); 
if (length==-1){
    printf("read() error");
    exit(1);
}

知识点1

设置服务端 serv_addr.sin_addr.s_addr 地址, 使用了函数 inet_addr

int_addr_t inet_addr(const char * string);
//成功时32位大端序整数值, 失败时返回 INADDR_NONE.

例:

printf("%d",inet_addr("192.168.2.1"));
//output: 16951488
printf("%d",inet_addr("192.168.2.256"));
//output: -1

相同功能函数, 只是简化了向 serv_addr.sin_addr.s_addr 赋值操作

int inet_aton(const char *stringstruct in_addr * addr);
//成功时返回1(true) 失败时返回0(false)
inet_aton(addr, &addr_inet.sin_addr)

其他函数:

char * inet_ntao(struct in_addr adr);
//成功时返回转换的字符串地址值, 失败时返回-1.

知识点2

● atoi():将字符串转换为整型值。

● atol():将字符串转换为长整型值。

printf("%d",atoi("123"));
//output : 123


对比服务端、客户端构造地址代码


服务端


serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//16位tcp/udp端口号
serv_addr.sin_port = htons(atoi(argv[1]));  

客户端

//32位的IPv4地址
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
//16位tcp/udp端口号
serv_addr.sin_port = htons(atoi(argv[2]));  


这里面包含上面讲到的一些知识点:


  1. 服务端因为使用INADDR_ANY实际等于 inet_addr("0.0.0.0"), 获取本机的IP地址

  2. 因为客户端接收了字符串IP地址, 所以使用了显示 inet_addr, 返回32位大端序整型数值

  3. htons 将短整型转换为网络字节序, 对于端口来说是比较合适的, 而对于IP类转换的整型数值, 一般需要 htonl 进行转换


前置C语言小知识点


stdin,stdout,stderr


名称全称含义
stdinstandard input标准输入流
stdoutstandard out标准输出流
stderrstandard error标准错误输出

我们来看下面几个函数

#include <stdio.h>

#define BUF_SIZE 5

int main(int argc, char *argv[])
{
    char message[BUF_SIZE];

    fputs("请向输入流一个字符串:"stdout); //printf
    fgets(message, BUF_SIZE, stdin); //scanf
    fputs(message,stderr); //output: message
}

上面我们使用到了stdout、 stdin, 并且最后还写入到 stderr流, 输出到了控制台.

stdout和stderr都能输出到控制台, 除了语义上区别外, stderr是没有缓冲的,他立即输出,而stdout默认是行缓冲,也就是它遇到‘\n’,才向外输出内容,如果你想stdout也实时输出内容,那就在输出语句后加上fflush(stdout),这样就能达到实时输出的效果


fputs、fgets指定到流的操作(文件流), 对应的直接输入输出还有 puts、gets,这里不再推荐使用puts、gets了, 他们之间也有区别。


gets()丢弃输入中的换行符,但是puts()在输出中添加换行符。另一方面,fgets()保留输入中的换行符,fputs()不在输出中添加换行符,因此,puts()应与gets()配对使用,fputs()应与fgets()配对使用。


编码实践 echo 小案例


echo_server.c


#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 5

int main(int argc, char *argv[])
{
    char message[BUF_SIZE];
    int str_len, i;

    struct sockaddr_in serv_addrclnt_addr;

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        printf("socket() error");
        exit(1);
    }

    memset(&serv_addr, 0sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(9600);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        printf("bind() error");
        exit(1);
    }

    if (listen(serv_sock, 5) == 1)
    {
        printf("listen() error");
        exit(1);
    }

    int clnt_addr_sz = sizeof(clnt_addr);
    for (i = 0; i < 5; i++)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_sz);
        if (clnt_sock == -1)
        {
            printf("accept() error");
            exit(1);
        }

        while (str_len = read(clnt_sock, message, BUF_SIZE) > 0)
        {
            write(clnt_sock, message, str_len);
        }

        close(clnt_sock);
    }

    close(serv_sock);
    return 0;
}
    

echo_client.c

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 5

int main(int argc, char *argv[])
{
    char message[BUF_SIZE];
    int str_len, i;

    struct sockaddr_in serv_addrclnt_addr;

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        printf("socket() error");
        exit(1);
    }

    memset(&serv_addr, 0sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(9600);

    if (connect(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        printf("connect() error");
        exit(1);
    }

    while (1)
    {
        fputs("请输入您的信息,按Q键退出\n"stdout);
        fgets(message, 1024stdin);

        //因为fgets会保留输入中换行符,故判断加\n
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
        {
            break;
        }

        write(serv_sock, message, sizeof(message));
        read(serv_sock, message, BUF_SIZE - 1);
        printf("Message from server: %s\n", message);
    }

    close(serv_sock);
    return 0;
}

上面代码简单完成了 echo 的操作(我们输入什么,服务端返回什么)

我们发现当数据超过5个字符时候(\n也默认为一个字符), 将会截断发送, 我们可以使用下面方式。

str_len = write(serv_sock, message, strlen(message));

recv_len = 0;
while (recv_len < str_len)
{
    recv_cnt = read(serv_sock, &message[recv_len], BUF_SIZE - 1);
    if (recv_cnt == -1)
    {
        printf("read() error");
        exit(1);
    }
    recv_len += recv_cnt;
}

上面将是循环接收数据, 直到接收完毕退出循环体


gethostbyname 函数 根据域名获取IP地址


#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    struct hostent *host;

    host = gethostbyname("www.xueba100.com");

    printf("h_name=%s\n", host->h_name);
    printf("h_addrtype=%d\n", host->h_addrtype);

    int i;
    for (i = 0; host->h_addr_list[i]; i++)
    {   
        //将IP指针转换为 in_addr 结构体, 再调用inet_ntoa转换为字符串形式
        printf("Ip addr: %s\n", inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
    }
}


setsockopt 设置socket选项


这里举例说明 设置 SO_REUSEADDR 选项


当我们主动关闭服务端时候, 将会产生TIME_OUT, 这样会导致端口地址无法重用,规范中规定等待 2MSL 时间才可以使用。我们可以使用 setsockopt 设置地址重用。


socklen_t option;
int optlen = sizeof(option);
option = 1;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);

与之对应的 getscokopt 函数, 获取选项


Nagle 算法


只有收到前一数据的 ACK 消息时, Nagle 算法才发送下一数据。


TCP 套接字默认使用的 Nagle 算法交换数据, 因此最大限度地进行缓冲, 直到收到 ACK。


如果不使用 Nagle 无需等待 ACK 的前提下连续传输, 大大提高传输速度.


使用 Nagle 交互图


把图画残了。

当我们传输大文件, 注重传输速度时候可以禁用 Nagle 算法, 如果考虑到传输内容很小, 头部信息就有可能几十个字节, 可以使用 Nagle 算法, 减少网络传输次数。

禁用 Nagle 算法

socklen_t option;
int optlen = sizeof(option);
option = 1;
setsockopt(serv_sock, IPPROTO_TCP, TCP_NODELAY, (void *)&option, optlen);



进程篇



fork


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

int gval = 10;
int main()
{
    pid_t pid;
    int lval = 20;
    gval++, lval += 5;

    pid = fork();

    //子进程
    if (pid == 0)
    {
        gval += 2, lval += 2;
    }
    else
    {
        gval -= 2, lval -= 2;
    }

    //子进程
    if (pid == 0)
    {
        printf("子进程[%d,%d]\n", gval, lval);
    }
    else
    {
        sleep(30);
        printf("父进程[%d,%d]\n", gval, lval);
    }


    printf("猜猜我是啥[%d,%d]\n", gval, lval);
}

fork函数子进程返回0, 父进程返回子进程的 pid


接收子进程返回值(wait)


#include <sys/wait.h>
pid_t wait(int * statloc);

成功时返回终止的子进程ID, 失败时返回 -1
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    int status;

    pid = fork();

    //子进程
    if (pid == 0)
    {
        sleep(10);
        return 44;
    }
    else
    {
        wait(&status);
        //正常退出
        if(WIFEXITED(status)){
            printf("获取子进程返回值%d\n", WEXITSTATUS(status));
        }
    }

    printf("猜猜我是啥\n");
}

output:

取子进程返回值44
猜猜我是啥

当你运行此段代码时候, 发现最少等待10s钟才能程序结束, 原因是wait是阻塞的, 父进程将等待子进程执行完毕, 获取其返回值。


接收子进程返回值(waitpid)


#include <sys/wait.h>
pid_t waitpid(pid_t pid, int * statloc, int options)

成功时返回终止的子进程ID(或0), 失败时返回 -1

具体参数:

参数含义
pid等待终止的子进程id, -1表示等待任意进程
statloc具体返回值指针
options具体参数常量
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    int status;

    pid = fork();

    //子进程
    if (pid == 0)
    {
        return 44;
    }
    else
    {
        while (!waitpid(-1, &status, WNOHANG))
        {
            sleep(3);
            printf("非阻塞等待\n");
        }
        //正常退出
        if (WIFEXITED(status))
        {
            printf("获取子进程返回值%d\n", WEXITSTATUS(status));
        }
    }

    printf("猜猜我是啥\n");
}

output:

非阻塞等待
获取子进程返回值44
猜猜我是啥

在这个示例里面, 我们使用了 waitpid 非阻塞等待子进程函数, 如果去掉我们的 while 等待, 一般是不会获取到子进程任何值就将结束了。


信号的使用


#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

void keycontrol(int sig)
{
    if (sig == SIGINT)
    {
        puts("CTRL+C pressed.");
    }
}

void child(int sig)
{
    int status;

    waitpid(-1, &status, WNOHANG);
    if (WIFEXITED(status))
    {
        printf("%d\n", WEXITSTATUS(status));
    }
}

int main()
{
    int i;
    pid_t pid;

    signal(SIGINT, keycontrol);
    signal(SIGCHLD, child);

    //假装在运行
    for (i = 0; i < 2; i++)
    {
        pid = fork();
        if (pid == 0)
        {
            puts("我是子进程");
            return 88;
        } else {
            puts("wait...");
            sleep(10);
        }
    }
    return 0;
}

output:

wait...
我是子进程
88
wait...
我是子进程
88

当你运行此代码时候发现, 我们的父进程并没有 sleep(10) 等待后返回, 而是早早的执行结束了。

发生信号时, 为了调用信号处理器, 将唤醒由于调用 sleep 函数而进入阻塞状态的进程, 所以 sleep 在信号发生时是失效的。

信号现在推荐使用 sigaction

实现多进程回声服务端

echo_server.c

#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

void read_childproc(int sig)
{
    pid_t pid;
    int status;

    pid = waitpid(-1, &status, WNOHANG);
    printf("removed proc id: %d\n", pid);
}

int main()
{

    //注册子进程信号
    struct sigaction act;
    act.sa_sigaction = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, 0);

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    //初始化地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(9200);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        printf("绑定地址失败 \n");
        exit(1);
    }

    if (listen(serv_sock, 5) == 1)
    {
        printf("绑定端口失败 \n");
        exit(1);
    }

    ////////接收请求///////////
    struct sockaddr_in clnt_adr;
    int clnt_sock, adr_sz, str_len;
    pid_t pid;
    char buf[BUF_SIZE];

    while (1)
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
        if (clnt_sock == -1)
        {
            continue;
        }
        else
        {
            puts("new client connected...");
        }

        pid = fork();
        if (pid == -1)
        {
            puts("-1  -1  -1");
            close(clnt_sock);
            continue;
        }

        //子进程处理
        if (pid == 0)
        {
            //关闭复制到的父文件号
            close(serv_sock);
            while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
                write(clnt_sock, buf, str_len);

            close(clnt_sock);
            puts("子进程受理");
            //正常退出子进程
            return 0;
        }
        else
        {
            puts("父进程不处理 clnt_sock");
            close(clnt_sock);
        }
    }

    close(serv_sock);
    return 0;
}

echo_client.c

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 5

int main(int argc, char *argv[])
{
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt, i;

    struct sockaddr_in serv_addrclnt_addr;

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        printf("socket() error");
        exit(1);
    }

    memset(&serv_addr, 0sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(9200);

    if (connect(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        printf("connect() error");
        exit(1);
    }

    while (1)
    {
        fputs("请输入您的信息,按Q键退出\n"stdout);
        fgets(message, 1024stdin);

        //因为fgets会保留输入中换行符,故判断加\n
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
        {
            break;
        }

        write(serv_sock, message, strlen(message));
        str_len = read(serv_sock, message, BUF_SIZE);

        printf("Message from server: %s\n", message);
    }

    close(serv_sock);
    return 0;
}



I/O复用select函数



之前我们使用了几种服务器模型,一个是单进程的, 同一时刻只能给一个客户端提供服务, 后来我们使用了多进程, 每个客户端fork新进程进行请求处理

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程,可以使用一个进程服务多个客户端.

多进程服务模型:

I/O 复用进程模型:

select 实现I/O 复用

select实现比较简单,主要使用select函数
函数原型:

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);

成功时返回大于0的值, 失败时返回 -1

参数解释:

maxfd 监视对象文件描述符的数量,举例写法 sever_sock+1
readset 存储的待读取数据文件描述符
writeset 可传输无阻塞数据文件描述符 
exceptset 发生异常的文件描述符
timeout 超时设置

select函数返回值, 如果返回大于0的整数, 说明相应数量的文件描述符发生了变化.

文件描述符管理

我们发现在 参数类型上有 fd_set类型,这是什么类型呢?
fd_set结构是文件描述符对应的位存储数据格式, 当我们管理这些监控的文件描述符时, 可以以下宏来实现

FD_ZERO(fd_set * fdset) 
将 fd_set 变量的所有位初始化位0

FD_SET(int fd, fd_set * fdset)
在参数 fd_set 指向的变量中注册文件描述符 fd 的信息

FD_CLR(int fd, fd_set * fdset)
从参数 fdset 指向的变量中清除文件描述符 fd 的信息

FD_ISSET(int fd, fd_set * fdset) 
若参数 fdset  指向的变量中包含文件描述符 fd 的信息,则返回真

timeval 超时设置结构体

struct timeval 
{

    long tv_sec;  //seconds
    long tv_usec; //microseconds
}

select函数执行前后示例图

示例代码(回声)

select.c

#include <stdio.h>
#include <sys/select.h>

#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
    //监视的文件描述符
    fd_set reads, temps;
    struct timeval timeout;
    int result, str_len;
    char buf[BUF_SIZE];

    FD_ZERO(&reads);
    FD_SET(0, &reads); //0 is standard input(console)

    while (1)
    {
        //因为每次select会重置监控句柄,所以赋值给临时
        temps = reads;

        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        // puts 只有事件发生或者发生超时才执行,否则select阻塞
        puts("xxxx");
        result = select(1, &temps, 00, &timeout);

        if (result == -1)
        {
            puts("select() error");
        }
        else if (result == 0)
        {
            puts("nothing event change..time out");
        }
        else
        {
            if (FD_ISSET(0, &temps))
            {
                str_len = read(0, buf, BUF_SIZE);
                printf("message from consle: %s\n", buf);
            }
        }
    }
}

执行输出

gcc select.c -o select

./select
xxxx
123456
message from consle: 123456

xxxx
7890ha
message from consle: 7890ha

xxxx
nothing event change..time out
xxxx
777
message from consle: 777

xxxx
^C


参考资料:


【1】《TCP/IP 网络编程》

https://blog.csdn.net/stalin_/article/details/80337915

嵌入式ARM 关注这个时代最火的嵌入式ARM,你想知道的都在这里。
评论
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 51浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 47浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 209浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 160浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 83浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 44浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 195浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 137浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 108浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 101浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 115浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 152浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦