TCP/IP网络编程_第4章基于TCP的服务器端/客户端(1) 4.1-4.2

在这里插入图片描述

4.1 理解 TCP 和 DUP

根据数据传输方式的不同, 基于网络协议的套接字一般分为 TCP 套接字和 UDP 套接字. 因为 TCP 套接字是面向连接的, 因此又基于流(stream) 的套接字.

TCP 是 (传输控制协议)的简写, 意为"对数据传输过程的控制". 因此, 学习控制方法及范围有助于正确理解 TCP 套接字.

TCP/IP 协议栈

讲解TCP前先介绍 TCP 所属的 TCP/IP 协议栈(Stack, 层), 如图4-1所示.
在这里插入图片描述
从图4-1可以看出, TCP/IP 协议栈共分4层, 可以理解为数据收发分成了 4 个层次过程. 也就是说, 面对 “基于互联网的有效数据传输” 的命题, 并非通过1个庞大协议解决问题, 而是化整数为零, 通过层次化方案-- TCP/IP协议栈解决. 通过 TCP 套接字收发数据时需要借助这4层, 如图4-2所示.
在这里插入图片描述
反之, 通过 UDP 套接字 收发数据时, 利用图4-3中的层协议栈完成.
在这里插入图片描述
各层可能通过操作系统等软件实现, 也可能通过类型 NIC 的硬件设备实现.
在这里插入图片描述
TCP/IP 协议的诞生背景
“通过因特网完成有效数据传输” 这个课题让很多专家聚集到了一起, 这些人是硬件、系统、路由算法等各领域的顶尖专家. 为何需要这么多领域的专家呢?

我们之前只关注套接字创建及应用, 却忽略了计算机网络问题并非仅凭软件就能解决. 编写软件前需要构建硬件系统, 在此基础是上需要通过软件实现各种算法. 所以才需要众多领域的专家进行讨论, 形成各种规定. 因此, 把这个大问题划分若干个小问题再逐个攻破, 将大幅度提高效率.

把"通过因特网完成有效数据传输" 问题按照不同领域划分成小问题后, 出现多种协议, 他们通过层级结构建立了紧密联系.

在这里插入图片描述
把协议分成多个层次具有哪些优点? 协议设计更容易? 当然这也足以成为优点之一. 但还有更重要的原因是, 为了通过标准化操作设计开发式系统.

标准本身就在于对外公开, 引导更多的人遵守规则. 以多个标准为依据设计的系统称为开发式系统, 我们现在学习的 TCP/IP 协议 栈也属于其中之一. 接下来了解一下开发式系统具有哪些优点. 路由器用来完成 IP 层交互任务. 某公司使用 A 公司的路由器, 现要将其替换成 B 公司的路由器, 是否可行? 这并非难事, 并不一定要换成同一公司的同一型号路由器, 因为所有生产商都会按 IP 层标准制造.

再举个例子. 各位的计算机是否装有网络接口卡, 也就是所谓的网卡? 尚未安装也无妨, 其实很容易买到, 因为所有网卡制造商都会遵守链路层的协议标准. 这就是开放式系统的优点.

链路层

接下来逐层了解 TCP/IP 协议栈, 先讲解链路层. 链路层是物理链接领域标准化的结果, 也是最基本的领域, 专门定义 LAN WAN MAN 等网络标准. 若两台主机通过网络进行数据交互, 则需要图 4-4 所示的物理连接, 链路层就负责这些标准.
在这里插入图片描述

IP 层

准备好物理连接后就要传输数据. 为了在复杂的网络中传输数据, 首先需要考虑路径的选择. 向目标传输数据需要经过那条路径? 解决此问题就是 IP 层, 该层使用的协议就是 IP.

IP 本身是面向消息的, 不可靠的协议. 每次传输数据时都会帮助我们选择路径, 但并不一致. 如果传输中发生路径错误, 则选择其他路径; 但如果发生数据丢失或错误, 则无法解决. 换言之. IP 协议无法应对数据错误.

TCP/UDP 层

IP 层解决数据传输中路径选择问题, 只需按照路径传输数据即可. TCP 和 UDP 层以IP层提供的路径信息为基础完成实际的数据传输, 故该层又称传输层(Transport). UDP 比 TCP 简单, 我们将在后续展开讨论, 现只解释TCP 可以保证可靠的数据传输, 但它发送数据时以IP 层为基础(这也是协议栈结构层次化的原因). 那么该如何理解二者关系呢?

IP层只关注1个数据包(数据传输的单位)的传输过程. 因此, 即使传输多个数据包, 每个数据包也是由IP层实际传输的, 也就是说传输顺序机传输本身是不可靠的. 若只利用IP层传输数据, 则有可能导致后传输的数据包B比先传输的数据包A提早到达. 另外, 传输的数据包A, B, C 中可能只收到 A和C , 甚至收到的C可能已损毁. 反之, 若添加TCP协议则按照如下对话方式进行数据交换.
在这里插入图片描述
这就是TCP 的作用. 如果数据交换过程中可以确认对方已收到数据, 并重传丢失的数据, 那么即使IP层不保证数据传输, 这类通信也是可靠的, 如图4-5所示.
在这里插入图片描述
图 4-5简单描述了 TCP 的功能. 总之, TCP 和 UDP 存在于IP 层之上, 决定主机之间的数据传输方式, TCP 协议确认后向不可靠的IP 协议赋予可靠性.

应用层

上述内容是套接字通信过程中自动处理的. 选择数据传输路径、数据确认过程都被隐藏到套接字内部. 而与其说是"隐藏", 到不如"使程序员从这些细节中解放出来"的表达更为精确. 程序编程时无需考虑这些过程, 但这并不意味着不用掌握这些知识. 只有掌握了这些理论, 才能编写出符合需求的网络程序.

总之, 向各为提供的工具就是套接字, 大家只需利用套机字编出程序即可. 编写软件的过程中, 需要根据程序特点决定服务器端和客户端之间的数据传输规则(规定), 这便是应用层协议. 网络编程的大部分内容就是设计并实现应用层协议.

4.2 实现基于 TCP 的服务器端/客户端

本节实现完整的 TCP 服务器端, 在此过程中各位将理解套接字使用方法及数据传输方法.

TCP 服务器端的默认函数调用顺序
图 4-6 给出了 TCP 服务器端默认的函数调用顺序, 绝大部分 TCP 服务器端安装该顺序调用.
在这里插入图片描述
调用socket 函数创建套接字, 声明并初始化地址信息结构体变量, 调用bind 函数向套接字分配地址. 这2个阶段之前都已讨论过, 下面讲解之后的几个过程.

进入等待连接请求状态
我们已调用bind函数给套接字分配了地址, 接下来就要通过调用 listen 函数进入等待连接请求状态. 只有调用了 listen 函数, 客户端才能进入可发出连接请求的状态. 换言之, 这时客户端才能调用connect 函数(若提起调用将发生错误).
在这里插入图片描述
先解析一下等待连接请求状态的含义和连接请求等待队列. “服务器端处于等待连接请求状态” 是指, 客户端请求连接时, 受理连接前一直使请求处于等待状态. 图 4-7 给出了这个过程.
在这里插入图片描述
由图4-7可知作为listen函数的第一个参数传递的文件描述符套接字的用途. 客户端连接请求本身也是从网络中接受到的一种数据, 而要想接收就需要套机字. 此任务就由服务器端套接字完成. 服务器端套接字是接收连接请求的一名门卫或一扇门.

客户端如果向服务器端询问:"请问我是否可以发起连接? ""服务器端套接字就会亲切应答: “你好! 当然可以, 但系统正忙, 请到等待室排号等待, 准备好后会立即受理你的连接” 同时将连接请求请到等候室. 调用listen 函数即可生成这种门卫(服务器端套接字), listen函数的第二个参数决定等候室的大小. 等候室称为连接请求等待队列, 准备好服务器端套接字和连接请求等待队列后, 这种课接收请求的状态称为等待连接状态.

listen 函数的第二个参数值与服务器的特性有关, 像频繁接收请求的 Web 服务器端至少应为 15. 另外, 连接请求队列的大小始终根据实验结果而定.

受理客户端连接请求
调用 listen 函数后, 若有新的连接请求, 则应按序受理. 受理请求意味着进入可接受数据的状态. 也许各位已经猜到进入这种状态所需部件-- 当然是套接字! 大家可能认为可以使用服务器端套接字, 但服务器端套接字是做门卫的. 如果在与客户端的数据交换中使用门卫, 那谁来守门呢? 因此需要另外一个套机字, 但不必亲自创建. 下面这个函数将自动创建套接字, 并连接到发起请求的客户端.
在这里插入图片描述
accept 函数受理连接请求等待队列中待处理的客户端连接请求. 函数调用成功时, accept 函数内部将产生用于数据 I/O 的套接字, 并返回文件描述符. 需要强调的是, 套接字是自动创建的, 并自动与发起连接请求的客户端建立连接. 图 4-8 展示了 accept 函数调用过程.
在这里插入图片描述
图4-8 展示了"从等待队列中取出1个连接请求, 创建套接字并完成连接请求"的过程. 服务器端单独创建的套机字与客户端建立连接后进行数据交换.

回顾 Hello word 服务器端

/* 20200521 tcpip网络编程 */

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

void error_handling(char* message);

int main(int argc, char *argv[])
{
    int serv_sock;
    int clnt_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

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

    /* 服务器端实现过程中要创建套接字. 第29行创建套接字, 但此时的套接字尚非真正的服务器端套接字 */
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }

    /* 36-42 为了完成套接字地址分配, 初始化结构体变量并用bind函数 */
    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");
    }

    /* 调用listen函数进入等待连接请求状态. 连接请求等待队列设置为5. 此时的套接字才是服务器端套接字 */
    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    clnt_addr_size = sizeof(clnt_addr);
    /* 调用accept函数从队列取一个连接请求也客户端建立连接, 并返回创建的套接字文件描述符. 
    另外, 调用accept函数时若等待队列为空, 则accept函数不会返回, 直到队列中出现新的客户端连接 */
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
    {
        error_handling("accept() error");
    }

    /* 调用write函数向客户端传输数据. 调用close函数关闭连接 */
    write(clnt_sock, message, sizeof(message));

    close(clnt_sock);
    close(serv_sock);

    return 0;
}

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

TCP 客户端的默认函数调用顺序

接下来讲解客户端实现顺序. 如前面所述, 这要比服务器端简单许多. 因为创建套接字和请求连接就是客户端的全部内容, 如图 4-9 所示.
在这里插入图片描述
与服务器端相比, 区别就在于"请求连接", 它是创建客户端套接字后向服务器端发起的连接请求. 服务器端调用listen函数后创建连接请求等待队列 之后客户端即可请求连接. 那如何发起连接请求呢? 通过调用如下函数完成.
在这里插入图片描述
客户端调用connect 函数后, 发生以下情况之一才会返回(完成函数调用).
在这里插入图片描述
需要注意, 所谓的"接收连接"并不是意味着服务器端调用accept 函数, 其实是服务器端把连接请求信息记录到等待队列. 因此 conncet 函数返回后并不立即进行数据交换.

在这里插入图片描述
实现服务器端必须经过程之一就是给套接字分配IP 和端口号. 但客户端实现过程中并未出现套接字地址分配, 而是创建套接字后立即调用connect 函数. 难道客户端套接字无需分配IP和端口号? 当然不是! 网络数据交换必须分配IP和端口. 既然如此, 那客户端套接字何时, 何地, 如何分配地址呢?
在这里插入图片描述
客户端的IP地址和端口号在调用connect 函数是自动分配, 无需调用标记的bind函数进行分配.

回顾 Hello word 客户端

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

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

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

    /* 创建准备连接服务器端的套接字,此时创建的是TCP套接字 */
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error");
    }

    /* 结构体变量serv_addr中初始化IP和端口号信息. 初始化值为目标服务器端套接字的IP和端口信息 */
    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]));

    /* 调用connect函数向服务器端发送连接请求 */
    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
    {
        error_handling("connect() error!");
    }

    /* 完成连接后, 接收服务器端传输的数据 */
    str_len = read(sock, message, sizeof(message));
    if (str_len == -1)
    {
        error_handling("reand() error!");
    }

    printf("Message from server : %s \n", message);

    /* 接收数据后调用close函数关闭套接字, 结束与服务器端的连接 */
    close(sock);

    return 0;
}

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

基于 TCP 的服务器端/客户端函数调用关系

前面讲解了 TCP 服务器端/客户端的实现顺序, 实际上二者并非相互独立, 各位应该可以勾勒出它们之间的交互过程, 如图 4-10 所示. 之前详细讨论过
在这里插入图片描述
图 4-10 的总体流程整理如下: 服务器创建套接字后连续调用bind, listen函数进入等待状态, 客户端通过调用connect 函数发起连接请求. 需要注意的是, 客户端只能等到服务器端调用listen 函数后才能调用connect函数. 同时要清楚在调用accept 函数时进入阻塞(blocking) 状态, 直到客户端调用 connect 函数为止.

结语:

你可以在下面网站下载这本书:
https://www.jiumodiary.com/

时间2020:05:26

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章