- 英文小册原文地址:beej.us/guide/bgnet…
- 作者:Beej
- 中文翻译地址:www.chanmufeng.com/posts/netwo…
现将目录贴下:
- 两种Socket
- 漫谈网络
- 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
而已。
第一个介绍完了。。。。。。简单吧。
但是从这儿开始就稍微有点不好理解了,大家跟上车速,慢慢来。
struct—addrinfo
第一个要介绍的struct
结构是addrinfo
,这个数据结构的发明时间还不算很久,是用来准备socket地址
等信息以供后续使用的。它也会被用在域名查找(host name lookups)以及服务查找(service name lookups)等方面。
这么听起来感觉很抽象,等之后我们实际使用的时候就好理解了,现在我们需要知道的就是:我们在建立网络连接的时候会用到addrinfo
这个数据结构。
接下来我们用域名查找来做个例子,帮助大家理解。
使用ip建立连接
通常情况下,我们都是直接利用IP和端口向服务器发起连接,像这样
如果没接触过C的socket编程,你可能已经开始打退堂鼓了。我懂你这种感觉,我都已经放弃好几次了。。。。。。
但是后来硬着头皮看其实也没什么,虽然写法怪异,但都是C语言上的套路而已,看多了也就那么回事儿!
上面的代码的意思就是对182.25.23.123
的80
端口发起了一个连接。前提是我们已经知道了主机的IP了,如果只有域名该怎么办呢?
那我们就得利用DNS
,用另一种方式来构建客户端套接字了,而这种方式就会用到addrinfo
。
使用域名建立连接
直接上代码!
getaddrinfo()
函数会创建一个叫做名字资源的新数据结构(也就是代码中的res
),给定域名
和端口号
以及hints
信息,该函数就会将名字资源的数据保存在了一个叫做res
的addrinfo
数据结构中,res
就包含了服务器的IP等下一步所需的信息。
在发明
struct addrinfo
之前,我们都需要手动填写res
中的每一个字段的,远不如现在getaddrinfo()
帮我们处理地这么好。
还没完呢,这只是获取到了服务器的IP信息而已,我们还得创建客户端socket
,然后进行connect()
,但是再进一步讲解之前,我想先稍微解释一下代码中hints
的信息分别是什么意思,毕竟让某些读者带着疑问往下读也是有些于心不忍。
hints.ai_family
有3种选择,
-
AF_INET
,表示强制使用IPv4 -
AF_INET6
,表示强制使用IPv6 -
AF_UNSPEC
,随便,IPv4或者IPv6都行
hints.ai_socktype
有2种选择,
-
SOCK_STREAM
,表示使用TCP协议 -
SOCK_DGRAM
,表示使用UDP协议
解释完了,然后我们用res
中的数据继续我们的连接过程。从下面的代码中你可以看到,创建socket
套接字以及connect
所需要的所有信息我们都可以从res
中直接获取到了。
有一点需要需要我们特别注意,在使用ip建立连接时,connect()
的第二个参数我们采用了类型强转的方式,将struct sockraddr_in *
强转成了struct sockaddr *
。但是在使用域名connect()
时,直接从res
这个addrinfo
结构中使用了struct sockaddr *ai_addr
这个字段(不清楚的话再看一看addrinfo
的数据结构)。
所以,sockaddr
就是我们要学习的下一个数据结构了。
注:有些数据结构属于 IPv4,而有些是 IPv6,有些两者皆可,我会特別注明它们属于哪一种。
struct—sockaddr
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 *
进行强转传入的原因。
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
结构:
用起来很简单,如果你声明了一个sockaddr_in
的变量sin
,那么sin.sin_addr.s_addr
表示的就是一个4字节的IP地址(符合网络字节序)。
IPv4的说完了,对应着,我们再看看IPv6的。
struct—sockaddr_in6
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字节,应该能装得下目前所以协议的地址结构了。
举个栗子吧:
总结一下你也就明白了,对于存储地址的数据结构,一共有4种,并且每种之间都可以进行转换
-
sockaddr
(最原始的数据结构,但是装不下IPv6) -
sockaddr_in
(专用于IPv4) -
sockaddr_in6
(专用于IPv6) -
sockaddr_storage
(相当于sockaddr
的补丁,能装不下IPv6)
在大多数情况下,后3种结构都需要强转为sockaddr
,你可能会问为什么不直接传入sockaddr_storage
呢?这也是没办法的事情,因为api从一开始的时候就已经确定好了,变了的话旧代码也就不能运行了。我们能做的就是打补丁。
很多奇奇怪怪数据结构的出现,就是由于最开始没想到会发展到现在这种情况而导致的。包括现在也一样,我们很难预测未来的全部变化,能想到最好,想不全或是低估未来才是常态。