TCP/IP协议-网络编程

时间:2023-03-08 16:50:01
TCP/IP协议-网络编程

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

开篇语

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

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

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

创建套接字(socket)

#include <sys/scoket.h>
int socket(int domain, int type, int protocol)
domain : 套接字中实用的协议族信息
type : 套接字数据传输类型信息
protocol : 计算机通信中实用的协议信息

1. domain参数 协议族

名称 协议族
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
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_INET IPv4互联网使用的地址族
AF_INET6 IPv6互联网使用的地址族
AF_LOCAL 本地通信unix使用的地址族
成员 sin_port

16位端口号

成员 sin_addr

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

成员 sin_zero

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

3. addrlen参数

传递地址信息的长度

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

//分配内存-构造服务端地址端口
memset(&serv_addr, 0, sizeof(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):高字节数据存放在低地址处,低字节数据存放在高地址处。

图例:

TCP/IP协议-网络编程TCP/IP协议-网络编程

大字节序更符合我们的阅读习惯。但是我们的主机使用的是哪种字节序取决于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, 0, sizeof(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 *string, struct 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

名称 全称 含义
stdin standard input 标准输入流
stdout standard out 标准输出流
stderr standard 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_addr, clnt_addr;     int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        printf("socket() error");
        exit(1);
    }     memset(&serv_addr, 0, sizeof(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_addr, clnt_addr;     int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        printf("socket() error");
        exit(1);
    }     memset(&serv_addr, 0, sizeof(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, 1024, stdin);         //因为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 的操作(我们输入什么,服务端返回什么)

TCP/IP协议-网络编程

我们发现当数据超过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 交互图

TCP/IP协议-网络编程TCP/IP协议-网络编程

把图画残了。。。

当我们传输大文件, 注重传输速度时候可以禁用 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, 0, sizeof(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_addr, clnt_addr;     int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        printf("socket() error");
        exit(1);
    }     memset(&serv_addr, 0, sizeof(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, 1024, stdin);         //因为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条件准备读取,它就通知该进程,可以使用一个进程服务多个客户端.

多进程服务模型:

TCP/IP协议-网络编程

I/O 复用进程模型:

TCP/IP协议-网络编程

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函数执行前后示例图

TCP/IP协议-网络编程

示例代码(回声)

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, 0, 0, &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