0x12-套接字编程-2
新时代的 套接字网络编程
- 首先有几个结构体,以及一个接口十分重要及常用:
struct sockaddr_in6
: 代表的是 IPv6 的地址信息struct addrinfo
: 这是一个通用的结构体,里面可以存储 IPv4 或 IPv6 类型地址的信息getaddrinfo
: 这是一个十分方便的接口,在上述 UDP 程序中许多手动填写的部分,都能够省去,有该函数替我们完成
改写一下上方的例子:
接收端:
int sock; /* 套接字 */
socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */
char mess[15];
char get_mess[GET_MAX]; /* 后续版本使用 */
struct sockaddr_in host_v4; /* IPv4 地址 */
struct sockaddr_in6 host_v6; /* IPv6 地址 */
struct addrinfo easy_to_use; /* 用于设定要获取的信息以及如何获取信息 */
struct addrinfo *result; /* 用于存储得到的信息(需要注意内存泄露) */
struct addrinfo * p;
/* 准备信息 */
memset(&easy_to_use, 0, sizeof easy_to_use);
easy_to_use.ai_family = AF_UNSPEC; /* 告诉接口,我现在还不知道地址类型 */
easy_to_use.ai_flags = AI_PASSIVE; /* 告诉接口,稍后“你”帮我填写我没明确指定的信息 */
easy_to_use.ai_socktype = SOCK_DGRAM; /* UDP 的套接字 */
/* 其余位都为 0 */
/* 使用 getaddrinfo 接口 */
getaddrinfo(NULL, argv[1], &easy_to_use, &result); /* argv[1] 中存放字符串形式的 端口号 */
/* 创建套接字,此处会产生两种写法,但更保险,可靠的写法是如此 */
/* 旧式方法
* sock = socket(PF_INET, SOCK_DGRAM, 0);
*/
/* 把IP 和 端口号信息绑定在套接字上 */
/* 旧式方法
* memset(&recv_host, 0, sizeof(recv_host));
* recv_host.sin_family = AF_INET;
* recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
* recv_host.sin_port = htons(6000); /* 使用6000 端口号 */
* bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
*/
for(p = result; p != NULL; p = p->ai_next) /* 该语法需要开启 -std=gnu99 标准*/
{
sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if(sock == -1)
continue;
if(bind(sock, p->ai_addr, p->ai_addrlen) == -1)
{
close(sock);
continue;
}
break; /* 如果能执行到此,证明建立套接字成功,套接字绑定成功,故不必再尝试。 */
}
/* 进入接收信息的状态 */
//recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
switch(p->ai_socktype)
{
case AF_INET :
addr_len = sizeof host_v4;
recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v4, &addr_len);
break;
case AF_INET6:
addr_len = sizeof host_v6
recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v6, &addr_len);
break;
default:
break;
}
freeaddrinfo(result); /* 释放这个空间,由getaddrinfo分配的 */
/* 接收完成,关闭套接字 */
close(sock);
代码解释:
首先解释几个新的结构体
struct addrinfo
这个结构体的内部顺序对于*nix
和Windows
稍有不同,以*nix
为例struct addrinfo{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr * ai_addr; /* 存放结果地址的地方 */
char * ai_canonname; /* 忽略它吧,很长一段时间你无须关注它 */
struct addrinfo * ai_next; /* 一个域名/IP地址可能解析出多个不同的 IP */
};
ai_family
如果设定为AF_UNSPEC
那么在调用getaddrinfo
时,会自动帮你确定,传入的地址是什么类型的ai_flags
如果设定为AI_PASSIVE
那么调用getaddrinfo
且向其第一个参数传入NULL
时会自动绑定自身 IP,相当于设定INADDR_ANY
ai_socktype
就是要创建的套接字类型,这个必须明确声明,系统没法预判(日后人工智能说不定呢?)ai_protocol
一般情况下我们设置为0
,含义可以自行查找,例如MSDN
或者UNP
ai_addr
这里保存着结果,可以通过 调用getaddrinfo
之后 的第四个参数获得。ai_addrlen
同上ai_next
同上getaddrinfo
强大的接口函数int getaddrinfo(const char * node, const char * service,
const struct addrinfo * hints, struct addrinfo ** res);
- 通俗的说这几个参数的作用
node
便是待获取或者待绑定的 域名 或是 IP,也就是说,这里可以直接填写域名,由操作系统来转换成 IP 信息,或者直接填写IP亦可,是以字符串的形式service
便是端口号的意思,也是字符串形式hints
通俗的来说就是告诉接口,我需要你反馈哪些信息给我(第四个参数),并将这些信息填写到第四个参数里。res
便是保存结果的地方,需要注意的是,这个结果在API内部是动态分配内存了,所以使用完之后需要调用另一个接口(freeaddrinfo
)将其释放实际上对于现代的 套接字编程 而言,多了几个新的存储 IP 信息的结构体,例如
struct sockaddr_in6
和struct sockaddr_storage
等。其中,前者是后者的大小上的子集,即一个
struct storage
一定能够装下一个struct sockaddr_in6
,具体(实际上根本看不到有意义的实现)struct sockaddr_in6{
u_int16_t sin6_family;
u_int16_t sin6_port;
u_int32_t sin6_flowinfo; /* 暂时忽略它 */
struct in6_addr sin6_addr; /* IPv6 的地址存放在此结构体中 */
u_int32_t sin_scope_id; /* 暂时忽略它 */
};
struct in6_addr{
unsigned char s6_addr[16];
}
------------------------------------------------------------
struct sockaddr_storage{
sa_family_t ss_family; /* 地址的种类 */
char __ss_pad1[_SS_PAD1SIZE]; /* 从此处开始,不是实现者几乎是没办法理解 */
int64_t __ss_align; /* 从名字上可以看出大概是为了兼容两个不同 IP 类型而做出的妥协 */
char __ss_pad2[_SS_PAD2SIZE]; /* 隐藏了实际内容,除了 IP 的种类以外,无法直接获取其他的任何信息。 */
/* 在各个*nix 的具体实现中, 可能有不同的实现,例如 `__ss_pad1` , `__ss_pad2` , 可能合并成一个 `pad` 。 */
};
在实际中,我们往往不需要为不同的IP类型声明不同的存储类型,直接使用
struct sockaddr_storage
就可以,使用时直接强制转换类型即可
改写上方 接收端 例子中,进入接收信息的状态部分
/* 首先将多于的变量化简 */
// - struct sockaddr_in host_v4; /* IPv4 地址 */
// - struct sockaddr_in6 host_v6; /* IPv6 地址
struct sockaddr_storage host_ver_any; /* + 任意类型的 IP 地址 */
...
/* 进入接收信息的状态部分 */
recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_ver_any, &addr_len); /* 像是又回到了只有 IPv4 的年代*/
补充完整上方对应的 发送端 代码
int sock;
const char* mess = "Hello Server!";
char get_mess[GET_MAX]; /* 后续版本使用 */
struct sockaddr_storage recv_host; /* - struct sockaddr_in recv_host; */
struct addrinfo tmp, *result;
struct addrinfo *p;
socklen_t addr_len;
/* 获取对端的信息 */
memset(&tmp, 0, sizeof tmp);
tmp.ai_family = AF_UNSPEC;
tmp.ai_flags = AI_PASSIVE;
tmp.ai_socktype = SOCK_DGRAM;
getaddrinfo(argv[1], argv[2], &tmp, &result); /* argv[1] 代表对端的 IP地址, argv[2] 代表对端的 端口号 */
/* 创建套接字 */
for(p = result; p != NULL; p = p->ai_next)
{
sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol); /* - sock = socket(PF_INET, SOCK_DGRAM, 0); */
if(sock == -1)
continue;
/* 此处少了绑定 bind 函数,因为作为发送端不需要讲对端的信息 绑定 到创建的套接字上。 */
break; /* 找到就可以退出了,当然也有可能没找到,那么此时 p 的值一定是 NULL */
}
if(p == NULL)
{
/* 错误处理 */
}
/* -// 设定对端信息
memset(&recv_host, 0, sizeof(recv_host));
recv_host.sin_family = AF_INET;
recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
recv_host.sin_port = htons(6000);
*/
/* 发送信息 */
/* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */
sendto(sock, mess, strlen(mess), 0, p->ai_addr, p->ai_addrlen);
/* 完成,关闭 */
freeaddrinfo(result); /* 实际上这个函数应该在使用完 result 的地方就予以调用 */
close(sock);
- 到了此处,实际上是开了网络编程的一个初始,解除了现代的 UDP 最简单的用法(甚至还算不上完整的使用),但是确实是进行了交互。
#
- 首先介绍 UDP 并不是因为它简单,而是因为他简洁,也不是因为它不重要,相反他其实很强大。
- 永远不要小看一个简洁的东西,就像 C语言
- 下一篇将详细记录 UDP 的相关记录
在这之前
- 首先还是科普记录一下协议的知识。
- 阮一峰的博客 : 互联网协议入门(一)
- 阮一峰的比克 : 互联网协议入门(二)
- 上述两篇文章十分浅显易懂,十分符合科普二字,下方将对上述两个文章进行适当的补充。
ARP 协议
- 最简便的方法就是找一个有 WireShark 软件或者
tcpdump
的*nix
平台,前者你可以选择随意监听一个机器,不多时就能看见 ARP
协议的使用,因为它使用的太频繁了。 - 对于 ARP 协议而言,首先对于一台机器 A,想与 机器B 通信,(假设此时 机器A 的高速缓存区(操作系统一定时间更新一次)中 没有 机器B的缓存),
- 那么机器A就向广播地址发出 ARP请求,如果 机器B 收到了这个请求,就将自己的信息(IP地址,MAC地址)填入 ARP应答 中,再发送回去就行。
- 上述中, ARP请求 和 ARP应答 是一种报文形式的信息,是 ARP协议 所附带的实现产品,也是用于两台主机之间进行通信。
- 这是当 机器A 和 机器B 同处于一个网络的情况下,可以借由本网络段的广播地址 发送请求报文。
- 对于不同网络段的 机器A 与 机器B 而言,想要通过 ARP协议 获取 MAC地址 ,就需要借助路由器的帮助了,可以想象一下,路由器(可以不止一个)在中间,机器A 和 机器B 分别在这些路由器的两边(即在不同子网)
- 由于 A 和 B 不在同一个子网内,所以没办法通过通过直接通过广播到达,但是有了路由器,就能进行 ARP代理 的操作,大概就是将路由器当成机器B, A向自己的本地路由器发送 ARP请求
- 之后路由器判断出是发送给B的ARP请求,又正好 B 在自己的管辖范围之内,就把自己的硬件地址 写入 ARP应答 中发回去,之后再有A向B 的数据,就都是A先发送给路由器,再经由路由器发往B了
- 一篇比较好的资源是 Proxy ARP
ICMP
- 这个协议比较重要,后方的概念也会涉及。
- 请求应答报文 和 差错报文 ,重点在于差错报文。
- 请求应答报文在
ICMP
的应用中可以拿来查询本机的子网掩码之类的信息,大致通过向本子网内的所有主机发送该请求报文(包括自己,实际上就是广播),后接收应答,得到信息 - 差错报文在后续中会有提到,这里需要科普一二。
- 首先对于差错报文的一大部分是关于 xxx不可达 的类型,例如主机不可达,端口不可达等等,每次出现错误的时候,ICMP报文总是第一时间返回给对端,(它一次只会出现一份,否则会造成网络风暴),但是对端是否能够接收到,就不是发送端的问题了。
- 这点上 套接字的类型 有着一定的联系,例如 UDP 在
unconnected
状态下是会忽略 ICMP报文的。而 TCP 因为总是connected
的,所以对于 ICMP报文能很好的捕捉。 - ICMP差错报文中总是带着 出错数据报中的一部分真实数据,用于配对。
注意,对于
UDP
而言,只有connected
状态下,才会收到 ICMP 报文,可以通过errno == ECONNREFUSED
来确定,具体来说就是在你发送完本次数据之后,的下一次系统调用时会有这个想想,代表你的小心没有被送到对方手里,而是被对方丢弃了。在没有一个完备的思路以及良好的设计之前,
UDP
是一个十分艰难的挑战,只有在TCP
实在无法满足我们的性能需求时,我们才来重新考虑UDP
。
当前内容版权归 Wrestle.Wu 或其关联方所有,如需对内容或内容相关联开源项目进行关注与资助,请访问 Wrestle.Wu .