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

开篇语

前两年, 就买了《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. 以双斜杠//开头的URL – 依赖协议的URL

    原文:以双斜杠//开头的URL – 依赖协议的URL 不知道大家有没有见过下面这种 url 写法: <img src="//domain.com/img/logo.png"& ...

  2. 八荣八耻 IT版

    八荣八耻 IT版以可配置为荣,以硬编码为耻:以系统互备为荣,以系统单点为耻:以随时可重启为荣,以不能迁移为耻:以整体交付为荣,以部分交付为耻:以无状态为荣,以有状态为耻:以标准化为荣,以特殊化为耻:以 ...

  3. DataTable,DataView 排序和使用

    我们都知道在Sql Server可以用order by来排序,所以很多朋友在DataTable中排序也想到了用order by关键字.但这样实现是比较困难的,下面,我们讲解一种比较简单的方法: 控制台 ...

  4. Entity Framework的查询

    Entity Framework是个好东西,虽然没有Hibernate功能强大,但使用更简便.今天整理一下常见SQL如何用EF来表达,Func形式和Linq形式都会列出来(本人更喜欢Func形式). ...

  5. Redis 高可用之哨兵模式

    参考   : https://mp.weixin.qq.com/s/Z-PyNgiqYrm0ZYg0r6MVeQ 一.redis高可用解决方案 redis主从 优点:1.高可靠性,主从实时备份,有效解 ...

  6. QT字符编码转换,可用于中文内码传输

    串口.TCP.UDP传输中文字符时,先将字符串转内码.客户端接收到数据后,将内码转为字符串就OK了 QByteArray CommonFunction::strToInterCode(constQSt ...

  7. 《CSS 设计指南》笔记(Ⅰ)

    在图书馆借了这本书,讲的非常好,条理清晰,深入浅出,真的有一种和作者交流的感觉,解决了自己很多困惑,于是决定针对一些平时并不常用但是感觉会用到的知识点做一些笔记,加深印象. 一. 块级元素盒子会扩展到 ...

  8. 【产品】张小龙《微信背后的产品观》之PPT完整文字版

    张小龙<微信背后的产品观>之PPT完整文字版 附:PPT下载地址:https://wenku.baidu.com/view/99d2910290c69ec3d5bb7573.html  微 ...

  9. Java基础(四) StringBuffer、StringBuilder原理浅析

    StringBuilder与StringBuffer作用就是用来处理字符串,但String类本身也具备很多方法可以用来处理字符串,那么为什么还要引入这两个类呢? 关于String的讲解请看Java基础 ...

  10. RSA der加密 p12解密以及配合AES使用详解

    在前面的文章中我有说过AES和RSA这两种加密方式,正好在前段时间再项目中有使用到,在这里再把这两种加密方式综合在一起写一下,具体到他们的使用,以及RSA各种加密文件的生成. 一: RSA各种加密相关 ...