APUE第16章 套接字

上一章的IPC仅限于本机的进程间通讯,这一章讲述的是网络间的进程间通讯(network IPC)

套接字描述符

Socket描述符在UNIX系统中被当做是一种文件描述符,有很多文件I/O相关函数可以直接用,但跟偏移相关等的函数不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <sys/socket.h>
int socket(int domain, int type.int protocal);
/**
domain{
AF_INET IPv4
AF_INET6 IPv6
AF_UNIX UNIX域
AF_UPSPEC 未指定
PF_PACKET 获取链路层的包
}
type{
SOCK_DGRAM UDP类型包
SOCK_RAW 原生的IP协议数据报接口
SOCK_SEQPACKET 固定长度的有序可靠报文传递
SOCK_STREAM TCP类型使用的包
}
protocol{
IPPROTO_IP IPv4
IPPROTO_IPV6 IPv6
IPPROTO_ICMP ICMP协议,ping用
IPPROTO_RAW 原始IP数据包协议
IPPROTO_TCP 传输控制协议
IPPROTO_UDP UDP层协议
}
*/
int shutdown(int sockfd, int how);
/*
how{
SHUT_RD 关闭读端(0)
SHUT_WR 关闭写端(1)
SHUT_RDWR 关闭读写(2)
}
*/

shutdown函数的应用和close的区别

  1. close函数只是减少引用计数1,只有引用计数为0了内核才会发送FIN包;而shutdown会影响所有持有这个socket的进程(如通过fork函数得到的)
  2. shutdown可以只关闭读端或者写端,close不可设置选项

shutdown后的影响

来自于Stack Overflow:

Shutting down the read side of a socket will cause any blocked recv (or similar) calls to return 0 (indicating graceful shutdown). I don’t know what will happen to data currently traveling up the IP stack. It will most certainly ignore data that is in-flight from the other side. It will not affect writes to that socket at all.

socket中遇到SIGPIPE信号

  1. 当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
  2. 又或者当一个进程向某个已经收到RST的socket执行写操作是,内核向该进程发送一个SIGPIPE信号。该信号的缺省行为是终止进程,因此进程必须捕获它以免不情愿的被终止。
  3. 检测方式:先设置忽略该信号,然后read或者write失败的话检测errno是不是EPIPE。
  4. 另外,如果使用多进程模型且父进程不想关心子进程的话,为防止子进程崩溃产生了僵死进程,应该设置忽略SIGCHLD信号。

数据链路层的包监听

sock_raw(注意一定要在root下使用)原始套接字编程可以接收到本机网卡上的数据帧或者数据包,对于监听网络的流量和分析是很有作用的。可用下列方式创建这种socket

  1. SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)```发送接收ip数据包,不能用IPPROTO_IP,因为如果是用了IPPROTO_IP,系统根本就不知道该用什么协议。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    2. ```socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))```发送接收以太网数据帧
    3. 如socket的第一个参数使用PF_INET,第二个参数使用SOCK_RAW,则可以得到原始的IP包。

    ## 寻址
    ### 字节序
    1. 大端:高字节地址放低位,低字节地址放高位
    2. 小端:高字节地址放高位,低字节地址放低位
    ```cpp
    #include <arpa/inet.h>
    //host to network in long
    uint32_t htonl(uint32_t hostint32);
    //host to network in short
    uint16_t htons(uint16_t hostint16);
    //network to host in long
    uint32_t ntohl(uint32_t netint32);
    //network to host in short
    uint16_t ntohs(uint16_t netint16);

地址格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct sockaddr{
sa_family_t sa_family; //address family
char sa_data[]; //variable-length address
//......
};
//For IPv4
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
};
//For IPv6
struct in6_addr{
uint8_t s6_addr[16]; //IPv6 address
};
struct sockaddr_in6{
sa_family_t sin6_family; //address family
in_port_t sin6_port;
unit32_t sin6_flowinfo; //traffic classs and flow info
struct in6_addr sin6_addr; //IPv6 address
uint32_t sin6_scope_id; //set of interfaces for scope
};
//For address transformation in IPv4
in_addr_t inet_addr(const char *cp);
//such as "127.0.0.1" to address
char *inet_ntoa (struct in_addr);
int inet_aton(const char *string, struct in_addr* addr);
//成功返回非0,失败返回0
//For address transformation in both IPv4 and IPv6
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, coklen_t size);
//成功返回字符串,否则NULL
int inet_pton(int domain, const char *restrict str, void *restrict addr);
//成功返回1,格式无效返回0,出错-1
/*
domain支持两个值:AF_INET和AF_INET6
socklen_t一般是INET_ADDRSTRLEN或INET6_ADDRSTRLEN
*/

地址查询

1
2
3
4
5
6
7
#include <netdb.h>
//成功返回指针,否则NULL
struct hostent *gethostent(void);
void sethostent(int stayopen);
void endhostent(void);
//......
//还有很多函数,不在此处赘述,详见《APUE》Chap16.3.3

bind

用于关联套接字和地址

1
2
3
4
5
6
7
8
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//一般的可以用sizeof(addr)来作为len参数传入
//若端口小于1024的话需要有超级用户权限
//INADDR_ANY的话则会绑定到所有网卡上
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
//0表示成功,-1表示出错

连接

1
2
3
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
//0表示成功,出错-1
  1. 如果connect的时候,sockfd没有绑定到一个地址,connect会给调用者默认绑定一个地址。
  2. 对于UDP,也可调用connect,这样每次发送的时候就不需要一直填地址。

    监听

    1
    2
    3
    4
    5
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    //backlog表示所要入队的未完成连接的请求数量,当队列满时,就会拒绝连接请求
    int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
    //成功返回套接字(文件)描述符,出错返回-1

数据传输

  1. send
  2. sendto
  3. sendmsg
  4. recv
  5. recvfrom
  6. recvmsg
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <sys/socket.h>
    //For send
    ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
    ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, conststruct sockaddr *destaddr, socklen_t destlen);
    ssize_t sendmsg(int sockfd,const struct msghdr *msg, int flags);
    struct msghdr{
    void *msg_name;
    socklen_t msg_namelen;
    struct iovec *msg_iov;
    int msg_iovlen;
    void *msg_control;
    socklen_t msg_controllen;
    int msg_flags;
    //.......
    };

    //For receive
    ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
    //出错返回-1,无数据返回0
    ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restict addr, socklen_t *restrict addrlen);
    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

套接字选项

1
2
3
4
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);
//成功返回0,出错返回-1
int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict lenp);

level的详细请见《APUE》P503,另外这个函数通常作用是来设置SO_REUSEADDR和SO_REUSEPORT等选项。两者的区别和相关的影响见0517日报中的相关内容。

带外数据

带外数据一般是针对TCP而言的,相当于紧急数据(urgent data),这些数据会优先发送,哪怕传输队列已经有数据。TCP仅支持一个字节的紧急数据,只需在发送函数中指定MSG_OOB的标识,当发送内容超过一个字节,只有最后一个字节被视为紧急数据。上一个紧急数据未处理又收到一个,会被覆盖。详见《APUE》16.7

1
2
3
#include <sys/socket.h>
int sockatmark(int sockfd);
//若紧急数据在标记处,返回1,否则返回0,出错-1

查了下应用场景,比较特殊,如远程传输文件突然想取消,则使用这种方式

非阻塞I/O

设置接受和发送为异步操作的顺序

  1. 建立套接字的所有权
  2. 通知套接字当I/O操作不会阻塞时发信号

    详细方式

  3. fcntl中使用F_SETOWN
  4. fcntl中使用F_SETFL并启用O_ASYNC

    其他方式见《APUE》P505