什么是socket
socket是使用 Unix 文件描述符 (fiel descriptor) 和其他程序通讯的方式。Unix 程序在执行任何形式的 I/O时, 程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。 但是,这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其他的东西。Unix 中所有的东西是文件。因此,想和 Internet 上别 的程序通讯的时候,将要通过文件描述符。
从哪里得到网络通讯的文件描述符呢,利用系统调用
socket()
。它返回套接口描述符(socket descriptor),然后你再通过它来调用
send()
和
recv()
。如果它是个文件描述符,那么为什么不用一般的调用
read()
和
write()
来通过套接口通讯。答案是可以,但是使用
send()
和
recv()
让程序员更好的控制数据传输。
在我们的世界上,有很多种套接口。有 DARPA Internet 地址 (Internet 套接口),本地节点的路径名 (Unix 套接口),CCITT X.25 地址 (可以完全忽略 X.25 套接口)。 也许在Unix 机器上还有其他的。这里只讲第一种:Internet 套接口。
Internet 套接口的两种类型
一种是 "Stream Sockets",另外一种是 "Datagram Sockets"。数据报套接口有时也叫无连接套接口。流式套接口是可靠的双向通讯的数据流。如果向套接口安顺序输出“1,2”,那么它们将安顺序“1,2”到达另一边。它们也是无错误的传递的,有自己的错误控制。
telnet
就使用流式套接口。WWW 浏览器使用的 HTTP 协议也使用他们。流式套接口可以达到高质量的数据传输,它使用了“传输控制协议 (The Transmission Control Protocol)”,TCP 控制数据按顺序到达并且没有错误。TCP/IP中IP 是指 “Internet 协议”, IP 只是处理 Internet 路由而已。
数据报套接口为什么不可靠。如果发送一个数据报,它可能到达,它可能次序颠倒了。如果它到达,那么在这个包的内部是无错误的。数据报也使用 IP 作路由,但是不选择 TCP。它使用“用户数据报协议 (User Datagram Protocol)”。为什么是无连接,主要原因是因为它并不象流式套接口那样维持一个连接。只要建立一个包,在目标信息中构造一个 IP 头,然后发出去。不需要连接。应用程序有: tftp, bootp 等等。
如果数据丢失了这些程序如何正常工作。每个程序在 UDP 上有自己的协议。例如,ftp 协议每发出一个包,收到者发回一个包来说“我收到了!” (一个“命令正确应答”也叫“ACK” 包)。如果在一定时间内,发送方没有收到应答, 它将重新发送,直到得到 ACK。这一点在实现
SOCK_DGRAM
应用程序的时候非常重要。
网络理论
数据封装 (Data Encapsulation)主要的内容是:一个包,先是被第一个协议包装(“封装”), 然后,整个数据被另外一个协议封装,然后下一个,一直重复下去,直到硬件(物理)层( Ethernet )。
网络分层模型 (Layered Network Model)。 这种网络模型在描述网络系统上相对其他模型有很多优点。例如,写一个套接口,程序而不用关心数据的物理传输(串行口,以太网,连接单元接口 (AUI) 还是其他介质)。 因为底层的程序为我们处理。实际的网络硬件和拓扑对于程序员来说是透明的。
整个层次模型:
应用层 (Application)
表示层 (Presentation)
会话层 (Session)
传输层 (Transport)
网络层 (Network)
数据链路层 (Data Link)
物理层 (Physical)
物理层是硬件(串口,以太网等等)。应用层是和硬件层相隔最远的—它是用户和网络交互的地方。
把它应用到 Unix,结果是:
应用层 (Application Layer) (telnet, ftp, 等等)
传输层 (Host-to-Host Transport Layer) (TCP, UDP)
Internet 层 (Internet Layer) (IP 和路由)
网络访问层 (Network Access Layer) (网络层,数据链路层和物理层)
现在,可能看到这些层次如何协调来封装原始的数据了。
struct
套接口用到的各种数据类型。
首先是简单的一个:socket descriptor。它是int类型,仅仅是一个常见的
int
。
两种字节排列顺序:重要的字节在前面(有时叫 "octet"),或者不重要的字节在前面。 前一种叫“网络字节顺序 (Network Byte Order)”。有些机器在内部是按照这个顺序储存数据,而另外一些则不然。当某数据必须按照 NBO 顺序,那么要调用函数(
htons()
)来将它从本机字节顺序(Host Byte Order) 转换过来。如果没有提到 NBO, 那么就让它是本机字节顺序。
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
"。
sa_data
为套接口储存目标地址和端口信息。为了对付
struct sockaddr
,程序员创造了一个并列的结构:
struct sockaddr_in
(in代表Internet)
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
这个数据结构让可以轻松处理套接口地址的基本元素。注意
sin_zero
(它被加入到这个结构,并且长度和
struct sockaddr
一样) 应该使用函数
bzero()
或
memset()
来全部置零。这样,即使
socket()
想要的是
struct sockaddr *
, 仍然可以使用
struct sockaddr_in。
注意
sin_family
和
sa_family
一致并能够设置为 "
AF_INET
"。最后,
sin_port
和
sin_addr
必须是网络字节顺序 (Network Byte Order)。
再看这个数据结构:
struct in_addr
, 有这样一个联合 (unions):
/* Internet address (a structure for historical reasons) */
struct in_addr {
unsigned long s_addr;
};
如果声明 "
ina
" 是 数据结构
struct sockaddr_in
的实例,那么 "
ina.sin_addr.s_addr
" 就储存4字节的 IP 地址(网络字节顺序)。如果你系统使用的还是联合
struct in_addr
,还是可以放心4字节的 IP 地址是和上面说的一样(这是因为
#define
。)
Convert the Natives
网络字节顺序和本机字节顺序之间的转换。能够转换两种类型:
short
(两个字节)和
long
(四个字节)。这个函数对于变量类型
unsigned
也适用。假设想将
short
从本机字节顺序转换为网络字节顺序。用 "h" 表示 "本机 (host)",接着是 "to",然后用 "n" 表示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者
htons()
("Host to Network Short")。
htons()
--"Host to Network Short"
htonl()
--"Host to Network Long"
ntohs()
--"Network to Host Short"
ntohl()
--"Network to Host Long"
记住:在将数据放到网络上的时候,确信它们是网络字节顺序。
为什么
struct sockaddr_in
数据结构中,
sin_addr
和
sin_port
需要转换为网络字节顺序,而
sin_family
不需要呢? 答案是:
sin_addr
和
sin_port
分别封装在包的 IP 和 UDP 层。因此,他们必须要是网络字节顺序。 但是
sin_family
域只是被内核 (kernel) 使用来决定在数据结构中包含什么 类型的地址,所以它应该是本机字节顺序。也即
sin_family
没有发送到网络上,它们是本机字节顺序。
IP 地址
首先,假设用
struct sockaddr_in ina
,想将 IP 地址 "132.241.5.10" 储存到其中。要用的函数是
inet_addr()
,转换 numbers-and-dots 格式的 IP 地址到 unsigned long。这个工作可以这样来做:
ina.sin_addr.s_addr = inet_addr("132.241.5.10");
注意:
inet_addr()
返回的地址已经是按照网络字节顺序的,没有必要再去调用
htonl()
。
上面的代码可不是很健壮 (robust),因为没有错误检查。
inet_addr()
在发生错误 的时候返回
-1
。记得二进制数,在IP 地址为 255.255.255.255 的时候返回的是
(unsigned)-1。
这是个广播地址。记住正确的使用错误检查。
数据结构
struct in_addr
如何按照 numbers-and-dots 格式打印。在这个时候,要用函数
inet_ntoa()
("ntoa" 意思是 "network to ascii"):
printf("%s",inet_ntoa(ina.sin_addr));
它将打印 IP 地址。注意的是:函数
inet_ntoa()
的参数是
struct in_addr
,而不是
long
。同时要注意的是它返回的是一个指向字符的指针。 在
inet_ntoa
内部的指针静态地储存字符数组,因此每次调用
inet_ntoa()
的时候它将覆盖以前的内容。例如:
char *a1, *a2;
……
a1 = inet_ntoa(ina1.sin_addr); /* this is 198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* this is 132.241.5.10 */
printf("address 1: %s/n",a1);
printf("address 2: %s/n",a2);
运行结果是:
address 1: 132.241.5.10
address 2: 132.241.5.10
DNS
DNS代表"域名服务 (Domain Name Service)"。主要的功能是:我们给它一个容易记忆的某站点的地址,它给我们IP 地址(然后就可以使用
bind()
,
connect()
,
sendto()
或者其他函数。)当一个人输入:
telnet whitehouse.gov
telnet
能知道它将连接 (
connect()
) 到 "198.137.240.100"。
但是这是如何工作的呢?可以调用函数
gethostbyname()
:
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
它返回一个指向
struct hostent
的指针。这个数据结构是这样的:
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]
这里是这个数据结构的详细资料:
struct hostent
:
h_name -
Official name of the host.
h_aliases -
A NULL-terminated array of alternate names for the host.
h_addrtype -
The type of address being returned; usually
AF_INET
.
h_length -
The length of the address in bytes.
h_addr_list -
A zero-terminated array of network addresses for the host. Host addresses are in Network Byte Order.
h_addr -
The first address in
h_addr_list
.
gethostbyname()
成功时返回一个指向
struct hostent
的 指针,或者是个空 (NULL) 指针。(但是和以前不同,
errno
不设置,
h_errno
设置错误信息。请看下面的
herror()
。)
但是如何使用呢? 这里是个例子:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
struct hostent *h;
if (argc != 2)
{ /* error check the command line */
fprintf(stderr,"usage: getip address/n");
exit(1);
}
if ((h=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror("gethostbyname");
exit(1);
}
printf("Host name : %s/n", h->h_name);
printf("IP Address : %s/n",inet_ntoa(*((struct in_addr *)h->h_addr)));
return 0;
}
在使用
gethostbyname()
的时候,不能用
perror()
打印错误信息(因 为
errno
没有使用),应该调用
herror()
。
相当简单,只是传递一个保存机器名的字符串(例如 "whitehouse.gov") 给
gethostbyname()
,然后从返回的数据结构
struct hostent
中 收集信息。