APUE第16章 套接字


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

套接字描述符

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

#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. socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)发送接收ip数据包,不能用IPPROTO_IP,因为如果是用了IPPROTO_IP,系统根本就不知道该用什么协议。
  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. 小端:高字节地址放高位,低字节地址放低位
    #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);
    

地址格式

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
*/

地址查询

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

bind

用于关联套接字和地址

#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表示出错

连接

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

    监听

    #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
    ```cpp
    #include
    //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);

## 套接字选项
```cpp
#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

#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