套接字接口(socket interface)是一组函数,和其他系统函数结合起来用于创建网络应用,下图给出了典型的客户端-服务器事务的上下文中的套接字接口描述:
大多数现代操作系统上都实现了同一套套接字接口,适用于任何底层的协议。
套接字地址结构
因特网的套接字地址存放在如下的sockaddr_in的16字节结构中,其中的IP地址和端口号总是以网络字节顺序(大端法)存放的。
#include
/* 通用的socket地址结构 (用于connect, bind, 和accept) */
struct sockaddr {
unsigned short sa_family; /* 协议家族 */
char sa_data[ 14]; /* 地址数据 */
};
/* 因特网形式的socket地址结构 */
struct sockaddr_in {
unsigned short sin_family; /* 地址家族,一般都是AF_INET */
unsigned short sin_port; /* 网络字节顺序(大端表示法)的端口号 */
struct in_addr sin_addr; /* 网络字节顺序(大端表示法)的IP地址 */
unsigned char sin_zero[ 8]; /* 对sizeof(struct sockaddr)的填补 */
};
其中_in后缀是互联网络(internet)的缩写
connect函数、bind和accept函数要求一个指向与协议相关的套接字地址结构的指针,如何定义这些函数,使之能够接受各种类型的套接字地址结构,解决办法就是这个stuct sockaddr结构,我们将所有的与协议特定的结构的指针转换成这个通用结构就可以,因此定义了一个类型typedef struct sockaddr SA,使用的时候,将所有的sockaddr_in转换成 SA类型。
套接字函数整理
socket函数
客户端和服务器使用socket函数来创建一个套接字描述符(socket descriptor)
#include
int socket( int domain, int type, int protocol);
因此在我们的代码中,经常这样调用socket函数:
AF_INTE表示使用因特网、SOCK_STREAM表示该套接字用于因特网连接的一个端点
clientfd描述符仅是部分打开,还不能进行读写,要完成打开套接字的工作,取决于是客户端还是服务器。
connect函数
客户端进程通过connect函数与服务器进程建立连接
// 如果成功返回0,否则返回-1
int connect( int sockfd, struct sockaddr *serv_addr, int addrlen);
客户端进程视图与套接字地址为serv_addr的服务器建立因特网连接,其中addrlen是sizeof(sockaddr_in)。
connect函数会阻塞,一直到连接成功或出现错误。如果成功的话,sockfd就可以用来读写,并且得到的连接是由(客户端IP:客户端端口号,服务器IP:服务器端口号)来唯一标志的。
open_clientfd函数(将socket函数和connect函数包装成一个函数)
客户端可以用该函数,替代socket和connect函数来和主机域名为hostname、端口为port的服务器进行连接。
// 如果成功返回描述符,Unix错误返回-1,DNS错误返回-2
int open_clientfd( char *hostname, int port);
它返回套接字描述符,可以用来输入和输出,以下是该辅助函数open_clientfd的代码:
int clientfd; // 客户端的描述符
struct hostent *hp; // DNS主机条目结构体
struct sockaddr_in serveraddr; // 套接字地址的结构体
// 建立套接字
if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return - 1;
// 根据hostname查找DNS得到主机的IP和端口的信息,然后建立连接
if ((hp = gethostbyname(hostname)) == NULL)
return - 2;
// 将服务器的套接字结构清空
bzero(( char *) &serveraddr, sizeof(serveraddr));
// 写入服务器的协议族
serveraddr.sin_family = AF_INET;
// 写入服务器的IP地址,第一个参数是DNS主机IP列表的第一个地址,第二个参数是要建立的服务器套接字
bcopy(( char *)hp->h_addr_list[ 0], ( char *)&serveraddr.sin_addr.s_addr, hp->h_length);
// 写入服务器的端口号,这里使用htons将服务器的端口号转成网络字节顺序(大端法)
serveraddr.sin_port = htons(port);
// 客户端与服务器建立连接,这里clientfd是客户端自身的描述符,serveraddr则是服务器的套接字地址,这样就完成了映射
// 这里第二个参数进行了强制转换,将因特网的套接字地址,转换成了通用的套接字地址
if (connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)
return - 1;
return clientfd;
}
最终如果返回的是客户端描述符,则说明建立连接成功,可以用来进行通信了。
bind函数
套接字函数bind和listen、accept一样,被服务器用来和客户端建立连接
// 如果成功返回0,失败返回1
// 该函数将my_addr中的服务器套接字地址和套接字描述符sockfd联系起来,addrlen是sizeof(sockaddr_in)
// 其中my_addr是服务器自身建立的套接字地址,sockfd则是自身建立的套接字描述符
int bind( int sockfd, struct sockaddr *my_addr, int addrlen);
listen函数
客户端发送请求(主动实体) ---------> 服务器端等待并处理连接请求(被动实体)
内核会将socket函数建立的描述符默认为是主动套接字,就是说它存在于一个客户端里。因此服务器端需要用listen函数告诉内核:这个描述符应该是被动的,而不是主动的。
// 如果成功返回0,失败返回1
// 该函数将sockfd从一个主动套接字转化为一个监听套接字(listening socket),然后该套接字可以被动的接收客户端的请求而不是发送
// backlog表示服务器端可以监听的连接数目,一般设置为1024
int listen( int sockfd, int backlog);
open_listenfd函数(服务器端的socket函数+bind函数+listen函数)
// 如果成功返回资源描述符,否则返回-1
int open_listenfd( int port);
服务器用该函数来打开和返回一个监听描述符,该描述符在端口port上接收连接请求,以下是open_listenfd函数的代码:
// listenfd:服务器端的套接字描述符
int listenfd, optval= 1;
// serveraddr:服务器套接字地址结构
struct sockaddr_in serveraddr;
// 创建套接字描述符
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return - 1;
// 配置服务器,使得该套接字能够被立即的终止或重启,若不设置,重启的30秒内,客户端将无法请求该连接
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); // 设定IP地址为任意请求的客户端IP地址,并将其转化为网络字节顺序
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函数
// 成功则返回“已连接描述符(connected descriptor,不是监听描述符)”,失败返回-1
// 该函数等待客户端的请求到达监听描述符listenfd,然后将客户端的套接字地址写入addr中
// 返回的“已连接描述符”,是真正和客户端通信的描述符
int accept( int listenfd, struct sockaddr *addr, int *addrlen);
监听描述符和已连接描述符的区别:
- 监听描述符是客户端请求连接的端点,被创建一次,然后一直存在
- 已连接描述符是客户端和服务器建立了连接之后的服务器端点,连接成功就建立,用完就释放
- 为什么设置已连接描述符,是为了建立并发服务器的需要,这样的话多个客户端连接,可以建立不同的进程处理,我们只要把已连接描述符传给该进程使用即可
如下图所示,为监听描述符和已连接描述符之间的区别:
由上图可以看出,建立连接后,服务器新建了一个connfd描述符用于在clientfd和connfd之间传送数据
echo客户端和服务器的示例
客户端代码:
* echoclient.c - An echo client
*/
/* $begin echoclientmain */
#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
exit( 0);
}
host = argv[ 1];
port = atoi(argv[ 2]);
clientfd = Open_clientfd(host, port);
Rio_readinitb(&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);
}
服务器端代码:
* echoserveri.c - An iterative echo server
*/
/* $begin echoserverimain */
#include " csapp.h "
void echo( int connfd);
int main( int argc, char **argv)
{
int listenfd, connfd, port, clientlen;
struct sockaddr_in clientaddr;
struct hostent *hp;
char *haddrp;
if (argc != 2) {
fprintf(stderr, " usage: %s
exit( 0);
}
port = atoi(argv[ 1]);
listenfd = Open_listenfd(port);
while ( 1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
/* determine the domain name and IP address of the client */
hp = Gethostbyaddr(( const char *)&clientaddr.sin_addr.s_addr,
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
haddrp = inet_ntoa(clientaddr.sin_addr);
printf( " server connected to %s (%s)\n ", hp->h_name, haddrp);
echo(connfd);
Close(connfd);
}
exit( 0);
}
csapp.h部分代码:
* echo - read and echo text lines until client closes connection
*/
/* $begin echo */
#include " csapp.h "
void echo( int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf( " server received %d bytes\n ", n);
Rio_writen(connfd, buf, n);
}
}