《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)

《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)

  • 《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)
    • 回声客户端的完美实现
      • 回声客户端的问题
      • 回声客户端问题的解决方法
      • 如果问题不在于回声客户端:定义应用层协议
      • 计算器服务器端/客户端示例
    • TCP 原理
      • TCP套接字中的I/O缓冲
      • TCP内部工作原理1:与对方套接字的连接(三次握手)
      • TCP内部工作原理2:与对方主机的数据交换
      • TCP内部工作原理3:断开与套接字的连接(四次挥手)
    • 基于 Windows 的实现
    • 习题
      • (1)请说明TCP套接字连接建立的三次握手过程。尤其是3次数据交换过程每次收发的数据内容。
      • (2)TCP是可靠的数据传输协议,但在通过网络通信的过程可能丢失数据。请通过ACK和SEQ说明TCP通过何种机制保证丢失数据的可靠传输。
      • (3)TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。
      • (4)对方主机的输入缓冲剩余50字节空间时,若本方主机通过write函数请求传输70字节,问TCP如何处理这种情况?
      • (5)第2章示例tcp_server.c(第一章的hello_server.c)和tcp_client.c中,客户端接收服务器端传输的字符串后便退出。现更改程序,使服务器端和客户端各传送1次字符串。考虑到使用TCP协议,所以传输字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。
      • (6)创建收发文件的服务器/客户端程序,实现顺序如下。

《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)

回声客户端的完美实现

在上一篇,我们发现并提出了对于回声客户端的缺陷。在这里,让我们一起来尝试解决这个问题。

回声客户端的问题

先回顾一下回声服务器端的I/O相关代码。

服务器端:

while((str_len = read(clnt_sock , message , BUF_SIZE)) != 0)
	write(clnt_sock , message , str_len);

客户端:

while(1)
{
    fputs("Input message(Q to quit): ", stdout);
    fgets(message , BUF_ SIZE , stdin);
 
    write(sock , message , strlen(message));
    str_len=read(sock , message , BUF_SIZE - 1);
    message[str_len] = 0;
    printf(" message from server: %s", message );
}

两者都在循环中调用read和write函数。而在回声客户端中传输字符串时,调用write函数将字符串一次性发送,在没有考虑可能的处理时延和传输时延情况下,只调用了一次read函数,意在读取发送出去的完整字符串,这就是问题所在。

那我们是否可以给read函数前加入一个延迟函数,过一段时间再去接收数据呢?

这是一种方法,但是延迟应该控制在多少?很显然,这个是不好把握的。我们应当尝试从“提前确认接收数据的大小”这个方向来入手。

回声客户端问题的解决方法

对接收到的字符串长度做记录,循环调用read函数,当接收到的字符串长度大于发送时的长度,停止循环,结束read函数。

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

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error");
    else
        puts("Connected..........");

    while (1)
    {
        fputs("Input message(Q to quit):", stdout);
        fgets(message, BUF_SIZE, stdin);

        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        str_len = write(sock, message, strlen(message));

        recv_len = 0;
        while (recv_len < str_len)
        {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
            if (recv_cnt == -1)
                error_handling("read() error");
            recv_len += recv_cnt;
        }

        message[str_len] = 0;
        printf("Message from server: %s", message);
    }

    close(sock);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

为何在循环中 recv_len<str_len 作为条件,而不用 recv_len != str_len呢?

因为存在一种可能——当接收到的字符串长度在某种条件下大于原本发出的字符串长度时,整个语句块将陷入死循环,反复调用read函数。

如果问题不在于回声客户端:定义应用层协议

若不能再用字符长度来界定的情况下,又该如何解决这类数据界限的问题呢?

答案在于——去定义应用层协议。在之前的回声服务器端/客户端中我们就定义过如下协议:“收到Q就立即终止连接”。同样,收发数据过程中也需要定好规则以表示数据的边界,又或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。

可以看出,应用层协议并不是高深莫测,只不过是为特定程序的实现而制定的规则。

下面编写一个示例程序以体验应用层协议的定义过程。该程序中,服务器端从客户端获得多个数字和运算符信息。服务器端收到数字后对其进行加减乘运算,然后把计算结果传回客户端。例如,向服务器端传递3、5、9的同时请求加法运算,则客户端收到 3+5+9 的运算结果;若请求做乘法运算,则客户端收到 3×5×9 的运算结果。而如果向服务器传递4、3、2 的同时要求做减法,则客户端将收到 4-3-2 的运算结果,即第一个参数成为被减数。

计算器服务器端/客户端示例

在编写程序之前,我们需要先设计一下应用层协议。为了简单起见,我们只设计了最低标准的协议,在实际的应用程序实现中需要的协议更详细、更准确。应用层协议规则定义如下:

  • 客户端连接到服务器端后以1字节整数形式传递待运算数字个数。
  • 客户端向服务器端传递的每个整数型数据占用4字节。
  • 传递整数型数据后接着传递运算符。运算符信息占用1字节。
  • 选择字符 +、-、* 之一传递。
  • 服务器端以4字节整数型向客户端传回运算结果。
  • 客户端得到运算结果后终止与服务器端的连接。

这种程度的协议相当于实现了一半程序,这也说明应用层协议设计在网络编程中的重要性。只要设计好协议,实现程序就不会成为大问题。另外要记住的一点,调用close()函数将向通信对端传递 EOF,请各位记住这一点并加以运用。

op_client.c:

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

#define BUF_SIZE 1024
#define OPSZ 4     // 操作数占用字节数
#define RLT_SIZE 4 // 运算结果数占用字节数

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char opmsg[BUF_SIZE];
    int result, opnd_cnt, i;
    struct sockaddr_in serv_addr;

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("socket() error!");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");
    else
        puts("Connected...........");

    fputs("Operand count: ", stdout);
    scanf("%d", &opnd_cnt);    // 输入操作数个数
    opmsg[0] = (char)opnd_cnt; // 将操作符个数存入字符数组,占用1个字节

    for (i = 0; i < opnd_cnt; i++)
    {
        printf("Operand %d: ", i + 1);
        scanf("%d", (int *)&opmsg[i * OPSZ + 1]); // 将4字节整型数保存到字符数组中,需要将其转换成int指针类型
    }
    fgetc(stdin);                             // 标准输入一个字符
    fputs("Operator: ", stdout);              // 标准输出
    scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]); // 将操作符存入字符数组,占用1个字节
    write(sock, opmsg, opnd_cnt * OPSZ + 2);  // 发送数据给服务器端
    read(sock, &result, RLT_SIZE);            // 接收运算结果数据,存入result变量中

    printf("Operation result: %ld\n", result);
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

我们给出客户端向服务器端传送的数据的数据格式示例,如下图所示:

在这里插入图片描述

可以看出,若想在同一数组中保存并传输多种数据结构,应把数组声明为char类型。而且需要额外做一些指针及数组运算。

op_server.c:

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

#define BUF_SIZE 1024
#define OPSZ 4 // 操作数占用字节数

void error_handling(char *message);
int calculate(int opnum, int opnds[], char operator);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char opinfo[BUF_SIZE] = {0};
    int result, opnd_cnt;
    int recv_cnt, recv_len;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_sz;

    if (argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error!");

    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(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error!");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error!")

            clnt_addr_sz = sizeof(clnt_addr);
    for (i = 0; i < 5; i++)
    {
        opnd_cnt = 0;
        clnt_sock = accept(serv_addr, (struct sockaddr *)&clnt_addr, &clnt_addr_sz);
        read(clnt_sock, &opnd_cnt, 1); // 读取1字节操作数个数,存入opnd_cnt变量中

        recv_len = 0;
        while (opnd_cnt * OPSZ + 1 > recv_len) // 循环读取剩余的数据
        {
            recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE);
            recv_len += recv_cnt;
        }
        result = calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len - 1]);
        write(clnt_sock, (char *)&result, sizeof(result)); // 向客户端传回运算结果消息
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

int calculate(int opnum, int opnds[], char op)
{
    int result = opnds[0], i;
    swith(op)
    {
    case '+':
        for (i = 1; i < opnum; i++)
            result += opnds[i];
        break;
    case '-':
        for (i = 1; i < opnum; i++)
            result -= opnds[i];
        break;
    case '*':
        for (i = 1; i < opnum; i++)
            result *= opnds[i];
        break;
    }
    return result;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

TCP 原理

TCP套接字中的I/O缓冲

我们已经知道,TCP套接字的数据收发无边界。服务器端即使调用1次write函数传输40字节的数据,客户端也有可能通过调用4次read函数每次读取10字节。但此处也有一些疑问,服务器端一次性传输了40字节,而客户端居然可以缓慢地分批接收。客户端接收10字节后,剩下的30字节在何处等候呢?是不是像飞机为了等待着陆而在空中盘旋一样,剩下的30字节也在网络中徘徊并等等接收呢?

实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确地说,如下图所示,write函数调用瞬间,数据被移至输出缓冲区(即发送缓冲区);read函数调用瞬间,从输入缓冲区(即接收缓冲区)读取数据。

在这里插入图片描述

调用write函数时,数据被移至输出缓冲,在适当的时候(不管是分别发送还是一次性发送)传向对端的输入缓冲。这时对方将调用read函数从输入缓冲读取数据。这些I/O缓冲特性可整理如下:

  • I/O缓冲在每个TCP套接字中单独存在。
  • I/O缓冲在创建套接字时自动生成。
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  • 关闭套接字将丢失输入缓冲(即接收缓冲)中的数据。

会不会有“客户端输入缓冲(即接收缓冲)为50字节,而服务器传输了100字节”的情况?

答:不会。TCP协议有流量控制机制,因此 “不会发生超过接收缓冲大小的数据传输”。

所谓流量控制(flow control)就是让发送发的发送速率不要太快,要让接收方来得及接收。TCP协议利用滑动窗口(Sliding Window)机制来实现流量控制。

write函数并不是在向通信对端传输完所有数据时才返回,而是在数据被移到TCP套接字的发送缓冲时就返回了。但TCP会保证对发送缓冲数据的传输,所以说write函数在数据传输完成时返回,我们要准确理解这句话的真正内涵。

TCP内部工作原理1:与对方套接字的连接(三次握手)

TCP套接字从创建到消失所经历过程分为如下3步:

  1. 与对方套接字建立连接。
  2. 与对方套接字进行数据交换。
  3. 断开与对方套接字的连接。

TCP在实际连接建立过程中会经过3次对话过程。因此,该过程又称 “Three-way handshaking(三报文握手)”。接下来给出连接过程中实际交换的信息格式,如下图所示:

在这里插入图片描述

TCP套接字是以全双工(Full-duplex)方式工作的。也就是说,它可以双向传递数据,即可接收,也可发送。因此,正式收发数据前需要做一些准备工作。

  1. 首先,请求连接的主机A向主机B传递如下信息:[SYN] SEQ: 1000, ACK: -

该消息中 SEQ为1000,ACK为空,而SEQ为1000的含义是:“现传递的数据报的初始序号为1000,如果接收无误,请通知我向您传递1001号数据包。”

这是首次请求连接时使用的消息,又称SYN(Synchronization,同步),表示收发数据前传输的同步消息。

  1. 接下来主机B向主机A传递如下消息:[SYN+ACK] SEQ: 2000, ACK: 1001

此时SEQ为2000,ACK为1001,SEQ为2000的含义是:“现传递的数据包初始序号为2000,如果接收无误,请通知我向您传递2001号数据包。”而ACK: 1001 的含义是:“刚才传输的SEQ为1000的数据包接收无误,现在请传递SEQ为1001的数据包。”

对主机A首次传输的数据包的确认消息(ACK:1001)和为主机B传输数据做准备的同步消息(SEQ:2000)捆绑发送,因此,此种类型的消息又称为 SYN+ACK。

通信双方收发数据前向数据包分配初始序号,并向对方通知此序号,这都是为了防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP可以保证可靠的数据传输。

  1. 最后主机A向主机B传递如下消息:[ACK] SEQ: 1001, ACK: 2001

因为主机A发送的 SYN 数据包需要消耗一个序号,因此此刻主机A发送的第二个数据包的序号在之前的序号1000的基础上加1,也就是分配1001。此时该数据包传递的信息含义是:“已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包了。”

至此,主机A和主机B的TCP连接就建立成功了,接下来就可以进行数据传递操作了。

TCP内部工作原理2:与对方主机的数据交换

通过第一步三报文握手过程成功建立起了TCP连接,完成了数据交换的准备工作,就下来就可以正式开始收发数据过程。

在这里插入图片描述

上图给出了主机A分2次(分2个TCP报文段)向主机B传递200字节数据的过程。首先,主机A通过第一个报文段发送100个字节的数据,报文段的SEQ为1200。主机B为了确认收到该报文段,向主机B发送 ACK 1301 确认。

此时的ACK号(确认号)为1301而非1201,原因在于ACK号的增量为传输的数据字节数。假设每次ACK号不加传输的字节数,这样虽然可以确认报文段的传输,但无法明确100字节数据是全部正确传递还是丢失了一部分。因此按如下公式传递ACK消息:

ACK号 = SEQ号 + 传递的数据字节数 + 1

与三报文握手过程相同,最后加1是为了告知对方下次要传递的SEQ号。

传输数据过程中报文段丢失的情况,如下图所示:

在这里插入图片描述

上图表示通过SEQ 1301 报文段向主机B传递100字节的数据。但中间发生了错误,主机B并未收到。经过一段时间后,主机A仍未收到对于 SEQ 1301 的ACK确认,因此主机A会重传该报文段。为了完成报文段的重传,TCP套接字会启动超时计时器以等待ACK应答。若超时计时器发生超时(Time-out)则重传。

TCP内部工作原理3:断开与套接字的连接(四次挥手)

先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认消息,如下图所示:

在这里插入图片描述

报文段内的 FIN 表示断开连接。也就是说,双方各发送1次 FIN 报文段后断开连接。SEQ 和 ACK 的含义与前面讲解的含义一样。在上图中,主机B向主机A传递了两次 ACK 5001,这是因为第二次FIN 报文段中的ACK 5001 只是因为接收ACK消息后未接收数据而重传给主机A的,以便其在要发出的第四个确认报文段中知晓自己的SEQ。

基于 Windows 的实现

op_client_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OP_SIZE 4

void ErrorHanding(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN serverAddr;

    char op_msg[BUF_SIZE];
    int result, opndCnt;

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        ErrorHanding("hSocket() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        ErrorHanding("connect() error!");
    else
        puts("Connected......");

    fputs("Operand count: ", stdout);
    scanf("%d", &opndCnt);
    op_msg[0] = (char)opndCnt;

    for (int i = 0; i < opndCnt; i++)
    {
        printf("Operand %d: ", i + 1);
        scanf("%d", (int *)&op_msg[i * OP_SIZE + 1]);
    }
    fgetc(stdin);
    fputs("Operator: ", stdout);
    scanf("%c", &op_msg[opndCnt * OP_SIZE + 1]);

    send(hSocket, op_msg, opndCnt * OP_SIZE + 2, 0);

    recv(hSocket, (char *)&result, RLT_SIZE, 0);
    printf("Operation result: %d\n", result);

    closesocket(hSocket);
    WSACleanup();

    return 0;
}

op_server_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024
#define OP_SIZE 4

void ErrorHanding(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int calculate(int op_num, int op_info[], char op)
{
    int result = op_info[0];

    switch (op)
    {
    case '+':
        for (int i = 1; i < op_num; i++)
            result += op_info[i];
        break;
    case '-':
        for (int i = 1; i < op_num; i++)
            result -= op_info[i];
        break;
    case '*':
        for (int i = 1; i < op_num; i++)
            result *= op_info[i];
        break;
    }

    return result;
}

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hServerSock, hClientSock;
    SOCKADDR_IN serverAddr, clientAddr;
    int clientAddrSize;
    char op_info[BUF_SIZE];
    int recvCnt, recvLen;
    int result, opndCnt;

    if (argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    hServerSock = socket(PF_INET, SOCK_STREAM, 0);
    if (hServerSock == INVALID_SOCKET)
        ErrorHanding("socket() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(atoi(argv[1]));

    if (bind(hServerSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        ErrorHanding("bind() error!");

    if (listen(hServerSock, 5) == SOCKET_ERROR)
        ErrorHanding("listen() error!");

    clientAddrSize = sizeof(clientAddr);
    for (int i = 0; i < 5; i++)
    {
        opndCnt = 0;
        hClientSock = accept(hServerSock, (SOCKADDR *)&clientAddr, &clientAddrSize);
        if (hClientSock == INVALID_SOCKET)
            ErrorHanding("accept() error!");
        else
            printf("Connect client %d\n", i + 1);

        recv(hClientSock, (char *)&opndCnt, 1, 0);

        recvLen = 0;
        while (recvLen < (opndCnt * OP_SIZE + 1))
        {
            recvCnt = recv(hClientSock, &op_info[recvLen], BUF_SIZE - 1, 0);
            recvLen += recvCnt;
        }

        result = calculate(opndCnt, (int *)op_info, op_info[recvLen - 1]);
        send(hClientSock, (char *)&result, sizeof(result), 0);

        closesocket(hClientSock);
    }

    closesocket(hServerSock);
    WSACleanup();

    return 0;
}

编译:

gcc op_server_win.c -lwsock32 -o opserv
gcc op_client_win.c -lwsock32 -o opclnt

运行结果:

// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opserv 9190
Connect client 1
Connect client 2
Connect client 3

// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opclnt 127.0.0.1 9190
Connected......
Operand count: 2
Operand 1: 24
Operand 2: 12
Operator: -
Operation result: 12

C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opclnt 127.0.0.1 9190
Connected......
Operand count: 3
Operand 1: 12
Operand 2: 24
Operand 3: 36
Operator: +
Operation result: 72

C:\Users\81228\Documents\Program\TCP IP Project\Chapter 5>opclnt 127.0.0.1 9190
Connected......
Operand count: 3
Operand 1: 2
Operand 2: 5
Operand 3: 10
Operator: *
Operation result: 100

习题

(1)请说明TCP套接字连接建立的三次握手过程。尤其是3次数据交换过程每次收发的数据内容。

在这里插入图片描述

初始状态:客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手。

第一次握手:客户端给服务端发一个 SYN 报文段,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SENT 状态。(在SYN报文段中同步位SYN=1,初始序号seq=x)SYN=1的报文段不能携带数据,但要消耗掉一个序号。

第二次握手:服务器收到客户端的 SYN 报文段之后,会以自己的 SYN 报文段作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN(c) + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN报文,此时服务器处于 SYN_RCVD 的状态。(在SYN ACK报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y)

第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN(s) + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。(在ACK报文段中ACK=1,确认号ack=y+1,序号seq=x+1)

(2)TCP是可靠的数据传输协议,但在通过网络通信的过程可能丢失数据。请通过ACK和SEQ说明TCP通过何种机制保证丢失数据的可靠传输。

TCP通过在TCP报文段首部中设置SEQ(序号)和ACK(确认号)字段,就可以知道传输的数据是否正确地被通信对端接收。SEQ表示当前发送的TCP报文段的第一个数据字节的序号,ACK表示期望收到对方下一个报文段的第一个数据字节的序号。当收到某个确认报文段时,若确认号ACK=N,则表明到序号 N-1 为止的所有数据对方都已正确收到。若等待确认报文段超时,则说明传输的数据可能丢失,需要重传。

(3)TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。

一个TCP套接字是有独立地接收缓冲和发送缓存的,它们是操作系统内核区分配的内存空间。当TCP套接字调用write函数时,就是将待发送数据移至TCP的发送缓冲区中,而调用read函数时,就是接收TCP的接收缓冲区中的数据。

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

通过TCP流量控制机制,对方主机会把输入缓冲大小传送给本方主机。因此即使要求传送70字节的数据,本方主机也不会传输超过50字节数据,剩余的部分保存在传输方的输出缓冲中,等待对方主机的输入缓冲有空余空间时再传输剩余数据。

这种交换缓冲区多余空间信息的协议被称为滑动窗口协议。

(5)第2章示例tcp_server.c(第一章的hello_server.c)和tcp_client.c中,客户端接收服务器端传输的字符串后便退出。现更改程序,使服务器端和客户端各传送1次字符串。考虑到使用TCP协议,所以传输字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。

在这里插入图片描述

另外,不限制字符串传输顺序及种类,但必须进行3次数据交换。

(6)创建收发文件的服务器/客户端程序,实现顺序如下。

  • 客户端接收用户输入的传输文件名。
  • 客户端请求服务器传输该文件名所指的文件。
  • 如果该文件存在,服务器端就将其发送给客户端;反之,则断开连接(回复文件不存在的提示信息)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/911015.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

类加载过程详解

类的生命周期 类从被加载到虚拟机内存中开始到卸载出内存为止&#xff0c;它的整个生命周期可以简单概括为 7 个阶段&#xff1a;加载&#xff08;Loading&#xff09;、验证&#xff08;Verification&#xff09;、准备&#xff08;Preparation&#xff09;、解析&#xff08…

03-构建数据中台的三要素:方法论、组织和技术

03-构建数据中台的三要素&#xff1a;方法论、组织和技术 知道要转型&#xff0c;要建设数据中台&#xff0c;却不知咋做&#xff0c;咋办&#xff1f; 现在有很多讲“如何建设数据中台”文章&#xff0c;观点各不相同&#xff1a; 数据中台是数据建设方法论&#xff0c;按照数…

华为Mate70前瞻,鸿蒙NEXT正式版蓄势待发,国产系统迎来关键一战

Mate 70系列要来了 上个月&#xff0c;vivo、小米、OPPO、荣耀等众多智能手机制造商纷纷发布了他们的年度旗舰产品&#xff0c;手机行业内竞争异常激烈。 同时&#xff0c;华为首席执行官余承东在其个人微博上透露&#xff0c;Mate 70系列将标志着华为Mate系列手机达到前所未有…

源代码防泄密管理分享

随着信息技术的快速发展&#xff0c;软件已成为现代企业不可或缺的核心资产之一。然而&#xff0c;源代码作为软件的心脏&#xff0c;其安全性直接关系到企业的核心竞争力。为了有效防止源代码泄露&#xff0c;构建一套全面且高效的源代码安全管理体系显得尤为重要。以下是六个…

从神经元到神经网络:深度学习的进化之旅

神经元、神经网络 神经元 Neuron )&#xff0c;又名感知机( Perceptron )&#xff0c;在模型结构上与 逻辑回归 一致&#xff0c;这里以一个二维输入量的例子对其进行进一步 的解释&#xff1a; 假设模型的输 入向 量是一 维特征向 (x1,x2). 则单神 经元的模型结构 如下…

[C语言]strstr函数的使用和模拟实现

1.strstr函数的使用 char * strstr ( const char *str1, const char * str2); 返回一个指向str1中str2第一次出现的指针&#xff0c;如果str2中没有str1则返回 NULL。。 实例&#xff1a; #include <stdio.h> #include <string.h> int main() {char str[] "…

【论文速读】| RePD:通过基于检索的提示分解过程防御越狱攻击

基本信息 原文标题&#xff1a;RePD: Defending Jailbreak Attack through a Retrieval-based Prompt Decomposition Process 原文作者&#xff1a;Peiran Wang, Xiaogeng Liu, Chaowei Xiao 作者单位&#xff1a;University of Wisconsin–Madison 关键词&#xff1a;越狱…

React 前端通过组件实现 “下载 Excel模板” 和 “上传 Excel 文件读取内容生成对象数组”

文章目录 一、Excel 模板下载01、代码示例 二、Excel 文件上传01、文件展示02、示例代码03、前端样式展示04、数据结果展示 三、完整代码 本文的业务需求是建立在批量导入数据的情况下&#xff0c;普通组件只能少量导入&#xff0c;数据较多的情况都会选择 Excel 数据导入&…

基于YOLOv8 Web的安全帽佩戴识别检测系统的研究和设计,数据集+训练结果+Web源码

摘要 在工地&#xff0c;制造工厂&#xff0c;发电厂等地方&#xff0c;施工人佩戴安全帽能有效降低事故发生概率&#xff0c;在工业制造、发电等领域需要进行施工人员安全帽监测。目前大多数的 YOLO 模型还拘泥于公司、企业开发生产的具体产品中&#xff0c;大多数无编程基础…

内部知识库:优化企业培训流程的关键驱动力

在当今快速变化的商业环境中&#xff0c;企业培训的重要性日益凸显。内部知识库作为整合、管理和分享企业内部学习资源的关键工具&#xff0c;正逐步成为优化企业培训流程的核心。以下将探讨内部知识库如何通过多种功能&#xff0c;助力企业提升培训效率、质量和员工满意度。 …

TapData 发布官方性能测试报告,针对各流行数据源,在多项指标中表现拔群

近日&#xff0c;TapData 官方发布了最新的性能测试报告&#xff0c;该报告详细展示了 TapData v3.5.13 在各种数据源下的性能表现&#xff0c;包括全量同步、增量同步、读写延迟等关键性能指标。 随着企业对实时数据集成和处理能力需求的提升&#xff0c;TapData 凭借其高效、…

JDK1.5 java代码打包jar HmacSha256

文章目录 demo地址背景实现编写代码编译class文件打包 JAR 文件执行生成的 JAR 文件辅助验证方式 常见问题和解决方法常规生成jar方案maven插件idea工具 demo地址 https://github.com/xiangge-zx/HmacSha256 背景 最近接到一个需求,做一个可以用来HmacSha256加密的小工具&am…

【Python TensorFlow】进阶指南

在前文中&#xff0c;我们介绍了TensorFlow的基础知识及其在实际应用中的初步使用。现在&#xff0c;我们将进一步探讨TensorFlow的高级特性&#xff0c;包括模型优化、评估、选择、高级架构设计、模型部署、性能优化等方面的技术细节&#xff0c;帮助读者达到对TensorFlow的精…

Vue实现登录功能

一、Vue登录逻辑梳理&#xff1a; 1、登录流程&#xff1a; 用户在前端输入用户名和密码&#xff0c;点击登录按钮。 登录成功后的逻辑&#xff1a; 主要功能和流程&#xff1a; 异步函数 signInSuccess&#xff1a;这是一个异步函数&#xff0c;使用了 async 关键字&#xff…

「Mac畅玩鸿蒙与硬件26」UI互动应用篇3 - 倒计时和提醒功能实现

本篇将带领你实现一个倒计时和提醒功能的应用&#xff0c;用户可以设置倒计时时间并开始计时。当倒计时结束时&#xff0c;应用会显示提醒。该项目涉及时间控制、状态管理和用户交互&#xff0c;是学习鸿蒙应用开发的绝佳实践项目。 关键词 UI互动应用倒计时器状态管理用户交互…

(62)使用RLS自适应滤波器进行系统辨识的MATLAB仿真

文章目录 前言一、基本概念二、RLS算法原理三、RLS算法的典型应用场景四、MATLAB仿真代码五、仿真结果1.滤波器的输入信号、参考信号、输出信号、误差信号2.对未知系统进行辨识得到的系数 总结与后续 前言 RLS&#xff08;递归最小二乘&#xff09;自适应滤波器是一种用于系统…

Oracle 12C安装教程

Oracle 12c&#xff0c;全称Oracle Database 12c&#xff0c;是Oracle 11g的升级版&#xff0c;新增了很多新的特性。 Oracle 12c下载 打开Oracle的官方中文网站&#xff0c;选择相应的版本即可。 下载地址&#xff1a;http://www.oracle.com/technetwork/cn/database/enterp…

探索空间计算与 VR 设备的未来:4K4DGen 高分辨率全景 4D 内容生成系统

在当今科技飞速发展的时代,空间计算和 VR 设备正逐渐成为人们体验沉浸式场景的重要工具。而今天,我们要为大家介绍一款具有创新性的技术 ——4K4DGen 高分辨率全景 4D 内容生成系统,它为 VR/AR 沉浸式体验带来了全新的可能性。 一、项目概述 4K4DGen 项目的核心目标是实现 …

【无标题】项目管理软件:日常任务管理,TODO任务清单

无论是在工作、学习还是个人事务的处理上&#xff0c;我们都面临着众多的任务和事项。而 TODO 任务管理&#xff0c;可以帮助我们高效、有序的完成工作任务。 TODO 任务管理的重要性&#xff1a; TODO 任务管理不仅仅是简单地列出要做的事情&#xff0c;它是一种系统性的方法…

数据库中的用户管理和权限管理

​ 我们进行数据库操作的地方其实是数据库的客户端&#xff0c;是我们在客户端将操作发送给数据库的服务器&#xff08;MySQL的服务器是mysqld&#xff09;&#xff0c;由数据库处理之后发送回来处理结果&#xff08;其实就是一种网络服务&#xff09;。所以可以存在多个客户端…