Linux系统学习笔记:套接字

时间:2021-10-17 10:19:05

Linux系统学习笔记:套接字

Yeolar   2012-05-18 14:22  

Linux系统学习笔记 Linux系统学习笔记:进程间通信

上一篇总结了Linux中的一些经典的进程间通信的机制,本篇总结使用套接字的进程间通信的方法。套接字的优势在于它采用同样的接口来处理计算机内和不同计算机间的通信,通常它用于网络进程间通信,在计算机内,UNIX域套接字可以作为全双工管道的实现。

目录

套接字接口

套接字接口是一组用来结合UNIX I/O函数进行进程间通信的函数,大多数系统上都实现了它,包括各种UNIX变种、Windows和Mac系统。

Linux系统学习笔记:套接字

套接字接口

套接字描述符

套接字是通信端点的抽象,使用套接字描述符访问套接字,Linux用文件描述符实现套接字描述符,很多处理文件描述符的函数也可以用于套接字描述符。

使用 socket 函数创建套接字。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 创建套接字
4 * @return 成功返回文件描述符,出错返回-1 */
5 int socket(int domain, int type, int protocol);

参数说明:

domain

确定通信的特性,包括地址格式,每个域都有自己的地址格式。POSIX.1指定的域包括:

  • AF_INET :IPv4地址域。
  • AF_INET6 :IPv6地址域。
  • AF_UNIX :UNIX域。
  • AF_UNSPEC :未指定,可以代表任何域。
type

确定套接字的类型。POSIX.1定义的套接字类型有:

  • SOCK_SEQPACKET :长度固定、有序、可靠的面向连接报文传递。
  • SOCK_STREAM :有序、可靠、双向的面向连接字节流。
  • SOCK_DGRAM :长度固定、不可靠的无连接报文传递。
  • SOCK_RAW :IP协议的数据报接口。
protocol
通常为0,表示按给定的域和套接字类型选择默认协议。在 domaintype 给定的情况下如果有多个协议,可以用 protocol 指定协议。

AF_INET 通信域, SOCK_SEQPACKET 套接字类型的默认协议是SCTP, SOCK_STREAM 套接字类型的默认协议是TCP, SOCK_DGRAM 套接字类型的默认协议是UDP。

面向连接的协议通信可比作打电话。在交换数据前,要求在本地套接字和远程套接字之间建立一个逻辑连接,即连接是端到端的通信信道。会话中不包含地址信息,它隐含在连接中。

SOCK_STREAM 套接字提供字节流服务,从套接字读取数据时可能需要多次函数调用。 SOCK_SEQPACKET 套接字提供报文服务,从套接字接收的数据量和对方发送的一致。

数据报提供了无连接服务,它是一种自含报文,发送数据报可比作寄邮件。可以发送很多数据报,但不保证它们的顺序,而且可能会丢失,数据报包含接收地址。

SOCK_RAW 套接字提供了一个数据报接口用于直接访问网络层(IP层),使用它时需要自己构造协议首部。创建 SOCK_RAW 套接字需要超级用户特权。

尽管套接字描述符是文件描述符,担不是所有使用文件描述符的函数都能处理套接字描述符,下面是有关函数的支持情况:

close 释放套接字
dup dup2 正常复制
fcntl 支持一些命令,如 F_DUPFDF_GETFDF_GETFLF_GETOWNF_SETFDF_SETFLF_SETOWN
fstat 支持一些 stat 结构成员,由实现定义
ioctl 支持部分命令,依赖于底层设备驱动
poll 正常使用
read readv 等价于无标志位的 recv
select 正常使用
write writev 等价于无标志位的 send

close 直到套接字的最后一个描述符关闭时才释放网络端点。

可以用 shutdown 函数来禁止套接字上的输入/输出。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 关闭套接字上的输入/输出
4 * @return 成功返回0,出错返回-1 */
5 int shutdown(int sockfd, int how);

how 可以取:

  • SHUT_RD ,关闭读端,即无法从套接字读取数据。
  • SHUT_WR ,关闭写端,即无法用套接字发送数据。
  • SHUT_RDWR ,关闭读写,同时无法读取和发送数据。

寻址

进程标识确定目标通信进程,它有两部分:计算机的网络地址确定计算机,服务确定计算机上的特定进程。

字节序

CPU有大端字节序和小端字节序两种字节表示顺序。为使不同的计算机可以正常交换信息,网络协议指定了字节序。

TCP/IP协议栈采用大端字节序。可以用下面的函数处理主机字节序和网络字节序之间的转换。

Linux系统学习笔记:套接字 1 #include <arpa/inet.h>
2
3 /* 将主机字节序的32位整型数转换为网络字节序 */
4 uint32_t htonl(uint32_t hostlong);
5 /* 将主机字节序的16位整型数转换为网络字节序 */
6 uint16_t htons(uint16_t hostshort);
7 /* 将网络字节序的32位整型数转换为主机字节序 */
8 uint32_t ntohl(uint32_t netlong);
9 /* 将网络字节序的16位整型数转换为主机字节序 */
10 uint16_t ntohs(uint16_t netshort);

地址格式

地址标识特定通信域中的套接字端点,地址格式和特定通信域相关。为兼容不同格式的地址,地址被强制转换为通用的地址结构 sockaddr ,Linux系统中该结构定义如下:

Linux系统学习笔记:套接字1 struct sockaddr {
2 sa_family_t sa_family; /* 地址类型 */
3 char sa_data[14]; /* 变长地址 */
4 };

因特网地址定义在 <netinet/in.h> 中。

AF_INET 域中,套接字地址结构如下:

Linux系统学习笔记:套接字 1 struct in_addr {
2 in_addr_t s_addr; /* IPv4地址 */
3 };
4
5 struct sockaddr_in {
6 sa_family_t sin_family; /* 地址类型 */
7 in_port_t sin_port; /* 端口号 */
8 struct in_addr sin_addr; /* IPv4地址 */
9 unsigned char sin_zero[8]; /* 0填充 */
10 };

AF_INET6 域中,套接字地址结构如下:

Linux系统学习笔记:套接字 1 struct in6_addr {
2 uint8_t s6_addr[16]; /* IPv6地址 */
3 };
4
5 struct sockaddr_in6 {
6 sa_family_t sin6_family; /* 地址类型 */
7 in_port_t sin6_port; /* 端口号 */
8 uint32_t sin6_flowinfo; /* 传输类和流信息 */
9 struct in6_addr sin6_addr; /* IPv6地址 */
10 uint32_t sin6_scope_id; /* 作用域的接口集 */
11 };

in_port_t 定义为 uint16_tin_addr_t 定义为 uint32_t

可以用 inet_ntopinet_pton 函数对IPv4和IPv6地址作二进制和点分十进制字符串之间的转换。类似的还有 inet_addrinet_ntoa 等函数,但它们只能用于IPv4地址。

Linux系统学习笔记:套接字1 #include <arpa/inet.h>
2
3 /* 将网络字节序的二进制地址转换为字符串格式
4 * @return 成功返回地址字符串指针,出错返回NULL */
5 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
6 /* 将字符串格式转换为网络字节序的二进制地址
7 * @return 成功返回1,格式无效返回0,出错返回-1 */
8 int inet_pton(int af, const char *src, void *dst);

af 只支持 AF_INETAF_INET6

size 指定字符串缓冲区 dst 的大小,可用 INET_ADDRSTRLENINET6_ADDRSTRLEN 来为IPv4和IPv6地址的字符串设置足够大的空间。

地址查询

使用 gethostent 函数可以找到给定计算机的主机信息。

Linux系统学习笔记:套接字 1 #include <netdb.h>
2
3 /* 获取文件的下一个hostent结构
4 * @return 成功返回指向hostent结构的指针,出错返回NULL */
5 struct hostent *gethostent(void);
6 /* gethostent的可重入版本,自设缓冲 */
7 int gethostent_r(struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop);
8 /* 回到文件开头 */
9 void sethostent(int stayopen);
10 /* 关闭文件 */
11 void endhostent(void);
12
13 /* 通过主机名或地址查询hostent结构
14 * @return 成功返回指向hostent结构的指针,出错返回NULL */
15 struct hostent *gethostbyname(const char *name);
16 #include <sys/socket.h> /* for AF_INET */
17 struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

结构 hostent 的定义如下:

Linux系统学习笔记:套接字1 struct hostent {
2 char *h_name; /* 主机名 */
3 char **h_aliases; /* 主机别名列表 */
4 int h_addrtype; /* 主机地址类型 */
5 int h_length; /* 地址长度 */
6 char **h_addr_list; /* 地址列表 */
7 };

其中的地址采用网络字节序。

gethostbynamegethostbyaddr 函数已经过时。

使用 getnetent 函数可以获取网络名和网络号。

Linux系统学习笔记:套接字1 #include <netdb.h>
2
3 struct netent *getnetent(void);
4 void setnetent(int stayopen);
5 void endnetent(void);
6
7 struct netent *getnetbyname(const char *name);
8 struct netent *getnetbyaddr(uint32_t net, int type);

结构 netent 的定义如下:

Linux系统学习笔记:套接字1 struct netent {
2 char *n_name; /* 网络名 */
3 char **n_aliases; /* 网络别名列表 */
4 int n_addrtype; /* 网络地址类型 */
5 uint32_t n_net; /* 网络号 */
6 };

网络号按网络字节序返回。地址类型为 AF_XX 的地址族常量。

使用 getprotoent 可以获取协议名字和对应协议号。

Linux系统学习笔记:套接字1 #include <netdb.h>
2
3 struct protoent *getprotoent(void);
4 void setprotoent(int stayopen);
5 void endprotoent(void);
6
7 struct protoent *getprotobyname(const char *name);
8 struct protoent *getprotobynumber(int proto);

结构 protoent 的定义如下:

Linux系统学习笔记:套接字1 struct protoent {
2 char *p_name; /* 协议名 */
3 char **p_aliases; /* 协议别名列表 */
4 int p_proto; /* 协议号 */
5 };

服务由地址的端口号部分表示,每种服务由一个唯一和熟知的端口号提供。使用 getservent 可以获取服务名字和对应端口号。

Linux系统学习笔记:套接字1 #include <netdb.h>
2
3 struct servent *getservent(void);
4 void setservent(int stayopen);
5 void endservent(void);
6
7 struct servent *getservbyname(const char *name, const char *proto);
8 struct servent *getservbyport(int port, const char *proto);

结构 servent 的定义如下:

Linux系统学习笔记:套接字1 struct servent {
2 char *s_name; /* 服务名 */
3 char **s_aliases; /* 服务别名列表 */
4 int s_port; /* 端口号 */
5 char *s_proto; /* 使用的协议 */
6 };

getaddrinfo 函数可以将一个主机名字和服务名字映射到一个地址, getnameinfo 的作用相反,用它们替换旧函数 gethostbynamegethostbyaddrfreeaddrinfo 用来释放 addrinfo 结构链表。

Linux系统学习笔记:套接字 1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netdb.h>
4
5 /* 获取地址信息
6 * @return 成功返回0,出错返回非0错误码 */
7 int getaddrinfo(const char *node, const char *service,
8 const struct addrinfo *hints, struct addrinfo **res);
9 /* 释放一个或多个addrinfo结构的链表 */
10 void freeaddrinfo(struct addrinfo *res);
11 /* 将错误码转换为错误信息 */
12 const char *gai_strerror(int errcode);
13
14 /* 获取主机名或/和服务名
15 * @return 成功返回0,出错返回非0值 */
16 int getnameinfo(const struct sockaddr *sa, socklen_t salen,
17 char *host, size_t hostlen,
18 char *serv, size_t servlen, int flags);

需要提供主机名或/和服务名,主机名可以是节点名或点分十进制字符串表示的主机地址。

结构 addrinfo 的定义如下:

Linux系统学习笔记:套接字 1 struct addrinfo {
2 int ai_flags; /* 自定义行为 */
3 int ai_family; /* 地址类型 */
4 int ai_socktype; /* 套接字类型 */
5 int ai_protocol; /* 协议 */
6 size_t ai_addrlen; /* 地址长度 */
7 struct sockaddr *ai_addr; /* 地址 */
8 char *ai_canonname; /* 主机规范名 */
9 struct addrinfo *ai_next; /* 链表中的下一个结构 */
10 };

可以用 hints 来过滤得到的结构,它只使用 ai_flagsai_familyai_socktypeai_protocol 字段,其他字段必须设为0或 NULL

ai_flags 用来指定如何处理地址和名字,可用标志有:

  • AI_ADDRCONFIG :查询配置的地址类型(IPv4或IPv6)。
  • AI_ALL :查找IPv4和IPv6地址,仅用于 AI_V4MAPPED
  • AI_CANONNAME :需要一个规范名而不是别名。
  • AI_NUMERICHOST :以数字格式返回主机地址。
  • AI_NUMERICSERV :以端口号返回服务。
  • AI_PASSIVE :套接字地址用于监听绑定。
  • AI_V4MAPPED :如果没找到IPv6地址,返回映射到IPv6格式的IPv4地址。

flags 参数指定一些转换的控制方式,有:

  • NI_DGRAM :服务基于数据报而非基于流。
  • NI_NAMEREQD :如果找不到主机名,将其视作错误。
  • NI_NOFQDN :对于本地主机,仅返回完全限定域名的节点名部分。
  • NI_NUMERICHOST :以数字形式返回主机地址。
  • NI_NUMERICSERV :以端口号返回服务地址。

绑定地址

对于服务器,需要给接收客户端请求的套接字绑定一个众所周知的地址。客户端则让系统选择一个默认地址即可。

可以用 bind 函数将地址绑定到一个套接字。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 将地址绑定到套接字
4 * @return 成功返回0,出错返回-1 */
5 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

对于地址,要求:

  • 地址必须有效,不能指定其他机器的地址。
  • 地址必须和套接字的地址族所支持的格式匹配。
  • 如果不是超级用户,端口号不能小于1024。
  • 一般只有套接字端点能够和地址绑定。

对于因特网域,如果指定IP地址为 INADDR_ANY ,套接字端点可以被绑定到所有的系统网络接口,这样它可以收到这个系统的所有网卡的数据包。

getsockname 函数可以获取绑定到套接字的地址。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 获取绑定到套接字的地址
4 * @return 成功返回0,出错返回-1 */
5 int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

addrlen 指定 addr 的缓冲区大小,返回时会被设置为返回地址的大小,如果地址过大,则会被截断而不报错。

套接字已经和对方连接时,可以用 getpeername 函数获取对方的地址。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 获取绑定到套接字的地址
4 * @return 成功返回0,出错返回-1 */
5 int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

建立连接

面向连接的网络服务在交换数据前需要首先在请求服务的套接字(客户端)和提供服务的套接字(服务器)之间建立连接。可以用 connect 函数建立连接。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 建立连接
4 * @return 成功返回0,出错返回-1 */
5 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

addr 为服务器的地址。如果 sockfd 还没有绑定地址,函数会给它绑定一个默认地址。

由于服务器的负载变化等问题, connect 可能会返回错误,通常会选择重连,如下面的指数补偿算法。

Linux系统学习笔记:套接字 1 #include <unistd.h>
2 #include <sys/socket.h>
3
4 #define MAXSLEEP 128
5
6 int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen)
7 {
8 int nsec;
9
10 /* Try to connect with exponential backoff. */
11 for (nsec = 1; nsec <= MAXSLEEP; nsec <<= 1) {
12 if (connect(sockfd, addr, alen) == 0) {
13 /* Connection accepted. */
14 return(0);
15 }
16 /* Delay before trying again. */
17 if (nsec <= MAXSLEEP/2)
18 sleep(nsec);
19 }
20 return(-1);
21 }

connect 也可用于无连接网络服务。所有发送报文的目标地址被设为 addr ,并且只能接收来自 addr 的报文。

服务器使用 listen 函数来说明可以接受连接请求。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 说明可以接受连接
4 * @return 成功返回0,出错返回-1 */
5 int listen(int sockfd, int backlog);

backlog 建议连接请求的队列大小,实际值由系统决定,上限为 SOMAXCONN ,该值为128。队列满后,系统会拒绝多余的连接请求。

之后用 accept 函数获得连接请求并建立连接。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 获得连接请求,建立连接
4 * @return 成功返回文件描述符,出错返回-1 */
5 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept 会将客户端地址设置到 addr 指向的缓冲区并更新 addrlen 指向的值,若不关心客户端可将它们设为 NULL

函数返回连接到调用 connect 的客户端的套接字描述符,它和 sockfd 有相同的套接字类型和地址族, sockfd 会被保持可用状态接受其他连接请求。

如果没有连接请求, accept 会阻塞直到有请求到来,如果 sockfd 为非阻塞模式,则返回-1,并设 errnoEAGAINEWOULDBLOCK (两者等价)。服务器也可以用 pollselect 来等待请求,请求套接字会被作为可读的。

数据传输

首先,因为套接字端点用文件描述符表示,所以可以用 readwrite 函数来交换数据,这会带来兼容性上的好处。但如果想获得更多的功能,需要使用 sendrecv 函数族。

send 函数族用于发送数据。

Linux系统学习笔记:套接字 1 #include <sys/types.h>
2 #include <sys/socket.h>
3
4 /* 发送数据
5 * @return 成功返回发送的字节数,出错返回-1 */
6 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
7 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
8 const struct sockaddr *dest_addr, socklen_t addrlen);
9 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

send 函数类似 write ,但它支持额外的参数 flags ,可用标志有:

  • MSG_DONTROUTE :勿将数据路由出本地网络。
  • MSG_DONTWAIT :允许非阻塞操作,等价于用 O_NONBLOCK
  • MSG_EOR :如果协议支持,此为记录结束。
  • MSG_OOB :如果协议支持,发送带外数据。

send 成功返回只代表数据已经正确发送到网络上,不表示连接另一端的进程接收数据。对于支持为报文设限的协议,如果报文大小超过协议支持的最大值, send 失败并设 errnoEMSGSIZE 。对于字节流协议, send 会阻塞直到整个数据被传输。

sendtosend 的区别是它支持在无连接的套接字上指定目标地址。无连接的套接字如果在调用 connect 时没有设置目标地址,则不能用 send

sendmsg 类似于 writev ,可以指定多重缓冲区来传输数据。结构 msghdr 的定义如下:

Linux系统学习笔记:套接字 1 struct msghdr {
2 void *msg_name; /* 可选地址 */
3 socklen_t msg_namelen; /* 地址大小 */
4 struct iovec *msg_iov; /* I/O缓冲区数组 */
5 size_t msg_iovlen; /* 数组中元素数 */
6 void *msg_control; /* 辅助数据 */
7 size_t msg_controllen; /* 辅助数据大小 */
8 int msg_flags; /* 接收到的数据的标志 */
9 };

相应地, recv 函数族用于接收数据。

Linux系统学习笔记:套接字 1 #include <sys/types.h>
2 #include <sys/socket.h>
3
4 /* 接收数据
5 * @return 成功返回接收的消息字节数,无可用消息或对方已按序结束返回0,出错返回-1 */
6 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
7 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
8 struct sockaddr *src_addr, socklen_t *addrlen);
9 ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

recv 函数类似 read ,但同样它支持额外的参数 flags ,可用标志有:

  • MSG_WAITALL :等待直到所有数据可用,只对 SOCK_STREAM 有效。
  • MSG_TRUNC :返回报文的实际长度,即使被截断。
  • MSG_PEEK :返回报文内容但不取走报文。
  • MSG_OOB :如果协议支持,接收带外数据。

SOCK_DGRAMSOCK_SEQPACKET 套接字类型一次读取就返回整个报文,所以 MSG_WAITALL 对它们没有作用。

如果发送者用 shutdown 结束传输,或网络协议支持顺序关闭且发送端已关闭,则数据接收完之后 recv 返回0。

recvfrom 可以得到发送者的源地址,通常用于无连接套接字。

recvmsg 类似于 readv ,可以将接收到的数据放入多个缓冲区,也可用于想接收辅助数据的情况。 msghdr 结构前面已经说明,从 recvmsg 返回时会设置 msg_flags 字段来表示接收的数据的特性,可能的值有:

  • MSG_DONTWAITrecvmsg 处于非阻塞模式。
  • MSG_TRUNC :一般数据被截断。
  • MSG_CTRUNC :控制数据被截断。
  • MSG_EOR :接收到记录结束符。
  • MSG_OOB :接收到带外数据。

套接字选项

有三种类型的套接字选项:

  1. 通用选项,适用于所有套接字类型。
  2. 特定套接字的选项,会依赖于下层协议。
  3. 特定协议的选项。

getsockoptsetsockopt 函数获取和设置套接字选项。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 获取和设置套接字选项
4 * @return 成功返回0,出错返回-1 */
5 int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
6 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数说明:

level
指定选项应用的协议。如果选项是通用的套接字选项,设为 SOL_SOCKET ,否则设为控制该选项的协议号。对TCP选项设为 IPPROTO_TCP ,对UDP选项设为 IPPROTO_UDP ,对IP选项设为 IPPROTO_IP
optname

指定套接字选项。通用套接字选项包括:

选项 参数 optval 类型 说明
SO_ACCEPTCONN int 返回信息指示该套接字是否能监听,仅 getsockopt
SO_BROADCAST int 如果 *optval 非0,广播数据包
SO_DEBUG int 如果 *optval 非0,启动网络驱动调试功能
SO_DONTROUTE int 如果 *optval 非0,绕过通常路由
SO_ERROR int 返回挂起的套接字错误并清除,仅 getsockopt
SO_KEEPALIVE int 如果 *optval 非0,启动周期性keep-alive消息
SO_LINGER struct linger 当有未发送消息并且套接字关闭时,延迟时间
SO_OOBINLINE int 如果 *optval 非0,将带外数据放到普通数据中
SO_RCVBUF int 接收缓冲区的字节大小
SO_RCVLOWAT int 接收调用中返回的数据最小字节数
SO_RCVTIMEO struct timeval 套接字接收调用的超时值
SO_REUSEADDR int 如果 *optval 非0,重用 bind 中的地址
SO_SNDBUF int 发送缓冲区的字节大小
SO_SNDLOWAT int 发送调用中发送的数据最小字节数
SO_SNDTIMEO struct timeval 套接字发送调用的超时值
SO_TYPE int 标识套接字类型,仅 getsockopt
optval
根据选项的不同指向一个数据结构或整数。

通常TCP不允许绑定同一个地址,可以用 SO_REUSEADDR 来越过这个限制。

Linux系统学习笔记:套接字 1 #include <errno.h>
2 #include <sys/socket.h>
3
4 int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen)
5 {
6 int fd, err;
7 int reuse = 1;
8
9 if ((fd = socket(addr->sa_family, type, 0)) < 0)
10 return(-1);
11 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)) < 0) {
12 err = errno;
13 goto errout;
14 }
15 if (bind(fd, addr, alen) < 0) {
16 err = errno;
17 goto errout;
18 }
19 if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
20 if (listen(fd, qlen) < 0) {
21 err = errno;
22 goto errout;
23 }
24 }
25 return(fd);
26 errout:
27 close(fd);
28 errno = err;
29 return(-1);
30 }

带外数据

带外数据是一些通信协议支持的可选特性,允许更高优先级的数据比普通数据优先传输。TCP支持带外数据,UDP不支持。

在TCP中带外数据称为紧急数据,TCP只支持1字节的紧急数据,允许紧急数据在普通数据数据流之外传输。在 send 函数族中指定 MSG_OOB 标志,就会产生紧急数据,取最后1个字节。

SO_OOBINLINE 套接字选项可以在普通数据中接收紧急数据,可以在TCP的普通数据流中查看紧急数据的位置,即紧急标记。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 下一个要读的字节在标志处返回1,没在标志处返回0,出错返回-1 */
4 int sockatmark(int sockfd);

在读取队列出现带外数据时, select 函数返回文件描述符并异常挂起。可在普通数据流或用加 MSG_OOB 标志的 recv 函数族接收紧急数据,后面的紧急数据会覆盖前面的。

UNIX域套接字

UNIX域套接字用于同一台机器上的进程间通信。因特网域套接字也可以实现但UNIX域套接字效率更高,后者只复制数据而不处理协议和其他和网络相关的工作。

UNIX域套接字有流和数据报两种接口,它的数据报服务是可靠的。

可以用 socketpair 函数创建一对非命名的、相互连接的UNIX域套接字。

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* 创建一对无名的相互连接的UNIX域套接字
4 * @return 成功返回0,出错返回-1 */
5 int socketpair(int domain, int type, int protocol, int sv[2]);

例:

Linux系统学习笔记:套接字1 #include <sys/socket.h>
2
3 /* Returns a full-duplex "stream" pipe (a UNIX domain socket) with the two file descriptors returned in fd[0] and fd[1]. */
4 int s_pipe(int fd[2])
5 {
6 return(socketpair(AF_UNIX, SOCK_STREAM, 0, fd));
7 }

也可以用 socket 创建命名的UNIX域套接字,用标准的 bindlistenacceptconnect 进行处理。

UNIX域套接字的地址定义在 <sys/un.h> 中,由 sockaddr_un 结构表示:

Linux系统学习笔记:套接字1 struct sockaddr_un {
2 sa_family_t sun_family; /* AF_UNIX */
3 char sun_path[108]; /* 路径名 */
4 };

可以用 offsetof 函数获取 sun_path 的偏移量再加上 sun_path 的长度得到绑定地址的长度。

将地址绑定到UNIX域套接字时,系统会用该路径名创建一个 S_IFSOCK 类型的文件。它只用于向客户进程告诉套接字名字,不能打开,也不能由应用程序用于通信。如果文件已经存在,则 bind 会失败。关闭套接字时,不会自动删除文件,因此还需要在程序中手动删除。

使用套接字的示例

面向连接的ruptime

客户端:

Linux系统学习笔记:套接字 1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <netdb.h>
5 #include <errno.h>
6 #include <sys/socket.h>
7 #include "error.h"
8
9 #define MAXADDRLEN 256
10 #define BUFLEN 128
11
12 extern int connect_retry(int, const struct sockaddr *, socklen_t);
13
14 void print_uptime(int sockfd)
15 {
16 int n;
17 char buf[BUFLEN];
18
19 while ((n = recv(sockfd, buf, BUFLEN, 0)) > 0)
20 write(STDOUT_FILENO, buf, n);
21 if (n < 0)
22 err_sys("recv error");
23 }
24
25 int main(int argc, char *argv[])
26 {
27 struct addrinfo *ailist, *aip;
28 struct addrinfo hint;
29 int sockfd, err;
30
31 if (argc != 2)
32 err_quit("usage: ruptime hostname");
33 hint.ai_flags = 0;
34 hint.ai_family = 0;
35 hint.ai_socktype = SOCK_STREAM;
36 hint.ai_protocol = 0;
37 hint.ai_addrlen = 0;
38 hint.ai_canonname = NULL;
39 hint.ai_addr = NULL;
40 hint.ai_next = NULL;
41 if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0)
42 err_quit("getaddrinfo error: %s", gai_strerror(err));
43 for (aip = ailist; aip != NULL; aip = aip->ai_next) {
44 if ((sockfd = socket(aip->ai_family, SOCK_STREAM, 0)) < 0)
45 err = errno;
46 if (connect_retry(sockfd, aip->ai_addr, aip->ai_addrlen) < 0) {
47 err = errno;
48 } else {
49 print_uptime(sockfd);
50 exit(0);
51 }
52 }
53 fprintf(stderr, "can't connect to %s: %s\n", argv[1], strerror(err));
54 exit(1);
55 }

服务器( popen 版本):

Linux系统学习笔记:套接字 1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <netdb.h>
4 #include <errno.h>
5 #include <syslog.h>
6 #include <sys/socket.h>
7 #include "error.h"
8
9 #define BUFLEN 128
10 #define QLEN 10
11
12 #ifndef HOST_NAME_MAX
13 #define HOST_NAME_MAX 256
14 #endif
15
16 extern int initserver(int, struct sockaddr *, socklen_t, int);
17
18 void serve(int sockfd)
19 {
20 int clfd;
21 FILE *fp;
22 char buf[BUFLEN];
23
24 for (;;) {
25 clfd = accept(sockfd, NULL, NULL);
26 if (clfd < 0) {
27 syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
28 exit(1);
29 }
30 if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
31 sprintf(buf, "error: %s\n", strerror(errno));
32 send(clfd, buf, strlen(buf), 0);
33 } else {
34 while (fgets(buf, BUFLEN, fp) != NULL)
35 send(clfd, buf, strlen(buf), 0);
36 pclose(fp);
37 }
38 close(clfd);
39 }
40 }
41
42 int main(int argc, char *argv[])
43 {
44 struct addrinfo *ailist, *aip;
45 struct addrinfo hint;
46 int sockfd, err, n;
47 char *host;
48
49 if (argc != 1)
50 err_quit("usage: ruptimed");
51 #ifdef _SC_HOST_NAME_MAX
52 n = sysconf(_SC_HOST_NAME_MAX);
53 if (n < 0) /* best guess */
54 #endif
55 n = HOST_NAME_MAX;
56 host = malloc(n);
57 if (host == NULL)
58 err_sys("malloc error");
59 if (gethostname(host, n) < 0)
60 err_sys("gethostname error");
61 daemonize("ruptimed");
62 hint.ai_flags = AI_CANONNAME;
63 hint.ai_family = 0;
64 hint.ai_socktype = SOCK_STREAM;
65 hint.ai_protocol = 0;
66 hint.ai_addrlen = 0;
67 hint.ai_canonname = NULL;
68 hint.ai_addr = NULL;
69 hint.ai_next = NULL;
70 if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
71 syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
72 exit(1);
73 }
74 for (aip = ailist; aip != NULL; aip = aip->ai_next) {
75 if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
76 aip->ai_addrlen, QLEN)) >= 0) {
77 serve(sockfd);
78 exit(0);
79 }
80 }
81 exit(1);
82 }

服务器( fork 版本):

Linux系统学习笔记:套接字 1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <netdb.h>
4 #include <errno.h>
5 #include <syslog.h>
6 #include <fcntl.h>
7 #include <sys/socket.h>
8 #include <sys/wait.h>
9 #include "error.h"
10
11 #define QLEN 10
12
13 #ifndef HOST_NAME_MAX
14 #define HOST_NAME_MAX 256
15 #endif
16
17 extern int initserver(int, struct sockaddr *, socklen_t, int);
18
19 void serve(int sockfd)
20 {
21 int clfd, status;
22 pid_t pid;
23
24 for (;;) {
25 clfd = accept(sockfd, NULL, NULL);
26 if (clfd < 0) {
27 syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
28 exit(1);
29 }
30 if ((pid = fork()) < 0) {
31 syslog(LOG_ERR, "ruptimed: fork error: %s", strerror(errno));
32 exit(1);
33 } else if (pid == 0) { /* child */
34 /*
35 * The parent called daemonize ({Prog daemoninit}), so
36 * STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO
37 * are already open to /dev/null. Thus, the call to
38 * close doesn't need to be protected by checks that
39 * clfd isn't already equal to one of these values.
40 */
41 if (dup2(clfd, STDOUT_FILENO) != STDOUT_FILENO ||
42 dup2(clfd, STDERR_FILENO) != STDERR_FILENO) {
43 syslog(LOG_ERR, "ruptimed: unexpected error");
44 exit(1);
45 }
46 close(clfd);
47 execl("/usr/bin/uptime", "uptime", (char *)0);
48 syslog(LOG_ERR, "ruptimed: unexpected return from exec: %s",
49 strerror(errno));
50 } else { /* parent */
51 close(clfd);
52 waitpid(pid, &status, 0);
53 }
54 }
55 }
56
57 int main(int argc, char *argv[])
58 {
59 struct addrinfo *ailist, *aip;
60 struct addrinfo hint;
61 int sockfd, err, n;
62 char *host;
63
64 if (argc != 1)
65 err_quit("usage: ruptimed");
66 #ifdef _SC_HOST_NAME_MAX
67 n = sysconf(_SC_HOST_NAME_MAX);
68 if (n < 0) /* best guess */
69 #endif
70 n = HOST_NAME_MAX;
71 host = malloc(n);
72 if (host == NULL)
73 err_sys("malloc error");
74 if (gethostname(host, n) < 0)
75 err_sys("gethostname error");
76 daemonize("ruptimed");
77 hint.ai_flags = AI_CANONNAME;
78 hint.ai_family = 0;
79 hint.ai_socktype = SOCK_STREAM;
80 hint.ai_protocol = 0;
81 hint.ai_addrlen = 0;
82 hint.ai_canonname = NULL;
83 hint.ai_addr = NULL;
84 hint.ai_next = NULL;
85 if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
86 syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
87 exit(1);
88 }
89 for (aip = ailist; aip != NULL; aip = aip->ai_next) {
90 if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
91 aip->ai_addrlen, QLEN)) >= 0) {
92 serve(sockfd);
93 exit(0);
94 }
95 }
96 exit(1);
97 }

无连接的ruptime

客户端:

Linux系统学习笔记:套接字 1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <netdb.h>
4 #include <errno.h>
5 #include <sys/socket.h>
6 #include "error.h"
7
8 #define BUFLEN 128
9 #define TIMEOUT 20
10
11 void sigalrm(int signo) {}
12
13 void print_uptime(int sockfd, struct addrinfo *aip)
14 {
15 int n;
16 char buf[BUFLEN];
17
18 buf[0] = 0;
19 if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen) < 0)
20 err_sys("sendto error");
21 alarm(TIMEOUT);
22 if ((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL)) < 0) {
23 if (errno != EINTR)
24 alarm(0);
25 err_sys("recv error");
26 }
27 alarm(0);
28 write(STDOUT_FILENO, buf, n);
29 }
30
31 int main(int argc, char *argv[])
32 {
33 struct addrinfo *ailist, *aip;
34 struct addrinfo hint;
35 int sockfd, err;
36 struct sigaction sa;
37
38 if (argc != 2)
39 err_quit("usage: ruptime hostname");
40 sa.sa_handler = sigalrm;
41 sa.sa_flags = 0;
42 sigemptyset(&sa.sa_mask);
43 if (sigaction(SIGALRM, &sa, NULL) < 0)
44 err_sys("sigaction error");
45 hint.ai_flags = 0;
46 hint.ai_family = 0;
47 hint.ai_socktype = SOCK_DGRAM;
48 hint.ai_protocol = 0;
49 hint.ai_addrlen = 0;
50 hint.ai_canonname = NULL;
51 hint.ai_addr = NULL;
52 hint.ai_next = NULL;
53 if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0)
54 err_quit("getaddrinfo error: %s", gai_strerror(err));
55 for (aip = ailist; aip != NULL; aip = aip->ai_next) {
56 if ((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0)) < 0) {
57 err = errno;
58 } else {
59 print_uptime(sockfd, aip);
60 exit(0);
61 }
62 }
63 fprintf(stderr, "can't contact %s: %s\n", argv[1], strerror(err));
64 exit(1);
65 }

服务器:

Linux系统学习笔记:套接字 1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <netdb.h>
4 #include <errno.h>
5 #include <syslog.h>
6 #include <sys/socket.h>
7 #include "error.h"
8
9 #define BUFLEN 128
10 #define MAXADDRLEN 256
11
12 #ifndef HOST_NAME_MAX
13 #define HOST_NAME_MAX 256
14 #endif
15
16 extern int initserver(int, struct sockaddr *, socklen_t, int);
17
18 void serve(int sockfd)
19 {
20 int n;
21 socklen_t alen;
22 FILE *fp;
23 char buf[BUFLEN];
24 char abuf[MAXADDRLEN];
25
26 for (;;) {
27 alen = MAXADDRLEN;
28 if ((n = recvfrom(sockfd, buf, BUFLEN, 0,
29 (struct sockaddr *)abuf, &alen)) < 0) {
30 syslog(LOG_ERR, "ruptimed: recvfrom error: %s", strerror(errno));
31 exit(1);
32 }
33 if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
34 sprintf(buf, "error: %s\n", strerror(errno));
35 sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)abuf, alen);
36 } else {
37 if (fgets(buf, BUFLEN, fp) != NULL)
38 sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)abuf, alen);
39 pclose(fp);
40 }
41 }
42 }
43
44 int main(int argc, char *argv[])
45 {
46 struct addrinfo *ailist, *aip;
47 struct addrinfo hint;
48 int sockfd, err, n;
49 char *host;
50
51 if (argc != 1)
52 err_quit("usage: ruptimed");
53 #ifdef _SC_HOST_NAME_MAX
54 n = sysconf(_SC_HOST_NAME_MAX);
55 if (n < 0) /* best guess */
56 #endif
57 n = HOST_NAME_MAX;
58 host = malloc(n);
59 if (host == NULL)
60 err_sys("malloc error");
61 if (gethostname(host, n) < 0)
62 err_sys("gethostname error");
63 daemonize("ruptimed");
64 hint.ai_flags = AI_CANONNAME;
65 hint.ai_family = 0;
66 hint.ai_socktype = SOCK_DGRAM;
67 hint.ai_protocol = 0;
68 hint.ai_addrlen = 0;
69 hint.ai_canonname = NULL;
70 hint.ai_addr = NULL;
71 hint.ai_next = NULL;
72 if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
73 syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
74 exit(1);
75 }
76 for (aip = ailist; aip != NULL; aip = aip->ai_next) {
77 if ((sockfd = initserver(SOCK_DGRAM, aip->ai_addr,
78 aip->ai_addrlen, 0)) >= 0) {
79 serve(sockfd);
80 exit(0);
81 }
82 }
83 exit(1);
84 }


</div>


<p class="text-success hidden-print"><i class="icon-external-link"></i> <i class="small"><a href="http://www.yeolar.com/note/2012/05/18/linux-socket/">http://www.yeolar.com/note/2012/05/18/linux-socket/</a></i></p>
<p class="visible-print"><i class="icon-external-link"></i> <i class="small">http://www.yeolar.com/note/2012/05/18/linux-socket/</i></p>