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

news2024/11/28 9:22:11

《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)创建收发文件的服务器/客户端程序,实现顺序如下。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用 FFmpeg 进行音视频转换的相关命令行参数解释

FFmpeg 是一个强大的多媒体框架&#xff0c;能够解码、编码、转码、录制、播放以及流化几乎所有类型的音频和视频。它广泛应用于音视频处理任务中&#xff0c;包括格式转换、剪辑、合并、水印添加等。本文中简鹿办公将介绍如何使用 FFmpeg 进行一些常见的音视频转换任务。 安装…

ctfshow(316)--XSS漏洞--反射性XSS

Web316 进入界面&#xff1a; 审计 显示是关于反射性XSS的题目。 思路 首先想到利用XSS平台解题&#xff0c;看其他师傅的wp提示flag是在cookie中。 当前页面的cookie是flagyou%20are%20not%20admin%20no%20flag。 但是这里我使用XSS平台&#xff0c;显示的cookie还是这样…

从0开始学习Linux——网络配置

往期目录&#xff1a; 从0开始学习Linux——简介&安装 从0开始学习Linux——搭建属于自己的Linux虚拟机 从0开始学习Linux——文本编辑器 从0开始学习Linux——Yum工具 从0开始学习Linux——远程连接工具 从0开始学习Linux——文件目录 上一个教程中&#xff0c;我们了解了…

python在word中插入图片

本文讲解python如何在word文档中插入图片&#xff0c;以及指定插入图片的段落。 1、在新建的word文档中插入图片 import win32com.client as win32 from win32com.client import constants # 1&#xff09;打开word应用程序 doc_app win32.gencache.EnsureDispatch(Word.App…

亚信安全新一代WAF:抵御勒索攻击的坚固防线

近年来&#xff0c;勒索攻击已成为黑客的主要攻击手段。新型勒索攻击事件层出不穷&#xff0c;勒索攻击形势愈发严峻&#xff0c;已经对全球制造、金融、能源、医疗、政府组织等关键领域造成严重危害。如今&#xff0c;勒索攻击手段日趋成熟、攻击目标愈发明确&#xff0c;模式…

Linux qt下是使用搜狗輸入發

1.下载一个编译好的包 https://github.com/sixsixQAQ/fcitx5-qt 出处&#xff1a;这里 2.根据QT5&#xff0c;或者QT6选择下载 3.使用 把那个libfcitx5platforminputcontextplugin.so放到下面的路径&#xff1a; <你的Qt安装目录>/gcc_64/plugins/platforminputcontex…

linux命令详解,账号相关

账号相关 用户账号数据库相关文件 /etc/password 存储系统中所有用户账户的基本信息 /etc/shadow 用于存储用户账户的密码和其他安全相关信息 /etc/gshdow 用于存储用户组的密码和其他安全相关信息/etc/passwd: username:password:UID:GID:GECOS:home_directory:shell字段解…

Sentinel — 微服务保护

微服务架构将大型应用程序拆分为多个小而独立的服务&#xff0c;每个服务可以独立部署和扩展。然而&#xff0c;微服务系统需要面对的挑战也随之增加&#xff0c;例如服务之间的依赖、分布式环境下的故障传播和安全问题。因此&#xff0c;微服务保护措施是确保系统在高并发、资…

使用Qt制作一个流程变更申请流程进度以及未读消息提醒

1.1加载界面&#xff1a; 界面要素&#xff1a; 成员信息 变更位置申请 接受消息列表 根据角色加载对应界面。 1.2发起变更申请&#xff1a; 用户点击“发起变更申请”按钮。变更申请对话框可编辑&#xff0c;用户填写申请信息&#xff1a; 申请方&#xff08;自动填充&…

Markdown 全面教程:从基础到高级

Markdown 全面教程&#xff1a;从基础到高级 Markdown 是一种轻量级的标记语言&#xff0c;它的设计目标是使书写和阅读文档变得简单而直观。无论是撰写博客、编写文档还是创建 README 文件&#xff0c;Markdown 都是一个非常实用的工具。 目录 Markdown 简介Markdown 的基…

无插件H5播放器EasyPlayer.js关于硬解码和软解码的详细介绍

在当今这个多媒体内容日益丰富的时代&#xff0c;视频播放体验的重要性不言而喻.EasyPlayer.js H5播放器作为一款专为现代Web环境设计的播放器&#xff0c;它不仅提供了流畅的播放体验&#xff0c;还特别注重性能优化。EasyPlayer.js支持多种解码方式&#xff0c;包括硬解码和软…

Multi‐modal knowledge graph inference via media convergenceand logic rule

摘要 媒体融合通过处理来自不同模式的信息并将其应用于不同的领域来实现。传统的知识图很难利用多媒体特征&#xff0c;因为从其他模态引入大量信息降低了表示学习的有效性&#xff0c;并降低了知识图推理的有效性。为了解决这一问题&#xff0c;提出了一种基于媒体融合和规则…

大模型应用编排工具Dify二开之工具和模型页面改造

1.前言 简要介绍下 dify&#xff1a; ​ 一款可以对接市面上主流大模型的任务编排工具&#xff0c;可以通过拖拽形式进行编排形成解决某些业务场景的大模型应用。 背景信息&#xff1a; ​ 环境&#xff1a;dify-0.8.3、docker-21 ​ 最近笔者在做 dify的私有化部署和二次…

【数学】通用三阶矩阵特征向量的快速求法 超简单!!!

目录 三个定理1、3个特征值&#xff08;即根互不相等&#xff09;例题实践2、2个特征值&#xff08;即有一个双重根&#xff09;3、1个特征值&#xff08;即有一个三重根&#xff09;定理证明 三个定理 本定理适用于 所有三阶矩阵 的特征向量求法&#xff01; 1、3个特征值&…

MapReduce 的 Shuffle 过程

MapReduce 的 Shuffle 过程指的是 MapTask 的后半程&#xff0c;以及ReduceTask的前半程&#xff0c;共同组成的。 从 MapTask 中的 map 方法结束&#xff0c;到 ReduceTask 中的 reduce 方法开始&#xff0c;这个中间的部分就是Shuffle。是MapReduce的核心&#xff0c;心脏。 …

【WebRTC】视频采集模块中各个类的简单分析

目录 1.视频采集模块中的类1.1 视频采集基础模块&#xff08;VideoCaptureModule&#xff09;1.2 视频采集工厂类&#xff08;VideoCaptureFactory&#xff09;1.3 设备信息的实现&#xff08;DeviceInfoImpl&#xff09;1.4 视频采集的实现&#xff08;VideoCaptureImpl&#…

江协科技STM32学习- P40 硬件SPI读写W25Q64

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

智慧场馆:安全、节能与智能化管理的未来

在当今社会&#xff0c;智慧场馆已经成为了现代场馆建设的一种重要模式。通过整合先进技术和智能系统&#xff0c;智慧场馆致力于提供全方位的解决方案&#xff0c;以实现场馆的安全性、节能性和智能化管理。本文将深入探讨智慧场馆如何实现安全、节能和全面智能化&#xff0c;…

Facebook与人工智能:推动社交媒体发展的新动力

在数字化时代的浪潮中&#xff0c;社交媒体已成为人们日常生活不可或缺的一部分。作为全球最大的社交平台之一&#xff0c;Facebook凭借其庞大的用户基础和先进的技术&#xff0c;正积极探索与人工智能&#xff08;AI&#xff09;的结合&#xff0c;以推动社交媒体的不断发展。…

【论文复现】自动化细胞核分割与特征分析

本文所涉及所有资源均在这里可获取。 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; 论文复现 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; 自动化细胞核分割与特征分析 引言效果展示HoverNet概述HoverNet原理分析整…