Unix环境高级编程学习笔记(十一) 网络IPC:套接字

Socket 描述符

socket 主要用于运行在不同服务器上的进程之间通信(服务器通过网络相连),也可以用于在同一服务器上的进程之间通信。而 Socket 描述符则是 Socket 的唯一标识,其本质是一种特殊的文件描述符。创建 Socket 描述符 的函数声明如下:

[cpp]  view plain copy
  1. int socket(int domain, int type, int protocol);       

domain 参数决定了通信的性质,这些性质包括地址格式等。domain 的取值如下:

AF_INET IPv4 因特网域。

AF_INET6 IPv6 因特网域。

AF_UNIX UNIX 域。

AF_UNSPEC 未指定,这是一个可以表达任何域的通配符。

type 参数的类型:

SOCK_DGRAM 固定长度,无连接,不可靠的消息。

SOCK_RAW IP 协议的数据报接口。(在 POSIX.1 中是可选的)

SOCK_SEQPACKET 固定长度,顺序式,面向连接的,可靠的消息。

SOCK_STREAM 顺序式,面向连接的,双向的,可靠的字节流。

protocol 参数代表协议号,如果给0,则使用对于给定 domain 和 type 下的默认协议。在 AF_INET 通信域下,SOCK_STREAM 类型的默认协议是 TCP (Transmission Control Protocol),而对于 SOCK_DGRAM 类型的默认协议则是 UDP (User Datagram Protocol)。

网络地址

不同的网络域具有不同的地址格式,sockaddr 是地址的通用结构体:

[cpp]  view plain copy
  1. struct sockaddr {  
  2.         sa_family_t     sa_family;/* address family */  
  3.         char            sa_data[];/* variable-length address */  
  4.         .  
  5.         .  
  6.         .  
  7.     };  

其中,sa_family 实际上就是前面提到过的网络域。

对于 AF_INET,其地址格式由 sockaddr_in 结构体呈现:

[cpp]  view plain copy
  1. struct sockaddr_in {  
  2.     sa_family_t     sin_family;/* address family */  
  3.     in_port_t       sin_port;/* port number */  
  4.     struct in_addr  sin_addr;/* IPv4 address */  
  5. };  
  6.   
  7. struct in_addr {  
  8.     in_addr_t   s_addr;/* IPv4 address */  
  9. };  

而对于 AF_INET6,其地址格式由 sockaddr_in6 结构体呈现:

[cpp]  view plain copy
  1. struct sockaddr_in6 {  
  2.     sa_family_t     sin6_family;/*address family */  
  3.     in_port_t       sin6_port;/*port number */  
  4.     uint32_t        sin6_flowinfo;/*traffic class and flow info */  
  5.     struct in6_addr     sin6_addr;/*IPv6 address */  
  6.     uint32_t        sin6_scope_id;/*IPv6 address */  
  7. };  
  8.   
  9. struct in6_addr {  
  10.     uint8_t s6_addr[16];/* IPv6 address */  
  11. };  

许多时候,我们需要在二进制地址和人类可识别的字符串之间进程转换,可以使用以下函数:

[cpp]  view plain copy
  1. const char *inet_ntop(int domain, const void *restrict addr,  
  2.     char *restrict str, socklen_t size);  
  3. int inet_pton(int domain, const char *restrict str, void *restrict addr);  

前者将二进制地址转换为人类可识别的字符串,后者则相反。

对于 domain 参数,只有 AF_INET 和 AF_INET6 是支持的。

在 inet_ntop 中,str 是 O 参数,size 是该缓存区的大小,缓存区必需足够大,以存放转换后的字符串。这里有两个比较方便的宏:INET_ADDRSTRLEN 和 INET6_ADDRSTRLEN,它们分别足够大对于存放 IPV4 以及 IPV6 的地址。

建立连接

当获得 Socket 描述符后,对于服务器,首先就是和一个具体的网络地址绑定,函数声明如下:

[cpp]  view plain copy
  1. int bind(int sockfd, const struct sockaddr *addr, socklen_t len);  

前面介绍了, sockaddr 是一个通用的地址结构,因此,任意的网络地址结构都可以作为参数在这里传递,len 是传递的结构体的长度。

对于 Internet 域,如果将 IP 地址指定为 INADDR_ANY,则该 Socket 描述符将绑定到该系统上的所有网络接口上。这意味着,我们可以从安装在该系统上的任何网卡上接收数据包。

如果我们要处理的是面向连接的服务(SOCK_STREAM or SOCK_SEQPACKET),那么,在我们可以交换数据之前,客户端必须首先和服务端建立连接。

[cpp]  view plain copy
  1. int connect(int sockfd, const struct sockaddr *addr, socklen_t len);  

该函数将使 sockfd 与 addr 所代表的网络地址建立连接。如果当建立连接时,客户端的 sockfd 还没有和地址绑定(实际上一般说来也不需要),则它将首先为 sockfd 绑定一个默认的地址。该函数也可以用在无连接的网络服务上(SOCK_DGRAM),如果我们使用 SOCK_DGRAM 类型的 socket 凋用该函数,则之后我们通过该 socket 发送的所有数据的目的地址都将被设置成 addr。对于通过该 socket 接收的所有数据的源地址也将被设置成 addr。

当客户端建立连接时,如果服务器没有对该地址进行监听,则连接将失败,服务端的监听函数如下:

[cpp]  view plain copy
  1. int listen(int sockfd, int backlog);  

backlog 参数表示能够排队最大连接请求数量,它只是为系统提供了一个建议值。

使用 accept 函数用于接受一个请求,我们知道,建立 TCP 连接是需要进行三次握手的,当客户端调用 connect 函数后实际上只是发了一个 SYN,只有当服务端 accept 了这个请求之后,才真正完成了这三次握手。

[cpp]  view plain copy
  1. int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);  

该函数将返回另一个 socket 描述符,该描述符与客户端的 socket 描述符建立了一个 TCP 连接,而原先的描述符则仍然可用于接收连接请求。

如果在调用该函数时,还没有等待响应的连接请求,则进程将默认阻塞,直到有请求到达。而如果,sockfd 是非阻塞模式(关于该模式后面会讲到),accept 将返回 -1,并设置 errno 为 EAGAIN 或是EWOULDBLOCK。

参数 addr 在这里是一个 O 参数,用于获取客户端的地址,如果对此并不关心,可以置 addr 为 NULL。

数据传输

前面说了,socket 描述符实际上就是文件描述符,因此,许多针对文件描述符的操作也可以直接使用到 socket 描述符上,例如 read 和 write 函数,可以使用它们来收发数据,当然,也有专门针对 socket 描述符的数据传输操作。最基本的就是 send 和 recv 函数:

[cpp]  view plain copy
  1. ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);  
  2. ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);  

send 支持四种 flag 参数:

MSG_DONTROUTE  数据不能被路由出本地网络。 

MSG_DONTWAIT 非阻塞操作(等价于 O_NONBLOCK 模式).。

MSG_EOR 如果协议支持,此为记录结束(end of record)。

MSG_OOB 如果协议支持,发送带外数据(out-of-band data),

对于一个支持消息边界的协议,如果我们试图发送的单个消息的长度大于该协议所支持的最大长度,send 将会失败,并设置 errno 为 EMSGSIZE。

对于字节流协议,默认情况下(非阻塞模式),send 将阻塞直到所有数据都发送完毕。

recv 也支持甲种 flag 参数:

MSG_OOB 如果协议支持,获取带外数据(out-of-band data)。

MSG_PEEK 返回包内容,但不取走。

MSG_TRUNC 即使包被截断,函数也将返回完整包的长度。

MSG_WAITALL 等到所有数据都有效(只有 SOCK_STREAM 支持)。. 

这两个函数是最基本的数据传输函数,更多的函数之后再讲。

基于 TCP 的例子程序

前面说了那么多,还是来看一个例子比较有深刻的印象。不过在学习例子之前,我们需要先理解大小端的概念。

所谓的大端模式,是指数据的高位,保存在内存的低地址中,而数据的低位,保存在内存的高地址中,而小端模式则正好相反。以下是四种常用系统的大小端模式:

FreeBSD 5.2.1  Intel Pentium little-endian 
Linux 2.4.22 Intel Pentium little-endian 
Mac OS X 10.3 PowerPC big-endian 
Solaris 9 Sun SPARC big-endian 
对于 TCP 协议,其规定的是大端模式,因此这就涉及到大小端的转换问题,以下函数用于解决这个问题:

[cpp]  view plain copy
  1. uint32_t htonl(uint32_t hostint32);  
  2. uint16_t htons(uint16_t hostint16);  
  3. uint32_t ntohl(uint32_t netint32);  
  4. uint16_t ntohs(uint16_t netint16);  

前两个函数用于将主机模式的数据转化为 TCP 模式的数据,后两者则相反。l 代表32位数据的,s 代表16位。

对于 sockaddr_in 数据结构,其中 端口号与 IP 地址都是 TCP 网络字节序。

服务端程序:

[cpp]  view plain copy
  1. /* 
  2.  *author: justaipanda 
  3.  *create time:2012/09/03 09:38:51 
  4.  */  
  5.   
  6. #include <stdio.h>  
  7. #include <stdlib.h>  
  8. #include <string.h>  
  9. #include <arpa/inet.h>  
  10. #include <sys/types.h>  
  11. #include <sys/socket.h>  
  12.   
  13. #define BUF_SIZE    1024  
  14. #define MY_PORT     6788  
  15.   
  16. int main() {  
  17.   
  18.     //set server socket address  
  19.     struct sockaddr_in server_addr;  
  20.     server_addr.sin_family = AF_INET;  
  21.     server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
  22.     server_addr.sin_port = htons(MY_PORT);  
  23.   
  24.     int sd;  
  25.     if((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {  
  26.         printf("socket error!\n");  
  27.         exit(0);  
  28.     }  
  29.   
  30.     if (bind(sd, (struct sockaddr*)&server_addr,   
  31.                                 sizeof(server_addr)) < 0) {  
  32.         printf("bind error!\n");  
  33.         exit(0);  
  34.     }  
  35.   
  36.     if (listen(sd, 128) < 0) {  
  37.         printf("listen error!\n");  
  38.         exit(0);  
  39.     }  
  40.   
  41.     while(1) {  
  42.   
  43.         int sclient = accept(sd, NULL, NULL);  
  44.         if (sclient < 0) {  
  45.             printf("accept error!\n");  
  46.             exit(0);  
  47.         }  
  48.   
  49.         char buffer[BUF_SIZE];  
  50.         ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);  
  51.         if (len < 0) {  
  52.             printf("recieve error!\n");  
  53.             close(sclient);  
  54.             continue;  
  55.         }  
  56.   
  57.         buffer[len] = '\0';  
  58.         printf("receive[%d]:%s\n", (int)len, buffer);  
  59.         if (len > 0) {  
  60.             if('q' == buffer[0]) {  
  61.                 printf("server over!\n");  
  62.                 exit(0);  
  63.             }  
  64.                   
  65.             char* buffer2 = "I'm a server!";  
  66.             len = send(sclient, buffer2, strlen(buffer2), 0);  
  67.             if (len < 0)  
  68.                 printf("send error!\n");  
  69.         }  
  70.         close(sclient);  
  71.     }  
  72.   
  73.     return 0;  
  74. }  

客户端程序:

[cpp]  view plain copy
  1. /* 
  2.  *author: justaipanda 
  3.  *create time:2012/09/03 10:47:35 
  4.  */  
  5.   
  6. #include <stdio.h>  
  7. #include <string.h>  
  8. #include <stdlib.h>  
  9. #include <arpa/inet.h>  
  10. #include <sys/types.h>  
  11. #include <sys/socket.h>  
  12.   
  13. #define SERVER_PORT 6788  
  14. #define SERVER_IP "127.0.0.1"  
  15. #define BUF_SIZE    1024  
  16.   
  17. int main() {  
  18.   
  19.     int ip;  
  20.     inet_pton(AF_INET, SERVER_IP, &ip);  
  21.   
  22.     //set server socket address  
  23.     struct sockaddr_in server_addr;  
  24.     server_addr.sin_family = AF_INET;  
  25.     server_addr.sin_addr.s_addr = ip;  
  26.     server_addr.sin_port = htons(SERVER_PORT);  
  27.   
  28.     int sd;  
  29.     if ((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {  
  30.         printf("socket error!\n");  
  31.         exit(0);  
  32.     }  
  33.   
  34.     if (connect(sd, (struct sockaddr*)&server_addr,  
  35.         sizeof(server_addr)) < 0) {  
  36.         printf("connect error!\n");  
  37.         exit(0);  
  38.     }  
  39.   
  40.     char buffer[BUF_SIZE];  
  41.     printf("input:");  
  42.     scanf("%s", buffer);  
  43.     int len = send(sd, buffer, strlen(buffer), 0);  
  44.     if (len < 0) {  
  45.         printf("send error!\n");  
  46.         exit(0);  
  47.     }  
  48.   
  49.     len = recv(sd, buffer, BUF_SIZE, 0);  
  50.     if (len < 0) {  
  51.         printf("recieve error!\n");  
  52.         exit(0);  
  53.     }  
  54.   
  55.     buffer[len] = '\0';  
  56.     printf("receive[%d]:%s\n", (int)len, buffer);  
  57.   
  58.     return 0;  
  59. }  


其它数据传输函数

sendto 与 recvfrom 函数:

[cpp]  view plain copy
  1. ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,  
  2.         const struct sockaddr *destaddr, socklen_t destlen);  
  3. ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,  
  4.         struct sockaddr *restrict addr, socklen_t *restrict addrlen);  

这两个函数都是用在无连接的 socket 数据传输中的,sendto 函数和 send 基本一致,它只是多了两个参数,可以用来指定发送的目的地址,而 recvfrom 多的那两个参数则是用来在接收数据时获取发送方的地址的。

sendmsg 与 recvmsg 函数:

[cpp]  view plain copy
  1. ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);  
  2. ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);  

这两个函数的作用有点儿类似于我以前介绍过的 writev 和 readv 函数(关于这两个函数请参见Unix环境高级编程学习笔记(九) 高级IO)。sendmsg 从 多个缓存区收集数据并发送到多个目的地址,而 readv 则可从多个源地址中接收数据。看一下 msghdr 结构体的成员:

[cpp]  view plain copy
  1. struct msghdr {  
  2.     void            *msg_name;/*optional address */  
  3.     socklen_t       msg_namelen;/*address size in bytes */  
  4.     struct iovec    *msg_iov;/*array of I/O buffers */  
  5.     int             msg_iovlen;/*number of elements in array */  
  6.     void            *msg_control;/*ancillary data */  
  7.     socklen_t       msg_controllen;/*number of ancillary bytes */  
  8.     int             msg_flags;/*flags for received message */  
  9.     .  
  10.     .  
  11.     .  
  12. };  

基于 UDP 的例子程序

服务端程序:

[cpp]  view plain copy
  1. /* 
  2.  *author: justaipanda 
  3.  *create time:2012/09/03 09:38:51 
  4.  */  
  5.   
  6. #include <stdio.h>  
  7. #include <stdlib.h>  
  8. #include <string.h>  
  9. #include <arpa/inet.h>  
  10. #include <sys/types.h>  
  11. #include <sys/socket.h>  
  12.   
  13. #define BUF_SIZE    1024  
  14. #define MY_PORT     6788  
  15.   
  16. int main() {  
  17.   
  18.     //set server socket address  
  19.     struct sockaddr_in server_addr;  
  20.     server_addr.sin_family = AF_INET;  
  21.     server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
  22.     server_addr.sin_port = htons(MY_PORT);  
  23.   
  24.     int sd;  
  25.     if((sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {  
  26.         printf("socket error!\n");  
  27.         exit(0);  
  28.     }  
  29.   
  30.     if (bind(sd, (struct sockaddr*)&server_addr,   
  31.                                 sizeof(server_addr)) < 0) {  
  32.         printf("bind error!\n");  
  33.         exit(0);  
  34.     }  
  35.   
  36.     while(1) {  
  37.   
  38.         char buffer[BUF_SIZE];  
  39.         struct sockaddr_in client_addr;  
  40.         int sock_len = sizeof(client_addr);  
  41.         ssize_t len = recvfrom(sd, buffer, BUF_SIZE, 0,  
  42.                 (struct sockaddr*)&client_addr, &sock_len);  
  43.         if (len < 0) {  
  44.             printf("recieve error!\n");  
  45.             continue;  
  46.         }  
  47.   
  48.         buffer[len] = '\0';  
  49.         printf("receive[%d]:%s\n", (int)len, buffer);  
  50.         if (len > 0) {  
  51.             if('q' == buffer[0]) {  
  52.                 printf("server over!\n");  
  53.                 exit(0);  
  54.             }  
  55.                   
  56.             char* buffer2 = "I'm a server!";  
  57.             len = sendto(sd, buffer2, strlen(buffer2), 0,  
  58.                     (struct sockaddr*)&client_addr, sock_len);  
  59.             if (len < 0)  
  60.                 printf("send error!\n");  
  61.         }  
  62.     }  
  63.   
  64.     return 0;  
  65. }  

客户端程序:

[cpp]  view plain copy
  1. /* 
  2.  *author: justaipanda 
  3.  *create time:2012/09/03 10:47:35 
  4.  */  
  5.   
  6. #include <stdio.h>  
  7. #include <string.h>  
  8. #include <stdlib.h>  
  9. #include <signal.h>  
  10.   
  11. #include <arpa/inet.h>  
  12. #include <sys/types.h>  
  13. #include <sys/socket.h>  
  14.   
  15. #define SERVER_PORT 6788  
  16. #define SERVER_IP "127.0.0.1"  
  17. #define BUF_SIZE    1024  
  18.   
  19. void sig_alarm(int signo) {  
  20.     printf("receive timeout!\n");  
  21.     exit(0);  
  22. }  
  23.   
  24. int main() {  
  25.   
  26.     int ip;  
  27.     inet_pton(AF_INET, SERVER_IP, &ip);  
  28.   
  29.     //set server socket address  
  30.     struct sockaddr_in server_addr;  
  31.     server_addr.sin_family = AF_INET;  
  32.     server_addr.sin_addr.s_addr = ip;  
  33.     server_addr.sin_port = htons(SERVER_PORT);  
  34.   
  35.     int sd;  
  36.     if ((sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {  
  37.         printf("socket error!\n");  
  38.         exit(0);  
  39.     }  
  40.   
  41.     if (connect(sd, (struct sockaddr*)&server_addr,  
  42.         sizeof(server_addr)) < 0) {  
  43.         printf("connect error!\n");  
  44.         exit(0);  
  45.     }  
  46.   
  47.     char buffer[BUF_SIZE];  
  48.     printf("input:");  
  49.     scanf("%s", buffer);  
  50.     int len = send(sd, buffer, strlen(buffer), 0);  
  51.     if (len < 0) {  
  52.         printf("send error!\n");  
  53.         exit(0);  
  54.     }  
  55.   
  56.     struct sigaction sa;  
  57.     sa.sa_handler = sig_alarm;  
  58.     sa.sa_flags = 0;  
  59.     sigemptyset(&sa.sa_mask);  
  60.     sigaction(SIGALRM, &sa, NULL);  
  61.     alarm(3);//set timeout  
  62.   
  63.     len = recv(sd, buffer, BUF_SIZE, 0);  
  64.     if (len < 0) {  
  65.         printf("recieve error!\n");  
  66.         exit(0);  
  67.     }  
  68.   
  69.     buffer[len] = '\0';  
  70.     printf("receive[%d]:%s\n", (int)len, buffer);  
  71.   
  72.     return 0;  
  73. }  

关于客户端这里的程序我需要说一下的是,由于 UDP 是不可靠的传输协议,因此,客户端调用 recv 就可能会因为丢包而永远阻塞。所以,这里,我添加了时钟信号用于唤醒(关于信号机制的问题可以参看Unix环境高级编程学习笔记(六) 信号机制)。

连接关闭及其他

前面我们使用的连接关闭方式是 close,这是从文件描述符继承下来的关闭方式,对于这种方式存在的问题是,只有当最后一个该描述符的活动引用(使用 dup 函数可增加引用)被关闭后,才会真正关闭连接。因此我们可以考虑使用 shutdown 函数:

[html]  view plain copy
  1. int shutdown (int sockfd, int how);  

how的意义:

SHUT_RD 关闭读。

SHUT_WR 关闭写。

SHUT_RDWR 关闭读写。

使用 getsockname 函数可以获得与该 socket 描述符绑定的地址:

[html]  view plain copy
  1. int getsockname(int sockfd, struct sockaddr *restrict addr,  
  2.         socklen_t *restrict alenp);  

函数返回时,alenp 会被设置成获取的地址长度,如果缓存区的大小不足以存放,则地址结构体会被截断,并且没有任何错误提示。

如果该 socket 已经建立了 TCP 连接,则可以使用 getpeername 函数可以获得对方的地址:

[html]  view plain copy
  1. int getpeername(int sockfd, struct sockaddr *restrict addr,  
  2.     socklen_t *restrict alenp);  

我们可以通过利用设置 socket 选项来设置一些高级属性,例如带外数据的发送与接收等等。

[html]  view plain copy
  1. int setsockopt(int sockfd, int level, int option, const void *val,   
  2.         socklen_t len);  
  3. int getsockopt(int sockfd, int level, int option, void *restrict val,  
  4.         socklen_t *restrict lenp);  

对于带外数据,最常见的例如紧急字节,当接收到该紧急字节时,socket 会向自己的属主或是属组发送 SIGURG 信号,可以使用 F_SETOWN 命令调用 fcntl 函数去设置一个 socket 描述符的属组或是属主:

[html]  view plain copy
  1. fcntl(sockfd, F_SETOWN, pid);  

如果 pid 大于0,则它指定的是属主,小与-1,则指定的是属组。

使用 GETOWN 可以获得一个 socket 描述符的属组或是属主:

[html]  view plain copy
  1. owner = fcntl(sockfd, F_GETOWN, 0);  

如果 owner 大于0,则它得到的是属主,小与-1,则得到的是属组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值