套接字接口(socket interface)是一组函数,和其他系统函数结合起来用于创建网络应用,下图给出了典型的客户端-服务器事务的上下文中的套接字接口描述:
大多数现代操作系统上都实现了同一套套接字接口,适用于任何底层的协议。
套接字地址结构
因特网的套接字地址存放在如下的sockaddr_in的16字节结构中,其中的IP地址和端口号总是以网络字节顺序(大端法)存放的。
#include <socketbits.h>
//
包含了struct sockaddr,该文件也被包含在socket.h中
#include <netinet/
in.h>
//
包含了struct sockaddr_in
/*
通用的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 <sys/types.h>
#include <sys/socket.h>
int socket(
int domain,
int type,
int protocol);
因此在我们的代码中,经常这样调用socket函数:
clientfd = Socket(AF_INET, SOCK_STREAM,
0);
AF_INTE表示使用因特网、SOCK_STREAM表示该套接字用于因特网连接的一个端点
clientfd描述符仅是部分打开,还不能进行读写,要完成打开套接字的工作,取决于是客户端还是服务器。
connect函数
客户端进程通过connect函数与服务器进程建立连接
#include <sys/socket.h>
//
如果成功返回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的服务器进行连接。
#include
"
csapp.h
"
//
如果成功返回描述符,Unix错误返回-1,DNS错误返回-2
int open_clientfd(
char *hostname,
int port);
它返回套接字描述符,可以用来输入和输出,以下是该辅助函数open_clientfd的代码:
int open_clientfd(
char *hostname,
int port) {
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一样,被服务器用来和客户端建立连接
#include <sys/socket.h>
//
如果成功返回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函数告诉内核:这个描述符应该是被动的,而不是主动的。
#include <sys/socket.h>
//
如果成功返回0,失败返回1
//
该函数将sockfd从一个主动套接字转化为一个监听套接字(listening socket),然后该套接字可以被动的接收客户端的请求而不是发送
//
backlog表示服务器端可以监听的连接数目,一般设置为1024
int listen(
int sockfd,
int backlog);
open_listenfd函数(服务器端的socket函数+bind函数+listen函数)
#include
"
csapp.h
"
//
如果成功返回资源描述符,否则返回-1
int open_listenfd(
int port);
服务器用该函数来打开和返回一个监听描述符,该描述符在端口port上接收连接请求,以下是open_listenfd函数的代码:
int open_listenfd(
int port){
//
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函数
#include <sys/socket.h>
//
成功则返回“已连接描述符(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 <host> <port>\n
", argv[
0]);
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 <port>\n
", argv[
0]);
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);
}
}
代码没有运行成功,提示缺少文件