一、常用头文件
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
struct in_addr;
二、常用结构及函数
int inet_aton(const char *cp, struct in_addr *inp); // 返回:若成功则为 1, 若出错则为0.
将一个点分十进制串(cp) 转换为一个网络字节顺序的 IP 地址(inp)。
char *inet_ntoa(struct in_addr in); // 返回: 指向点分十进制字符串的指针
将一个网络字节顺序的IP 地址转换为它所对应的点分十进制串。
/*Internet address structure*/
struct in_addr {
unsigned int s_addr; /* Network byte order (big-endian) */
};
hostent // DNS 主机条目结构
hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表。
之所以主机的地址是一个列表的形式,原因是当一个主机有多个网络接口时,自然有多个地址。
struct hostent { char *h_name; // 地址的正式名称 char **h_aliases; // 空字节-地址的预备名称的指针 int h_addrtype; // 地址类型; 通常是AF_INET。 int h_length; // 地址的比特长度。 char **h_addr_list; //零字节-主机网络地址指针。网络字节顺序。 };
示例1:hostinfo.c 检索并打印一个DNS 主机条目
#include <stdio.h> #include <netdb.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc, char **argv) { char **pp; struct in_addr addr; struct hostent *hostp; if (argc != 2) { fprintf(stderr, "usage: %s <domain name of dotted-decimal> ", argv[0]); exit(0); } if (inet_aton(argv[1], &addr) != 0) hostp = gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET); else hostp = gethostbyname(argv[1]); printf("official hostname:%s ",hostp->h_name); for (pp = hostp->h_addr_list; *pp != NULL; pp++) printf("alias: %s ", *pp); for (pp = hostp->h_addr_list; *pp != NULL; pp++) { addr.s_addr = ((struct in_addr *)*pp)->s_addr; printf("address: %s ",inet_ntoa(addr)); } exit(0); }
借助以上程序来挖掘一些DNS 映射的特性,这个程序从命令行读取一个域名或点分十进制地址,并显示相应的主机条目。没太因特网主机都有本地定义的域名 localhost, 这个域名总是映射为本地回送地址( loopback address ) 127.0.0.1:
./hostinfo localhost
official hostname:USER-20140129AU
address: 127.0.0.1
三、套接字
一个连接是由它两端的套接字地址唯一确定的。
这个套接字地址叫做 套接字对(socket pair),由下列元组表示:
(cliaddr: cliport, servaddr: servport)
即:
(客户端IP地址 : 客户端端口, 服务器IP地址:服务器端口)
客户端的套接字地址中的端口是由内核自动分配的,称为 临时端口。
服务器端套接字地址中的端口通常是某个知名端口,是和这个服务相对应的。
套接字地址的结构
/* generic socket address structure (for connect, bind, and accept) */ struct sockaddr{ unsigned short sa_family; /* protocol family */ char sa_data[14]; /* address data. */ } /* internet-style socket address structure */ struct sockaddr_in{ unsigned short sin_family; /* address family (always AF_INET) */ unsigned short sin_port; /* port number in network byte order */ struct in_addr sin_addr; /* IP address in network byte order */ unsigned char sin_zero[8];/* Pad to sizeof(struct sockaddr) */ }
socket 函数
客户端和服务器使用 socket 函数来创建一个 套接字描述符。
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); // 返回: 若成功返回非负描述符,若出错返回 -1
在我们的代码中,我们总是带这样的参数来调用 socket 函数:
clientfd = socket(AF_INET, SOCK_STREAM, 0);
其中,AF_INET 表明我们正在用因特网, 而 SOCK_STREAM 表示这个套接字是因特网连接的一个端点。
socket 返回的 clientfd 描述符仅是部分打开的,还不能用于读写。 如何完成打开套接字的工作,取决于
我们的是客户端还是服务器端。
connect 函数
客户端通过调用connect 函数来建立和服务器的连接。
#include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); //返回: 若成功则为 0 ,若出错返回 -1
connect 函数试图与套接字地址为 serv_addr 的服务器建立一个因特网连接,其中 adderlen 是 sizeof(sockaddr_in)。
connect 函数会阻塞,一直到连接成功建立或是发生错误。如果成功, sockfd 描述符现在就准备好可以读写了,并且得到
的连接是有套接字对
(x:y, serv_addr.sin_addr:serv_addr.sin_port)
刻画的, 其中 x 表示客户端的 IP 地址, 而 y 表示临时端口, 它唯一确定了客户端主机上的客户端进程。
open_clientfd 函数
我们把 socket 和 connection 函数包装成一个叫做 open_clientfd 的函数会很方便,客户端可以利用它来和服务器建立连接。
int open_clientfd(char *hostname, int port); 返回: 若成功则返回描述符,若失败则返回 -1, 若 DNS 若错则返回 -2
open_clientfd 函数和运行在主机hostname 上的服务器建立一个连接,并在知名端口 port 上监听连接请求。
它返回一个打开的套接字描述符,该描述符准备好了,可以用 Unix I/O 函数做输入和输出。
实验二:open_clientfd 的代码 csapp.c
int open_clientfd(char *hostname, int port) { int clientfd; struct hostent *hp; struct sockaddr_in serveraddr; if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) // 创建套接字描述符 return -1; /* check errno for cause of error */ /* Fill in the server's IP address and port */ if ((hp = gethostbyname(hostname)) == NULL) // 开始检索服务器的DNS 主机条目,并拷贝主机条目中的第一个IP地址(已经是按照网络字节顺序了)到服务器的套接字地址结构 return -2; /* check h_errno for cause of error */ bzero((char *) &serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->h_length); serveraddr.sin_port = htons(port); // 在用按照网络字节顺序的服务器的知名端口号初始化套接字地址结构之后, /* establish a connection with the server */ if (connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0) // 我们发起了一个到服务器的连接请求 return -1; return clientfd; // 当 connect 函数返回时,我们返回套接字描述符给客户端,客户端就可以立即开始用 Unix I/O 和 服务器通信了 }
bind 函数
剩下的套接字函数 bind、listen 和 accept 被服务器用来和客户端建立连接。
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addrlen); // 返回: 若成功则为 0, 若出错则为 -1
bind 函数告诉内核将 my_addr 中的服务器套接字地址和套接字描述符 sockfd 联系起来。
参数 addrlen 就是 sizeof(sockaddr_in)。
listen 函数
客户端是发起连接请求的主动实体。服务器是等待来自客户端连接请求的被动实体。
默认情况下,内核会认为 socket 函数创建的描述符对应于 主动套接字 ,它存在
于一个连接的客户端。
服务器调用 listen 函数告诉内核,描述符是被服务器而不是客户端使用的。
#include <sys/socket.h> int listen(int sockfd, int backlog);
//返回: 若成功返回 0, 若出错则返回 -1
listen 函数将 sockfd 从一个 主动套接字 转化为一个 监听套接字,该套接字可以接受
来自客户端的连接请求。
backlog 参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成连接
请求的数量。 backlog 参数的确切含义要求对 TCP/IP 协议的理解。
通常我们会把它设置为一个较大的值,例如 1024。
open_listenfd 函数
我们将 socket、bind 和 listen 函数结合成一个叫做 open_listenfd 的辅助函数是很有
帮助的,服务器可以用它来创建一个监听描述符。
int open_listenfd(int port); // 返回: 若成功则返回描述符,若 Unix 出错则返回 -1.
open_listenfd 函数打开和返回一个监听描述符,这个描述符准备好在知名端口 port 上
接收连接请求。
下面的示例展示了 open_listenfd 的代码。在我们创建了 listenfd 套接字描述符之后,我们
使用 setsockopt 函数(在这里没有描述)来配置服务器,使得它能够被立即终止和重启。
默认地,一个重启的服务器将在大约 30 秒内拒绝客户端的连接请求,严重地阻碍了调试。
示例三:open_listenfd: 打开和返回一个监听套接字的辅助函数
int open_listenfd(int port) { int listenfd, optval = 1; struct sockaddr_in serveraddr; /* create a socket descriptor */ if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1; /* eliminates "address already in use" error from bind */ if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int)) < 0) return -1; /*listenfd will be an end point for all requests to port on any IP address for this host */ bzero((char *) &serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons((unsigned short)port); // 20 行 if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0) return -1; /* make it a listening socket ready to accept connection requests */ if (listen(listenfd, LISTENQ) < 0) // 25 行 return -1; return listenfd; }
接下来,我们初始化服务器的套接字地址结构,为调用 bind 函数做准备。
在这个例子中,我们用 INSDDR_ANY 通配符地址来告诉内核这个服务器将接受到这台主机的任何 IP地址(19行)
和到知名端口 port(第20行)的请求。
注意,我们将 listenfd 转换为一个监听描述符(第25行),并将它返回给调用者。
accept 函数
服务器通过调用 accept 函数来等待来自客户端的连接请求:
#include <sys/socket.h> int accept(int listenfd, struct sockaddr *addr, int *addrlen); // 返回: 若成功则返回非负连接描述符,若出错则为 -1
accept 函数等待来自客户端的连接请求到达侦听描述符 listenfd, 然后在 addr 中填写客户端的套接字地址,并返回一个
已连接描述符,这个描述符可被用来利用 Unix I/O 函数与客户端通信。
监听描述符 和 已连接描述符 的区别:
监听描述符是作为客户端连接请求的一个端点。典型的,它被创建一次,并存在于服务器的整个声明周期。
已连接描述符是客户端和服务器之间已经建立起来的一个连接的一个端点。服务器每次接受连接请求时都会创建一次,它只
存在于服务器为一个客户端服务的过程中。
下图描绘了监听描述符和已连接描述符的角色:
图1 监听描述符和已连接描述符的角色
第一步:服务器调用 accept, 等待连接请求到达监听描述符,具体地我们设定为描述符 3。回忆一下,描述符 0~2 是预留给了标准文件的。
第二步:客户端调用 connect 函数,发送一个连接请求到 listenfd。
第三步:accept 函数打开了一个新的已连接描述符 connfd(我们假设是描述符 4),在 clientfd 和 connfd 之间建立连接,并且随后返回 connfd
给应用程序。客户端也从 connect 返回,在这一点以后,客户端和服务器就可以分别通过读和写 clientfd 和 connfd 来回传送数据了。
echo 客户端和服务器的示例