TCP/IP网络编程_第5章基于TCP的服务器端/客户端(2)

在这里插入图片描述

5.1 回声客户端的完美实现

第4章已分析过回声客户端存在的问题, 此处无再说. 如果大家不太理解, 请复习第2章的 TCP 传输特性和第4章的内容.

回声服务器端没有问题, 只有回声客户端有问题?
问题不在服务器端, 而在客户端. 但只看代码也许不太好理解, 因为I/O中使用了相同的函数. 先回顾一下回声服务器端的I/O 相关代码, 下面是 echo_server 的 50~51行代码.
在这里插入图片描述
接下来回顾回声服务器客户端代码, 下面是echo_client.c 的第45~46行代码.
在这里插入图片描述
二者都在循环调用read 或 write 函数. 实际上之前的回声客户端将 100% 接收自己传输的数据, 只不过接收数据时的单位有些问题. 扩展客户端代码回顾范围, 下面是echo_client.c 第37 行开始的代码.
在这里插入图片描述
大家现在理解了吧? 回声客户端传输的是字符串, 而且是通过调用write函数一次性发送的. 之后还调用一次read 函数, 期待着接收自己传输的字符串. 这就是问题所在.
在这里插入图片描述
的确, 过一段时间后立即可接收, 但需要等多久? 等10分钟吗? 这不符合常理, 理想的客户端应在收到字符串数据时立即读取并输出.

回声客户端问题解决方法

我说的回答客户端问题实际上初级程序员经常犯的错误, 其实很容易解决, 因为可以提前确定接收数据的大小. 若之前传输了20字节长的字符串, 则在接收时循环调用read 函数读取20个字节即可. 既然有了解决方法, 接下来给出其代码.

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

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

代码第46行循环可以写成如下形式, 可能这种方式更容易理解.
在这里插入图片描述
接收的数据大小应和传输的相同, 因此, recv_len 中保存的值等于str_len 中的值时, 即可跳出 while 循环. 也许各位认为这种循环更符合逻辑, 但有可能发生无限循环. 假设发生异常情况, 读取数据过程中 recv_len 超过 str_len, 此时就无法退出循环. 而如果 while 循环写成下面这种形式, 则即使异常也不会陷入无限循环.
在这里插入图片描述
写循环语句时应尽量降低因异常情况而陷入无限循环的可能. 以上实例可以结合第4章的echo_server.c 运行. 各位已经非常熟悉运行结果,

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

回声客户端可以提前知道接收接收的数据长度, 但我们应该意识到, 更多情况下这不太可能. 既然如此, 若无法预知接收数据长度时应如何收发数据? 此时需要的就是应用层协议的定义. 之前的回声服务器端/客户端中定义了如下协议.
在这里插入图片描述
同样, 收到数据过程中也需要定好规则(协议)以表示数据的边界, 或提前告知收发数据的大小. 服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议. 可以看出, 应用层协议并不是高深莫测的存在, 只不过是为特定程序的实现而制定的规则.

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

请各位根据以上要求编写服务器端/客户端, 细节部分可以自定义. 我实现的程序运行结果如下. 先给出服务器运行结果.

在这里插入图片描述
可以看出, 服务器端的运行结果并没有特别区别. 可以通过如下客户端运行结果了解程序运行原理.
在这里插入图片描述
从运行结果可以看出, 客户端首先询问用户待算数字的个数, 再输入相应个数的整数, 最后以运算符的形式输入运算符号信息, 并输出运算结果(+ - * ). 当然, 实际的运算时由服务器端做的, 客户端只输出结果. 为了更准确地理解, 再给出1个客户端运行结果. 这次是请求2次个数的减法运算.
在这里插入图片描述
运行结果并不一定要一样.

我编写程序前设计了如下应用层序协议, 但这只是为实现程序而设计的最低协议, 实际的应用程序中需要的协议更详细, 精确.
在这里插入图片描述
这种程度协议相当于实现了一半程序, 这也是应用层协议设计在网络编程中的重要性. 只要设计好协议, 实现就不会成为大问题, 另外, 之前也讲过, 调用close函数将向对方传递EOF, 请各位记住这一点并加以运用. 接下来给出我实现的计算器客户端代码. 实际上, 与服务器端相比, 客户端中有更多需要学习的内容.

客户端
to do....

客户端实现的讲解到此结束, 最后给出客户端向服务器端传输的数据结构实例, 如图5-1所示.
在这里插入图片描述
从图5-1中可以看出, 若想在同一数组中保存并传输多种数据类型, 应把数组声明为char类型. 而且需要额外做一些指针及数组运算. 接下来给出服务器端代码.

服务器端
to do ...

TCP 套接字中的 I/O 缓冲

如前所述, TCP 套接字的数据收发无边界. 服务器端即使调用1次write函数传输40字节的数据, 客户端也有可能通过4次read 函数调用读取10字节. 但此处也有一次疑问, 服务器端一次传输了 40 字节, 而客户端居然可以缓慢地分批接收. 客户端接收10 字节后, 剩下的30在何处等待呢?

实际上, write 函数调用后并非立即传输数据, read 函数调用后也并非马上接收数据. 更准确地说, 如图5-2所示, write函数调用瞬间, 数据将移至输出缓存; read 函数调用瞬间, 从缓存读取数据.
在这里插入图片描述
如图5-2所示, 调用write 函数时, 数据将移到输出缓冲, 在适当的时候(不管是分别传送还是一次性传送)传向对方的输入缓冲. 这时对方将调用read 函数从输入缓冲读取数据. 这些 I/O 缓冲特性如下.
在这里插入图片描述
那么, 下面这种情况会引发什么事情? 理解了I/哦缓冲后, 各位应该猜出其流程:
在这里插入图片描述
这的确是个问题. 输入缓冲区只有50个字节, 却受到100字节的数据, 可以提出如下解决方案:
在这里插入图片描述
当然, 这只是我的一个小玩笑, 相信大家会当真, 那么马上给出结论:
在这里插入图片描述
也就是说, 根本不会发生着类问题, 因为 TCP 会控制数据流. TCP 中有滑动窗口(Sliding Window) 协议, 用对话方式呈现如下.
在这里插入图片描述
数据收发也是如此, 因此 TCP 中不会因为缓存溢出而丢失数据.
在这里插入图片描述

TCP 内部工作原理1: 与对方套接字的连接

TCP 套接字从创建到消失所经过程分为如下3步.
在这里插入图片描述
首先讲解与对方套接字建立连接的过程. 连接过程中套接字之间的对话如下.
在这里插入图片描述
TCP 在实际通信过程中也会经过3次对话过程, 因此, 该过程又称Three-way handshaking(三次握手). 接下来给出过程中实际交换的信息格式, 如图5-3所示.
在这里插入图片描述
套接字是以全双工(Full-duplex)方式工作的, 也就是说, 它可以双向传递数据. 因此, 发数据前需要做一些准备. 首先, 请求连接的主机A向主机B传递如下信息:
在这里插入图片描述
该消息中SEQ为1000, ACK为空, 而SEQ为1000的含义如下:
在这里插入图片描述
这是首次请求连接时使用的消息, 又称 SYN. SYN是Synchronization 的简写, 表示收发数据前传输的同步消息. 接下来主机B向A传递如下消息:
在这里插入图片描述
此时SEQ为2000, ACK为1001, 而SEQ为2000的含义如下:
在这里插入图片描述
而ACK 1001的含义如下:
在这里插入图片描述
对主机A首次传输的数据包确认消息(ACK 1001)和为主机B传输数据做准备的同步消息(SEQ 2000)绑定发送, 因此, 此种类型的消息又称SYN+ACK.

收发数据前向数据包分配序号, 并向对方通报此序号, 这都是为防止数据丢失所做的准备. 通过向数据包分配序号并确认, 可以在数据丢失时马上查看丢失的数据包. 因此, TCP可以保证可靠的数据传输. 最后观察主机A向主机B传输的消息:
在这里插入图片描述
之前也讨论过, TCP 连接过程中发送数据包时需分配序号. 在之前的序号1000的基础上加1, 也就是分配1001. 此时该数据包传递如下消息:
在这里插入图片描述
这样就传输了添加ACK 2001 的ACK消息. 至此, 主机A和主机B确认了彼此均就绪.

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

通过第一步三次握手过程完成了数据交换准备, 下面就正式开始收发数据, 其默认方式如图5-4 所示.
在这里插入图片描述
图5-4给出了主机A分2次(分2次数据包)向主机B传递200字节的过程. 首先, 主机A通过1个数据包发送100个字节的数据, 数据包的SEQ为1200. 主机B为确认这一点, 向主机A发送ACK1301消息

此时的ACK号为1301而非1201, 原因在于ACK号的增量为传输的数据字节数. 假设每次ACK号不加传输的字节数, 这样虽然可以确认数据包的传输, 但无法明确100字节都正确传递还是丢失一部分, 比如只传递了80字节. 因此按如下公式传递ACK消息:
在这里插入图片描述
与三次握手协议相同, 最后加1 是为了告知对方下次要传递的SEQ号. 下面分析传输过程中数据包消失的情况, 如图5-5所示.
在这里插入图片描述
图5-5表示通过SEQ 1301数据包向主机B传输100字节数据. 但中间发生了错误, 主机B未收到. 经过一段时间后, 主机A仍未收到对应 1301的ACK确认, 因此试着重传该数据包. 为了完成数据包重传, TCP 套接字启动计时器以等待ACK 应答. 若相应计时器超时(Time-out!)则重传.

TCP 的内部工作原理 3: 断开与套接字的连接

TCP 套接字的结束过程也非常优雅. 如果对方还有数据需要传输时直接断掉连接出问题, 所以断开连接时需要双方协商. 断开连接时双方对话如下.
在这里插入图片描述
先由套接字A向套接字B传递断开的消息, 套接字B发出确认的消息, 然后向套接字A传递可以断开连接的消息, 然后向套接字A传递可以断开连接的消息, 套接字A同样发出确认消息, 如图5-6 所示.
在这里插入图片描述
图5-6 数据包内的 FIN 表示断开连接, 也就是说, 双方各发送1次 FIN 消息后断开连接. 此过程经历 4 阶段, 因此又称四次握手(Four-way handshaking). SEQ和ACK的含义与之前讲解的内容一致, 故省略, 图5-6中主机A传递了两次 ACK 5001, 也许这会样各位感到困惑. 其实, 第二次FIN 数据包中的ACK 5001 只是因为接收ACK 消息后未接收数据而重传的.

前面讲解了TCP 协议基本内容TCP流控制(Flow Control), 希望这有助于大家理解TCP 数据传输特性.

结语:

时间: 2020-05-29

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值