码迷,mamicode.com
首页 > 其他好文 > 详细

【网络编程】网络编程相关知识点总结1

时间:2020-05-28 19:39:39      阅读:100      评论:0      收藏:0      [点我收藏+]

标签:忽略   message   报错   文件创建   api   应用层   网卡   换行符   避免   

bind

客户端可以调用bind函数吗?可以,可以指定端口 详见复习资料

客户端为何不调用bind函数,什么时候像套接字分配IP和端口号

listen

它现在定义的是已完成连接队列的最大长度,表示的是已建立的连接(established connection),正在等待被接收(accept 调用返回),而不是原先的未完成队列的最大长度

connect

TCP套接字调用其出错返回的可能情况如下:

  1. 三次握手无法建立,客户端发出的SYN包没有任何响应,返回TIMEOUT错误,这种情况比较常见的原因是对应的服务端IP写错;
  2. 客户端收到RST回答,这时候客户端会返回CONNECTION REFUSED错误。这种情况常见于客户端发送连接请求时端口写错;
  3. 客户发出的SYN包在网络上引起了destination unreachable即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通。

三次握手

技术图片

准备工作

  1. 服务器端经过socket,bind和listen完成被动套接字的准备工作,然后调用accept阻塞等待客户端的连接;

  2. 客户端通过调用socket,connect,也会阻塞;

  3. 由操作系统(内核网络协议栈)来处理。

三次握手过程

  1. 第一次握手)客户端协议栈向服务器端发送SYN包,并告诉服务器当前发送的序列号j,客户端进入SYN_SENT状态;

  2. 第二次握手)服务器端协议栈收到这个包之后,与客户端进行ACK应答,应答的值为j+1,表示对SYN包j的确认,同时服务器也发送一个SYN包,告诉客户端我当前发送的序列号是k,服务器端进入SYN_RCVD状态;

  3. 第三次握手)客户端协议栈收到ACK之后,使得应用程序从connect调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为ESTABLISHED,同时客户端协议栈也会对服务器端的SYN包进行应答,应答数据为k+1; 应答包到达服务器后,服务器端协议栈使得accept阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入ESTABLISHED状态。

面试题:

1. 客户端的第三次应答,服务器没有收到会怎样?

第三次的ACK在网络中丢失,那么Server 端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包。而Server重发SYN+ACK包的次数,可以通过设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5.

如果重发指定次数之后,仍然未收到 client 的ACK应答,那么一段时间后,Server自动关闭这个连接。

client 一般是通过 connect() 函数来连接服务器的,而connect()是在 TCP的三次握手的第二次握手完成后就成功返回值。也就是说 client 在接收到 SYN+ACK包,它的TCP连接状态就为 established (已连接),表示该连接已经建立。那么如果 第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误。

2. TCP连接的建立为什么是三次?

讲道理,C要确认S是否能够正常收发消息,需要发一条消息给S,并且接受S的一条确认消息才行,这一来一回就是两条。同理,S要确认与C的连接,也需要这样。总共就是四次通信。三次握手把中间两次给合并为了一次,减少资源的消耗;

一次握手、两次握手都确认不了C和S的收发消息的能力是否OK,三次握手是比较简洁有效的方式,大于三次以上的握手机制也可以确认,不过有些浪费资源,毕竟三次就能搞定的事情,没必要搞三次以上。

阻塞调用 vs 非阻塞调用

使用场景

区别

缓冲区

数据发送过程中,往往是将借助io函数数据从应用程序中拷贝到操作系统内核的(接收)发送缓冲区中;一下两种情况。

  1. 操作系统内核的发送缓冲区足够大?io函数直接返回
  2. 发送缓冲区很大,数据没有发完
  3. 数据发完了,操作系统内核的发送缓冲区不足以容纳数据

2和3两种情况发生时,操作系统内核不会立即返回,也不报错,而是程序别阻塞。等到应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。

理解缓冲区

/*服务器端代码*/
#include <strings.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

/*从socketfd中读取size个字节*/
size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = (char*)buffer;
    int length = size;

    while (length > 0) {
        int result = read(fd, buffer_pointer, length);

        if (result < 0) {
            if (errno == EINTR)
                continue;         /* 考虑非阻塞的情况,这里需要再次调用read */
            else
                return (-1);
        } else if (result == 0)
            break;                /* EOF(End of File)表示对端发送FIN包,套接字关闭 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length);       /* 返回的是实际读取的字节数*/
}

/*每次从缓冲区中读取1024个字节*/
void read_data(int sockfd) {
    ssize_t n;
    char buf[1024];

    int time = 0;
    for (;;) {
        fprintf(stdout, "block in read ");
        if ((n = readn(sockfd, buf, 1024)) == 0)
            return;

        time++;
        fprintf(stdout, "1K read for %d ", time);
        fprintf(stdout, "\n");
        usleep(100000);
    }
}



int main(int argc, char **argv) {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(12345);

    /* bind到本地地址,端口为12345 */
    bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    /* listen的backlog为1024 */
    listen(listenfd, 1024);

    /* 循环处理用户请求 */
    for (;;) {
        struct sockaddr_in cliaddr;
        socklen_t clilen = sizeof(cliaddr);
        int connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
        read_data(connfd);   /* 读取数据 */
        close(connfd);       /* 关闭连接套接字,注意不是监听套接字*/
    }
    close(listenfd);
    return 0;
}
/*客户端代码*/
#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>

#define MESSAGE_SIZE 10240000

void send_data(int sockfd) {
    char *query;
    query = (char*)malloc(MESSAGE_SIZE + 1);
    for (int i = 0; i < MESSAGE_SIZE; i++) {
        query[i] = ‘a‘;
    }
    query[MESSAGE_SIZE] = ‘\0‘;

    const char *cp;
    cp = query;
    size_t remaining = strlen(query);
    while (remaining) {
        int n_written = send(sockfd, cp, remaining, 0);
        fprintf(stdout, "send into buffer %d \n", n_written);
        if (n_written <= 0) {
            error(1, errno, "send failed");
            return;
        }
        remaining -= n_written;
        cp += n_written;
    }
    return;
}

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        error(1, 0, "usage: tcpclient <IPaddress>");

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(12345);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    int connect_rt = connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }
    send_data(sockfd);
    exit(0);
}

发送成功仅仅表示数据被拷贝到了发送缓冲区中,并不意味着链接对端已经收到所有的数据,至于什么时候发送到对端的接受缓冲区,或者什么时候被对方应用程序缓冲所接受,对我们来说完全是透明的。

面试题

1. 一段数据流从应用程序发送端一直到应用程序接受端,总共拷贝了几次?

技术图片

发送端,当应用程序将数据发送到发送缓冲区时,调用的是 send 或 write 方法,如果缓存中没有空间,系统调用就会失败或者阻塞。我们说,这个动作事实上是一次“显式拷贝”。而在这之后,数据将会按照 TCP/IP 的分层再次进行拷贝,这层的拷贝对我们来说就不是显式的了。接下来轮到 TCP 协议栈工作,创建 Packet 报文,并把报文发送到传输队列中(qdisc),传输队列是一个典型的 FIFO 队列,队列的最大值可以通过 ifconfig 命令输出的 txqueuelen 来查看。通常情况下,这个值有几千报文大小。TX ring 在网络驱动和网卡之间,也是一个传输请求的队列。网卡作为物理设备工作在物理层,主要工作是把要发送的报文保存到内部的缓存中,并发送出去。

接收端,报文首先到达网卡,由网卡保存在自己的接收缓存中,接下来报文被发送至网络驱动和网卡之间的 RX ring,网络驱动从 RX ring 获取报文 ,然后把报文发送到上层。这里值得注意的是,网络驱动和上层之间没有缓存,因为网络驱动使用 Napi 进行数据传输。因此,可以认为上层直接从 RX ring 中读取报文。最后,报文的数据保存在套接字接收缓存中,应用程序从套接字接收缓存中读取数据

四次挥手

技术图片

  1. 第一次挥手:主机1先发送FIN m报文,主机2进入CLOSE_WAIT状态;
  2. 第二次挥手:主机2发送一个ACK m+1应答;
  3. 第三次挥手:主机2通过read调用得到EOF,将结果通知应用程序主动关闭操作,发送FIN n报文。主机接收到FIN n报文;
  4. 第四次挥手:主机1在接收到FIN n报文后发送ACK n+1应答,进入TIME_WAIT状态。主机2收到ACK后,进入CLOSED状态,主机1在TIME_WAIT停留 持续时间是固定的,是2*MSL。

TIME_WAIT

只有发起连接终止的一方才会进入TIME_WAIT状态。

作用

  1. 确保最后的ACK能够让被动关闭放接收,从而帮助其正常关闭。
  2. 与连接“化身”和报文迷走有关,为了让旧连接的重复分节在网络中自然消失。

危害

  1. 内存资源占用,不是很严重,基本可以忽略。
  2. 端口资源的占用,一个TCP连接至少消耗一个本地端口,端口资源也是有限的,一般可以开启的端口为32768~61000。通过设置net.ipv4.ip_local_port_range指定。如果TIME_WAIT状态过多,会导致无法创建新连接。

优化

  1. 通过sysctl命令,将net.ipv4_tcp_max_tw_buckets这个值调小,默认为18000,当系统处于TIME_WAIT的连接一旦超出这个值,系统就会将所有TIME_WAIT连接状态重置,并且只打印出警告信息;
  2. 重新编译系统,调低TCP_TIMEWAIT_LEN;
  3. 通过设置套接字选项SO_LINGER,调整close或者shutdown关闭连接时的行为。
  4. 更安全的做法:设置net.ipv4.tcp_tw_reuse

shutdown优雅关闭

TCP是双向的,双向指的是数据流的写入-读出方向

close如何关闭连接?

close关闭连接是对套接字引用计数减一,一旦返现引用计数为0,彻底释放套接字,关闭TCP两个方向的数据流。

具体些,如何关闭两个方向的数据流?

  1. 输入方向,系统内核将该套接字设置为不可读,任何读操作都异常

  2. 输出方向,系统内核尝试将发送缓冲区的数据发送到对端,并最后相对端发送一个FIN报文,接下来任何对该套接字的写操作都会异常。

    如果对端还是没有检测到套接字已关闭,继续发送报文,接收端收到一个RST报文,告诉对端,我已关闭,不要给我发数据了。

shutdown

可以关闭连接的一个方向

SHUT_RD-读 、SHUT_WR-写、SHUT_RDWR-读+写

shutdown与close的区别?

close shutdown
资源释放 关闭后,会释放所有连接对应的资源 关闭后,不会释放掉套接字和所有资源
引用计数 close存在引用计数,不一定会导致该套接字不可用 不存在,直接使得该套接字不可用
FIN结束报文 close有引用计数,不一定会发出FIN 总是发出FIN结束报文

服务器端

接收客户端的应答显示到标准输出上

#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define MAXLINE 4096
#define SERV_PORT 9091
static int count;

/*信号处理函数,避免程序莫名退出*/
static void sig_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}


int main(int argc, char **argv) {
    /*创建套接字*/
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    /*绑定端口号和ip*/
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);
    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }
    /*转为被动套接字*/
    int rt2 = listen(listenfd, 5);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }
    /*信号处理,避免程序莫名退出*/
    signal(SIGINT, sig_int);
    
    signal(SIGPIPE, SIG_DFL);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    char message[MAXLINE];
    count = 0;

    
    for (;;) {
        /*获取客户端数据*/
        int n = read(connfd, message, MAXLINE);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }
        /*格式化原字符*/
        message[n] = 0;
        printf("received %d bytes: %s\n", n, message);
        
        count++;
        char send_line[MAXLINE];
        sprintf(send_line, "Hi, %s", message);

        sleep(5);

        /*发送数据给客户端*/
        size_t write_nc = send(connfd, send_line, strlen(send_line), 0);
        printf("send bytes: %zu \n", write_nc);
        if (write_nc < 0) {
            error(1, errno, "error write");
        }
    }

}

客户端

从标准输入不断接受用户输入,把输入的字符串通过套接字发送给服务器端。

  1. 输入close,调用close关闭连接,休眠一段时间,等待服务器端处理后退出。
  2. 输入shutdown函数关闭连接的写方向,不会直接退出,等待服务器端的应答,直到服务器端完成自己的操作,在另一个方向上完成关闭。
#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define MAXLINE 4096
#define SERV_PORT 9091
int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: graceclient <IPaddress>");

    }
    /*创建套接字*/
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    /*设置端口号和ip*/
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;

    fd_set readmask;
    fd_set allreads;

    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);
    for (;;) {
        readmask = allreads;
        int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
        if (rc <= 0)
            error(1, errno, "select failed");
        /*有数据可读,将数据读入程序缓冲区*/
        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {
                error(1, errno, "read error");
            } else if (n == 0) {
                error(1, 0, "server terminated \n");
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }
        /*当标准输入上有数据可读,读入后判断*/
        if (FD_ISSET(0, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
                /*shutdown关闭标注输入的I/O事件感知*/
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    FD_CLR(0, &allreads);
                    if (shutdown(socket_fd, 1)) {
                        error(1, errno, "shutdown failed");
                    }
                /*close关闭连接*/
                } else if (strncmp(send_line, "close", 5) == 0) {
                    
                    FD_CLR(0, &allreads);
                    if (close(socket_fd)) {
                        error(1, errno, "close failed");
                    }
                    sleep(6);
                    exit(0);
                /*处理正常输入,把回车符截掉*/
                } else {
                    int i = strlen(send_line);
                    if (send_line[i - 1] == ‘\n‘) {
                        send_line[i - 1] = 0;
                    }

                    printf("now sending %s\n", send_line);
                    size_t rt = write(socket_fd, send_line, strlen(send_line));
                    if (rt < 0) {
                        error(1, errno, "write failed ");
                    }
                    printf("send bytes: %zu \n", rt);
                }
            }
        }
    }
}

思考题:

  1. 服务器端程序中为什么调用exit(0)完成了FIN报文发送?为啥不调用close或者shutdown

    因为在调用exit之后进程会退出,而进程相关的所有的资源,文件,内存,信号等内核分配的资源都会被释放,在linux中,一切皆文件,本身socket就是一种文件类型,内核会为每一个打开的文件创建file结构并维护指向改结构的引用计数,每一个进程结构中都会维护本进程打开的文件数组,数组下标就是fd,内容就指向上面的file结构,close本身就可以用来操作所有的文件,做的事就是,删除本进程打开的文件数组中指定的fd项,并把指向的file结构中的引用计数减一,等引用计数为0的时候,就会调用内部包含的文件操作close,针对于socket,它内部的实现应该就是调用shutdown,只是参数是关闭读写端,从而比较粗暴的关闭连接。

  2. 信号量处理中,默认处理和自定义函数的区别?

    信号的处理有三种,默认处理,忽略处理,自定义处理。默认处理就是采用系统自定义的操作,大部分信号的默认处理都是杀死进程,忽略处理就是当做什么都没有发生。

连接状态的检测

keep-alive

TCP保持活跃机制,定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到相应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知给上层应用程序。

相关的可定义变量systctl变量(linux)和默认值

  1. 保活时间:net.ipv4.tcp_keepalive_time = 7200s
  2. 保活时间间隔:net.ipv4.tcp_keepalive_intvl = 75s
  3. 保活探测次数:net.ipv4.tcp_keepalive_probes = 9次

开启TCP保活,有以下几种情况

  1. 对端程序是正常工作的

    当TCP保活的探测报文发送给对端,对端会正常响应,这样TCP保活时间会被重置。

  2. 对端程序崩溃并重启

    当TCP保活的探测报文发送给对端后,对端是可以相应的,但是由于没有该连接的有效信息,会产生一个RST报文,这样很快就会发现TCP链接已经被重置。

  3. 是对端程序崩溃,或对端由于其他原因导致报文不可达

    当TCP保活的探测报文发送给对端后,石沈大海,没有响应连续几次,达到保活探测次数后,TCP会报告该TCP连接已经死亡。

TCP保活机制默认是关闭的,可以选择在连接的两个方向开启,也可以单独在一个方向上开启。如果开启服务器端到客户端的检测,可以再客户端非正常断连的情况下清楚服务器端保留的脏数据;而开启客户端到服务器端的检测,可以在服务器无响应的情况下,重新发起连接。

面试题:

  1. 为什么TCP不提供频率较高的保活机制呢?

    早期的网络宽带非常有限,如果提供一个频率很高的保活机制,对有限的带宽是一个比较严重的浪费。

应用层探活

使用TCP自身的保活机制,时间间隔比较长,对于有时延要求的系统是无法接受的。所以必须在应用层寻找好的解决方案。

可以设计一个PING-PONG的机制,需要保活的一方,比如客户端,在保活时间到达后,发起对连接的PING操作,如果服务器端对PING有回应,则重新设置保活时间,否则对探测次数进行计数,如果最终探测次数达到了保活探测次数预先设置的值之后,则认为连接无效。

关键点:

  1. 需要使用定时器,通过使用I/O复用自身的机制来实现;
  2. 需要设计一个PING-PONG的协议。

消息格式设计

四种类型消息

#ifndef MESSAGE_OBJECTE_H
#define MESSAGE_OBJECTE_H

#include <sys/types.h>

typedef struct {
    u_int32_t type;
    char data[1024];
} messageObject;

#define MSG_PING          1
#define MSG_PONG          2
#define MSG_TYPE1        11
#define MSG_TYPE2        21

#endif //MESSAGE_OBJECTE_H

客户端

模拟TCP Keep-Alive的机制,在保活时间达到后,探活次数加一,同时向服务器发送PING格式的消息。此后以预设的保活时间间隔,不断向服务器发送PING格式的消息,如果收到服务器端的应答,则结束报货,将保活时间置0;

使用select自带的定时器

#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#include "message_objecte.h"

#define MAXLINE 4096
#define KEEP_ALIVE_TIME 10
#define KEEP_ALIVE_INTERVAL 3
#define KEEP_ALIVE_PROBETIMES 3
#define SERV_PORT 9091

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: tcpclient <IPaddress>");
    }

    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    char recv_line[MAXLINE + 1];
    int n;

    fd_set readmask;
    fd_set allreads;

    
    struct timeval tv;
    int heartbeats = 0;
	/*设置超时时间,即保活时间*/
    tv.tv_sec = KEEP_ALIVE_TIME;
    tv.tv_usec = 0;

    messageObject messageObject;

    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);
    for (;;) {
        readmask = allreads;
        int rc = select(socket_fd + 1, &readmask, NULL, NULL, &tv);
        if (rc < 0) {
            error(1, errno, "select failed");
        }
        /*
        客户端已经在KEEP_ALIVE_TIME这段时间内没有收到任何当前连接的反馈,于是发起PING消息
        通过传送一个类型为MSG_PING的消息对象来完成PING操作
        */
        if (rc == 0) {
            if (++heartbeats > KEEP_ALIVE_PROBETIMES) {
                error(1, 0, "connection dead\n");
            }
            printf("sending heartbeat #%d\n", heartbeats);
            messageObject.type = htonl(MSG_PING);
            rc = send(socket_fd, (char *) &messageObject, sizeof(messageObject), 0);
            if (rc < 0) {
                error(1, errno, "send failure");
            }
            tv.tv_sec = KEEP_ALIVE_INTERVAL;
            continue;
        }
        /*
        在接收到服务器端程序之后的处理
        实际中应该对报文进行解析后处理,只要PONG类型的回应才是PING探活的结果。
        这里认为只要收到服务器端的报文,连接就是正常,探活计数器和探活时间都置零,等待下一次探活时间的来临
        */
        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {
                error(1, errno, "read error");
            } else if (n == 0) {
                error(1, 0, "server terminated \n");
            }
            printf("received heartbeat, make heartbeats to 0 \n");
            heartbeats = 0;
            tv.tv_sec = KEEP_ALIVE_TIME;
        }
    }
}


服务器端

服务器端的程序在收到客户端发来的各种消息后,进行处理,发现如果是PING类型的消息休眠一段时间后回复一个PONG消息;如果休眠时间很长,客户端就无法知道服务器端是否存活,实际情况应该是系统崩溃或者网络异常。

#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#include "message_objecte.h"
#define SERV_PORT 9091
static int count;

static void sig_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}


int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: tcpsever <sleepingtime>");
    }

    int sleepingTime = atoi(argv[1]);

    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, 5);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGINT, sig_int);
    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    messageObject message;
    count = 0;

    for (;;) {
        int n = read(connfd, (char *) &message, sizeof(messageObject));
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }

        printf("received %d bytes\n", n);
        count++;

        switch (ntohl(message.type)) {
            /*处理MSG_TYPE1的消息*/
            case MSG_TYPE1 :
                printf("process  MSG_TYPE1 \n");
                break;
			/*处理MSG_TYPE2的消息*/
            case MSG_TYPE2 :
                printf("process  MSG_TYPE2 \n");
                break;
			/*
			处理MSG_PING的消息
			通过休眠模拟相应是否及时,调用send发送一个PONG报文
			*/
            case MSG_PING: {
                messageObject pong_message;
                pong_message.type = MSG_PONG;
                sleep(sleepingTime);
                ssize_t rc = send(connfd, (char *) &pong_message, sizeof(pong_message), 0);
                if (rc < 0)
                    error(1, errno, "send failure");
                break;
            }
			/*异常行为处理,消息格式不认识,程序出错退出*/
            default :
                error(1, 0, "unknown message type (%d)\n", ntohl(message.type));
        }
    }
}

这种保活机制的建立依赖于系统定时器和适合的应用层报文协议。

面试题:

  1. TCP探活的方法适用于UDP吗?

    ? UDP里面各方并不会维护一个socket上下文状态是无连接的,如果为了连接而保活是不必要的,如果为了探测对端是否正常工作而做ping-pong也是可行的。

  2. 额外的探活报文占用了有限带宽?为什么需要多次探活才能决定一个TCP连接是否已死。

    ? 额外的探活报文是会占用一些带宽资源,可根据实际业务场景,适当增加保活时间,降低探活频率,简化ping-pong协议。有必要判断存活,举一个打游戏的例子,电脑突然蓝屏,但是游戏的角色还残留在游戏中,所以服务器为了判断它是否真的存活还是需要一个心跳包,隔一段时间过后把它踢下线。

    ? 多次探活是为了防止误伤,避免ping包在网络中丢失掉了,而误认为对端死亡。

TCP协议中的动态数据传输

禁用Nagle算法

SO_REUSEADDR

TIME_WAIT会导致Address already in use的错误

Linux对端口重用问题的优化

  1. 新连接SYN告知的初始序列号,一定比TIME_WAIT老连接的末序列号大,这样通过序列号就可以区别出新老连接;
  2. 开启了tcp_timestamps,使得新连接的时间戳比老连接的时间戳大,通过时间戳也可以区别出新旧连接;

由于1和2,一个TIME_WAIT的TC连接链接可以忽略掉旧连接,重新被新的连接接使用,通过设置套接字选项SO_REUSEADDR来实现。该选项允许启动绑定在一个端口上,即使之前存在一个和该端口一样的链接。

int flag = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));

总结:服务器端程序,在bind之前,都应该设置SO_REUSEADDR套接字选项,以便服务器端程序可以再较短的时间复用一个端口启动。

面试题

  1. TCP可以使用SO_REUSEADDR,UDP可以使用吗

    UDP的SO_REUSEADDR使用场景比较多的是组播网络,好处是,如我们在接收组播流的时候,比如用ffmpeg拉取了一个组播流,但是还想用ffmpeg拉取相同的组播流,这个时候就需要地址重用了

  2. 服务器端程序中,为什么设置SO_REUSEADDR要在bind函数之前对监听的套接字进行设置,而不是对已连接的套接字进行设置。

    因为SO_REUSEADDR是针对新建立的连接才起作用,对已建立的连接设置是无效的

滑动窗口

对方主机的输入缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?

TCP 中有滑动窗口控制协议,所以传输的时候会保证传输的字节数小于等于自己能接受的字节数。

网络字节序

  1. 大端字节序:将高字节存放在起始地址(低地址),为网络字节序
  2. 小端字节序:将低字节存放在起始地址(低地址)

网络字节序转换相关函数
技术图片

n表示network,h表示host,s表示short,l表示long,分别表示16位和32位整数

报文读取和解析

如何确定报文的边界

  1. 发送端要把发送的报文长度预先通过报文告知给接收端

    报文格式:

    ? 技术图片

    发送报文

    #include <string.h>  
    #include <stdio.h>
    #include <stdlib.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <unistd.h>
    #include <signal.h>
    #include <errno.h>
    #include <error.h>
    
    #define SERV_PORT 9091
    
    int main(int argc, char **argv) {
        if (argc != 2) {
            error(1, 0, "usage: tcpclient <IPaddress>");
        }
    
        int socket_fd;
        socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        struct sockaddr_in server_addr;
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(SERV_PORT);
        inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
    
        socklen_t server_len = sizeof(server_addr);
        int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
        if (connect_rt < 0) {
            error(1, errno, "connect failed ");
        }
    
        struct {
            u_int32_t message_length;
            u_int32_t message_type;
            char data[128];
        } message;
    
    
        int n;
    
        while (fgets(message.data, sizeof(message.data), stdin) != NULL) {
            n = strlen(message.data);
            message.message_length = htonl(n);
            message.message_type = htonl(1);
            if (send(socket_fd, (char *) &message, sizeof(message.message_length) + sizeof(message.message_type) + n, 0) <
                0)
                error(1, errno, "send failure");
    
        }
        exit(0);
    }
    
    
    

    解析报文

    #include <string.h>  
    #include <stdio.h>
    #include <stdlib.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <unistd.h>
    #include <signal.h>
    #include <errno.h>
    #include <error.h>
    
    #define SERV_PORT 9091
    static int count;
    
    static void sig_int(int signo) {
        printf("\nreceived %d datagrams\n", count);
        exit(0);
    }
    
    size_t readn(int fd, void *buffer, size_t size) {
        char *buffer_pointer = (char *)buffer;
        int length = size;
    
        while (length > 0) {
            int result = read(fd, buffer_pointer, length);
    
            if (result < 0) {
                if (errno == EINTR)
                    continue;     /* 考虑非阻塞的情况,这里需要再次调用read */
                else
                    return (-1);
            } else if (result == 0)
                break;                /* EOF(End of File)表示套接字关闭 */
    
            length -= result;
            buffer_pointer += result;
        }
        return (size - length);        /* 返回的是实际读取的字节数*/
    }
    
    size_t read_message(int fd, char *buffer, size_t length) {
        u_int32_t msg_length;
        u_int32_t msg_type;
        int rc;
    
        /* Retrieve the length of the record */
    
        rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t));
        if (rc != sizeof(u_int32_t))
            return rc < 0 ? -1 : 0;
        msg_length = ntohl(msg_length);
    
        rc = readn(fd, (char *) &msg_type, sizeof(msg_type));
        if (rc != sizeof(u_int32_t))
            return rc < 0 ? -1 : 0;
    
        /* 判断buffer是否可以容纳下数据  */
        if (msg_length > length) {
            return -1;
        }
    
        /* Retrieve the record itself */
        rc = readn(fd, buffer, msg_length);
        if (rc != msg_length)
            return rc < 0 ? -1 : 0;
        return rc;
    }
    
    
    int main(int argc, char **argv) {
        int listenfd;
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
    
        struct sockaddr_in server_addr;
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(SERV_PORT);
    
        int on = 1;
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
        if (rt1 < 0) {
            error(1, errno, "bind failed ");
        }
    
        int rt2 = listen(listenfd, 5);
        if (rt2 < 0) {
            error(1, errno, "listen failed ");
        }
    
        signal(SIGPIPE, SIG_IGN);
    
        int connfd;
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
    
        if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
            error(1, errno, "bind failed ");
        }
    
        char buf[128];
        count = 0;
    
        while (1) {
            int n = read_message(connfd, buf, sizeof(buf));
            if (n < 0) {
                error(1, errno, "error read message");
            } else if (n == 0) {
                error(1, 0, "client closed \n");
            }
            buf[n] = 0;
            printf("received %d bytes: %s\n", n, buf);
            count++;
        }
    
        exit(0);
    
    }
    
    
    
    
  2. 通过一些特殊的字符来进行边界的划分

    HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界

    技术图片

    
    int read_line(int fd, char *buf, int size) {
        int i = 0;
        char c = ‘\0‘;
        int n;
    
        while ((i < size - 1) && (c != ‘\n‘)) {
            n = recv(fd, &c, 1, 0);
            if (n > 0) {
                if (c == ‘\r‘) {
                    n = recv(fd, &c, 1, MSG_PEEK);
                    if ((n > 0) && (c == ‘\n‘))
                        recv(fd, &c, 1, 0);
                    else
                        c = ‘\n‘;
                }
                buf[i] = c;
                i++;
            } else
                c = ‘\n‘;
        }
        buf[i] = ‘\0‘;
    
        return (i);
    }
    

TCP的可靠性

超时重传

TCP通过超时重传机制来保证丢失数据的可靠传输,如果报文发出去的特定时间内,发送消息的主机没有收到另一个主机的回复,那么就继续发送这条消息,直到收到回复为止。

TCP是否可靠?

TCP 是一种可靠的协议,这种可靠体现在端到端的通信上。这似乎给我们带来了一种错觉,从发送端来看,应用程序通过调用 send 函数发送的数据流总能可靠地到达接收端;而从接收端来看,总是可以把对端发送的数据流完整无损地传递给应用程序来处理。这种理解是不对的!!!

发送端通过调用 send 函数之后,数据流并没有马上通过网络传输出去,而是存储在套接字的发送缓冲区中,由网络协议栈决定何时发送、如何发送。当对应的数据发送给接收端,接收端回应 ACK,存储在发送缓冲区的这部分数据就可以删除了,但是,发送端并无法获取对应数据流的 ACK 情况,也就是说,发送端没有办法判断对端的接收方是否已经接收发送的数据流,如果需要知道这部分信息,就必须在应用层自己添加处理逻辑,例如显式的报文确认机制。从接收端来说,也没有办法保证 ACK 过的数据部分可以被应用程序处理,因为数据需要接收端程序从接收缓冲区中拷贝,可能出现的状况是,已经 ACK 的数据保存在接收端缓冲区中,接收端处理程序突然崩溃了,这部分数据就没有办法被应用程序继续处理。

TCP 连接建立之后,能感知 TCP 链路的方式是有限的,一种是以 read 为核心的读操作,另一种是以 write 为核心的写操作。

故障模式演示

技术图片

对端无FIN包发送

1. 网络中断导致的

很多原因都会造成网络中断,在这种情况下,TCP 程序并不能及时感知到异常信息。除非网络中的其他设备,如路由器发出一条 ICMP 报文,说明目的网络或主机不可达,这个时候通过 read 或 write 调用就会返回 Unreachable 的错误。

可惜大多数时候并不是如此,在没有 ICMP 报文的情况下,TCP 程序并不能理解感应到连接异常。如果程序是阻塞在 read 调用上,那么很不幸,程序无法从异常中恢复。这显然是非常不合理的,不过,我们可以通过给 read 操作设置超时来解决。在接下来的第 18 讲中,我会讲到具体的方法。如果程序先调用了 write 操作发送了一段数据流,接下来阻塞在 read 调用上,结果会非常不同。

Linux 系统的 TCP 协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传 12 次、合计时间约为 9 分钟之后,协议栈会标识该连接异常,这时,阻塞的 read 调用会返回一条 TIMEOUT 的错误信息。如果此时程序还执着地往这条连接写数据,写操作会立即失败,返回一个 SIGPIPE 信号给应用程序

2. 系统崩溃导致的

当系统突然崩溃,如断电时,网络连接上来不及发出任何东西。这里和通过系统调用杀死应用程序非常不同的是,没有任何 FIN 包被发送出来。这种情况和网络中断造成的结果非常类似,在没有 ICMP 报文的情况下,TCP 程序只能通过 read 和 write 调用得到网络连接异常的信息,超时错误是一个常见的结果。

不过还有一种情况需要考虑,那就是系统在崩溃之后又重启,当重传的 TCP 分组到达重启后的系统,由于系统中没有该 TCP 分组对应的连接数据,系统会返回一个 RST 重置分节,TCP 程序通过 read 或 write 调用可以分别对 RST 进行错误处理。

  1. 如果是阻塞的 read 调用,会立即返回一个错误,错误信息为连接重置(Connection Reset)。如果是一次 write 操作,也会立即失败,应用程序会被返回一个 SIGPIPE 信号。

对端有FIN包的演示

服务器端程序

#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define SERV_PORT 9091


int tcp_server(int port) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, 5);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    return connfd;
}

int main(int argc, char **argv) {
    int connfd;
    char buf[1024];

    connfd = tcp_server(SERV_PORT);

    for (;;) {
        int n = read(connfd, buf, 1024);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }

        sleep(5);

        int write_nc = send(connfd, buf, n, 0);
        printf("send bytes: %d \n", write_nc);
        if (write_nc < 0) {
            error(1, errno, "error write");
        }
    }

    exit(0);

}

客户端程序

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

# define MESSAGE_SIZE 102400000
#define SERV_PORT 9091

int tcp_client(char *address, int port) {
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, address, &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    return socket_fd;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: reliable_client01 <IPaddress>");
    }

    int socket_fd = tcp_client(argv[1], SERV_PORT);
    char buf[129];
    int len;
    int rc;

    while (fgets(buf, sizeof(buf), stdin) != NULL) {
        len = strlen(buf);
        rc = send(socket_fd, buf, len, 0);
        if (rc < 0)
            error(1, errno, "write failed");
        sleep(3);
        rc = read(socket_fd, buf, sizeof(buf));
        if (rc < 0)
            error(1, errno, "read failed");
        else if (rc == 0)
            error(1, 0, "peer connection closed\n");
        else
            fputs(buf, stdout);
    }
    exit(0);
}

1. read直接感知FIN包

依次启动服务器端和客户端,在服务器端输入good字符之后,迅速结束服务器端

技术图片

2. 通过write产生RST,read调用感知RST

依次启动服务器端和客户端,在服务器端输入bad字符之后,等待服务器端收到,再次杀死服务器端,客户端再次输入bad2

技术图片

服务器端程序

#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define SERV_PORT 9091

int tcp_server(int port) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, 5);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    return connfd;
}
int main(int argc, char **argv) {
    int connfd;
    char buf[1024];
    int time = 0;

    connfd = tcp_server(SERV_PORT);

    while (1) {
        int n = read(connfd, buf, 1024);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }

        time++;
        fprintf(stdout, "1K read for %d \n", time);
        usleep(10000);
    }
    exit(0);
}

客户端程序

#include <string.h>  
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define SERV_PORT 9091
# define MESSAGE_SIZE 102400

int tcp_client(char *address, int port) {
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, address, &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    return socket_fd;
}
int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: reliable_client02 <IPaddress>");
    }

    int socket_fd = tcp_client(argv[1], SERV_PORT);

    signal(SIGPIPE, SIG_IGN);

    char *msg = (char *)"network programming";
    ssize_t n_written;

    int count = 10000000;
    while (count > 0) {
        n_written = send(socket_fd, msg, strlen(msg), 0);
        fprintf(stdout, "send into buffer %ld \n", n_written);
        if (n_written <= 0) {
            error(1, errno, "send error");
            return -1;
        }
        count--;
    }
    return 0;
}


3. 向一个已关闭连接连续写,最终导致SIGPIPE

如果在服务端读取数据并处理过程中,突然杀死服务器进程,我们会看到客户端很快也会退出

技术图片

检查数据的有效性

缓冲区处理的错误演示

缓冲区溢出

缓冲区溢出,是指计算机程序中出现的一种内存违规操作。本质是计算机程序向缓冲区填充的数据,超出了原本缓冲区设置的大小限制,导致了数据覆盖了内存栈空间的其他合法数据。这种覆盖破坏了原来程序的完整性,导致应用程序崩溃。

char Response[] = "COMMAND OK";
char buffer[128];

while (1) {
    int nBytes = recv(connfd, buffer, sizeof(buffer), 0);
    if (nBytes == -1) {
        error(1, errno, "error read message");
    } else if (nBytes == 0) {
        error(1, 0, "client closed \n");
    }
    //可能发生buffer[128] = ‘\0‘的情况
    buffer[nBytes] = ‘\0‘; 
    if (strcmp(buffer, "quit") == 0) {
        printf("client quit\n");
        send(socket, Response, sizeof(Response), 0);
    }

    printf("received %d bytes: %s\n", nBytes, buffer);
}

解决办法:留下 buffer 里的一个字节,以容纳后面的‘\0‘

int nBytes = recv(connfd, buffer, sizeof(buffer) - 1, 0);

你会发现我们发送过去的字符串,调用的是sizeof,那也就意味着,Response 字符串中的‘\0‘是被发送出去的,而我们在接收字符时,则假设没有‘\0‘字符的存在。为了统一,我们可以改成如下的方式,使用 strlen 的方式忽略最后一个‘\0‘字符。

send(socket, Response, strlen(Response), 0);

对变长报文解析的两种手段

将报文信息的长度编码进入消息

size_t read_message(int fd, char *buffer, size_t length) {
    u_int32_t msg_length;
    u_int32_t msg_type;
    int rc;

    rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;
    msg_length = ntohl(msg_length);

    rc = readn(fd, (char *) &msg_type, sizeof(msg_type));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;
	/*对实际的报文长度msg_length和应用程序分配的缓冲区大小进行了比较,很重要!!*/
    /*如果不判断 msg_length比length大,那么会产生缓冲区溢出*/
    if (msg_length > length) {
        return -1;
    }

    /* Retrieve the record itself */
    rc = readn(fd, buffer, msg_length);
    if (rc != msg_length)
        return rc < 0 ? -1 : 0;
    return rc;
}

使用特殊的边界符号

使用换行符作为边界符号

  1. 一个简单的想法是每次读取一个字符,判断这个字符是不是换行符。这里有一个这样的函数,这个函数的最大问题是工作效率太低,要知道每次调用 recv 函数都是一次系统调用,需要从用户空间切换到内核空间,上下文切换的开销对于高性能来说最好是能省则省。

    size_t readline(int fd, char *buffer, size_t length) {
        char *buf_first = buffer;
    
        char c;
        while (length > 0 && recv(fd, &c, 1, 0) == 1) {
            *buffer++ = c;
            length--;
            if (c == ‘\n‘) {
                *buffer = ‘\0‘;
                return buffer - buf_first;
            }
        }
    
        return -1;
    }
    
  2. 一次性读取最多 512 字节到临时缓冲区,之后将临时缓冲区的字符一个一个拷贝到应用程序最终的缓冲区中,这样的做法明显效率会高很多。

    
    size_t readline(int fd, char *buffer, size_t length) {
        char *buf_first = buffer;
        static char *buffer_pointer;
        int nleft = 0;
        static char read_buffer[512];
        char c;
    
        while (length-- > 0) {
            if (nleft <= 0) {//判断临时缓冲区的字符有没有被全部拷贝完
                //如果被全部拷贝完,就会再次尝试读取最多 512 字节
                int nread = recv(fd, read_buffer, sizeof(read_buffer), 0);
                if (nread < 0) {
                    if (errno == EINTR) {
                        length++;
                        continue;
                    }
                    return -1;
                }
                if (nread == 0)
                    return 0;
                //在读取字符成功之后,重置了临时缓冲区读指针、临时缓冲区待读的字符个数
                buffer_pointer = read_buffer;
                nleft = nread;
            }
            //在拷贝临时缓冲区字符,每次拷贝一个字符,并移动临时缓冲区读指针,对临时缓冲区待读的字符个数进行减 1 操作
            c = *buffer_pointer++;
            *buffer++ = c;
            nleft--;
            if (c == ‘\n‘) {//判断是否读到换行符,如果读到则将应用程序最终缓冲区截断,返回最终读取的字符个数
                *buffer = ‘\0‘;
                return buffer - buf_first;
            }
        }
        return -1;
    }
    
    //输入字符为: 012345678\n
    char buf[10]
    readline(fd, buf, 10)
        /*当读到最后一个\n 字符时,length 为 1,问题是在第 30 行和 31 行,如果读到了换行符,就会增加一个字符串截止符,这显然越过了应用程序缓冲区的大小*/
    

    正确方式

    先对 length 进行处理,再去判断 length 的大小是否可以容纳下字符

    
    size_t readline(int fd, char *buffer, size_t length) {
        char *buf_first = buffer;
        static char *buffer_pointer;
        int nleft = 0;
        static char read_buffer[512];
        char c;
    	//先对处理
        while (--length> 0) {
            if (nleft <= 0) {
                int nread = recv(fd, read_buffer, sizeof(read_buffer), 0);
                if (nread < 0) {
                    if (errno == EINTR) {
                        length++;
                        continue;
                    }
                    return -1;
                }
                if (nread == 0)
                    return 0;
                buffer_pointer = read_buffer;
                nleft = nread;
            }
            c = *buffer_pointer++;
            *buffer++ = c;
            nleft--;
            if (c == ‘\n‘) {
                *buffer = ‘\0‘;
                return buffer - buf_first;
            }
        }
        return -1;
    }
    

面试题

1. 在读数据的时候,一般都需要给应用程序最终缓冲区分配大小,这个大小有什么讲究吗?

最终缓冲区的大小应该比预计接收的数据大小大一些,预防缓冲区溢出。

2. 例子中所分配的缓冲是否可以换成动态分配吗?比如调用 malloc 函数来分配缓冲区

完全可以动态分配,但是要记得在return前释放缓冲区

四次挥手理解

技术图片

面试题

listen 函数中参数 backlog

CLOSE_WAIT

面试题

  1. 如果发现大量的CLOSE_WAIT状态,怎么解决?

【网络编程】网络编程相关知识点总结1

标签:忽略   message   报错   文件创建   api   应用层   网卡   换行符   避免   

原文地址:https://www.cnblogs.com/Trevo/p/12982881.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!