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\n");
        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, "\n\thost %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, "\n");
    }
    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
相關文章
相關標籤/搜索