資訊人筆記

Work hard, Have fun, Make history!

使用者工具

網站工具


course:nctu-高等unix程式設計:chapter16

Network IPC:Socket

0x00 Outline

  • Introduction
  • Socket Descriptors
  • Addressing
  • Connection Establishment
  • Data Transfer
  • Socket Option

0x01 Introduction

  • classical IPC’s 讓跑在同一台電腦上的 processes 可以互相通訊
  • network IPC 則可讓跑在不同電腦的 processes 互相通訊
  • network IPC 可用在 inter-machine communication 和 intra-machine communication
  • 本章節主要討論 TCP/IP Sockets

0x02 Socket Descriptors

socket descriptor

  • A socket is an abstraction of a communication endpoint
  • socket descriptor 在 UNIX 系統中是以 file descriptor 方式實作的
  • 對於 file descriptor 操作的那些 function 如 read、write 也同樣能對 socket descriptor 操作
#include <sys/types.h>
#include <sys/socket.h>
 
int socket(int domain, int type, int protocol);
/*return file (socket) descriptor if success, or -1 on error*/
  • 建立一個 socket
  • domain:
    • AF_INET: IPv4 Internet protocols
    • AF_INET6: IPv6 Internet protocols
    • AF_UNIX or AF_LOCAL: Local communication
  • type:
    • SOCK_STREAM: stream TCP
    • SOCK_DGRAM: datagram UDP
  • protocol: 0(default), IPPROTO_TCP, IPPROTO_UDP

Socket Descriptors and File I/O Functions

  • close
  • dup,dup2
  • fcntl
  • read
  • write

Release a socket descriptor

#include <sys/socket.h>
 
int shutdown(int socket, int how);
/*returns 0 if success, or -1 on error*/
  • how:
    • SHUT_RD
    • SHUT_WR
    • SHUT_RDWR
  • socket communication 是雙向的,相較於 close,利用 shutdown 可以僅關閉單一方向的通訊
  • close 會關閉呼叫它的 process 的 file descriptor 參照,file descriptor 參照數量 -1,但其他 process 仍可對 file descriptor 繼續操作,直到 file descriptor 參照數量為 0 時 file descriptor 才會被關閉
  • 如果多個 processes 參照同個 file descriptor,而其中一個 process 呼叫 shutdown,則 file descriptor 會立即被關閉,其他 processes 也無法繼續操作 file descriptor

0x03 Addressing

identify socket

  • 使用 addressing schemes 來識別 socket
    • AF_UNIX : a pathname
    • AF_INET + SOCK_STREAM + IP_PROTO_TCP : IPv4 address and TCP port number
    • AF_INET + SOCKET_DGRAM + IP_PROTO_UDP : IPv4 address and UDP port number
    • AF_INET6 + SOCK_STREAM + IP_PROTO_TCP : IPv6 address and TCP port number
    • AF_INET6 + SOCKET_DGRAM + IP_PROTO_UDP : IPv6 address and UDP port number

byte ordering

  • 電腦世界中有兩種 byte order
    • Big-Endian(正序)
    • Little-Endian(倒序)
  • Network Byte Order 是 Big-Endian
  • Host Byte Order 則由電腦架構而定,x86 為 Little-Endian
  • 我們在寫網路程式時,需要確認 port number 跟 IP address 都是 Network Byte Order,而 C 語言提供了一些函式,讓我們直接假設 Host Byte Order 皆不是 Network Byte Order,而可使用這些函式進行轉換
#include <arpa/inet.h>
 
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 這些函式可使我們將 unsigned integer,unsigned short integer 在 Host Byte Order 和 Network Byte Order 間轉換
    • h means host
    • n means network
    • l means long
    • s means short

address formats

  • Generic:
/*On Linux*/
struct sockaddr
{
    sa_family_t sa_family;    /* address family */
    char sa_data[14];         /* variable-length address */
};
 
/*On some other system*/
struct sockaddr
{
    unsigned char sa_len;     /* total length */
    sa_family_t sa_family;    /* address family */
    char sa_data[14];         /* variable-length address */
};
  • IPv4 on Linux
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
 
struct in_addr
{
    in_addr_t 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];
};
  • IPv6 on Linux
typedef uint16_t in_port_t;
struct in6_addr
{
    union
    {                                /* IPv4 address */
        uint8_t __u6_addr8[16];
        uint16_t __u6_addr16[8];
        uint32_t __u6_addr32[4];
    } __in6_u;
};
 
struct sockaddr_in6
{
    sa_family_t sin_family;                /* address family */
    in_port_t sin6_port;                   /* port number */
    uint32_t sin6_flowinfo;                /* IPv6 flow info */
    struct in6_addr sin6_addr;             /* IPv6 address */
    uint32_t sin6_scope_id;                /* IPv6 scope id */
};

conversion of address

  • 為了將 address 已可讀的形式印出來,我們須將 numeric address 轉換為 text
#include <arpa/inet.h>
 
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/*Returns: pointer to address string on success, or NULL on error*/
  • 將 text formatted address 轉回 numeric address
#include <arpa/inet.h>
 
int inet_pton(int af, const char *src, void *dst);
/*Returns: 1 on success, 0 if the format is invalid, or -1 on error and errno=EAFNOSUPPORT*/
if(inet_pton(AF_INET, argv[1], &addr4) == 1)
{
    printf("IPv4: 0x%08x\n", htonl(addr4.s_addr));
}
 
if(inet_pton(AF_INET6, argv[1], &addr6) == 1)
{
    printf("IPv6: 0x%08x%08x%08x%08x\n",
        htonl(addr6.__in6_u.__u6_addr32[0]),
        htonl(addr6.__in6_u.__u6_addr32[1]),
        htonl(addr6.__in6_u.__u6_addr32[2]),
        htonl(addr6.__in6_u.__u6_addr32[3]));
}

address lookup

  • 本機上我們可以由三項條件做位址查詢
    • known hosts
    • known protocols
    • known services
  • 另外可通過 DNS 查詢 hostname
  • known hosts
    • 在系統中可以查看 /etc/hosts 檔案
struct hostent
{
    char *h_name;          /*official name of host*/
    char **h_aliases;      /* alias list */
    int h_addrtype;        /* host address type */
    int h_length;          /* length of address */
    char **h_addr_list;    /* list of addresses */
}
/* for AF_INET */
#include <sys/socket.h>
 
struct hostent *gethostent(void);
/*Returns: valid pointer if success, or NULL on error*/
 
void sethostent(int stayopen);
void endhostent(void);
  • gethostent() is not thread-safe
  • sethostent(),若 database 未開啟則 open 已開啟則 rewind
  • endhostent(),關閉 host database
  • 這兩個 function 在 DNS lookup 時會有不同意義
int main()
{
    int i;
    char buf[64];
    struct hostent *h;
 
    while((h = gethostent()) != NULL)
    {
        if(h->h_addrtype != AF_INET)
            continue;
        printf(“name=%s, addr={, h->h_name);
 
        for(i = 0; h->h_addr_list[i] != NULL; i++)
        {
            printf("%s ", inet_ntop(AF_INET, h->h_addr_list[i], buf, sizeof(buf)));
        }
 
        printf("}\n");
    }
 
    return(0);
}
name=cshome.cs.nctu.edu.tw, addr={ 140.113.235.101 }
name=csduty.cs.nctu.edu.tw, addr={ 140.113.235.102 }
  • known protocols
    • 在系統中可以查看 /etc/protocols 檔案
truct protoent
{
    char *p_name;        /*officialprotocolname*/
    char **p_aliases;    /* alias list */
    int p_proto;         /* protocol number */
}
#include <netdb.h>
 
struct protoent *getprotoent(void);
/*Returns: valid pointer if success, or NULL on error*/
 
struct protoent *getprotobyname(const char *name);
/*returns a protoent structure for the entry from the database that matches the protocol name,NULL on error*/
 
struct protoent *getprotobynumber(int proto);
/*returns a protoent structure for the entry from the database that matches the protocol number,NULL on error*/
 
void setprotoent(int stayopen);
void endprotoent(void);
  • getprotobyname 和 getprotobynumber 都不是 thread-safe
int main()
{
    int i;
    struct protoent *p;
 
    while((p = getprotoent()) != NULL)
    {
        printf("name=%s (%d), ", p->p_name, p->p_proto);
        printf("alias={ ");
 
        for(i = 0; p->p_aliases[i] != NULL; i++)
        {
            printf("%s ", p->p_aliases[i]); printf("}\n");
        }
    }
 
    return(0);
}
name=ip (0), alias={ IP }
name=icmp (1), alias={ ICMP }
name=igmp (2), alias={ IGMP }
...
name=tcp (6), alias={ TCP }
...
name=udp (17), alias={ UDP }
...
  • known services
    • 在系統中可以查看 /etc/services 檔案
struct servent
{
    char *s_name;      /*officialservicename*/
    char **S_aliases;  /* alias list */
    int s_port;        /* port number */
    char *s_proto;     /*protocoltouse*/
}
#include <netdb.h>
 
struct servent *getservent(void);
/*Returns: valid pointer if success, or NULL on error*/
 
struct servent *getservbyname(const char *name, const char *proto);
/*returns a servent structure for the entry from the database that matches the service name using protocol proto, or NULL on error*/
 
struct servent *getservbyport(int port, const char *proto);
/*returns a servent structure for the entry from the database that matches the service port(network byte order) using protocol proto, or NULL on error*/
 
void setservent(int stayopen);
 
void endservent(void);
  • getservbyport 的 port 須為 network byte order
  • getservent, getservbyname, getservbyport 都不是 thread-safe
int main()
{
    int i;
    struct servent *s;
 
    while((s = getservent()) != NULL)
    {
        printf("name=%s (%s/%d), ", s->s_name, s->s_proto, ntohs(s->s_port));
        printf("alias={ ");
 
        for(i = 0; s->s_aliases[i] != NULL; i++)
        {
            printf("%s ", s->s_aliases[i]); printf("}\n");
        }
    }
    return(0);
}
...
name=telnet (tcp/23), alias={ }
name=ftp (tcp/21), alias={ }
name=pop3 (tcp/110), alias={ pop-3 }
name=www (tcp/80), alias={ http }
...
  • hostname via DNS
#include <netdb.h>
extern int h_errno;
 
struct hostent *gethostbyname(const char *name);
/*Returns: valid pointer if success, or NULL on error*/
 
#include <sys/socket.h>
 
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
/*Returns: valid pointer if success, or NULL on error*/
  • gethostbyaddr 的參數 type 可為 AF_INET 或 AF_INET6
  • 此兩個 function 預設是使用 UDP protocol,可使用 sethostent() 變更,如果 stayopen 為 true,會使用 TCP socket 連線,且 query 成功時,連線會保持,若 stayopen 為 false,則採用預設 UDP datagrams
  • 此兩個 function 也是 not thread-safe

thread-safe query

  • 前面提了許多 function 都是 not thread-safe 的,接下來介紹幾個 thread-safe 的 function
  • query address and port
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
 
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
/*Returns: zero if success, or nonzero error code on error*/
  • node: 要查詢的節點(name or address)
  • service: service 名稱
  • hints: 查詢標準
    • flags (see the next slide)
    • address family (AF_INET/AF_INET6)
    • socktype (SOCK_DGRAM/SOCK_STREAM, can be 0)
    • protocol (can be 0)
    • Other fields must be zero
  • res: return the queried result
struct addrinfo
{
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    struct sockaddr *ai_addr;
    char *ai_canonname;
    struct addrinfo *ai_next;
};
  • 當 getaddrinfo 有 error 時無法從 perror 或 stderr 取得錯誤訊息,需要呼叫 gai_strerror function 將 error code 轉為可讀的文字訊息
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
 
const char *gai_strerror(int error);
int main(int argc, char *argv[])
{
    int s;
    struct addrinfo hints, *result, *rp;
 
    if (argc < 3)
    {
        fprintf(stderr, "usage: %s host port\n", argv[0]);
        exit(-1);
    }
 
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;                 /* allow IPv4 or IPv6 */
    hints.ai_socktype = SOCK_STREAM;           /* stream socket */
    hints.ai_flags = 0;
    hints.ai_protocol = 0;                     /* any protocol */
 
    if((s=getaddrinfo(argv[1], argv[2], &hints, &result))!=0)
    {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(-1);
    }
 
    for(rp = result; rp != NULL; rp = rp->ai_next)
    {
        struct sockaddr_in *p = (struct sockaddr_in*) rp->ai_addr;
        printf( "%s:%d\n", inet_ntoa(p->sin_addr), ntohs(p->sin_port));
    }
 
    return(0);
}
$ ./getaddrinfo google.com www
74.125.45.100:80
209.85.171.100:8
74.125.67.100:80
  • query of name and service
int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);
/*Returns:zero if success,or nonzero on error*/
int main(int argc, char *argv[])
{
    struct sockaddr_in sin;
    char host[64], serv[64];
    int s;
 
    if (argc < 3)
    {
        fprintf(stderr, "usage: %s ip port\n", argv[0]);
        exit(-1);
    }
 
    bzero(&sin, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr(argv[1]);
    sin.sin_port = htons(atoi(argv[2]));
 
    if((s = getnameinfo((struct sockaddr*) &sin, sizeof(sin), host, sizeof(host), serv, sizeof(serv), 0)) != 0)
    {
        fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));
        exit(-1);
    }
 
    printf("%s:%s\n", host, serv);
    return(0);
}

0x04 Connection Establishment

server connect step

  • 建立 socket file descriptor
  • 準備一個 sockaddr_in data structure,將 socket address bind 到 socket descriptor
    • port number 不能小於 1024
  • listen for incoming connection
  • accept incoming connection

client connect step

  • 建立 socket file descriptor
  • 準備一個 sockaddr_in data structure
  • connect to server

socket function

#include <sys/socket.h>
 
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
/*Returns: zero if success, or -1 on error*/
#include <sys/socket.h>
 
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
/*Returns: zero if success, or -1 on error*/
#include <sys/socket.h>
 
int listen(int socket, int backlog);
/*Returns: zero if success, or -1 on error*/
  • backlog is the number of outstanding connect requests in a queue
  • backlog 在 Linux 上最大是 128 (SOMAXCONN constant)
  • queue 一旦滿了,系統會拒絕其他新的連線
#include <sys/socket.h>
 
int accept(int sockfd, struct sockaddr *restrict address, socklen_t *restrict address_len);
/*Returns: file (socket) descriptor if success, or -1 on error*/
  • The returned descriptor is the socket connected to the client
  • 新的 socket descriptor 和舊的 sockfd 會有相同 socket type 和 address family
  • address 保存了 client address 和 port,若我們不需要這些資訊可將此設為 NULL
  • 如果沒有等到 connect request,connect 會 block 直到有連線請求到達
#include <sys/socket.h>
 
int getsockname(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
int getpeername(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
  • getsockname get the local address bound to a socket
  • getpeername get the remote address bound to a socket
  • address 和 address_len 要先宣告,在呼叫這兩個函式前,address_len 要設成 address 的 data structure(sockaddr) 的長度

0x05 Data Transfer

send data

  • 可以使用 write function 對 file descriptor 操作
  • 也可使用較彈性的 send/sendto function
#include <sys/socket.h>
 
ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);
/*Returns: number of bytes sent if success, or -1 on error*/
  • send function 適用於連線導向的 socket
  • sendto function 可用於連線或非連線導向的 socket
  • 如果 send function 的 flag 設為 0,基本上相等於 write function
  • 在非連線導向中,我們要明確指定 dest_addr 參數

receive data

  • 可以使用 read function 對 file descriptor 操作
  • 也可使用較彈性的 recv/recvfrom function
#include <sys/socket.h>
 
ssize_t recv(int socket, void *buffer, size_t length, int flags);
ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);
/*Returns: number of bytes received if success, 0 if no messages are available and peer has done an orderly shutdown, or -1 on error*/
  • recv function 適用於連線導向的 socket
  • recvfrom function 可用於連線或非連線導向的 socket
  • 如果 recv function 的 flag 設為 0,基本上相等於 read function
  • 在非連線導向中,recvfrom 的 address 參數會記錄 data sender 的 address 和 port

0x06 Socket Option

#include <sys/socket.h>
 
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
int getsockopt(int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict option_len);
/*Returns: zero if success, or -1 on error*/
  • The level argument identify the protocol (by a protocol number) to apply.For example, IPPROTO_IP, IPPROTO_TCP…

0x06 參考資料

course/nctu-高等unix程式設計/chapter16.txt · 上一次變更: 127.0.0.1