Beej's Guide to Network Programming 读书笔记(上)
何谓 Socket
Socket 是利用标准 UNIX file descriptors(文件描述符)与其它程序沟通的一种方式。可以用一般的read()
与write()
调用通过socket进行通讯,但是send()
与recv()
让你能对数据传输有更多的控制权。
一般的 socket 只能读取传输层以上(不含)的数据,raw socket 一般用在设计 network sniffer,可以让应用程序取得网路数据包底层的数据(如 TCP 层、IP 层、数据链路层),并用以分析数据包。
主要讨论两种 Internet sockets,一个是 Stream Sockets;而另一个是 Datagram Sockets,分别以SOCK_STREAM
与SOCK_DGRAM
来表示。不讨论 raw socket,以SOCK_RAW
来表示。
为什麽你要用一个不可靠的底层协议(UDP)?有两个理由:第一个理由是速度,第二个理由还是速度。
结构与数据转换
字节顺序
网络序为大端序,即数字先存储比较大那一边(高位放在左边)
1 | // Network Byte Order <-> Host Byte Order |
数据结构
struct addrinfo
,这个数据结构是最近的发明,用来准备之后要用的 socket 地址数据结构,也用在主机名(host name)及服务名(service name)的查询。
1 | struct addrinfo { |
要注意的是,这是个链表,ai_next
是指向下一个成员(element), 可能会有多个结果让你选择。
struct sockaddr
记录了很多 sockets 类型的 socket 的地址资料。
1 | struct sockaddr { |
sa_data
包含一个 socket 的目地地址与端口号。这样很不方便,因为你不会想要手动的将地址封装到sa_data
里。
为了处理struct sockaddr
,程序设计师建立了对等平行的数据结构:struct sockaddr_in
(in是代表internet)用在 IPv4。指向struct sockaddr_in
的指针可以转型为指向struct sockaddr
的指针,反之亦然。
1 | // (IPv4 only--see struct sockaddr_in6 for IPv6) |
要注意的是sin_zero
(这是用来将数据结构补足符合struct sockaddr
的长度),应该要使用memset()
函数将sin_zero
整个清为零。还有struct in_addr
和sin_port
必须是 Network Byte Order(利用htons()
)。
1 | // (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4) |
这个简单的struct sockaddr_storage
是设计用来足够储存 IPv4 与 IPv6 结构的结构体。你可以在ss_family
栏位看到地址家族,检查它是AF_INET
或AF_INET6
(是 IPv4 或 IPv6)。之后如果你愿意的话,你就可以将它转型为struct sockaddr_in
或struct sockaddr_in6
。
1 | struct sockaddr_storage { |
IP地址
使用inet_pton()
函数将 IP address 字符串格式转换成网络地址格式,并依照你指定的AF_INET
或AF_INET6
来决定要储存在struct in_addr
或struct in6_addr
。
1 | struct sockaddr_in sa; // IPv4 |
目前上述的代码片段还不是很可靠,因为没有错误检查。inet_pton()
在错误时会返回-1,而若地址被搞砸了,则会返回0。所以在使用之前要检查,并确认结果是大于0的。
1 | // 你有一个 struct in_addr 且你想要以数字与句号的字符串格式打印出来 |
IPv4的老方法是用inet_addr()
和inet_ntoa()
实现字符串IP地址和IPv4地址结构in_addr值的转换
1 | // !!! 这 是 老 方 法 !!! |
私有网络
防火墙会用所谓的网路地址转换(NAT,Network Address Translation)的方法,将内部的 IP 地址转换为外部的 IP address。
10.x.x.x 是其中一个少数保留的网路,只能用在完全无法连上 Internet 的网路,或是在防火墙的网路。一般而言,你较常见的是 10.x.x.x 及 192.168.x.x,这里的 x 是指 0-255。较少见的是 172.y.x.x,这里的 y 范围在 16 与 31 之间。
System Call
getaddrinfo() - 准备开始
它前身是你用来做 DNS 查询的gethostbyname()
,你需要手动将资料写入struct sockaddr_in
,并在你的调用中使用。现在有getaddrinfo()
函数,可以帮你做许多事情,包含 DNS 与 service name 查询,并填好你所需的structs。
1 | // 函数原型 |
hints
参数指向一个你已经填好相关资料的struct addrinfo
。下面是一个调用示例, 如果你是一个 server, 想要在你主机上的 IP address 及 port 3490 运行 listen。
1 | int status; |
在这里看到AI_PASSIVE
flag;这个会告诉 getaddrinfo()
要将我本地端的地址指定给 socket structure。 这样你就不用写固定的地址了,或者你可以将特定的地址放在getaddrinfo()
的第一个参数中,现在写 NULL 的那个参数。
1 | /* |
socket() - 取得 File Descriptor
1 | // 函数原型 |
domain
是PF_INET
或PF_INET6
(在struct sockaddr_in
中使用AF_INET
,而在调用socket()
时使用PF_INET
);type
是SOCK_STREAM
或SOCK_DGRAM
;而protocol
可以设置为0,用来帮给予的type
选择适当的协议。 或者你可以调用 getprotobyname() 来查询你想要的协议,"tcp"或"udp"。
1 | int s; |
socket()
单纯返回一个之后的 system call 要用的 socket descriptor 给你,错误时会返回 -1,errno 全局变量会设置为该错误的值。
bind() - 绑定端口
port number 是用来让 kernel 可以比对出进入的数据包是属于哪个 process 的 socket descriptor。一个 process 可以有多个 socket fd,绑定多个 port number。
1 | // 函数原型 |
1 | struct addrinfo hints, *res; |
bind() 在错误时也会返回 -1, 并将 errno 设置为该错误的值。
1 | // !!! 这 是 老 方 法 !!! |
在上列的代码中, 如果你想要 bind 到你本地端的 IP address(就像上面的AI_PASSIVE
flag),你也可以将INADDR_ANY
指定给s_addr
栏位。INADDR_ANY
的 IPv6 版本是一个in6addr_any
全局变量,它会被指定给你的struct sockaddr_in6
的sin6_addr
栏位。
INADDR_ANY
转换过来就是 0.0.0.0,泛指本机的意思,也就是表示本机的所有 IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡 IP 地址的意思。所以出现INADDR_ANY
,你只需绑定INADDR_ANY
,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
你可能有注意到,有时候你试着重新运行 server,而bind()
却失败了,它声称"Address already in use."。有些连接到 socket 的连接还悬在 kernel 里面,而它占据了这个 port。你可以等待它自行清除(一分钟左右),或者在你的程序中新增代码,让它重新使用这个 port,类似这样:
1 | int yes=1; |
若你正使用connect()
连接到远端的机器,你可以不用管 local port 是多少,你可以单纯地调用connect()
,它会检查 socket 是否尚未绑定, 并在有需要的时候自动将 socket bind()
到一个尚未使用的 local port。
connect() - 嘿!你好
1 | // 函数原型 |
1 | struct addrinfo hints, *res; |
要确定有检查connect()
返回的值,它在错误时会返回 -1,并设定 errno 变量。
listen() - 有人会调用我吗
1 | // 函数原型 |
backlog
是进入的队列(incoming queue)中所允许的连接数目。如同往常,listen()
会返回 -1 并在错误时设置 errno。
1 | 如果你正在 listen 进入的连接, 你会运行的 system call 顺序如下 |
accept() - 谢谢你的调用
很远的人会试着connect()
到你的电脑正在listen()
的 port。他们的连接会排队等待被accept()
。你调用accept()
,并告诉它要取得搁置的(pending)连接。它会返回专属这个连接的一个新 socket file descriptor 给你!你突然有了两个 socket file descriptor!原本的 socket file descriptor 仍然正在 listen 之后的连线,而新建立的 socket file descriptor 则是在最後要准备给send()
与recv()
用的。
1 | // 函数原型 |
sockfd 是正在进行listen()
的 socket descriptor。很简单,addr
通常是一个指向 local struct
sockaddr_storage 的指针,是关于进来的连接将往哪里去的资料,你可以用它来得知是哪一台主机从哪一个 port 调用你的。addrlen
是一个 local 的整数变量,应该在将它的地址传递给accept()
以前, 将它设置为sizeof(struct sockaddr_storage)
。
1 |
|
若你只是要取得一个连接,你可以用close()
关闭正在 listen 的 sockfd,以避免有更多的连接进入同一个 port, 若你有这个需要的话。
send() 与 recv() - 跟我说说话
这两个用来通讯的函数是透过 stream socket 或 connected datagram socket。若你想要使用常规的 unconnected datagram socket,你会需要参考底下的 sendto()
及recvfrom()
的章节。
1 | // 函数原型 |
send()
会返回实际有送出的 byte 数目,这可能会少于你所要传送的数目!有时候你告诉send()
要送整笔的资料,而它就是无法处理这麽多资料。它只会尽量将资料送出,并认为你之后会再次送出剩下没送出的部分。要记住,如果send()
返回的值与 len 的值不符合的话,你就需要再送出字串剩下的部分。
1 | // 函数原型 |
recv()
返回实际读到并写入到缓冲区的 byte 数目,而错误时返回 -1。recv()
会返回 0,这只能表示一件事情:远端那边已经关闭了你的连接!
sendto() 与 recvfrom() - 用 DGRAM 风格跟我说说话
1 | // 函数原型 |
tolen
是一个 int,可以单纯地将它设置为sizeof *to
或sizeof(struct sockaddr_storage)
。(sizeof
对于变量不用加括号,对于类型需要加括号)
为什麽我们要用struct sockaddr_storage
做为 socket 的型别呢?为什麽不用struct sockaddr_in
呢?因为我们不想要让自己绑在 IPv4 或 IPv6, 所以我们使用通用的泛型struct sockaddr_storage
,我们知道这样有足够的空间可以用在 IPv4 与 IPv6。
记住,如果你connect()
到一个 datagram socket,你可以在你全部的交易中只使用send()
与recv()
。socket本身仍然是 datagram socket,数据包仍然使用 UDP,但是 socket interface 会自动帮你增加目的与来源资料。
close() 与 shutdown() - 从我面前消失吧
1 | //函数原型 |
如果你想要能多点控制 socket 如何关闭,可以使用 shutdown()
函数。它让你可以切断单向的通信,或者双向(就像是close()
所做的)。0 不允许再接收数据;1 不允许再传送数据;2 不允许再传送与接收数据。
重要的是shutdown()
实际上没有关闭 file descriptor,它只是改变了它的可用性。如果要释放 socket descriptor, 你还是需要使用close()
。
若你在 unconnected datagram socket 上使用shutdown()
,它只会单纯的让 socket 无法再进行send()
与recv()
调用;要记住你只能在有connect()
到 datagram socket 的时候使用。
getpeername() - 你是谁
1 | // 函数原型 |
getpeername()
函数会告诉你另一端连接的 stream socket 是谁。一旦你取得了它们的地址,你就可以用 inet_ntop()
、getnameinfo()
或gethostbyaddr()
取得更多的资料。
gethostname() - 我是谁
1 | // 函数原型 |
比getpeername()
更简单的函数是gethostname()
,它会返回你运行程序的电脑名,这个名称之后可以用在gethostbyname()
,用来定义你本地端电脑的 IP 地址。
IPv4的老方法使用gethostbyname()
来得到包含域名和IP地址信息的struct hostent
。
1 | // !!! 这 是 老 方 法 !!! |