套接字协议与数据传输
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
下面给出参数的详细说明。
协议族(Protocol Family)
名称 | 协议族 |
PF_INET(最常用) | IPv4互联网协议族 |
PF_INET6 | IPv6互联网协议族 |
PF_LOCAL | 本地通信的UNIX协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPX Novell协议族 |
套接字类型(Type)
套接字类型决定套接字的数据传输方式,即在传输层采用TCP,还是UDP。协议族是在网络层。
名称 | 套接字类型 | 特点 |
SCOK_STREAM | 面向连接的套接字 | 可靠、有序、基于字节的 |
SOCK_DGRAM | 面向消息的套接字 | 快速、不可靠、数据有边界(无序) |
协议的最终选择
大部分情况可以向第三个参数传0,除非遇到以下情况:
“同一协议族中存在多个数据传输方式相同的协议”
目前,PF_INET
协议族中,SOCK_STREAM
数据传输方式的协议,满足的只有IPPROTO_TCP
同理,满足PF_INET
和SOCK_DGRAM
的只有IPPROTO_UDP
。
地址族与数据序列
地址信息的表示 socketaddr_in
数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最终的目的应用程序。
所以对于IPv4来讲,定义了以下结构体表示地址信息给bind
函数。
struct socketaddr_in {
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero; //不使用
}
struct in_addr {
int_addr_t s_addr; //32位IP地址
}
结构体sockaddr_in
的成员分析
- 成员
sin_family
名称 | 协议族 |
PF_INET(最常用) | IPv4互联网协议族 |
PF_INET6 | IPv6互联网协议族 |
PF_LOCAL | 本地通信的UNIX协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPX Novell协议族 |
- 成员
sin_port
该成员保存16位端口号,重点:使用网络字节序 - 成员
sin_addr
32为IP地址,也是网络字节序 - 成员
sin_zero
没啥用,为了与sockaddr结构体保持一致而插入的成员,必须为0。
网络字节序与地址变换
CPU 向内存保存数据的方式有两种:
- 大端序:高位字节存放到低位地址。网络字节序为大端序。
- 小端序:高位字节存放到高位地址。目前主流的 Intel 和AMD系列 CPU 按小端序方式保存数据。
在使用网络发送数据时要先把数据转化成大端序,接收时也要先转换为主机字节序。
链接: Linux一句命令之判断大小端序
字节序转换
htons 中的 h 代表主机字节序,n 代表网络字节序。
s 代表 short 类型,处理 2 字节数据,用于端口号转换;l 代表 long 类型(Linux 中 long 占用 4 字节),处理 4 字节数据,用于 IP 地址转换。
'short 类型,用于端口号的转换'
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
'long 类型,用于 IP 地址的转换'
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
网络地址分配与初始化
sockaddr_in 中保存地址信息的成员是 32 位整型,而一般我们描述 IP 地址时用的是字符串格式的点分十进制表示法,因此需要将字符串形式的 IP 地址转换为 32 位整型数据。
有两个函数可以完成以上功能:inet_addr
函数和 inet_aton
函数。
- inet_addr函数
inet_addr 函数在转换类型的同时也会完成网络字节序的转换,它还可以检测无效的 IP 地址。
#include <arpa/inet.h>
// 功能:将字符串形式的 IP 地址转换为 32 位整型数据并返回。
// 返回值:成功时返回 32 位大端序整型值,失败时返回 INADDR_NONE。
in_addr_t inet_addr(const char* string);
- inet_aton函数
inet_aton 函数和 inet_addr 函数的功能相同,也是将字符串形式的 IP 地址转换为 32 位网络字节序整数,但是它利用了 in_addr 结构体,使用频率更高。
inet_aton 需要传递一个 in_addr 类型结构体的指针,它会将转换结果直接放入该指针所指的 in_addr 结构体中。
#include <arpa/inet.h>
// 功能:将字符串形式的 IP 地址转换为 32 位网络字节序整数并存储到 addr 中。
// 返回值:成功时返回 1,失败时返回 0
int inet_aton(const char* string, struct in_addr* addr);
- inet_ntoa函数
inet_ntoa 函数与 inet_aton 函数相反,它将网络字节序的整数型 IP 地址转换为字符串形式。
#include <arpa/inet.h>
// 功能:将网络字节序的整数型 IP 地址转换为字符串形式
// 返回值:成功时返回转换的字符串地址值,失败时返回 -1
char* inet_ntoa(struct in_addr adr);
该函数使用时要小心:返回值类型为 char 指针,返回字符串地址意味着字符串已保存到内存空,但该函数是在内部申请了内存并保存了字符串,因此如果再次调用 inet_ntoa 函数,也有可能覆盖之前保存的字符串信息。因此要将返回的字符串信息复制到其他内存空间。
网络地址初始化
struct sockaddr_in addr;
char *serv_ip = "211.217.168.13"; // 声明 IP 地址字符串
char *serv_port = "9190"; // 声明端口号字符串
memset(&addr, 0, sizeof(addr)); // 结构体变量 addr 的所有成员初始化为 0,主要是为了将 sockaddr_in 的成员 sin_zero 初始化为 0。
addr.sin_family = AF_INET; // 指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的 IP 地址初始化
addr.sin_port = htons(atoi(serv_port)); // 基于字符串的端口号初始化
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))
服务器端和客户端都要进行网络地址信息的初始化,但目的不同:
- 服务器端要将声明的 sockaddr_in 结构体变量初始化为自己的 IP 地址和端口号,用于在 bind 函数中与自己的套接字相绑定。
- 客户端也要将声明的 sockaddr_in 结构体变量初始化为服务器端的 IP 地址和端口号,用于在 connect 函数中向服务器发起连接请求。
INADDR_ANY
可以用常数 INADDR_ANY 来获取服务器端的 IP 地址
// INADDR_ANY 相当于主机字节序的 32 位整型 IP
addr.sin_addr.s_addr = htonl(INADDR_ANY); 地址
使用 INADDRY_ANY,如果同一个计算机具有多个 IP 地址,那么可以从不同 IP 地址(的同一端口号)接收数据,因此服务器端中优先使用 INADDR_ANY,而客户端不应该采用。
demo
- 练习网络字节序与主机字节序之间的转换
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
unsigned short host_port = 0x1234;
unsigned short net_port;
unsigned long host_addr = 0x12345678;
unsigned long net_addr;
net_port = htons(host_port);
net_addr = htonl(host_addr);
printf("Host ordered port: %#x \n", host_port); // 打印结果:0x1234
printf("Network ordered port: %#x \n", net_port); // 打印结果:0x3412
printf("Host ordered address: %#x \n", host_addr); // 打印结果:0x12345678
printf("Network ordered address: %#x \n", net_addr); // 打印结果:0x78563412
return 0;
}
- 了解 inet_addr 函数的用法
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
char *addr1 = "1.2.3.4";
char *addr2 = "1.2.3.256"; // 错误的 IP 地址:一个字节能表示的最大整数是 255
unsigned long conv_addr = inet_addr(addr1); // 将 "1.2.3.4" 转换为 0x4030201 并返回
if (conv_addr == INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);
conv_addr = inet_addr(addr2); // 因为 IP 地址无效而返回 INADDR_NONE
if (conv_addr == INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n\n", conv_addr);
return 0;
}
- 了解 inet_aton 函数
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *addr = "127.232.124.79";
struct sockaddr_in addr_inet;
if (!inet_aton(addr, &addr_inet.sin_addr)) // 注意 inet_aton 的用法
printf("Conversion error");
else
printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr);
return 0;
}
- 了解 inet_ntoa 函数的用法
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
struct sockaddr_in addr1, addr2;
char *str_ptr;
char str_arr[20];
addr1.sin_addr.s_addr = htonl(0x1020304);
addr2.sin_addr.s_addr = htonl(0x1010101);
str_ptr = inet_ntoa(addr1.sin_addr);
strcpy(str_arr, str_ptr);
printf("Dotted-Decimal notation1: %s \n", str_ptr);
inet_ntoa(addr2.sin_addr);
printf("Dotted-Decimal notation2: %s \n", str_ptr);
printf("Dotted-Decimal notation3: %s \n", str_arr);
return 0;
}
- 在 Windows 系统中练习网络字节序与主机字节序之间的转换的程序。
#include <WinSock2.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
WSADATA wsa_data;
unsigned short host_port = 0x1234;
unsigned short net_port;
unsigned long host_addr = 0x12345678;
unsigned long net_addr;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
printf("WSAStarup() error!");
net_port = htons(host_port);
net_addr = htonl(host_addr);
printf("Host ordered port: %#x \n", host_port); // 打印结果:0x1234
printf("Network ordered port: %#x \n", net_port); // 打印结果:0x3412
printf("Host ordered address: %#x \n", host_addr); // 打印结果:0x12345678
printf("Network ordered address: %#x \n", net_addr); // 打印结果:0x78563412
WSACleanup();
return 0;
}
- 了解 inet_addr 函数和 inet_ntoa 函数在 windows 系统中的用法。
注意:windows 中没有 inet_aton 函数。
虽然 Windows 中的 in_addr 结构体和之前不一样了,但是一样可以使用 inet_ntoa 函数。
#pragma execution_character_set("utf-8")
#include <WinSock2.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
printf("WSAStartup() error");
// inet_addr 函数调用示例
{
char* addr = "127.212.124.78";
unsigned long conv_addr = inet_addr(addr);
if (conv_addr == INADDR_NONE)
printf("Error occured!\n");
else
printf("Network ordered interger addr: %#lx \n", conv_addr);
}
// inet_ntoa 函数调用示例
{
struct sockaddr_in addr;
char* str_ptr;
char str_arr[20];
addr.sin_addr.S_un.S_addr = htonl(0x1020304);
str_ptr = inet_ntoa(addr.sin_addr);
strcpy(str_arr, str_ptr);
printf("Dotted-Decimal notation3 %s \n", str_arr);
}
WSACleanup();
return 0;
}