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

开篇语

前两年, 就买了《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):高字节数据存放在低地址处,低字节数据存放在高地址处。

图例:

大字节序更符合我们的阅读习惯。但是我们的主机使用的是哪种字节序取决于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 的操作(我们输入什么,服务端返回什么)

我们发现当数据超过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, 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条件准备读取,它就通知该进程,可以使用一个进程服务多个客户端.

多进程服务模型:

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

TCP/IP协议-网络编程的更多相关文章

  1. TCP/IP协议网络编程以及UDP和TCP之传输协议

    1.什么是TCP/IP协议? 网络编程协议有很多,目前应用最广泛的是TCP/IP协议(Transmission Control Protocal/Internet Protoal 传输控制协议/英特网 ...

  2. 从网卡发送数据再谈TCP/IP协议—网络传输速度计算-网卡构造

    在<在深谈TCP/IP三步握手&四步挥手原理及衍生问题—长文解剖IP>里面提到 单个TCP包每次打包1448字节的数据进行发送(以太网Ethernet最大的数据帧是1518字节,以 ...

  3. 【转】qt ,使用tcp/ip协议网络传输数据时,字节序转换方法

    网络传输数据是需要保证字节序的正确,才能保证传输数据的准确,网络字节序一般是大端字节序.qt提供了以下两种方法来将本地字节序转换为网络字节序: 方法一,使用qt提供的字节序转换函数 T qFromBi ...

  4. TCP/IP协议、三次握手、四次挥手

    1.什么是TCP/IP协议 TCP/IP 是一类协议系统,它是用于网络通信的一套协议集合. 传统上来说 TCP/IP 被认为是一个四层协议 1) 网络接口层: 主要是指物理层次的一些接口,比如电缆等. ...

  5. TCP/IP协议介绍

    一.什么是TCP/IP TCP/IP是一类协议系统,它是用于网络通信的一套协议集合 TCP/IP是供已连接因特网的计算机进行通信的通信协议 TCP/IP指传输控制协议/网际协议 TCP/IP定义了电子 ...

  6. Android网络编程系列 一 TCP/IP协议族

    在学习和使用Android网路编程时,我们接触的仅仅是上层协议和接口如Apache的httpclient或者Android自带的httpURlconnection等等.对于这些接口的底层实现我们也有必 ...

  7. 嵌入式linux的网络编程(1)--TCP/IP协议概述

    嵌入式linux的网络编程(1)--TCP/IP协议概述 1.OSI参考模型及TCP/IP参考模型 通信协议用于协调不同网络设备之间的信息交换,它们建立了设备之间互相识别的信息机制.大家一定都听说过著 ...

  8. 基于TCP/IP协议的C++网络编程(API函数版)

    源代码:http://download.csdn.net/detail/nuptboyzhb/4169959 基于TCP/IP协议的网络编程 定义变量——获得WINSOCK版本——加载WINSOCK库 ...

  9. 网络编程3之TCP/IP协议

    在TCP/IP协议中,最重要的协议是[TCP.UDP.IP]协议 1.TCP/IP协议特点 1)Internet上不同系统之间互联的一组协议 2)为分散和不同类型的硬件提供通用的编程接口. 3)TCP ...

随机推荐

  1. Delphi 7下最小化到系统托盘(主要是WM_TRAYMSG和WM_SYSCOMMAND消息)

    在Delphi 7下要制作系统托盘,只能制作一个比较简单的系统托盘,因为ShellAPI文件定义的TNotifyIconData结构体是比较早的版本.定义如下: 123456789   _NOTIFY ...

  2. kafka 遇到的错

    D:\cluster\kafka_2.->.\bin\windows\kafka-topics.bat --create --zookeeper localhost: --replication ...

  3. linux c 读写 ini 配置文件

    .ini 文件格式如下: [section1] key1=value ... keyn=value [section2] key1=value ... keyn=value 代码如下: #define ...

  4. 让Qt在MIPS Linux上运行 good

    下载 首先下载Qt everywhere,当前的版本是4.7.2,可以从nokia的网站上下载,也可以从git服务器上下载.考虑到文件有200M 以上的大小,下载速率低于25kBPS的,需要考虑从什么 ...

  5. Delphi驱动开发研究第一篇--实现原理

    Delphi能不能开发Windows的驱动程序(这里的驱动程序当然不是指VxD了^_^)一直是广大Delphi fans关注的问题.姑且先不说能或者不能,我们先来看看用Delphi开发驱动程序需要解决 ...

  6. Linux命令行和shell编程

    Shell Shell是一个程序,用户输入的命令通过shell来传达给内核或其它程序.用户在linux打开一个终端,终端就会自动调用用户的shell. Linux上的Shell有很多种,用的最多是sh ...

  7. Spring Boot从入门到实战:集成AOPLog来记录接口访问日志

    日志是一个Web项目中必不可少的部分,借助它我们可以做许多事情,比如问题排查.访问统计.监控告警等.一般通过引入slf4j的一些实现框架来做日志功能,如log4j,logback,log4j2,其性能 ...

  8. kubernetes实战篇之部署一个.net core微服务项目

    目录 继上一篇kubernetes理论知识完结.本篇主要讲解基于nexus搭建一个docker镜像仓库(当然大家实践过程是不必完全跟着做,也可以搭建harbor仓库或者直接把镜像推送到docker h ...

  9. Spring_One

    Spring_01 Spring概述 Spring是分层的Java2E应用full-stack轻量级开源框架,,以IoC(Inverse Of Control:反转控制)和AOP(Aspect Ori ...

  10. OVS实现VXLAN隔离

    一.实验环境 1.准备3个CentOS7 mini版本的虚拟机,每个主机3个网卡.如图: 图中OVS-1.OVS-2.OVS-3分别为三台CentOS7 mini版虚拟机,分别配备3个虚拟网卡.如图中 ...