套接字接口(socket interface)是一组函数,他们和Unix I/O函数结合起来,用于创建网络应用。大多数现代系统上都实现套接字接口,包括所有的Unix变种,Windows和Macintosh系统。
从Unix内核的角度来看,一个套接字就是通信的一个端点。从Unix程序的角度来看,套接字就是一个有相应描述符的打开文件。
因特网的套接字地址存放在如下代码所示的类型为sockaddr_in的16字节结构中。对于因特网应用,sin_family成员是AF_INEF,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32位的IP地址。IP地址和端口号总是以网络字节顺序(大端法)存放的。
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;
unsigned shot sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
]
connect,bind和accept函数要求一个指向与协议相关的套接字地址结构的指针。套接字接口的设计者面临的问题是,如何定义这些函数,使之能接受各种类型的套接字地址结构。现在,我们可以使用通用的void*指针,那时在C中并不存在这种类型的指针。解决办法是定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。
Socket函数
#include
#include
int socket(int domain, int type, int protocol);
在我们的代码中总是带这样的参数来调用socket函数:
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
其中,AF_INET表明我们正在使用因特网,而SOCK_STREAM表示这个套接字是因特网连接的一个端点。socket返回的clientfd描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作,取决与我们是客户端还是服务器端。
connect函数
客户端通过调用connect函数来建立和服务器的连接。
#include
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
connect函数试图与套接字地址为serv_addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到连接成功建立或发生错误。如果成功,sockfd描述符现在就准备好了可以读写了,并且得到的连接是由套接字对
(x:y, serv_addr.sin_addr:serv_addr.sin_port)
刻画的,其中x表示客户端IP地址,而y表示临时端口,他唯一的确定了客户端主机上客户端进程。
open_clientfd函数
我们发现将socket和connect函数包装成一个叫做open_clientfd的辅助函数是很方便的,客户端可以用它来和服务器建立连接。
#include "csapp.h"
int open_clientfd(char *hostname,int port);
open_clientfd函数和运行在主机hostname上的服务器建立一个连接,并在知名端口port上监听连接请求。他返回一个打开的套接字描述符,该描述符准备好了,可以用UNIX I/O函数做输入和输出。
/* DNS host entry structure */
struct hostent{
char *h_name; /* Official domain name of host */
char **h_aliases; /* Null-terminated array of domain names */
int h_addrtype; /* Host address type (AF_INET)*/
int h_length; /* Length of an address, in bytes*/
char **h_addr_list; /* Null-terminated array of in_addr structs */
}
/* 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) */
]
int open_clientfd(char *hostname, int port)
{
int clientfd;
struct hostent *hp;
struct sockaddr_in serveraddr;
if ((clientfd = socket(AF_INEF,SOCK_STREAM, 0)) < 0)
return -1;
/* Fill in the server's IP address and port*/
if ((hp = gethostbyname(hostname)) == Null)
return -2;
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);
if(connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)
return -1;
return clientfd;
]
在创建了套接字描述符后,我们检索服务器DNS主机条目,并拷贝主机条目中的第一个IP地址(已经是按照网络字节顺序了)到服务器的套接字地址结构。在用按照网络字节顺序的服务器的知名端口号初始化套接字地址结构之后,我们发起了一个到服务器的连接请求。当connect函数返回时,我们返回套接字描述符给客户端,客户端就可以立即开始用Unix I/O和服务器通信了。
bind函数
剩下的套接字函数bind,listen和accept被服务器用来和客户端建立连接。
#include
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);/*返回:若成功则为0,若出错则为-1*/
bind函数告诉内核将my_addr中的服务器套接字地址和套接字描述符sockfd联系起来。参数addrlen就是sizeof(sockaddr_in).
listen函数
客户端是发起连接请求的主动实体。服务器是等待来自客户端的连接请求的被动实体。默认情况下,内核会认为socket函数创建的描述符对应于主动套接字(active socket),它存在于一个连接的客户端。服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。
#include
int listen(int sockfd, int backlog);
listen函数将sockfd从一个主动套接字转化为一个监听套接字(listening socket),该套接字可以接受来自客户端的连接请求。backlog参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成连接请求的数量。backlog参数的确切含义要求对TCP/IP协议的理解,这超出了我们的讨论范围。通常我们会把它设置成一个较大的值,比如1024.
open_listenfd函数
我们发现将socket,bind和listen函数结合成一个叫做open_listenfd的辅助函数是很有帮助的,服务器可以用它来创建一个监听描述符。
#include "csapp.h"
int open_listenfd(int port);
open_listenfd函数打开和返回一个监听描述符,这个描述符准备好在知名端口port上接受连接请求。
int open_listenfd(int port){
int listenfd, optval = 1;
struct sockaddr_in serveraddr;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return - 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int)) < 0)
return -1;
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);
if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)
return -1;
if (listen(listenfd,LISTENQ) < 0)
return - 1;
return listenfd;
}
accept函数
服务器通过调用accept函数来等待来自客户端的连接请求:
#include
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
accept函数等待来自客户端的连接请求到达监听描述符listendfd,然后再addr中填写客户端的套接字地址,并返回一个已连接描述符(connected descriptor),这个描述符可被用来利用UNIX I/O函数与客户端通信。
监听描述符和已连接描述符之间的区别使很多人感到迷惑。监听描述符是作为客户端连接请求的一个端点。典型的,他被创建一次,并存在于服务器的整个生命周期。已连接描述符是客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次接受连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。
第一步中,服务器调用accept,等待连接请求到达监听描述符,具体的我们设定为描述符3。在第二步中,客户端调用connect函数,发送一个连接请求到listenfd。第三步,accept函数打开一个新的已连接描述符connfd(我们假设是描述符4),在clientfd和connfd之间建立连接,并且随后返回connfd给应用程序。客户端从connect返回,在这一点以后,客户端和服务器就可以分别通过读和写clientfd和connfd来回传送数据了。
echo客户端和服务器端的示例
#include "csapp.h"
int main(int argc, char **argv)
{
int clientfd,port;
char *host, buf[MAXLINE];
rio_t rio;
if (argc != 3){
fprintf(stderr, "usage: %s \n", argv[0]);
exit(0);
}
host = argv[1];
port = atoi(argv[2]);
clientfd = Open_clientfd(host,port);
Rio_readlinitb(&rio, clientfd);
while (Fgets(buf, MAXLINE, stdin) != NULL){
Rio_writen(clientfd, buf, strlen(buf));
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
Close(clientfd);
exit(0);
}
上述代码展示了一个echo客户端的代码,在和服务器建立接连后,客户端进入一个循环,反复从标准输入读取文本行,发送文本行给服务器,从服务区读取回送的行,并输出结果到标准输出。当fgets在标准输入上遇到EOF时,或者因为用户在键盘上键入ctrl-d,或者因为在一个重定向的输入文件中用尽了所有的文本行时,循环就终止了。
循环终止之后,客户端关闭描述符。这会导致发送一个EOF通知服务器,当服务器从他的rio_readlineb函数收到一个为零的返回码时,就会检测到这个结果。在关闭他的描述符后,客户端就终止了。既然客户端内核在一个进程终止时会自动关闭所有打开的描述符,显示的Close就没有必要了。不过,显示的关闭已经打开的任何描述符是一个良好的编程习惯。