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_PASSIVEflag;这个会告诉 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 | // !!! 这 是 老 方 法 !!! |