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

本文详细介绍了套接字接口的功能及其实现方式,包括客户端-服务器通信的基本原理、套接字地址结构、创建套接字的各种函数(如socket、connect、bind、listen、accept等)及其用法,并附带示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

套接字接口(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);
    }
}

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

转载于:https://www.cnblogs.com/crazyant/archive/2012/04/11/2442372.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值