APUE 学习笔记(十一) 网络IPC:套接字

1. 网络IPC 

套接字接口既可以用于计算机之间进程通信,也可以用于计算机内部进程通信
 
套接字描述符在Unix系统中是用文件描述符实现的
 
/* 创建一个套接字 */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

protocol通常是0,表示按给定的域或套接字类型选择默认协议
在AF_INET中,SOCK_STREAM的默认协议是 TCP
在AF_INET中,SOCK_DGRAM的默认协议是 UDP
 
套接字通信是双向的,可以采用 shutdown来禁止套接字上的 输入/输出
 
#include <sys/socket.h>
int shutdown(int sockfd, int how);
how为SHUT_RD(关闭读端)时,无法从套接字上读取数据
how为SHUT_WR(关闭写端)时,无法向套接字发送数据
 

2. 套接字地址

大端:最大字节地址对应于数字最低有效字节,阅读序
小端:最小字节地址对应于数字最低有效字节,逆阅读序
 
例如:0x04030201
大端机:04==> ch[0]  01==> ch[3]
小端机:04==> ch[3]  01==> ch[0]
不管字节如何排序,数字最高位总是在左边,最低位总是在右边

 

TCP/IP协议栈采用大端字节序
TCP/IP提供4个通用函数来处理字节序转换:
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostint32);   // 返回值:以网络字节序表示的32位整型数
uint16_t htons(uint16_t hostint16);  // 返回值:以网络字节序表示的16位整型数
uint32_t ntohl(uint32_t  netint32);   // 返回值:以主机字节序表示的32位整型数
uint16_t ntohl(uint16_t  netint16);   // 返回值:以主机字节序表示的16位整型数
"h"代表 host主机字节序, “n”代表net网络字节序,“l”代表32位long整型,“s”代表16位short整型
 
通用地址结构sockaddr
struct sockaddr {
    sa_family_t   sa_family;         /* address family */
    char          sa_data[14];     /* variable-length address */
};
struct sockaddr 一共为18字节(32位地址+14字节填充) 

 在IPv4 套接字地址结构 sockaddr_in (in表示internet网络):

struct in_addr {
    in_addr_in   s_addr;           /* IPv4 address*/
};

struct sockaddr_in {
    sa_family_t       sin_family;   /* address family */
    in_port_t         sin_port;      /* port number */
    struct in_addr    sin_addr;     /* IPv4 address */
    unsigned char     sin_zero[8];
};
struct sockaddr_in 一共为18字节(4字节family + 16位端口号 + 32位IPv4地址 + 8字节填充)

 二进制地址格式和 点分十进制格式转换:

#include <arpa/inet.h>

const char* inet_ntop(int domain, const char* addr, char* str, socklen_t size);
int         inet_pton(int domain, const char* str, void* str);
参数domain可以支持 AF_INET和AF_INET6
 
地址信息查询:
#include <sys/socket.h>
#include <netdb.h>

int   getaddrinfo(const char* host, const char* service, const struct addrinfo* hint, struct addrinfo* res);
void  freeaddrinfo(struct addrinfo* ai);
struct addrinfo {
    int                       ai_flags;        /* customize behavior */
    int                       ai_family;       /* address family */
    int                       ai_socktype;     /* socket type */
    int                       ai_protocol;     /* protocol */
    socklen_t                 ai_addrlen;      /* length in bytes of address */
    struct sockaddr*          ai_addr;         /* address */
    char*                     ai_canonname;
    struct addrinfo*          ai_next;        /* next in list */
    ....
}; 
函数getaddrinfo 可以将 IPv4和IPv6代码统一起来,所以网络编程中套接字地址信息都必须使用此函数,便于统一和移植
 
函数getaddrinfo 允许将一个主机名和服务器名映射到一个地址,需要提供 host主机名 或 service 服务器名,否则指针设为空
主机名字可以是一个 节点名或者点分十进制表示的主机地址
函数getaddrinfo返回 一个 结构体 struct addrinfo的链表,freeaddrinfo来释放这个结构体链表
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <stdio.h>

void print_family(struct addrinfo* aip)
{
    fprintf(stdout, "family:");
    switch (aip->ai_family) {
        case AF_INET:
            fprintf(stdout, "inet");
            break;
        case AF_INET6:
            fprintf(stdout, "inet6");
            break;
        case AF_UNIX:
            fprintf(stdout, "unix");
            break;
        case AF_UNSPEC:
            fprintf(stdout, "unspecfied");
            break;
        default:
            fprintf(stdout, "unknown");
    }
}

void print_type(struct addrinfo* aip)
{
    fprintf(stdout, "type");
    switch (aip->ai_socktype) {
        case SOCK_STREAM:
            fprintf(stdout, "stream");
            break;
        case SOCK_DGRAM:
            fprintf(stdout, "datagram");
            break;
        case SOCK_SEQPACKET:
            fprintf(stdout, "seqpacket");
            break;
        case SOCK_RAW:
           fprintf(stdout, "raw");
            break;
        default:
            fprintf(stdout, "unknown (%d)", aip->ai_socktype);
    }
}

void print_protocol(struct addrinfo* aip)
{
    fprintf(stdout, "protocol");
    switch (aip->ai_protocol) {
        case 0:
            fprintf(stdout, "default");
            break;
        case IPPROTO_TCP:
           fprintf(stdout, "tcp");
            break;
        case IPPROTO_UDP:
            fprintf(stdout, "udp");
            break;
        case IPPROTO_RAW:
            fprintf(stdout, "raw");
            break;
        default:
            fprintf(stdout, "unknown (%d)", aip->ai_protocol);
    }
}

void print_flags(struct addrinfo* aip)
{
    fprintf(stdout, "flags");
    if (aip->ai_flags == 0) {
       fprintf(stdout, "0");
    } else {
        if (aip->ai_flags & AI_PASSIVE) {
            fprintf(stdout, "passive");
        }
        if (aip->ai_flags & AI_CANONNAME) {
            fprintf(stdout, "canon");
        }
        if (aip->ai_flags & AI_NUMERICHOST) {
            fprintf(stdout, "numhost");
        }
    }
}
int main(int argc, char* argv[])
{
    struct addrinfo* ailist = NULL;
    struct addrinfo* aip = NULL;
    struct addrinfo hint;
    struct sockaddr_in* sinp;
    const  char* addr = NULL;
    char   abuf[INET_ADDRSTRLEN];

    if (argc != 3) {
        fprintf(stdout, "usage:%s <hostname> <service>", argv[0]);
        return 1;
    }

    hint.ai_flags = AI_CANONNAME;
    hint.ai_family = 0;
    hint.ai_socktype = 0;
    hint.ai_protocol = 0;
    hint.ai_addrlen = 0;
    hint.ai_canonname = NULL;
    hint.ai_addr = NULL;
    hint.ai_next = NULL;
    int ret = getaddrinfo(argv[1], argv[2], &hint, &ailist);
    if (ret != 0) {
        fprintf(stderr, "getaddrinfo error
");
        return 1;
    }

    for (aip = ailist; aip != NULL; aip = aip->ai_next) {
        print_flags(aip);
        print_family(aip);
        print_type(aip);
        print_protocol(aip);
        fprintf(stdout, "
	host %s", aip->ai_canonname ? aip->ai_canonname : '-');
        if (aip->ai_family == AF_INET) {
            sinp = (struct sockaddr_in*)aip->ai_addr;
            addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN);
            fprintf(stdout, "address %s", addr ? addr : "unknown");
            fprintf(stdout, "port %d", ntohs(sinp->sin_port));
        }
       fprintf(stdout, "
");
    }
    return 0;
}

套接字与地址绑定:

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr* addr, socklen_t len);
如果将addr指定为 INADDR_ANY,则套接字可以接收到这个系统所安装的所有网卡的数据包
 

3. 建立连接

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* addr, socklen_t len);
客户端调用connect函数,请求与服务器建立连接,addr为服务器地址
 
处理瞬时connect错误:
#include <sys/socket.h>
#define MAXSLEEP 128

int connect_retry(int sockfd, const struct sockaddr* addr, socklen_t len)
{
    /* try to connect with exponential backoff */
    for (int nsec = 1; nsec <= MAXSLEEP; nsec << 1) {
        if (connect(sockfd, addr, len) == 0) {
            /* connection accepted */
            return 0;
        }

        /* delay before trying again */
        if (nsec <= MAXSLEEP / 2)
            sleep(nsec);
    }
    return -1;
}
这个函数使用了 指数补偿的算法,如果调用connect失败,进程就休眠一小段时间再尝试连接,每循环一次就加倍每次尝试的延迟
#include <sys/socket.h>

int listen(int sockfd, int backlog);
服务器调用listen函数来宣告自己可以接受连接请求
参数backlog提供了一个提示,用于表示该进程所要入队的连接请求数量,一旦队列满,系统会拒绝多余连接请求
 
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* len); 
服务器调用accept函数来获取连接请求并建立连接,函数返回 已连接描述符,已连接描述符与监听描述符不同
 
addr和len都是客户端地址参数,如果不关心客户端标识,可以将这两个参数设为NULL,否则,在调用accept之前,必须将addr设为足够大的缓冲区来存放地址,accept调用返回时 会回填客户端的地址和地址大小
 
服务器可以使用select或epoll来等待一个连接请求,一个等待连接的客户端请求套接字会以可读的形式出现

 4. 数据传输

#include <sys/socket.h>

ssize_t send(int sockfd, const  void* buf, size_t bytes, int flags);    // 等同于 write,套接字必须已连接
ssize_t sendto(int sockfd, const void* buf, size_t bytes, int flags, const struct sockaddr* dstaddr, socklen_t dstlen);
对于面向连接的套接字,使用send函数,目标地址蕴含在连接中,在此忽略
对于面向无连接的套接字,使用sendto函数,必须指定 目标地址
 
 
#include <sys/socket.h>

ssize_t recv(int sockfd, void* buf, size_t bytes, int flags);  //类似于 read
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* addr, socklen_t* addrlen);
对于面向连接的套接字,使用recv函数,目标地址蕴含在连接中,在此忽略
对于面向无连接的套接字,使用recvfrom函数,必须指定 目标地址
 
 
/* tcp_connect for client:
 * hostname or ip:   www.google.com or 127.0.0.1
 * service  or port: http or 9877
 */
int tcp_connect(const char* hostname, const char* service) 
{
    struct addrinfo hints;
    struct addrinfo* result;
    struct addrinfo* rp;

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    int res = getaddrinfo(hostname, service, &hints, &result);
    if (res != 0) {
            fprintf(stderr, "tcp_connect error for %s, %s: %s", hostname, service, gai_strerror(res));
            exit(0);
    }

    int sockfd;    
    for (rp = result; rp != NULL; rp = rp->ai_next) {
        sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sockfd < 0)
            continue;
        int rc = connect(sockfd, rp->ai_addr, rp->ai_addrlen);
        if (rc == 0)
            break;
        close(sockfd);
    }
    if (rp == NULL) {
        unix_error("tcp_connect error");
    }
    freeaddrinfo(result);
    return sockfd;
}
/* tcp_listen for server:
 * hostname or ip:   www.google.com or 127.0.0.1
 * service  or port: http or 9877
 */

int tcp_listen(const char* hostname, const char* service, socklen_t* paddrlen) 
{
    struct addrinfo hints;
    struct addrinfo* result;
    struct addrinfo* rp;

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_flags    = AI_PASSIVE;
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    int res = getaddrinfo(hostname, service, &hints, &result);
    if (res != 0) {
        fprintf(stderr, "tcp_listen error for %s, %s: %s", hostname, service, gai_strerror(res));
        exit(0);
    }

    int listenfd;
    for (rp = result; rp != NULL; rp = rp->ai_next) {
        listenfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (listenfd < 0)
            continue;
        int rc = bind(listenfd, rp->ai_addr, rp->ai_addrlen);
        if (rc == 0)
            break;
        Close(listenfd);
    }
    if (rp == NULL) {
        unix_error("tcp_listen error");
    }
    Listen(listenfd, LISTENQ);
    if (paddrlen) {
        *paddrlen = rp->ai_addrlen;
    }
    freeaddrinfo(result);
    return listenfd;
}

5. 套接字选项

#include <sys/socket.h>

int setsockopt(int sockfd, int level, int option, const void* val, socklen_t len);
int getsockopt(int sockfd, int level, int option, void* val, socklen_t lenp);
 

6. 带外数据

带外数据 允许更高优先级的数据比普通数据优先传输,TCP支持带外数据,UDP不支持
TCP仅支持一个字节的带外数据,但是允许带外数据在普通传输机制流之外传输,为了产生带外数据,需要在send函数中指定 MSG_OOB标志
当带外数据出现在套接字读取队列时,select函数会返回一个文件描述符并且拥有一个异常状态挂起
 

7. 非阻塞I/O

recv函数没有数据可读时会阻塞等待,当套接字输出队列没有足够空间来发送消息时 send函数会阻塞
如果套接字是非阻塞模式,这些情况下,这些函数不是阻塞而是失败,设置errno为EWOULDBLOCK或者EAGAIN
原文地址:https://www.cnblogs.com/wwwjieo0/p/3741177.html