Linux网络编程系列二:套接字接口

套接字接口(socket interface)是一组函数,和其他系统函数结合起来用于创建网络应用,下图给出了典型的客户端-服务器事务的上下文中的套接字接口描述:

Linux网络编程系列二:套接字接口_第1张图片 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

大多数现代操作系统上都实现了同一套套接字接口,适用于任何底层的协议。

套接字地址结构

因特网的套接字地址存放在如下的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);

监听描述符和已连接描述符的区别:

  • 监听描述符是客户端请求连接的端点,被创建一次,然后一直存在
  • 已连接描述符是客户端和服务器建立了连接之后的服务器端点,连接成功就建立,用完就释放
  • 为什么设置已连接描述符,是为了建立并发服务器的需要,这样的话多个客户端连接,可以建立不同的进程处理,我们只要把已连接描述符传给该进程使用即可

如下图所示,为监听描述符和已连接描述符之间的区别:

Linux网络编程系列二:套接字接口_第2张图片

由上图可以看出,建立连接后,服务器新建了一个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);
    }
}

代码没有运行成功,提示缺少文件

你可能感兴趣的:(linux)