当前位置: 首页 > article >正文

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

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

http://www.kler.cn/a/384300.html

相关文章:

  • ASAN ThreadSanitizer定位多线程(资源管理)
  • Day 52 || 739. 每日温度 、 496.下一个更大元素 I 、503.下一个更大元素II
  • 【Python】强大的正则表达式工具:re模块详解与应用
  • 超级大项目招标:1000台AGV,12月13日截至
  • 京准时钟:无人机卫星信号安全防护隔离装置
  • k8s图形化显示(KRM)
  • Nextjs14记录
  • 文件系统和日志管理 附实验:远程访问第一台虚拟机日志
  • Java:网络原理-TCP/IP
  • TARE-PLANNER学习记录
  • Chat GPT英文学术写作指令
  • HTML第二次作业
  • 力姆泰克电动缸助力农业机械装备,提高农机的自动化水平
  • ubuntu 22.04 硬件配置 查看 显卡
  • 轻型民用无人驾驶航空器安全操控------理论考试多旋翼部分笔记
  • 【C/C++】strncpy函数的模拟实现
  • 科技查新在人工智能领域的重要性
  • php扩展安装
  • Zookeeper 简介 | 特点 | 数据存储
  • spring boot 难点解析及使用spring boot时的注意事项
  • 原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
  • 2024 开源社年度评选
  • sql server 文件备份恢复
  • 论文速读:简化目标检测的无源域适应-有效的自我训练策略和性能洞察(ECCV2024)
  • 浏览器内置对象XMLHttpRequest
  • 写了个建表语句 review 的 prompt