TCP/IP网络编程_第2章套接字类型与协议

第2章套接字类型与协议

因为涉及套机字编程的基本内容, 所以第2章和第3章显得相对枯燥一些. 但本章内容是第4章介绍的实际网络编程基础, 希望各位反复精读.

大家已经对套接字的概念有所理解, 本章将讲解套接字创建方法及不同套接字的特性. 在本章仅需了解创建套机字调用的socket 函数, 所以希望大家以轻松的心态开始学习.

2.1 套接字协议及其数据传输特性

“协议” 这个词给人的第一印象总是相当困难, 我在学生时代也是这么想. 但各位要慢慢熟悉"协议", 因为它几乎是网络编程的全部内容. 首先解析其定义.

关于协议(Protocol)
如果相隔很远的两个人想展开对话, 必须先决定对话的方式. 如果一方使用电话, 那么另一方也只能使用电话, 而不是书信. 可以说, 电话就是两个人对话的协议. 协议是对话中使用的通讯规则, 把上述概念扩展到计算机领域可整理为"计算机之间对话必备通信规则".

各位是否已理解了协议的含义? 简而言之, 协议就是为了完成数据交换而定好的约定.

创键套机字
创建套机字所用的socket函数已经在第1章中简单介绍过. 但为了完全理解该函数, 此处将再次展开讨论, 本章的目的也是于此.
在这里插入图片描述
第一章并未提及该函数的参数, 但他们对创建套机字来说是不可或缺的. 下面给出详细说明.

协议族(Protocol Family)
奶油意大利和番茄酱意大利面均属于意大利面的一种, 与之类似, 套机字通信中的协议也具有一些分类. 通过socket 函数的第一个参数传递套机字中使用的协议分类信息. 次协议分类信息称为协议族, 可分成如下几类.
在这里插入图片描述
本书将着重讲解表 2-1 中 PF_INET 对应的IPV4 互联网协议族. 其他协议族并不常用或未普及, 因此本书将重点放在 PF_INTE 协议族上, 另外, 套机字中实际采用的最终协议信息是通过 socket 函数的第三个参数传递的. 指定的协议族范围内通过第一个参数决定第三个参数.

套机字类型(Type)
套机字类型指的是套机字的数据传输方式, 通过socket 函数的第二个参数传递, 只有这样才能决定创建的套机字是数据传输方式. 这种说法可能会使各位感到疑惑. 已通过第一个参数传递了协议信息, 还有决定数据传输方式? 问题就在于, 决定了协议族并不能同时决定数据传输方式, 换言之, socket 含数第一个参数 PF_INET 协议中也存在多种数据传输方式.

下面介绍2种具有代表性的数据传输方式. 这是理解好套接字的重要前提. 请各位务必掌握.

套接字类型 1: 面向连接的套接字(SOCK_STREAM)
如果向socket 函数的第二个参数传递 SOCK_STREAM , 将创建面向连接的套接字. 面向连接的套接字到底具有哪些特点呢? 右图的数据(糖果)传输方式特征整理如下.
在这里插入图片描述
(1) 传输过程中数据不会消息.
(2) 按序传输数据
(3) 传输数据不存在数据边界(Boundary)
图中通过独立的传输数据(糖果), 只要传送带本身没有问题, 就能保证数据不会丢失. 同时, 较晚传递的数据不会先到达, 因为传送带保证了数据的按序传递, 最后, 下面这句话说明的确不存在数据边界:

“100个糖果是分批传递的, 但接收者凑齐100个装袋.”

这种情况可以适用到之前说过的write 和 read 函数.

"传递数据的计算机通过3次调用write 函数传递了 100 字节的数据, 但接收数据的计算机仅通过1次read 函数调用就接收了全部100个字节. "

收发数据的套机字内部有缓存(buffer), 简而之就是字节数组. 通过套机字传输的数据将保存到该数组. 因此, 收到数据并意味着马上调用read 函数. 只要不超过数组容量, 则有可能在数据填充满缓冲后通过1次read 函数调用读取全部, 也有可能分成多次 read 函数调用进行读取. 也就是说, 在面向连接的套机字中, read函数和 write 函数的调用次数并无太大意义. 所以说面向连接的套接字不存在数据边界. 稍后将给出示例以查看该特性.

在这里插入图片描述
之前讲过, 为了接受数据, 套机字内部有一个由字节数组构成的缓冲. 如果这个缓冲被接受的数据填满会发生什么事情? 之后传递的数据是否会丢弃?

首先调用 read 函数从缓冲读取部分数据, 因此, 缓冲并不总是满的. 但如果 read 函数读取速度比接受数据的速度慢, 则缓冲有可能被填满. 此时套接字无法再接受数据, 但即使这样也不会发生数据丢失, 因为传输端套接字将停止传输. 也就是说, 面向连接的套接字会根据接收端的状态传输数据, 如果传输出错还会提供重传服务. 因此, 面向连接的套机字除特殊情况外不会发生数据丢失.

还有一点需要说明. 上面中传输和接收端各有一名工人, 这说明面向连接的套接字还有如下特点:

“套接字连接必须一一对应.”

面向连接的套接字只能与另外一个同样特性的套接字连接. 用一句话概括面向连接的套接字如下:

“可靠的, 按序传递的, 基于字节的面向连接的数据传输方式的套机字”

这是我自己总结, 希望各位深入理解其含义, 不要仅停留于字面表达.

套接字类型2: 面向消息的套机字(SOCK_DGRAM)
如果向 socket 函数的第二个参数传递 SOCK_DGRAM, 则将创建面向消息的套机字. 面向消息的套接字可以比喻成高速移动的摩托车快递. 右图中摩托车快递的包裹(数据)传输方式如下.
(1) 强调快速传输而非传输顺序.
(2) 传输的数据可能丢失也可能损毁.
(3) 传输的数据有数据边界.
(4) 限制每次传输的数据大小.

总所周知, 快递行业的速度就是生命. 用摩托车发往同一目标地的2件包裹无需保证顺序, 只有以最快速度交给客户即可. 这种方式存在损坏或丢失的风险, 而且包裹大小有一定限制. 因此, 若要传递大量包裹, 则需分批发送. 另外用2辆摩托车分别发送2件包裹, 则接收者也需要分2次接收. 这种特性就是"传输的数据具有数据边界".

以上就是面向消息的套接字具有的特性. 即, 面向消息的套机字比面向连接的套接字具有更快的传输速度, 但无法避免数据丢失或损失. 另外, 每次传输的数据大小具有一定限制, 并存在数据边界. 存在数据边界意味着接收次数的和传输次数相同. 面向消息的套接字特性总结如下:

不可靠的, 不按序传递的, 以数据的高速传输为目的的套接字

另外, 面向消息的套接字不存在连接的概念, 这一点将在以后章节介绍.

协议的最终选择
下面讲解 socket 函数的第三个参数, 该参数决定最终采用的协议. 各位是否觉有些困惑?

前面已经通过 socket 函数的前两个参数 传递协议信息套接字数据传递方式, 这些信息还不足以决定采用的协议吗? 为什么还需要第3个参数呢?

正如各位所想, 传递前两个参数即可创建所需套机字. 所以大部分情况下可以向第三个参数传递0, 除非遇到以下情况:

“同一协议族中存在多个数据传输方式相同的协议”

数据传输方式相同, 但协议不同. 此时需要通过第三个参数具体指定协议信息.

下面以前讲解内容为基础, 构建向 socket 函数传递的参数. 首先创建满足如下要求的的套接字:

“IPV4 协议族中面向连接的套接字”

IPV4 与网络地址系统相关, 关于这一点将给出单独说明, 目前: 本书是基于 IPV4 展开的. 参数 PF_INET 值IPV4 网络协议族, SOCKET__STREAM 是面向连接的数据传输. 满足这2条件的协议 只有IPPROTO_TCP, 因此可以如下调用 socket 函数创建套接字, 这种套接字称为TCP 套接字.
在这里插入图片描述
下面创建满足如下求的套接字:

“IPV4协议中面向消息的套接字”

SOCK_DGRAM 指的是面向消息的数据传输方式, 满足上述条件的协议只有 IPPROTO_UDP. 因此, 可以如下调用 socket 函数创建套接字, 这种套接字称为 UDP 套接字.
在这里插入图片描述
前面进行了大量描述以解释着两行代码, 这是为了让大家理解他们创建的套接字的特性.

面向连接的套接字: TCP 套机字实例

其他章节将讲解 UDP 套接字, 此处只给出面向连接的TCP 套机字示例. 本示例是第 1 章的如下2 个源文件基础进行修改的而成的.
在这里插入图片描述
之前的hello_server.c 和 hello_client.c 是基于TCP 套接字的实例. 先调整其中一部分代码, 以验证 TCP 套接字的如下特性:

“传输的数据不存在数据边界.”

为了验证这一点, 需要让write 函数的调用次数不同于read 函数的调用次数. 因此, 在客户端中分多次调用 read 函数以接收服务器端发送的全部数据.

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

void error_handling(const char* message);

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

    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!");
    }

    /* while循环中反复调用read函数, 每次读取1个字节. 如果read返回0, 则循环条件为假, 跳出while循环. */
    while (read_len = read(sock, &message[idx++], 1))
    {
        if (read_len == -1)
        {
            error_handling("read() error!");
        }
        str_len += read_len;
    }

    printf("Message from server: %s \n", message);
    printf("Function read call count : %d \n", str_len);
    close(sock);
    
    return 0;
}

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

运行结果:
服务端:
在这里插入图片描述
客户端:
在这里插入图片描述
与该实例配套使用的服务端tcp_server.c与hello_server.c 完全相同, 故省略源代码. 执行方式也与hello_server.c 和 hello_client.c 相同, 因此只能给出最终运行的结果.

从运行结果可以看出, 服务器端发送了13 字节的数据, 客户端调用13次 read 函数进行读取. 希望各位通过该实例深入理解TCP 套接字的数据传输方式.

2.2 Windows 平台下的实现及验证

前面讲过的套接字类型及传输特性与操作系统无关. Windows 平台下的实现方式也类似, 不需要过多说明, 只需稍加了解 socket 函数返回类型即可.

Windows 操作系统的 socket 函数
Windows 的函数名和参数名都与 Linux 平台相同, 只是返回值类型稍有不同. 再次给出 socket 函数的声明.
在这里插入图片描述
该函数的参数种类及含义与 Linux 的 socket 函数完全相同, 故略过, 只讨论返回值类型. 可以看出返回类型 为 SOCKET , 此结构体用来保存整数型套接字句柄值. 实际上, socket 函数返回整数型数据, 因此可以通过 int 型变量接收, 就像在 Linux 中做的一样. 但考虑到以后的扩展性, 定义 SOCKET 数据类型, 希望各位也使用 SOCKET 结构体变量保存 套接字句柄, 这也是微软希望看到的. 以后即可将 SOCKET 视作保存套接字句柄的一个数据类型.

同样, 发生错误时返回 INVALID_SOCKET , 只需将其理解为提示错误的常数即可. 实际值为-1, 但值是否为-1 并不重要, 除非编写如下代码.
在这里插入图片描述
如果这样编写代码, 那么微软定义的 INVALID_SOCKET 常数将失去意义! 应该如下编写, 这样, 即使日后微软更改 INVALID_SOCKET 常量值, 也不会发生问题.
在这里插入图片描述
这些问题虽然琐碎却非常重要.

基于 Windows 的 TCP 套机字实例
把之前的tcp_server.c, tcp_client.c 如下改为 基于 Windows 的程序.
在这里插入图片描述
与之前一样, 只给出tcp_client_win.c 源代码及运行结果. 各位若想请自查看 tcp_server_win.c 的代码, 可以参考第1章的hello_server_win.c, 或到 OrangeMedia 主页(http://www.orentec.co.kr/)

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

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

	char message[30];
	int strLen = 0;
	int idx = 0, readLen = 0;

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

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

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

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == INVALID_SOCKET)
	{
		ErrorHandling("connect() error!");
	}

	while (readLen = recv(hSocket, &message[idx++], 1, 0))
	{
		if (readLen == -1)
		{
			ErrorHandling("read() error!");
		}
		strLen += readLen;
	}

	printf("Message from server : %s \n", message);
	printf("Function read call count : %d \n", strLen);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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

运行结果:
服务端:
在这里插入图片描述

客户端:
在这里插入图片描述

该实例的运行方式与第1章的hello_server_win.c , hello_client_win.c 相同, 因此只给出客户端的运行结果. 以上就是第2章全部内容, 相信各位对服务器端和客户端有了更深入的理解.

你们可以从: https://www.jiumodiary.com/
下载本书: TCP IP网络编程

2.3 习题

(1) 什么是协议? 在收发数据中定义协议有何意义?

(2) 面向连接的TCP 套接字传输特性有3点, 请分别说明.

(3)

时间: 20200523

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