本文将介绍在计算机网络编程中如何协议和地址进行通讯。
对于计算机网络而言,协议是一个绕不开的东西。我们需要定义好各种各样的协议来适应不同的通信环境。因此在进行网络编程的时候,我们需要和对方互相协商好,我们使用什么协议来进行通讯。
回到我们一开始所创建的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
的标准,如下图
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
这个中介来转换我们所需的信息。