网络编程:理解TCP中的“流”

TCP是一种流式协议

TCP数据是流式的特性,可分别从发送端接收端来阐述
发送端:当调用send函数完成数据“发送”后,数据并没有真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中,至于什么时候发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件,也就是说,不能假设每次send调用发送的数据,都会作为一个整体完整地发送出去。
接收端:先调用send函数发送的字节,总在后调用send函数发送字节的前面,这个是由TCP严格保证的;如果发送过程中有TCP分组丢失,但其后续分组陆续到达,那么TCP协议栈会缓存后续分组,直到前面丢失的分组到达,最终,形成可以被应用程序读取的数据流。

网络字节排序


大端字节序:将0x02高字节存放在起始地址(高存低)
小端字节序:将0x01低字节存放在起始地址(低存低)
网络协议使用的是大端字节序
为了保证网络字节序一致,POSIX标准提供了如下的转换函数:

uint16_t htons (uint16_t hostshort)
uint16_t ntohs (uint16_t netshort)
uint32_t htonl (uint32_t hostlong)
uint32_t ntohl (uint32_t netlong)

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

报文读取和解析

TCP报文是以字节流的形式呈现给应用程序,那应用程序如何解读字节流呢?
报文格式和解析

  • 报文格式就是定义了字节的组织形式,发送端和接收端按照统一的报文格式进行数据传输和解析,保证批次能够完成交流
  • 知道了报文格式,接收端才能针对性地进行报文读取和解析工作

报文格式最重要的是如何确定报文的边界。常见的报文格式有两种:

  • 一种是发送端把要发送的报文长度预先通过报文告知给接收端
  • 另一种是通过一些特殊的字符来进行边界的划分

显示编码报文长度

报文格式

首先4个字节大小的消息长度,目的是将真正发送的字节流的大小显示通过报文告知接收端,接下来的4个字节大小的消息类型;真正需要发送的数据则紧随其后。
发送报文
发送端的程序:


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 buf[128];
    } message;

    int n;

    while (fgets(message.buf, sizeof(message.buf), stdin) != NULL) {
        n = strlen(message.buf);
        message.message_length = htonl(n);
        message.message_type = 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);
}

htonl函数将字节大小转化为网络字节顺序。

解析报文:
服务端的程序,服务端需要对报文进行解析

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;
    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, LISTENQ);
    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);

}

调用read_message函数进行报文解析工作,并把报文的主体通过标准输出打印出来。

readn函数

read函数的语意:读取报文预设大小的字节,readn调用会一直循环,尝试读取预设大小的字节,如果接收缓冲区数据空,readn函数会阻塞在那里,直到有数据到达。

size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = 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);        /* 返回的是实际读取的字节数*/
}

解析报文:read_message函数

以readn函数为基础,read_message对报文的解析处理:

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)); //获取4个字节的消息长度数据
    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)); //获取4个字节的消息类型数据
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;

    if (msg_length > length) {
        return -1;
    }

    rc = readn(fd, buffer, msg_length); //一次性读取已知长度的消息体
    if (rc != msg_length)
        return rc < 0 ? -1 : 0;
    return rc;
}

特殊字符作为边界

通过特殊字符作为报文边界。
HTTP是一个很好的例子:

HTTP通过设置回车符、换行符作为HTTP报文协议的边界
read_line函数在尝试读取一行数据,即读到回车符\r,或者读到回车换行符\r\n为止。

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);
}

实践

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define SERV_PORT 43211

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        perror("usage: streamclient <IPaddress>");
        return -1;
    }

    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)
    {
        perror("connect failed");
        return -1;
    }

    struct 
    {
        /* data */
        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 = 1;
        if(send(socket_fd, (char *)&message, sizeof(message.message_length) 
            + sizeof(message.message_type) + n, 0) < 0)
            {
                perror("send failure");
                return -1;   
            }
    }

    exit(0);
}

服务端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <netinet/in.h>

#define SERV_PORT 43211
#define LISTENQ 1024

static int count;

size_t readn(int fd, void *buffer, size_t size)
{
    char *buffer_pointer = 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;

    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(u_int32_t));
    if(rc != sizeof(u_int32_t))
    {
        return rc < 0 ? -1 : 0;
    }

    // 判断buffer是否可以容纳下数据
    if(msg_length > length)
    {
        perror("msg_length > length");
        return -1;
    }

    rc = readn(fd, buffer, msg_length);
    if(rc != msg_length)
    {
        return rc < 0 ? -1 : 0;
    }
    return rc;

}

int main(int argc, char *argv[])
{
    int listen_fd;
    listen_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_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

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

    int rt1 = bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if(rt1 < 0)
    {
        perror("bind failed");
        return -1;
    }

    int rt2 = listen(listen_fd, LISTENQ);
    if(rt2 < 0)
    {
        perror("listen failed");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);

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

    if((connfd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr)) < 0)
    {
        perror("accept failed");
        return -1;
    }

    char buf[128];
    count = 0;
    
    while(1)
    {
        int n = read_message(connfd, buf, sizeof(buf));
        if(n < 0)
        {
            perror("error read message");
            return -1;
        }
        else if(n == 0)
        {
            printf("client will closing");
            return 0;
        }
        buf[n] = 0;
        printf("received %d bytes: %s\n",n, buf);
        count++;
    }
    exit(0);

}

运行结果:

小结

TCP 数据流特性决定了字节流本身是没有边界的,一般我们通过显式编码报文长度的方式,以及选取特殊字符区分报文边界的方式来进行报文格式的设计。而对报文解析的工作就是要在知道报文格式的情况下,有效地对报文信息进行还原。

posted @ 2022-03-18 23:30  牛犁heart  阅读(375)  评论(0编辑  收藏  举报