TCP/IP网络编程(二)

时间:2022-12-13 22:58:57

本文将介绍在计算机网络编程中如何协议和地址进行通讯。
对于计算机网络而言,协议是一个绕不开的东西。我们需要定义好各种各样的协议来适应不同的通信环境。因此在进行网络编程的时候,我们需要和对方互相协商好,我们使用什么协议来进行通讯。

回到我们一开始所创建的socket上,对于上一节所介绍的socket函数,他的几个参数便分别说明了该socket的协议类型

int socket(int domain, int type, int protocol)
// domain socket中使用的协议族(Protocol Family)信息
// type socket数据传输类型信息
// protocol 计算机之间使用的协议信息

下面一一进行详解。

协议族(Protocol Family)

协议族就类似于一个大的概念,它将网络协议分为了几个大类,如下所示

名称 协议族
PF_INET IPv4互联网协议
PF_INET6 IPv6互联网协议
PF_LOCAL 本地同学的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell协议族

我们将重点放置于PF_INET协议族上,对于其他的协议并不做深究。对于socket使用的最终协议是通过第三个参数指定的。在指定的协议族范围内通过第一个参数决定第三个参数的选择范围。

socket类型

对于第一个参数决定的协议族下,存在有许多种数据传输方式。下面我们将了解最常用的两种传输方式。

面向连接的套接字(SOCK_STREAM)

这里面向连接的套接字就是我们常说的TCP协议。在网络编程中,使用SOCK_STREAM传输类型的话,系统将会维护一个套接字缓冲区,我们可以调用read函数,多次从中读取数据。
一旦出现缓冲区满了的情况,此时套接字不会再接收数据的,也就是说,使用该种类型的套接字,能够保证在及其特殊的情况以外,不会有数据的丢失出错。

面向消息的套接字(SOCK_DGRAM)

如果要求可靠,那么速度就一定会受影响。因此在对可靠性要求并不那么高,但是对速度有一定要求的场合下,我们需要一种更快的传输类型。此时我们可以采用面向消息的套接字,也就是我们常说UDP类型。

最终的协议选择

第三个参数决定了该socket使用的互联网协议,前两个参数的目的都是在缩小协议范围。
实际上通过前两个协议就可以确定所需的套接字类型了。除非存在以下情况

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

下面我们来创建常用的两个连接
在IPv4协议族中面向连接的套接字
在上述的两个协议要求下,符合条件的协议只有IPPROTO_TCP协议,这种套接字称为TCP套接字

int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)

在IPv4协议族中面向消息的套接字
在上述的协议要求下,同样只有IPPROTO_UDP协议满足要求,这种套接字称为UDP套接字。通过下面的参数建立该套接字

int udp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_UDP)

TCP缓冲区说明

如果我们使用的是TCP套接字,那么系统会自动维护一个缓冲区,修改我们的客户端代码,用于验证套接字类型

int idx = 0;
int read_len = 0;
int str_len = 0;
while(read_len = read(sock, &message[idx++], 1)){
if (read_len == -1)
error_handling("Read Error");
str_len += read_len;
}
print ("Message is %s.\n", message);
print ("The Length Of Message Is %d.\n", str_len);

通过上面的调用方式,我们一次读取一个字节的数据,可以发现系统会反反复复的读取缓冲区的数据,直到其为空。

地址和端口

这一块相关的内容是计网的内容,此处不再复述。
对于我们的套接字,一定得地址和端口,才能够在网络上正常的进行通信。对于socket的地址和端口的表示,我们采用结构体的方式来进行绑定。
表示IPv4的结构体定义如下

struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port; //16位的TCP/UDP端口
struct int_addr sin_addr; //32位的IP地址
char sin_zero[8]; //不使用,用于填充位置
};
//其中in_addr的定义如下
struct in_addr{
In_addr_t s_addr //32位的IPv4地址
}

对于上述定义中的数据类型,详见于POSIX的标准,如下图
TCP/IP网络编程(二)
PS:对于32位的IPv4地址为何如此定义,网上查阅了相关的博客,得到的应该就是一个历史包袱的问题。

sockaddr_in成员分析

sin_family

每一种协议族使用的地址族均不同,IPv4协议族使用4字节地址族,IPv6使用16字节地址族。
各地址族名称如下

地址族 含义
AF_INET IPv4网络协议使用的地址族
AF_INET6 IPv6使用的地址族
sin_port

该成员保存16位的端口号。它以网络字节序保存!!

sin_addr

该成员用于保存32位的IP地址信息,也是以网络字节序保存。

sin_zero

该成员没有任何含义,只是为了与sockaddr结构体保持一致而插入的成员

代码解析

首先回到一开始的代码中。

    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");

其中sockaddr结构体定义如下:

struct sockaddr{
sa_family_t sin_family; //地址族
char sa_data[14]; //地址信息
};

bind函数中,第二个参数希望得到一个sockaddr结构体的变量地址值,但是直接填充的话,还是挺麻烦的,所以我们通过sockaddr_in这个中介来转换我们所需的信息。