网络数据传输,recv && send?没那么简单!

2021-09-18 11:29:07 浏览数 (1)

文章目录

    • 网络通信流程
    • 缓冲区
      • recv && send
    • 缓冲区处理
      • 示例一:
      • 示例二:
      • 示例三:
      • 总结

网络通信流程

服务端和客户端通信时时怎么个流程呢?让我来写个流程:

代码语言:javascript复制
打开通信套接字
打开监听套接字
监听客户端连接

通过recv来读取数据
|
通过send来发送数据

真就这么简单吗?没有听过缓冲区的存在吗?


缓冲区

同步Socket的send函数的执行流程,当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的长度(因为待发送数据是要copy到套接字s的发送缓冲区的,注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里):

这时候就会出现以下情况: 1.如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;(切包准备去了解一下)

2.如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么 send就比较s的发送缓冲区的剩余空间和len:

代码语言:javascript复制
  (i)如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完;

  (ii)如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里。

3.如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

注意:send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)

同步Socket的recv函数的执行流程:当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,(发送先)

如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;

如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕;

当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数;

如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

我说明白了吗?


recv && send

socket函数创建一个文件描述符fd,一个fd 对应两个缓冲区,一个输入缓冲区,一个输出缓冲区。 而recv和send函数就是对这两个函数进行操作。

recv函数

代码语言:javascript复制
int recv(SOCKET s,char *buf, int len, int flags);

函数功能:不论客户端还是服务端都能通过recv从TCP另一端接收数据。

参数释义: 参数一:指定接收端套接字描述符; 参数二:指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据; 参数三:指明buf的长度; 参数四 :一般置为0。


send函数

代码语言:javascript复制
int send( SOCKET s,char *buf,int len,int flags );

功能:不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。

参数一:指定发送端套接字描述符; 参数二:存放应用程序要发送数据的缓冲区; 参数三:实际要发送的数据的字节数; 参数四:一般置为0。


我想,上面这些东西也不是什么很那啥的了,到处都是嘛,反复写也没意思。


缓冲区处理

一个设计良好的网络程序,应该可以在随机输入的情况下表现稳定。 不仅是这样,随着互联网的发展,网络安全也愈发重要,我们编写的网络程序能不能在黑客的刻意攻击之下表现稳定,也是一个重要考量因素。

那么程序都有可能出现哪几种漏洞呢?


示例一:

代码语言:javascript复制
char Response[] = "kudliugf";
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[nBytes] = '';
    if (strcmp(buffer, "quit") == 0) {
        printf("client quitn");
        send(socket, Response, sizeof(Response), 0);
    }
 
    printf("received %d bytes: %sn", nBytes, buffer);
}

这段代码从连接套接字中获取字节流,并且判断了出差和 EOF 情况,乍看上去一切正常。

但仔细看一下,这段代码很有可能会产生下面的结果。

代码语言:javascript复制
char buffer[128];
buffer[128] = '';

通过 recv 读取的字符数为 128 时,就会是文稿中的结果。因为 buffer 的大小只有 128 字节,最后的赋值环节,产生了缓冲区溢出的问题。


示例二:

代码语言:javascript复制
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;
 
    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;
}

在进行报文解析时,第 15 行对实际的报文长度msg_length和应用程序分配的缓冲区大小进行了比较,如果报文长度过大,导致缓冲区容纳不下,直接返回 -1 表示出错。千万不要小看这部分的判断,试想如果没有这个判断,对方程序发送出来的消息体,可能构建出一个非常大的msg_length,而实际发送的报文本体长度却没有这么大,这样后面的读取操作就不会成功,如果应用程序实际缓冲区大小比msg_length小,也产生了缓冲区溢出的问题。


示例三:

如果我们需要开发一个函数,这个函数假设报文的分界符是换行符(n),一个简单的想法是每次读取一个字符,判断这个字符是不是换行符。

要知道每次调用 recv 函数都是一次系统调用,需要从用户空间切换到内核空间,上下文切换的开销对于高性能来说最好是能省则省。

这个函数一次性读取最多 512 字节到临时缓冲区,之后将临时缓冲区的字符一个一个拷贝到应用程序最终的缓冲区中,这样的做法明显效率会高很多。

代码语言:javascript复制
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 = '';
            return buffer - buf_first;
        }
    }
    return -1;
}

总结

今天的内容到这里就结束了。让我们总结一下: 在网络编程中,是否做好了对各种异常边界的检测,将决定我们的程序在恶劣情况下的稳定性,所以,我们一定要时刻提醒自己做好应对各种复杂情况的准备,这里的异常情况包括缓冲区溢出、指针错误、连接超时检测等。

0 人点赞