持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
- 英文小册原文地址:beej.us/guide/bgnet…
- 作者:Beej
- 中文翻译地址:www.chanmufeng.com/posts/netwo…
现将目录贴下:
- 什么是socket
- IP地址、struct以及地址转换
- IP地址,IPv4和IPv6
- 子网
- 字节序
- socket相关的数据结构
- 再谈IP地址
- 从IPv4迁移到IPv6
- IP地址,IPv4和IPv6
- 字节序
- struct结构
- 再谈IP地址
- System call 或 Bust
- getaddrinfo()—准备开始!
- socket()—拿到文件描述符!
- bind()—我在监听哪个端口?
- connect()—嘿,你好啊!
- listen()—会有人联系我吗?
- accept()—感谢呼叫3490端口
- send() and recv()—跟我唠唠吧,宝儿!
- sendto() and recvfrom()—用DGRAM风格跟我说话
- close() and shutdown()—滚犊子!
- getpeername()—你哪位?
- gethostname()—我是谁?
- Client-Server基础
- 一个简单的流服务器
- 一个简单的流客户端
- Datagram Sockets
- 技术进阶
- Blocking—何谓阻塞?
- poll()—同步的I/O多路复用
- select()—老古董的同步I/O多路复用
- 数据只传了一部分怎么办?
- Serialization-如何封装数据?
- 数据封装
- 广播数据包-大声说「Hello,World」
终于讲到这里了,现在该聊一聊和编程直接相关的一些内容了,本节会介绍多种Socket库使用的数据结构。
socket描述符
首先介绍一个最简单的:socket描述符。它的类型是:
int
没错,就只是个普普通通的int而已。
第一个介绍完了。。。。。。简单吧。
但是从这儿开始就稍微有点不好理解了,大家跟上车速,慢慢来。
struct—addrinfo
第一个要介绍的struct结构是addrinfo,这个数据结构的发明时间还不算很久,是用来准备socket地址等信息以供后续使用的。它也会被用在域名查找(host name lookups)以及服务查找(service name lookups)等方面。
这么听起来感觉很抽象,等之后我们实际使用的时候就好理解了,现在我们需要知道的就是:我们在建立网络连接的时候会用到addrinfo这个数据结构。
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // 链表结构,指向下一个节点
};
接下来我们用域名查找来做个例子,帮助大家理解。
使用ip建立连接
通常情况下,我们都是直接利用IP和端口向服务器发起连接,像这样
struct sockaddr_in si;
//这一行你可能还不知道什么意思,别急,下文会解释
memset(&si, 0, sizeof(si));
si.sin_family = AF_INET;
si.sin_addr.s_addr = inet_addr("182.25.23.123");
si.sin_port = htons(80);
connect(s, (struct sockaddr *) &si, sizeof(si));
如果没接触过C的socket编程,你可能已经开始打退堂鼓了。我懂你这种感觉,我都已经放弃好几次了。。。。。。
但是后来硬着头皮看其实也没什么,虽然写法怪异,但都是C语言上的套路而已,看多了也就那么回事儿!
上面的代码的意思就是对182.25.23.123的80端口发起了一个连接。前提是我们已经知道了主机的IP了,如果只有域名该怎么办呢?
那我们就得利用DNS,用另一种方式来构建客户端套接字了,而这种方式就会用到addrinfo。
使用域名建立连接
直接上代码!
// 为了使用getaddrinfo()函数,需要这个头文件
#include <netdb.h>
...
struct addrinfo *res;
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.chanmufeng.com", "80", &hints, &res);
getaddrinfo()函数会创建一个叫做名字资源的新数据结构(也就是代码中的res),给定域名和端口号以及hints信息,该函数就会将名字资源的数据保存在了一个叫做res的addrinfo数据结构中,res就包含了服务器的IP等下一步所需的信息。
在发明
struct addrinfo之前,我们都需要手动填写res中的每一个字段的,远不如现在getaddrinfo()帮我们处理地这么好。
还没完呢,这只是获取到了服务器的IP信息而已,我们还得创建客户端socket,然后进行connect(),但是再进一步讲解之前,我想先稍微解释一下代码中hints的信息分别是什么意思,毕竟让某些读者带着疑问往下读也是有些于心不忍。
hints.ai_family有3种选择,
AF_INET,表示强制使用IPv4AF_INET6,表示强制使用IPv6AF_UNSPEC,随便,IPv4或者IPv6都行
hints.ai_socktype有2种选择,
SOCK_STREAM,表示使用TCP协议SOCK_DGRAM,表示使用UDP协议
解释完了,然后我们用res中的数据继续我们的连接过程。从下面的代码中你可以看到,创建socket套接字以及connect所需要的所有信息我们都可以从res中直接获取到了。
int s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(s, res->ai_addr, res->ai_addrlen);
有一点需要需要我们特别注意,在使用ip建立连接时,connect()的第二个参数我们采用了类型强转的方式,将struct sockraddr_in *强转成了struct sockaddr *。但是在使用域名connect()时,直接从res这个addrinfo结构中使用了struct sockaddr *ai_addr这个字段(不清楚的话再看一看addrinfo的数据结构)。
所以,sockaddr就是我们要学习的下一个数据结构了。
注:有些数据结构属于 IPv4,而有些是 IPv6,有些两者皆可,我会特別注明它们属于哪一种。
struct—sockaddr
struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
sa_family的可选值有很多,但是本小册中只会使用AF_INET(IPv4)或 AF_INET6(IPv6)。
sa_data包含了socket需要的目的地址以及端口号,但是这样实在是很不方便,因为你需要手动把ip地址和端口号打包到14字节的数组中。
为了解决这个问题,大佬们又创造了两个替代品,sockaddr_in和sockaddr_in6。
struct—sockaddr_in
后缀in表示internet,而且这个数据结构只能用在IPv4!
非常重要的一点(其实上文已经提到过),struct sockaddr_in *类型可以和 struct sockaddr *相互进行类型转换。这就是为什么connect()函数需要一个struct sockaddr *参数,我们却可以通过使用struct sockaddr_in *进行强转传入的原因。
// (IPv4专用--IPv6见下文的 sockaddr_in6)
struct sockaddr_in {
short int sin_family; // Address family, AF_INET
unsigned short int sin_port; // Port number
struct in_addr sin_addr; // Internet address
unsigned char sin_zero[8]; // Same size as struct sockaddr
};
sockaddr_in中的每个字段看起来就比sockaddr要清晰很多了。
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节,使用之前应该使用memset()将所有数据置为0。
sin_family对应的就是sockaddr中的sa_family,应该设置为AF_INET。sin_port必须使用htons()使其符合网络字节序。
再继续挖得深一点!sockaddr_in的sin_addr字段是struct in_addr结构:
// (IPv4 专用--IPv6见下文的in6_addr)
// Internet address (由于历史原因而保留的一个数据结构)
struct in_addr {
uint32_t s_addr; // that's a 32-bit int (4 bytes)
};
用起来很简单,如果你声明了一个sockaddr_in的变量sin,那么sin.sin_addr.s_addr表示的就是一个4字节的IP地址(符合网络字节序)。
IPv4的说完了,对应着,我们再看看IPv6的。
struct—sockaddr_in6
// (IPv6 专用--IPv4见上文的sockaddr_in)
struct sockaddr_in6 {
u_int16_t sin6_family; // address family, AF_INET6
u_int16_t sin6_port; // port number, Network Byte Order
u_int32_t sin6_flowinfo; // IPv6 flow information
struct in6_addr sin6_addr; // IPv6 address
u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 address
};
sin6_family对应的是sockaddr_in的sin_family字段,sin6_port对应的是sockaddr_in的sin_port字段,sin6_addr对应的是sockaddr_in的sin_addr字段。
至于sin6_flowinfo和sin6_scope_id本小册就不会涉及了,毕竟我们是简明教程嘛。
hold on~hold on~
还没结束,最后再介绍一个数据结构,那句英文怎么说来着?Last but not least,虽然它排在最后,但是也不容忽视。
struct—sockaddr_storage
sockaddr_storage是一个与sockaddr同一级别的数据结构,用来保存IPv4地址和IPv6地址。
不是已经有了sockaddr_in来保存IPv4,sockaddr_in6来保存IPv6了嘛?甚至sockaddr还通用,为什么还需要sockaddr_storage呢?
因为有些时候你可能无法提前确定你要使用IPv4还是IPv6!
你可能会问,这有什么关系呢?我们还有sockaddr啊,它可是通吃啊。
对,通吃!但只是名义上的。我们来分析一下。
sockaddr身为一个通用的地址数据结构,理论上的大小就应该是所有具体协议地址结构大小的最大值。但是sizeof(struct sockaddr) = 16, 而sizeof(struct sockaddr_in6) = 28,sockaddr没有能力保存IPv6啊。
于是sockaddr_storage就诞生了。它的大小为128字节,应该能装得下目前所以协议的地址结构了。
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
举个栗子吧:
struct sockaddr_storage addr;
memset(&addr, 0, sizeof(struct sockaddr_storage));
if (isIPv6 == TRUE)
{
struct sockaddr_in6 *addr_v6 = (struct sockaddr_in6 *)&addr;
addr_v6->sin6_family = AF_INET6;
addr_v6->sin6_port = 80;
inet_pton(AF_INET6, “2201:3212::1”, &(addr_v6->sin6_addr));
}
else
{
struct sockaddr_in *addr_v4 = (struct sockaddr_in *)&addr;
addr_v4->sin_family = AF_INET;
addr_v4->sin_port = 80;
inet_aton(“192.168.0.45”, &(addr_v4->sin_addr));
}
sendto(sock, buf, len, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_storage));
总结一下你也就明白了,对于存储地址的数据结构,一共有4种,并且每种之间都可以进行转换
sockaddr(最原始的数据结构,但是装不下IPv6)sockaddr_in(专用于IPv4)sockaddr_in6(专用于IPv6)sockaddr_storage(相当于sockaddr的补丁,能装不下IPv6)
在大多数情况下,后3种结构都需要强转为sockaddr,你可能会问为什么不直接传入sockaddr_storage呢?这也是没办法的事情,因为api从一开始的时候就已经确定好了,变了的话旧代码也就不能运行了。我们能做的就是打补丁。
很多奇奇怪怪数据结构的出现,就是由于最开始没想到会发展到现在这种情况而导致的。包括现在也一样,我们很难预测未来的全部变化,能想到最好,想不全或是低估未来才是常态。